DMA,全称是Direct Memory Access(直接内存访问)。可以在存储器和存储器之间或者外设和存储器之间传输数据,而不需要CPU的干预,这样可以节省CPU的资源,提高工作效率。
1.功能框图
STM32F103RCT6有两个DMA控制器,需要DMA传输的时候,外设会向DMA发送请求,DMA也会进行应答,随后会进行DMA传输。
2.DMA通道
两个DMA控制器一共有12个通道(DMA1有7个,DMA2有5个),上面图中列出了每个外设的请求与DMA通道的对应关系。当有多个DMA通道请求时,由于每次只能响应一个DMA通道请求,所以DMA仲裁器会根据软件优先级以及硬件优先级来确定先响应哪个通道。
3.优先级
1° 软件优先级可以在DMA_CCRx寄存器中设置,一共有最高优先级、高优先级、中等优先级、低优先级这4个等级。
2° 硬件优先级由通道号决定,通道号小的优先级高于通道号大的,比如通道2优先级高于通道4.
DMA仲裁器会优先对比软件优先级,软件优先级相同时再对比硬件优先级。
4.DMA传输模式
DMA传输的数据量可以通过DMA_CNDTRx寄存器最大编程为65535,每次传输后会递减,在传输了一半的数据和数据传输完成后都会有相应的标志位标识,并且可以使能相应的中断。
1° 当通道配置为非循环模式时,完成数据传输后将不再产生DMA操作,如果要开始新的DMA传输,需要在关闭DMA通道的情况下,重新配置要传输的数据个数(重新配置DMA_CNDTRx寄存器);
2° 在循环模式下,最后一次传输结束时,DMA_CNDTRx寄存器会被自动重载为初始数值。
3° 存储器到存储器模式。DMA的传输方向可以是存储器到外设、外设到存储器,也可以是存储器到存储器。这种情况下需要借用外设端口,当使用外设通道代表存储器时,通道可以随意选择,注意不能与循环模式同时使用。
5.软件编程
1° 在内存中开辟两个数组空间 mData_Buffer and pData_Buffer
2° 向 mData_Buffer 中存入数据
3° 使用 DMA 将 mData_Buffer 中数据移动到 pData_Buffer 中
/* 全局变量 */
char mData_Buffer[7] = "eckard";
char pData_Buffer[7] = "before";
/* 在while循环之前添加 */
//开启 DMA1 时钟, 使用外设之前一定要开启时钟
RCC->AHBENR |= (uint16_t)0x01 << 0;
//清除 CCR 寄存器
DMA1_Channel1->CCR &= (uint16_t)0xffff8000;
//数据从存储器读, 因为我们这里是要验证存储器到存储器模式
DMA1_Channel1->CCR |= (uint16_t)0x01 << 4;
//不执行循环操作, 注意存储器到存储器模式不能与循环模式同时使用
DMA1_Channel1->CCR |= (uint16_t)0x00 << 5;
//外设地址增量操作,地址递增的意思是传输完当前地址中的数据后,地址变为下一个要传输的数据的地址
DMA1_Channel1->CCR |= (uint16_t)0x01 << 6;
//存储器地址增量操作
DMA1_Channel1->CCR |= (uint16_t)0x01 << 7;
//外设数据宽度8位
DMA1_Channel1->CCR |= (uint16_t)0x00 << 8;
//存储器数据宽度8位
DMA1_Channel1->CCR |= (uint16_t)0x00 << 10;
//通道优先级低,这里的优先级是软件设置的优先级,硬件优先级由通道号决定的
DMA1_Channel1->CCR |= (uint16_t)0x00 << 12;
//启动存储器到存储器模式
DMA1_Channel1->CCR |= (uint16_t)0x01 << 14;
//数据传输数量,每次传输后会递减,如果在循环模式下,最后一次传输结束时,此寄存器会被自动重载为初始数值,比如7
DMA1_Channel1->CNDTR = (uint16_t)7;
//外设地址,我们上述设置的是从存储器读,所以数据传输的方向是mData_Buffer到pData_Buffer, 此地址为目标地址
DMA1_Channel1->CPAR = (uint32_t)pData_Buffer;
//存储器地址, 此地址是源地址
DMA1_Channel1->CMAR = (uint32_t)mData_Buffer;
printf("transfer start.\r\n");
printf("mData_Buffer = %s\r\n", mData_Buffer);
printf("pData_Buffer = %s\r\n", pData_Buffer);
printf("\r\n");
//开启通道
DMA1_Channel1->CCR |= (uint16_t)0x01 << 0;
//等待传输完成
while(DMA1->ISR & ((uint32_t)0x01 << 1));
/* 清除传输完成标志 */
DMA1->IFCR |= (uint32_t)0x01 << 1;
printf("transfer completed.\r\n");
printf("mData_Buffer = %s\r\n", mData_Buffer);
printf("pData_Buffer = %s\r\n", pData_Buffer);
将程序下载后,打开串口调试助手,可以查看打印的信息。
上述使用的是寄存器编程,下面使用STM32CubeMX来初始化DMA
生成代码后,发现新增了dma.c文件,已经完成了DMA的初始化
在main函数中也有调用
STM32CubeMX只能帮助我们初始化外设的配置,应用程序需要我们自己编写,我们这里需要调用HAL_DMA_Start函数来开启DMA传输
/* 全局变量 */
char mData_Buffer[7] = "eckard";
char pData_Buffer[7] = "before";
/* 在while循环之前添加 */
printf("transfer start.\r\n");
printf("mData_Buffer = %s\r\n", mData_Buffer);
printf("pData_Buffer = %s\r\n", pData_Buffer);
printf("\r\n");
/* 开启DMA传输 */
HAL_DMA_Start (&hdma_memtomem_dma1_channel1, (uint32_t)mData_Buffer, (uint32_t)pData_Buffer, 7);
/* 等待传输完成 */
while(__HAL_DMA_GET_FLAG(&hdma_memtomem_dma1_channel1, __HAL_DMA_GET_TC_FLAG_INDEX(&hdma_memtomem_dma1_channel1)) == RESET);
/* 清除传输完成标志 */
__HAL_DMA_CLEAR_FLAG(&hdma_memtomem_dma1_channel1, __HAL_DMA_GET_TC_FLAG_INDEX(&hdma_memtomem_dma1_channel1));
printf("transfer completed.\r\n");
printf("mData_Buffer = %s\r\n", mData_Buffer);
printf("pData_Buffer = %s\r\n", pData_Buffer);
将程序下载后,打开串口调试助手,可以查看到打印的信息与上述采用寄存器编程时是一样的。
本例程代码可以在HAL库工程模板这一章节的最后,百度网盘链接分享处获取
以上是通过开发板进行实际验证的,下面使用软件仿真,
我们进入调试界面,打开串口窗口,然后点击运行( 前面章节有提到,所以本篇以及后续章节都不再重复提及 )
运行结果如下,与在开发板上验证的结果一致。