stm32 DMA理论+实践

基于stm32f103的使用如果是f4或者h7之类的也是可以看看原理的。

这篇文章完成三个实验:

1 通过使用DMA完成存储器到存储器之间的数据移动

2 通过DMA转运ADC单次触发扫描模式转换后的数据

3 通过DMA转移ADC连续触发扫描模式转换后的数据

如果不想看的可以直接使用git把我的代码下载出来,里面工程挺全的,后期会慢慢的补注释之类的

stm32学习笔记: stm32学习笔记源码

如果不会使用git快速下载可以选择直接下载压缩包或者去看看git的使用

git的使用(下载及上传_gitcode怎么下载文件_是小刘不是刘的博客-CSDN博客

目录

一 理论知识

1 DMA描述 

2 存储器映像

3 DMA框图

 1 )首先是两个DMA

2 )主动单元与被动单元

3)DMA挂在在哪里

 4)DMA请求

 4 DMA逻辑图

1 传输方向

2 外设寄存器可配置的参数

 3 传输计数器和重载器

4 选择器

5 DMA请求映像

 6 数据传输

二 代码部分

1 存储器传到存储器

1)存储地址测试

 2)DMA传输框图

3)DMA单次传输代码

4)DMA循环转移代码

 2 ADC扫描模式+DMA

1) DMA多通道单次触发

 1  ADC开启两个通道

2 开启2个通道序列

3 使能ADC_DMA的信号

 4 配置DMA

5 启动部分

6 主函数

 2) DMA连续转换+扫描模式

 1 更改ADC触发模式

 2 循环模式

3 加入触发

4 删除函数

5 主函数

6 转换结果


一 理论知识

1 DMA描述 

DMA就是一个数据搬运工,它不需要CPU的干预就能自动帮你搬运数据,也就不会占用你的时间,如果有很多数据要搬运,DMA就能直接帮你般,但是不用DMA就会CPU自己搬就好有很长的阻塞时间。

stm32有两个DMAstm32有两个DMA,每个外设都有单独的DMA通道,但是一个DMA下所有通道又是同用一个传输通道,所以并不能同时传输也是分先后的。但是也得分芯片型号,并不是所有型号都有两个DMA通道的好吧。

2 存储器映像

 存储器的映射这里映射了32位的地址,总共是4G的地址,STM32内存一般才几十K,几百K,所以肯定能存进去,但是这也造成了大部分的地址都没存东西,这里看个大概位置就行。

3 DMA框图

这个其实算是整体的时钟图,但是也能很好的看出DMA的运行,下面分开讲

 1 )首先是两个DMA

 这里明确说明DMA有两个并且,DMA1有7通道,2有5个通道,然后还有个仲裁的存在,就是上面说过的,其实一个DMA共用一个传输通道,所以需要仲裁他们到底谁传。

2 )主动单元与被动单元

 这里将上面这部分分别划分为了1234  其中1 2 3为主动单元,享有访问权和控制权4为被动单元,只能被访问。(首先是1 Dcode这个设备是专门用来访问Flash的  2为系统总线访问各个外设  3为DMA了,因为DMA要在各个存储器之间搬运数据,所以他也享有主动权。

3)DMA挂在在哪里

既然是个外设,那应该就有相应的寄存器,可以让处理器来操作他,可以看出来两个DMA都是直接挂在在了AHB总线上的,并且还有个以太网外设(因为以太网外设的DMA是单独的,所以并不算在我们这次要讲的DMA里面,但是他有自己的一条DMA通道。

 4)DMA请求

DMA的开始指令有软件控制和硬件控制两种,其中如果只是存储器搬运到存储器那么可以软件控制,但是如果是存储器和外设,那么我们就要判断外设的数据是否处理完成,外设的处理完成之后我们才能去搬移他的数据。

 4 DMA逻辑图

(江科大的PPT) 

1 传输方向

可以从图上看出有三种模式

