从字面意思上看,DMA即为“直接内存读取”的意思,换句话说DMA就是用来传输数据的,它也属于一个外设。只是在传输数据时,无需占用CPU。
DMA请求
某个外设在通过DMA传输数据前,必须先给DMA控制器发送请求,控制器会返回一个应答信号给外设,外设应答后并且DMA控制器收到外设应答信号后,便会启动DMA传输。这个过程类似于TCP的“三次握手”。
DMA有DMA1和DMA2两个控制器,每个控制器都有不同的通道,每个通道对应不同的外设请求。如图12-1为DMA1的通道请求、图12-2为DMA2的通道请求。
图12-1
图12-2
如以上两图所示,DMA1有7个通道,DMA2有5个通道,每个通道都对应着不同的外设请求。既然这样,就很有可能出现多个外设同时请求同一通道的情况。这响应先后顺序该如何处理是好?那么,这就涉及到仲裁器管理了,用仲裁器来处理请求响应先后的问题。需要分两个阶段,第一阶段是在DMA_CCRx寄存器中设置通道优先级,第二阶段则需要判断其通道编号,编号越低优先级越高。还有一点,DMA1优先级要高于DMA2。
DMA传输方向
DMA传输方向有三个:外设到内存,内存到外设,内存到内存。
外设到内存。即从外设读取数据到内存。例如ADC采集数据到内存,ADC寄存器地址为源地址,内存地址为目标地址。
内存到外设。即从内存读取数据到外设。例如串口向电脑发送数据,内存地址为源地址,串口数据寄存器地址为目标地址。此时内存存储了需要发送的变量数据。
内存到内存。以内部flash向内部sram传输数据为例,此时内部flash地址即为源地址,内部sram地址即为目标地址。同时,需要将DMA_CCRx寄存器的MEM2MEM置位。
传输配置
我们需要确定数据每次传输的量,这个参数由DMA_CNDTRx寄存器配置。
再者,还有一个源地址和目标地址数据宽度的参数配置。由DMA_CCRx的PSIZE位和MSIZE位配置。可配置为8位、16位、32位。源地址和目标地址的数据宽度需要一致才可传输。
此外,数据想有序地传输,还需要配置源和目标数据指针的增量模式。由DMA_CCRx寄存器的PINC位和MINC位配置。例如串口向电脑发送数据,内存中的地址指针应该递增的发送数据,而串口外设只有一个,所以外设的地址指针不变,无递增。
传输状态标识
可以通过查询DMA_ISR寄存器的相应位的值来判断传输状态。如果在DMA_CCRx寄存器的相应位使能了相应中断,则会产生中断。
另外,传输完成还分成一次传输完成和循环传输完成。DMA在传输完成后,需要失能DMA后重新配置才能继续传输。具体配置由DMA_CCRx寄存器的CIRC位完成。
DMA_InitTypeDef /** * @brief DMA Init structure definition */ typedef struct { uint32_t DMA_PeripheralBaseAddr; // 外设地址 uint32_t DMA_MemoryBaseAddr; // 内存地址 uint32_t DMA_DIR; // 传输方向 uint32_t DMA_BufferSize; // 传输数量 uint32_t DMA_PeripheralInc; // 外设地址增量模式 uint32_t DMA_MemoryInc; // 内存地址增量模式 uint32_t DMA_PeripheralDataSize; // 外设数据宽度 uint32_t DMA_MemoryDataSize; // 内存数据宽度 uint32_t DMA_Mode; // 模式选择 uint32_t DMA_Priority; // 通道优先级 uint32_t DMA_M2M; // 内存到内存模式 } DMA_InitTypeDef;
以上结构体代码来自库函数,我大概在每个成员后做了简单注释,在进行DMA编程时,需要对结构体成员进行配置。
内存到外设的DMA数据传输实验
既然之前写过串口通讯编程的相关文章,那干脆用串口外设来和内存进行数据传输吧。这里简单讲解一个从内存读取数据到外设的DMA传输实验,并且在实验里用led灯验证DMA传输时不占用CPU。
这里关于USART的配置就不继续赘述,之前的文章有详细的介绍,可移步阅读。直接开始DMA配置。
#define SENDBUFF_SIZE 5000 //传输的数据量 uint8_t SendBuff[SENDBUFF_SIZE]; //内存里等待传输数据的数组 void USART_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 串口外设为目标地址 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)USART1_BASE + 0x04; // 内存为源地址 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SendBuff; // 传输方向,即从内存读取数据到串口外设 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 数据传输量,初始化DMA_CNDTRx寄存器 DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE; // 外设地址不递增 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 内存地址递增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 外设数据宽度 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 内存数据宽度 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 一次循环模式 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 通道优先级 DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 不使用内存到内存模式 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // DMA通道配置 DMA_Init(DMA1_Channel4, &DMA_InitStructure); // 清除传输完成标志位,避免产生不必要干扰 DMA_ClearFlag(DMA1_FLAG_TC4); DMA_Cmd(DMA1_Channel4, ENABLE); }
在main函数里调用USART、DMA、LED等的配置函数,用循环的方式往内存的数组里填充SENDBUFF_SIZE(5000)数量的字符作为等待传输的数据。然后调用库函数USART_DMACmd()向DMA发出USART_DMAReq_Tx请求。同时,可以设置led频闪状态作为在DMA传输时占用CPU的进程,以验证DMA传输不占用CPU。
程序编译完成烧写到开发板后,能看到在DMA传输过程中led同时也在频闪,说明DMA传输过程确实不占用CPU资源,可以边传输边运行其他任务。
PS:有关led闪烁的延时控制,可用普通的软件延时也可用SysTick定时器来完成,也很简单。有关SysTick定时器的应用在之前的文章有过介绍,可移步阅读。
分享一些关于DMA在数据传输方面的资料作为参考
stm32 如何用DMA搬运数据
http://www.makeru.com.cn/live/detail/1484.html?s=45051