第一次接触DMA是在学校学习ARM9裸板程序的时候,想起来都时隔快2年了。现在来看看STM32平台的DMA,一样,在标准外设库的支持下,STM32的DMA编程十分简单,但是既是学习,那还是花点时间看看DMA的相关概念及原理的了解下。
1. DMA简介
DMA是Direct Memory Access的简称,是直接存储器访问的意思。DMA是STM32单片机的外设之一,主要功能是用来搬移数据的。通过DMA搬移数据不需要CPU直接参与控制,也不需要中断处理方式那样保留现场和恢复现场。在传输数据的时候,CPU可以干其他事情。
无使用DMA的数据传输:
使用DMA后的数据传输:
DMA数据传输支持从外设到存储器、存储器到外设、存储器到存储器(这里所讲的存储器可以是SRAM,也可以是FLASH)。DMA控制器包含了DMA1控制器和DMA2控制器,分别由7和5个通道作为数据传输。每个通道专门用来管理来自一个或者多个外设对存储器访问的请求,还有一个仲裁器用于协调各个外设对DMA传输请求的优先权。注意,DMA2只存在于大容量或互联型的STM32单片机中。
2. DMA功能框图
2.1 STM32外设对DMA的请求及通道
请求及通道对应图中的标号1和标号2:STM32外设想要通过DMA来传输数据,需先给DMA控制器发送DMA请求,控制器在收到外设的DMA请求之后会给外设一个应答信号,外设应答且DMA控制器收到外设的应答后,DMA启动传输,直至传输完毕。
为什么需要发出请求,应答和接收应答这几个繁琐的步骤?由图中蓝色框框可以看出,DMA传输和CPU是共用系统总线的,要启动DMA传输的前提是系统总线是空闲的,换句话说是CPU没有占用系统总线,所以启动DMA传输前需要以上几个应答机制,其最底层是DMA控制器和CPU正为系统总线作出协调。DMA1有7个通道,DMA2有5个通道,不同的外设请求要通过对应的DMA通道发给DMA控制器。将不同的外设请求传输至对应的通道,这个是我们在软件编程上设置的。
DMA1开放的通道及对应请求:
DMA2开放的通道及对应请求:
虽然每个通道可以接收多个外设的请求,但是同一时间内只能接收一个。
2.2 仲裁器
仲裁器对应图中的标号3:当DMA控制器的多个通道发生DMA请求时,就需要仲裁器管理响应处理的顺序。仲裁器通过软件和硬件来管理DMA请求:软件指的是我们写的代码,在DMA_CCRx(x指通道号)寄存器中设置,有4个等级,非常高(DMA_Priority_VeryHigh)、高(DMA_Priority_High)、中(DMA_Priority_Medium)和低(DMA_Priority_Low)。硬件则是指若有两个或以上的DMA通道请求设置的优先级一样,则它们的响应顺序取决于通道编号,编号低者优先级高,在有DMA2的STM32中,DMA1控制器拥的响应优先级高于DMA2。
2.3 配置DMA控制器
配置DMA控制器,无非就是下图这几个寄存器:
前面说到,DMA数据传输机制并不需要CPU的参与,但是DMA控制器要正常工作,数据要正确传输,需有三个必要条件:源地址、目的地址和数据大小,对于数据分批传输的情况,数据大小这个条件还包含每次传输的大小及单位。
(1)源地址和目的地址
DMA的传输数据的方向有三个:从外设到存储器、从存储器到外设、从存储器到存储器。DMA_CCR的BIT[4]DIR就是用于配置数据传输方向的:
取值为0表从外设到存储器,取值为1表从存储器到外设。外设地址在DMA_CPAR寄存器配置,存储器地址在DMA_CMAR寄存器配置。
(2)传输数据的大小及单位
以串口向电脑发送数据为例(存储器->外设方向),开发板软件可以一次性给电脑发送大量数据,具体多少在DMA_CNDTR配置:
DMA_CNDTR低16位有效,一次最多只能传输65535个数据。
数据要正确传输,源、目标存储的数据宽度必须一致。串口数据寄存器是8位的,也就是外设数据宽度设置寄存器DMA_CCRx的BIT[9:8]PSIZE取值为0:
存储器的数据宽度设置寄存器DMA_CCRx的BIT[11:10]MSIZE取值也为0:
DMA传输数据,还需要设置源地址上的数据发送指针和目的地址数据存放指针的增量模式。开发板串口向电脑发送数据,假设要发送的数据很多,那么存储器(源地址)上数据发送指针每次发送完毕需要加1,而串口数据寄存器则不需要,因为该寄存器只有一个,数据寄存器上的数据传送到电脑后被清空了(就算不清空,数据直接覆盖也没关系)。外设的地址指针增量模式由DMA_CCRx的PINC配置,存储器的地址指针则由MINC配置。
(3)传输结束
DMA中断状态寄存器DMA_ISR可以设置每个DMA通道传输过半、传输完成和传输错误示产生对应标志,
在DMA_CCRx位1、2、3可以设置发生传输过半、传输完成和传输错误时产生中断:
另外补充一点,位0用于使能DMA传输
传输完成分两种模式:一次传输和循环传输,一次传输指传输一次后就停止,要再传输需要关闭DMA使能后重新配置后才能继续传输。循环传输则是一次传输完成后又恢复第一次传输时的配置循环传输,如此循环。设置位在DMA_CCRx寄存器的CIRC。
3. DMA功能模块描述结构体
标准库的一贯风格,在stm32f10x_dma.h文件中定于可DMA_InitTypeDef初始化结构体,DMA_Init()函数定义在stm32f10x_dma.c中。
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;
(1)DMA_PeripheralBaseAddr:外设地址,若是存储器到存储器模式,此成员设置为其中一个存储器的地址,否则设置为外设的地址。
(2)DMA_MemoryBaseAddr:存储器地址,一般设置为程序中存放数据的容器(数组)的首地址。
(3)DMA_DIR:传输方向,可设置为外设到存储器,存储器到外设。注意这里没有存储器到存储器的选项,当使用存储器到存储器时,只需要把其中一个存储器当做外设使用。
(4)DMA_BufferSize:设定待传输数据数目。(5)DMA_PeripheralInc:外设地址增量模式,若取值为DMA_PeripheralInc_Enable表使能外设地址自动递增功能。一般外设都是只有一个数据寄存器,所以不会使能该位。
(6)DMA_MemoryInc:若配置为DMA_MemoryInc_Enable表使能存储器地址自动递增功能。一般存储器都是我们自定义的,区域内存放多个数据,所以一般使能该位。
(7)DMA_MemoryDataSize:外设数据宽度,可选8位(字节)、16位(半字)、32位(字)
(8)DMA_MemoryDataSize:存储器数据宽度,可选8位(字节)、16位(半字)、32位(字)
(9)DMA_Mode:传输模式选择,一次传输或循环传输
(10)DMA_Priority:通道优先级设置,非常高、高、中、低可选
(11)DMA_M2M:存储器到存储器模式
4. 编程常用函数
4.1 DMA时钟使能
函数原型:RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState)
使用示例:RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
4.2 初始化DMA功能模块描述结构体
函数原型:void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct)
DMAy_Channelx指定哪一个DMA通道,DMA_InitStruct就是前面解析的描述结构体
使用示例:DMA_Init(DMAy_Channel1, &DMA_InitStruct);
4.3 使能外设DMA发送
以启动DMA发送功能为例:
函数原型:void USART_DMACmd(USART_TypeDef* USARTx, uint16_t USART_DMAReq, FunctionalState
NewState)
使用示例:
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
外设的DMA传输需要相应的设置,而存储器是不需要的。存储器到存储器,在DMA_InitTypeDef结构体中有DMA_M2M成员需要开启。
4.4 使能DMA通道,开启DMA传输
函数原型:void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
使用示例:DMA_Cmd(DMAy_Channel1, ENABLE);
使能之后,DMA控制器开始工作,在合适的时机(CPU无占据总线)开始DMA控制下的数据传输。
4.5 查询DMA传输状态
在DMA传输过程中,我们可以通过函数来查询传输通道的状态:
函数原型:FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG)
假设要查询DMA通道4传输是否完成:
DMA_GetFlagStatus(DMA1_FLAG_TC4);
返回值为RESET表示传输尚未完成,SET表传输完成。
获取当前剩余数据量大小的函数:
函数原型:uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
使用示例,获取DMA通道4还有多少数据没有传输:
DMA_GetCurrDataCounter(DMAy_Channel4);
5. 编程实践
5.1 DMA传输–存储器到存储器模式
硬件平台是正点原子MiniSTM32,板载有两个LED分别为红色和绿色。
程序功能实现把STM32内置的FLASH数据拷贝到内置的SRAM中:定义一个const静态变量为源数据,使用DMA传输将源数据拷贝到目标地址中,比对源数据和目标数据是否相同,若相同亮绿色LED灯,反之亮红色LED灯。
DMA的编程核心在于
(1)使能DMA时钟
(2)配置DMA初始化结构体参数
(3)使能DMA,开始进行数据传输
(4)等待数据传输完成
工程结构为:
BSP_LED.c实现配置LED引脚:
#include void LED_Configuration(void) { GPIO_InitTypeDef GPIO_InitTypeStu; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOD, ENABLE); //³õʼ»¯PA8ÍÆÍìÊä³ö GPIO_InitTypeStu.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitTypeStu.GPIO_Pin = GPIO_Pin_8; GPIO_InitTypeStu.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitTypeStu); GREEN_LED_OFF; GPIO_InitTypeStu.GPIO_Pin = GPIO_Pin_2; GPIO_Init(GPIOD, &GPIO_InitTypeStu); RED_LED_OFF; } BSP_USART.c实现配置USART1功能: #include "BSP_USART.h" #pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; _sys_exit(int x) { x = x; } int fputc(int ch, FILE *f) { while((USART1->SR&0X40) == RESET); USART1->DR = (u8) ch; return ch; } void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStu; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStu.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStu.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStu.NVIC_IRQChannelSubPriority = 1; NVIC_InitStu.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStu); } void USART_Configuration(void) { GPIO_InitTypeDef GPIO_InitStu; USART_InitTypeDef USART_InitStu; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); GPIO_InitStu.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStu.GPIO_Pin = GPIO_Pin_9; GPIO_InitStu.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStu); GPIO_InitStu.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_InitStu.GPIO_Pin = GPIO_Pin_10; GPIO_Init(GPIOA, &GPIO_InitStu); USART_InitStu.USART_BaudRate = 115200; USART_InitStu.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_InitStu.USART_Parity = USART_Parity_No; USART_InitStu.USART_StopBits = USART_StopBits_1; USART_InitStu.USART_WordLength = USART_WordLength_8b; USART_InitStu.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_Init(USART1, &USART_InitStu); NVIC_Configuration(); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); } void USART_SendChar(USART_TypeDef* pUSARTx, uint8_t c) { USART_SendData(pUSARTx, c); while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); } void USART_SendString(USART_TypeDef* pUSARTx, char* str) { uint32_t n = 0; while (*(str + n) != '