1) 外设寄存器到FLASH和SRAM(存储器

2) 存储器到外设寄存器

3 )存储器到存储器  

2 外设寄存器可配置的参数

外设寄存器和存储器寄存器都有三个可以配置的模式:1 起始地址  2数据宽度(这个可以配置3个,8位(字节)  16位(半字) 32位(字)  3地址是否自增(就传输完成一个,地址+1 就和指针是差不多的

 3 传输计数器和重载器

传输计数器:每传输一次计数器就会减1   自动重载,归0后是否重载计数器

4 选择器

M2M给1:软件触发   M2M给0:硬件触发

 可以选择软件触发或者硬件触发:但是这个软件触发就是连续触发,一直触发将计数器剪到0,所以这个东西不能和自动重载一起开,不然会一直减,和硬件不一样,硬件是触发一次减一次。

5 DMA请求映像

 这是DMA1的内部映像

 1 是通道1的EN位 这个位控制了通道1的开关

 2 是M2M位  如果M2M位1则为软件触发

在图中还有触发位的选择项。可以看看这个图,这个图看着比上面的舒服一点。

 6 数据传输

 这里是前面说的数据传输:说了有8 16 32位

如果源端数据宽度为8 目标数据宽度为8 传输数目为4  写B0到目标  收到的就为B0

如果是8位到16位  收到的就会是00B0.16到8就会只有B0(很明显,接收到的数据是以接收方的宽度来决定的)

二 代码部分

1 存储器传到存储器

1)存储地址测试

首先看一下前面说的存储器地址(首先你要知道一般的数据也就是变量会存在运行内存中,如果是不需要改变变量(静态常量)和函数及代码会存在程序存储器FLASH里面我们可以测试一下先,虽然后面我们是直接取地址,不需要知道数据的地址,但是可以先了解一下。

 

 测试一下啊,定一个变量和常量测试一下地址

代码如下

const int num1=1;

int main(void)
{
	int num=1;

	Usart_Config();
	TIM6_Init();
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);		
	
    while(1)
	{		
		   if(time==1000)
			 {
				  printf("num=%p num1=%p\r\n",&num,&num1);
				  time=0;
			 }
	}	
}

定义了一个常量num1 和一个变量num,我们打印他们的地址,可以看出num是在2000开头的RAM区,常量则是在0800开头的FLASH,和刚刚给出的图一致,一般flash都会比ram大很多倍,所以不需要改变的数据可以存到flash去,节省ram'的空间

 2)DMA传输框图

 通过DMA将一个数据传输到另外一个地方,每次地址给他配置为自加,传输一位DMA的计数器就会自减一次,所以可以根据自己需要传输的数据大致算一下位数。

3)DMA单次传输代码

首先先把初始化代码放在这

void MyDMA_Init(uint16_t size,uint32_t MAddr,uint32_t PAddr)
{
	 DMA_InitTypeDef  DMA_InitStruct;
	
	 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	DMA_InitStruct.DMA_BufferSize=size;
	DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralSRC;
	DMA_InitStruct.DMA_M2M=DMA_M2M_Enable;
	DMA_InitStruct.DMA_Mode=DMA_Mode_Normal;
	DMA_InitStruct.DMA_Priority=DMA_Priority_High;
	DMA_InitStruct.DMA_MemoryBaseAddr=MAddr;
	DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;
	DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralBaseAddr=PAddr;
	DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte;
	DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Enable;

	DMA_Init(DMA1_Channel1,&DMA_InitStruct); 
	
	DMA_Cmd(DMA1_Channel1,ENABLE);
}

1 首先还是熟悉的写一个初始化,首先还是声明一个结构体变量,声明到第一行

然后开启DMA1的时钟,前面说过DMA是挂载在AHB总线上的,1为互联型芯片的参数 2为其余的芯片  我们用的不是互联型的,所以这里去沾2 里面的DMA1

 之后是往刚刚声明的结构体对象里面写参数

 这里看着参数很多,其实可以对照前面说的流程,

