DMA主要功能是传输数据,但是不需要占用CPU,即在传输数据时,CPU可以做别的事,像多线程。数据传输从外设到存储器或者从存储器到存储器。DMA控制器包含了DMA1和DMA2,其中DMA1有7个通道,DMA2有5个通道,可以理解为传输数据的一种管道。要注意的是,DMA2只存在于大容量单片机中。
一、DMA框图解析
DMA控制器独立于内核,属于一个单独外设,结构结合下图来看
1.DMA请求
如果外设想通过DMA传输数据,必须先向DMA控制器发送DMA请求,DMA收到请求信号后,控制器会给外设一个应答信号,当外设应答且DMA控制器收到应答信号后,就会启动DMA传输,直到传输完毕。
DMA有DMA1和DMA2两个控制器,DMA1有两个控制器,DMA1有7个通道,DMA2有5个通道,不同DMA控制器的通道有不同的外设请求。
2、通道
DMA有12个独立可编程的通道,DMA1有7个通道,DMA2有5个通道,每个通道对应不同外设的DMA请求。虽然每个通道可以接收多个外设请求,但是同一时间只能接收一个,不能同时接收多个。
3、仲裁器
当同时有多个DMA请求时,就意味着有先后响应的问题,这个就由仲裁器管理。仲裁器管理DMA请求分为2个阶段:第一阶段属于软件阶段,可以在MDA_CCRx寄存器中设置,有 4 个等级:非常高、高、中和低四个优先级。第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越低优先权越高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,DMA1 控制器拥有高于 DMA2 控制器的优先级。
二、DMA数据配置
使用DMA,最核心的就是配置要传输的数据。
1、从哪儿来,到哪儿去
DMA传输数据 的方向有3个:外设到存储器,存储器到外设,存储器到存储器。具体方向由DMA_CCR中第四位DIR配置:0表示外设到存储器,1表示存储器到外设。涉及的地址由DMA_CPAR配置,存储器地址由DMA_CMAR配置。
1)从外设到存储器
以ADC采集为例,DMA外部寄存器地址对应ADC数据寄存器地址,DMA存储器地址是我们自定义的变量的地址。方向设置为源地址。
2)存储器到外设
存储器到外设传输以串口向电脑端发送为例,DMA 外设寄存器的地址对应的就是串口数据寄存器的地址,DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址。
3)存储器到存储器
当我们使用从存储器到存储器传输时,以内部 FLASH 向内部 SRAM 复制数据为例。
DMA外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH 当作一个外设来看)的地址,DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。方向我们设置外设(即内部 FLASH)为源地址。跟上面两个不一样的是,这里需要把 DMA_CCR 位 14:MEM2MEM:存储器到存储器模式配置为 1,启动 M2M 模式。
2、要传什么,单位是多少
以串口向电脑发送数据为例,我们可以一次性给电脑发送很多数据,具体多少由DMA_CNDTR 配置,这是一个 32位的寄存器,一次最多只能传输 65535 个数据。
要想数据传输正确,源和目标地址存储的数据宽度还必须一致,串口数据寄存器是 8位的,所以我们定义的要发送的数据也必须是 8 位。外设的数据宽度由 DMA_CCR 的PSIZE[1:0]配置,可以是 8/16/32位,存储器的数据宽度由 DMA_CCR 的 MSIZE[1:0]配置,可以是 8/16/32 位。
在 DMA 控制器的控制下,数据要想有条不紊的从一个地方搬到另外一个地方,还必须正确设置两边数据指针的增量模式。外设的地址指针由 DMA_CCRx 的 PINC 配置,存储器的地址指针由 MINC 配置。以串口向电脑发送数据为例,要发送的数据很多,每发送完一个,那么存储器的地址指针就应该加 1,而串口数据寄存器只有一个,那么外设的地址指针就固定不变。具体的数据指针的增量模式由实际情况决定。
3、什么时候传输完成
数据什么时候传输完成,我们可以通过查询标志位或者通过中断的方式来鉴别。每个DMA 通道在 DMA 传输过半、传输完成和传输错误时都会有相应的标志位,如果使能了该类型的中断后,则会产生中断。有关各个标志位的详细描述请参考 DMA 中断状态寄存器DMA_ISR的详细描述。
传输完成还分两种模式,是一次传输还是循环传输,一次传输很好理解,即是传输一次之后就停止,要想再传输的话,必须关断 DMA 使能后再重新配置后才能继续传输。循环传输则是一次传输完成之后又恢复第一次传输时的配置循环传输,不断的重复。具体的由 DMA_CCR寄存器的 CIRC 循环模式位控制。
三、DMA初始化结构体
结构体 xxx_InitTypeDef 定义在stm32f10x_xxx.h(后面xxx为外设名称)文件中,库函数xxx_Init定义在stm32f10x_xxx.c文件中。
DMA_ InitTypeDef 初始化结构体
1 typedef struct
2 {
3 uint32_t DMA_PeripheralBaseAddr; // 外设地址
4 uint32_t DMA_MemoryBaseAddr; // 存储器地址
5 uint32_t DMA_DIR; // 传输方向
6 uint32_t DMA_BufferSize; // 传输数目
7 uint32_t DMA_PeripheralInc; // 外设地址增量模式
8 uint32_t DMA_MemoryInc; // 存储器地址增量模式
9 uint32_t DMA_PeripheralDataSize; // 外设数据宽度
10 uint32_t DMA_MemoryDataSize; // 存储器数据宽度
11 uint32_t DMA_Mode; // 模式选择
12 uint32_t DMA_Priority; // 通道优先级
13 uint32_t DMA_M2M; // 存储器到存储器模式
14 } DMA_InitTypeDef;
1) DMA_PeripheralBaseAddr:外设地址,设定 DMA_CPAR 寄存器的值;一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储器地址。
2) DMA_Memory0BaseAddr:存储器地址,设定 DMA_CMAR 寄存器值;一般设置为我们自定义存储区的首地址。
3) DMA_DIR:传输方向选择,可选外设到存储器、存储器到外设。它设定DMA_CCR 寄存器的 DIR[1:0]位的值。这里并没有存储器到存储器的方向选择,当使用存储器到存储器时,只需要把其中一个存储器当作外设使用即可。
4) DMA_BufferSize:设定待传输数据数目,初始化设定 DMA_CNDTR 寄存器的值。
5) DMA_PeripheralInc:如果配置为 DMA_PeripheralInc_Enable,使能外设地址自动递增功能,它设定 DMA_CCR 寄存器的 PINC 位的值;一般外设都是只有一个数据寄存器,所以一般不会使能该位。
6) DMA_MemoryInc:如果配置为DMA_MemoryInc_Enable,使能存储器地址自动递增功能,它设定 DMA_CCR 寄存器的 MINC 位的值;我们自定义的存储区一般都是存放多个数据的,所以要使能存储器地址自动递增功能。
7) DMA_PeripheralDataSize:外设数据宽度,可选字节(8位)、半字(16位)和字(32位),它设定 DMA_CCR寄存器的 PSIZE[1:0]位的值。
8) DMA_MemoryDataSize:存储器数据宽度,可选字节(8 位)、半字(16 位)和字(32位),它设定 DMA_CCR 寄存器的 MSIZE[1:0]位的值。当外设和存储器之间传数据时,两边的数据宽度应该设置为一致大小。
9) DMA_Mode:DMA 传输模式选择,可选一次传输或者循环传输,它设定DMA_CCR 寄存器的 CIRC 位的值。例程我们的 ADC 采集是持续循环进行的,所以使用循环传输模式。
10) DMA_Priority:软件设置通道的优先级,有 4 个可选优先级分别为非常高、高、中和低,它设定 DMA_CCR 寄存器的 PL[1:0]位的值。DMA 通道优先级只有在多个 DMA 通道同时使用时才有意义,如果是单个通道,优先级可以随便设置。
11) DMA_M2M:存储器到存储器模式,使用存储器到存储器时用到,设定DMA_CCR 的位 14MEN2MEN 即可启动存储器到存储器模式。
四、存储器到存储器的实验
先定义一个静态的源数据,存放在内部Flash存储器中,使用DMA传输,把源数据拷贝到目标地址上(内部SRAM),最后对比源数据和目标地址的数据,看看是否准确传输。
1、思路要点
1)使能DMA时钟
2)配置DMA数据参数
3)使能DMA,进行传输
4)等待传输完成,并对源数据和目标地址数据进行比较。
2、DMA宏定义以及变量定义
1 // 当使用存储器到存储器模式时候,通道可以随便选,没有硬性的规定
2 #define DMA_CHANNEL DMA1_Channel6
3 #define DMA_CLOCK RCC_AHBPeriph_DMA1
4
5 // 传输完成标志
6 #define DMA_FLAG_TC DMA1_FLAG_TC6
7
8 // 要发送的数据大小
9 #define BUFFER_SIZE 32
10
11 /* 定义 aSRC_Const_Buffer 数组作为 DMA 传输数据源
12 * const 关键字将 aSRC_Const_Buffer 数组变量定义为常量类型
13 * 表示数据存储在内部的 FLASH 中
14 */
15 const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]=
16 {
17 0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
18 0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
19 0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
20 0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
21 0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
22 0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
23 0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
24 0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80
25 };
26 /* 定义 DMA 传输目标存储器
27 * 存储在内部的 SRAM 中
28 */
29 uint32_t aDST_Buffer[BUFFER_SIZE];
aSRC_Const_Buffer[BUFFER_SIZE]定义用来存放源数据,并且使用了const关键字修饰,即常量类型,使得变量存储在内部Flash空间上。
3、DMA数据配置
void DMA_Config(void)
2 {
3 DMA_InitTypeDef DMA_InitStructure;
4
5 // 开启 DMA 时钟
6 RCC_AHBPeriphClockCmd(DMA_CLOCK, ENABLE);
7 // 源数据地址
8 DMA_InitStructure.DMA_PeripheralBaseAddr =
9 (uint32_t)aSRC_Const_Buffer;
10 // 目标地址
11 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)aDST_Buffer;
12 // 方向:外设到存储器(这里的外设是内部的 FLASH)
13 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
14 // 传输大小
15 DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
16 // 外设(内部的 FLASH)地址递增
17 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
18 // 内存地址递增
19 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
20 // 外设数据单位
21 DMA_InitStructure.DMA_PeripheralDataSize =
22 DMA_PeripheralDataSize_Word;
23 // 内存数据单位
24 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
25 // DMA 模式,一次或者循环模式
26 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal ;
27 //DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
28 // 优先级:高
29 DMA_InitStructure.DMA_Priority = DMA_Priority_High;
30 // 使能内存到内存的传输
31 DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
32 // 配置 DMA 通道
33 DMA_Init(DMA_CHANNEL, &DMA_InitStructure);
34 // 使能 DMA
35 DMA_Cmd(DMA_CHANNEL,ENABLE);
36 }
使用 DMA_InitTypeDef 结构体定义一个 DMA 初始化变量,这个结构体内容我们之前已经有详细讲解。
调用 RCC_AHBPeriphClockCmd 函数开启 DMA时钟,使用 DMA控制器之前必须开启对应的时钟。
源地址和目标地址使用之前定义的数组首地址,传输的数据量为宏 BUFFER_SIZE 决定,源和目标地址指针地址递增,使用一次传输模式不能循环传输,因为只有一个 DMA通道,优先级随便设置,最后调用 DMA_Init 函数完成 DMA 的初始化配置。
DMA_ClearFlag函数用于清除DMA标志位,代码用到传输完成标志位,使用之前先清除传输完成标志位以免产生不必要干扰。DMA_ClearFlag 函数需要 1 个形参,即事件标志位,可选有传输完成标志位、半传输标志位、FIFO 错误标志位、传输错误标志位等等,非常多,我们这里选择传输完成标志位,由宏 DMA_FLAG_TC 定义。
DMA_Cmd 函数用于启动或者停止 DMA 数据传输,它接收两个参数,第一个是 DMA通道,另外一个是开启 ENABLE 或者停止 DISABLE。
4、存储器数据对比
1 uint8_t Buffercmp(const uint32_t* pBuffer,
2 uint32_t* pBuffer1, uint16_t BufferLength)
3 {
4 /* 数据长度递减 */
5 while (BufferLength--) {
6 /* 判断两个数据源是否对应相等 */
7 if (*pBuffer != *pBuffer1) {
8 /* 对应数据源不相等马上退出函数,并返回 0 */
9 return 0;
10 }
11 /* 递增两个数据源的地址指针 */
12 pBuffer++;
13 pBuffer1++;
14 }
15 /* 完成判断并且对应数据相对 */
16 return 1;
17 }
判断指定长度的两个数据源是否完全相等,如果完全相等返回 1;只要其中一对数据不相等返回 0。它需要三个形参,前两个是两个数据源的地址,第三个是要比较数据长度。
5、main函数
1 int main(void)
2 {
3 /* 定义存放比较结果变量 */
4 uint8_t TransferStatus;
5
6 /* LED 端口初始化 */
7 LED_GPIO_Config();
8
9 /* 设置 RGB 彩色灯为紫色 */
10 LED_PURPLE;
11
12 /* 简单延时函数 */
13 Delay(0xFFFFFF);
14
15 /* DMA 传输配置 */
16 DMA_Config();
17
18 /* 等待 DMA 传输完成 */
19 while (DMA_GetFlagStatus(DMA_FLAG_TC)==RESET)
20 {
21
22 }
23
24 /* 比较源数据与传输后数据 */
25 TransferStatus=Buffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE);
26
27 /* 判断源数据与传输后数据比较结果*/
28 if (TransferStatus==0)
29 {
30 /* 源数据与传输后数据不相等时 RGB 彩色灯显示红色 */
31 LED_RED;
32 }
33 else
34 {
35 /* 源数据与传输后数据相等时 RGB 彩色灯显示蓝色 */
36 LED_BLUE;
37 }
38
39 while (1)
40 {
41 }
42 }
首先定义一个变量用来保存存储器数据比较结果。
RGB 彩色灯用来指示程序进程,使用之前需要初始化它,LED_GPIO_Config 定义在bsp_led.c 文件中。开始设置 RGB 彩色灯为紫色,LED_PURPLE 是定义在 bsp_led.h 文件的一个宏定义。
Delay函数只是一个简单的延时函数。
调用 DMA_Config 函数完成 DMA 数据流配置并启动 DMA 数据传输。
DMA_GetFlagStatus 函数获取 DMA 事件标志位的当前状态,这里获取 DMA 数据传输完成这个标志位,使用循环持续等待直到该标志位被置位,即 DMA 传输完成这个事件发生,然后退出循环,运行之后程序。
确定 DMA 传输完成之后就可以调用 Buffercmp 函数比较源数据与 DMA 传输后目标地址的数据是否一一对应。TransferStatus 保存比较结果,如果为 1 表示两个数据源一一对应相等说明 DMA 传输成功;相反,如果为 0 表示两个数据源数据存在不等情况,说明 DMA传输出错。
如果 DMA传输成功设置 RGB彩色灯为蓝色,如果 DMA传输出错设置 RGB彩色灯为红色。
来源:https://blog.csdn.net/zxh1592000/article/details/78678132