DMA_BufferSize

配置传输计数器的大小BUFFerSize 说了是传一次减一个,所以这里你传几个数据你就写几,这里我们将参数作为了形参传进来,后期好调用。

 3 DMA_DIR

配置方向DIR 前面说过需要配置是存储器到存储器还是存储器到外设,或者外设到存储器,这个参数的意思是,指定外设地址是要传输的源或者目标,这里有两个参数可以选,一个是是后缀SRC(soure源头)一个是DST(destination目的地),我这里将方向配置为了存储器到外设

 3 DMA_M2M

这个说过是配置我们是软件触发或者硬件触发 这里选择enable就是软件触发了,前面说过选1就是软件触发嘛,这里看一看一下位,很明显enble是填入的1

 4 DMA_Mode

模式,其实是配置前面所说的,重载器的模式,我们可以配置为重载或者不重载

因为是软件触发所以我们不能开重载(前面说了,这个东西和软件触发效果冲突,一个想尽快减完一个又想减完让你重新减)我们这里就选第二个正常模式了

 5 DMA_Priority

 优先级 :前面说过DMA有仲裁器,会判断优先级的,这里就是就算通道号在后面,你也可以给他高优先级

 6 DMA_MemoryBaseAddr&DMA_PeripheralBaseAddr

存储器和外设地址:这里写形参的名字,方便等会传参进来

 7 DMA_MemoryDataSize&DMA_PeripheralDataSize

这个是刚刚说的,数据传输的那点了,配置双方的字节大小,可以配置为8位16位32位,我配置为了8位

 8 DMA_PeripheralInc&DMA_MemoryInc

是否自增,就是传输完一个数据后,地址会不会自己+1

这里我们配置为开启

9 初始化结构体和使能DMA 

将刚刚配置的参数,通过Init函数写入到DMA的配置中,之后开启cmd开启MDA传输数据,,这里随便选DMA通道都行,因为是软件触发,所以每个通道都能支持。

 10 主函数

uint8_t Data1[]={0x01,0x02,0x03,0x14};
uint8_t Data2[]={0,0,0,0};

int main(void)
{
	Usart_Config();
	
	MyDMA_Init(4,(uint32_t)Data2,(uint32_t)Data1);
	
	printf("0x%02x 0x%02x 0x%02x 0x%02x\r\n",Data1[0],Data1[1],Data1[2],Data1[3]);
	printf("0x%02x 0x%02x 0x%02x 0x%02x\r\n",Data2[0],Data2[1],Data2[2],Data2[3]);
    while(1)
	{		
	}	
}

声明两个数组,Data1里面有数据,2里面没有数据

调用MyDMA_Init()里面写入参数,第一个为传输个数,第二个是存储器,第三个是外设,因为我们前面配置的为存储器到外设。

调用之后我们打印一下数组内容。

结果(很明显看出,即使这里是吧data1的数据搬移到了data2但是1里面数据依然在,所以说是数据搬移,其实是数据复制了

4)DMA循环转移代码

首先我们上面说过,软件触发是不能配合重载的,所以转移一次之后就会停止,所以下一次就算数据变动了,他也不会管,这里我们就写个函数,手动来对他进行使能和计数器赋值。

代码如下

1 添加函数

void MyMDA_Transfer(void)
{
		/*重置计数值*/
	  DMA_Cmd(DMA1_Channel1,DISABLE);
	  DMA_SetCurrDataCounter(DMA1_Channel1,MyMDA_Size);
	  DMA_Cmd(DMA1_Channel1,ENABLE);
	  /*等待传输完成*/
	   while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
			DMA_ClearFlag(DMA1_FLAG_TC1);
}

2 改Init函数

 

 添加全局变量MyMDA_size 方便将传入的形参保存,后面好重新给计数器赋值

之后失能DMA只进行初始化,后面我们手动使能

函数分析

 重置计数器部分,这里前面说过,DMA在开启的时候是不能更改计数器的值的,所以我们先失能之后更改完值重新使能

 等待传输完成部分:等待传输完成的标志位为1退出循环

这里一般看有没有标志位主要是看参考手册:这里说了标志位名字和是否需要手动清除,他说在ifcr的相应标志写1就能清楚

 然后看了下中文参考手册,应该是翻译错了,因为的里面是1为清除

 

 之后我们写主函数

volatile uint32_t time = 0; // ms 计时变量 

uint8_t Data1[]={0x01,0x02,0x03,0x14};
uint8_t Data2[]={0,0,0,0};

int main(void)
{
	Usart_Config();
	TIM6_Init();
	MyDMA_Init(4,(uint32_t)Data2,(uint32_t)Data1);
	MyMDA_Transfer();

  while(1)
	{		

		if(time==1000)
		{
			 	Data1[0]++;
				Data1[1]++;
				Data1[2]++;
				Data1[3]++;
				MyMDA_Transfer();
				printf("0x%02x 0x%02x 0x%02x 0x%02x\r\n",Data1[0],Data1[1],Data1[2],Data1[3]);
				printf("0x%02x 0x%02x 0x%02x 0x%02x\r\n",Data2[0],Data2[1],Data2[2],Data2[3]);
				time=0;
		}
	}	
}

这里用了一个基本定时器以前写过,不知道的朋友们可以先去看一下。

stm32f103基本定时器的使用_stm32f103定时器时钟_是小刘不是刘的博客-CSDN博客

逻辑就是将数组1的值每秒+1,然后给数组2赋值,这样循环改变,结束之后重置定时器

效果如下:从这也可以看出来,处理这个数据是花了多少时间,1000进入定时器,这段时间只做了time里面的东西,所以处理一次大致是0.003秒的样子,结果也是符合的。

 

 2 ADC扫描模式+DMA

这个是之前写过的一个ADC采集的后续了stm32f103 ADC采集_是小刘不是刘的博客-CSDN博客

因为之前说了扫描模式下循环采集需要ADC来实现,所以吧ADC的多通道扫描采集写到了这里来了,所以没有看ADC的朋友可以先去看看ADC是怎么使用的。

首先还是看一下图

 ADC触发之后,信号给DMA,这时候DMA会帮助ADC进行数据转移,也就不会像之前说的那样,不管转换多少个通道的数据都会只保存最后一个转换数据了

1) DMA多通道单次触发

OK打开我们之前写好的ADC的代码,将我们写的DMA的初始化加进去

整体代码如下,后面开始解释

//adc.c文件
uint16_t addr[2]={0};
static void AD_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	ADC_InitTypeDef  ADC_InitStruct;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);

	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AIN;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1 | GPIO_Pin_2;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_10MHz;
	GPIO_Init(GPIOC,&GPIO_InitStruct);

	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	ADC_RegularChannelConfig(ADC1,ADC_Channel_11,1,ADC_SampleTime_1Cycles5);
	ADC_RegularChannelConfig(ADC1,ADC_Channel_12,2,ADC_SampleTime_1Cycles5);
	
	ADC_InitStruct.ADC_ContinuousConvMode=DISABLE;
	ADC_InitStruct.ADC_DataAlign=ADC_DataAlign_Right;
	ADC_InitStruct.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;
	ADC_InitStruct.ADC_Mode=ADC_Mode_Independent;
	ADC_InitStruct.ADC_NbrOfChannel=2;
	ADC_InitStruct.ADC_ScanConvMode=ENABLE;
	ADC_Init(ADC1,&ADC_InitStruct);

	ADC_DMACmd(ADC1,ENABLE);
	ADC_Cmd(ADC1,ENABLE);
	
	//校验
	ADC_ResetCalibration(ADC1);
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while(ADC_GetCalibrationStatus(ADC1) == SET);
}


static void MyDMA_Init(void)
{
	DMA_InitTypeDef  DMA_InitStruct;
 
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
		DMA_InitStruct.DMA_BufferSize=2;
	DMA_InitStruct.DMA_DIR=DMA_DIR_PeripheralSRC;
	DMA_InitStruct.DMA_M2M=DMA_M2M_Disable;
	DMA_InitStruct.DMA_Mode=DMA_Mode_Normal;
	DMA_InitStruct.DMA_Priority=DMA_Priority_High;
	DMA_InitStruct.DMA_MemoryBaseAddr=(uint32_t)addr;
	DMA_InitStruct.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;
	DMA_InitStruct.DMA_MemoryInc=DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;
	DMA_InitStruct.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;
	DMA_InitStruct.DMA_PeripheralInc=DMA_PeripheralInc_Disable;
	DMA_Init(DMA1_Channel1,&DMA_InitStruct); 
		DMA_Cmd(DMA1_Channel1,ENABLE);
}

void MyAD_Init(void)
{
	  AD_Init();
	  MyDMA_Init();
}

void AD_GetValue(void)
{
	
		DMA_Cmd(DMA1_Channel1,DISABLE);
	  DMA_SetCurrDataCounter(DMA1_Channel1,2);
	  DMA_Cmd(DMA1_Channel1,ENABLE);
	
	  ADC_SoftwareStartConvCmd(ADC1,ENABLE);

		 /*等待传输完成*/
	   while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
		 DMA_ClearFlag(DMA1_FLAG_TC1);
}
//adc.h文件
#ifndef __ADC_H
#define	__ADC_H

#include "stm32f10x.h"

extern uint16_t addr[2];
void AD_GetValue(void);
void MyAD_Init(void);

#endif /*__ADC_H*/

这里首先是AD的配置

 1  ADC开启两个通道

(和ADC章节我开的两个通道的引脚是一样的是一样的) 我使用的为PC1 PC2 所以通道是11和12,

2 开启2个通道序列

(就是前面说的序列)之后我们把扫描模式使能。 

3 使能ADC_DMA的信号

 4 配置DMA

 1 更改计数器的值

2 关闭软件触发

3 填写存储器的地址,我们这里定义了一个数组,直接将数组名填上就行,然后强转为32位,

 4 改为接受半字 16位,因为AD转换的数据是12位存在了16位的寄存器里面,所以我们这里也只接受16位就够了

5 填写外设地址 这里ADC1为一个结构体指针,所以还是需要取地址的

6 改为半字(转换出来就是16的寄存器在存

7 关闭DMA的地址自增,我们只需要从转换的地方一直获取数据就行,不需要他自增地址

5 启动部分

1  首先DMA的触发,因为这里我们还是单次触发,所以还是需要停一下触发之后重置计数值,然后启动

2  启动

3 等待DMA传输完成,因为DMA传输完成肯定在转换完成之后,所以只用判断DMA是否转换完成即可

6 主函数
int main(void)
{
	Usart_Config();
  MyAD_Init();
	
	printf("addr[0]=%d",addr[0]);
  while(1)
	{	
		  AD_GetValue();
		  printf("%d %d\r\n",addr[0],addr[1]);
		  Delay(5000000);
	}	
}

这就是很正常的测试是否有值了,大家可以自己发挥

结果(扭动可变电阻正常变化,第二个引脚直接测的3.3 所以基本是满量程

 2) DMA连续转换+扫描模式

很简单,对比刚刚的单次采集更改如下

 1 更改ADC触发模式

将ADC的单次触发改为连续触发

 2 循环模式

这里开启循环模式,等于计数器减到0后,会自动帮你重载计数器

3 加入触发

在DMA最后加上触发即可

4 删除函数

删除我们单次触发写的AD_GetValue()即可,这里只需要触发一次就可,不需要我们来判断是否转换完成,他会循环刷新,轮不到我们判断的。

5 主函数

我们只需要做个初始化即可,这样转换值就已经在自动刷新和转运了。

6 转换结果

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值