概述:DMA(存储器直接存取)
一个完整的微控制器(处理器)通常由CPU、存储器和外设等组件构成。这些组件一般在结构和功能上都是独立的,即一个组件能持续正常工作并不一定建立在另一个组件正常工作的前提上,而各个组件之间的协调与交互就由CPU来完成。如此一来,CPU作为整个芯片的“大脑”,其职能范围可谓广阔吗,如CPU先从A外设拿到一个数据送给B外设使用,同时C外设又需要D外设提供一个数据……这样的数据搬运工作使得CPU的负荷显得相当繁重。
严格来说,搬运数据只是CPU众多职能中比较不重要的一种。CPU最重要的工作是进行数据的运算,从加减乘除四种基本运算到一些高级运算,包括浮点、积分、微分、FFT等运算。而在一些嵌入式的实时应用场合中,CPU还负责对复杂的中断申请进行响应,以保证主控芯片的实时性能。
理论上,常见的控制器外设,比如USART、I2C、SPI甚至是USB等通信接口,单纯的利用CPU进行协议模拟也是可以实现的,比如51单片机平台经常使用模拟I/O来实现I2C协议通信。但这样即浪费了CPU的资源,同时实现后的性能表现往往和使用专用的硬件模块实现的效果相差甚远。从这个角度来看,各个外设控制器的存在,无疑是降低了CPU的负担,解放了CPU的资源,使其有更多的自由去做数据运算工作。实践表明,“搬运数据"这一工作占用了相当大一部分的CPU资源,成为降低CPU工作效率的主要原因之一。于是需要有一种硬件结构来分担CPU的这一职能,这种硬件结构就是——DMA。
从数据搬运的效果来看,使用DMA也要比使用CPU来执行显得快速而高效得多。先从CPU搬运数据的过程上来分析,如果要把某个存储地址A的数值赋给另外一个地址上B的变量,CPU是这样处理的:首先读出A地址上的数据存储在某个中间变量里(该变量可能位于CPU寄存器里,也有可能位于内存中),然后再转附送到B地址的变量上。在这个过程里,CPU通过一个中间变量扮演了一种“中介”的角色。而若使用DMA传输,则不再需要通过中间变量,而将A地址的数据直接传送到B地址的变量里。在这个过程里,CPU只需要告诉DMA什么时候开始传送,DMA在完成传送之后回馈一个信号通知CPU,而期间的数据搬运过程完全不需要CPU进行干预。这样无疑是一个双赢的局面:既减轻了CPU的负担,又提高了数据搬运的效率,这就是DMA存在的意义。
STM32配备了相当完善的DMA资源:两个DMA控制器共有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的DMA请求,还有一个仲裁器来协调各个DMA请求的优先权。DMA单元的特性如下:
- 拥有12个独立的可配置的通道:DMA1有7个通道,DMA2有5个通道。
- 每个通道都对应连接专门的硬件DMA请求,每个通道都同样支持软件触发。这些功能可通过软件来配置。
- 在同一个DMA模块上,多个DMA请求间的优先级关系可以通过软件编程设置(共有4级:最高、高、中、低),优先级相等时由DMA通道号决定(请求0优先于请求1,以此类推)。
- 独立数据源和目标数据区的传输宽度可为字节、半字和字。源和目标地址必须按数据传输宽度对齐。
- 支持循环缓冲器管理。
- 每个DMA通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错中断),这三个事件标志通过逻辑“或”关系合并为一个单独的中断请求
- 可实现存储器和存储器间的传输。
- 可实现外设和存储器、存储器和外设之间的传输。
- 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标。
- 数据传输数量最多可达65535。
实验设计
目的是为了实现STM32微控制器的DMA数据搬运功能,并在搬运成功的基础上,考察其相对于使用CPU进行数据搬运的做法在效率上的提升。
硬件电路
因为DMA和Flash都属于STM32的内部设备,所以本节程序只需要一个USART电平转换电路供给显示实验结果即可。
程序设计(软件设计)
注意要点:
- 配置RCC寄存器组,打开DMA时钟
- 配置NVIC,给予DMA传输完成中断0级先占优先级
- 配置SysTick定时器,产生1us时间间隔的中断请求用以计时
- 配置DMA寄存器组各参数,这是本次实验成功与否的关键
主函数 main.c
#include"stm32f10x_lib.h"
#include"stdio.h"
#include"string.h"
#define BufferSize 32
vu16 CurrDataCounter=0; //定义DMA传输数目变量
vu32 Tick=0; //计时变量
uc32 SRC_Const_Buffer[BufferSize]= //定义外设数据,注意此处数据定义在FLASH中
{
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80;
}
u32 DST_Buffer[BufferSize]; //在RAM中开辟一片空间用作DMA目的空间
void RCC_Configuration(void);
void NVIC_Configuration(void);
void GPIO_Configuration(void);
void USART_Configuration(void);
void DMA_Configuration(void);
void SysTick_Configuration(void);
int main(void)
{
u8 i=0;
u8 TickCntCPU=0;
u8 TickCntDMA=0;
RCC_Configuration(); //设置系统时钟
NVIC_Configuration(); //设置NVIC
GPIO_Configuration(); //设置GPIO端口
USART_Configuration(); //设置USART
DMA_Configuration(); //DMA初始化
SysTick_Configuration(); //SysTick初始化
//开始使用CPU搬运数据并计时
//计时变量清零
Tick=0;
for(i=0;i<BufferSize;i++)
{
DST_Buffer[i]=SRC_Const_Buffer[i];
}
TickCntCPU=Tick; //保存计时数据
// CPU搬运数据完成
//缓冲清空
for(i=0;i<BufferSize;i++)
{
DST_Buffer[i]=0;
}
//开始使用DMA搬运数据并计时
Tick=0; //计时变量清零
DMA_Cmd(DMA1_Channel6,ENABLE); //开启DMA6通道传输
while(CurrDataCounter!=0); //等待传输完成
TickCntDMA=Tick; //保存计时数据
//DMA搬运数据完成
//实验结果
if(strncmp((const char*)SRC_Const_Buffer,(const char*)DST_Buffer,BufferSize)==0)
{
printf("\r\n Transmit Success!\r\n");
}
else
{
printf("\r\n Transmit Fault!\r\n");
}
printf("\r\n The CPU transfer,time consume:%dus!\n\r",TickCntCPU);
printf("\r\n The CPU transfer,time consume:%dus!\n\r",TickCntDMA);
while(1);
}
设置系统各部分时钟 RCC_Configuration
void RCC_Configuration(void)
{
ErrorStatus HSEStartUpStatus; //定义枚举类型变量 HSEStartUpStatus
RCC_DeInit(); //复位系统时钟设置
RCC_HSEConfig(RCC_HSE_ON); //开启HSE
HSEStatrtUpStatus=RCC_WaitForHSEStartUp(); //等待HSE起振并稳定
if(HSEStatrtUpStatus==SUCCESS) //判断HSE是否起振成功,是则进入if()内部
{
RCC_HCLKConfig(RCC_SYSCLK_Div1); //选择HCLK(AHB)时钟源为SYSCLK分频
RCC_PCLK2Config(RCC_HCLK_Div1); //选择PCLK2时钟源为HCLK(AHB)1分频
RCC_PCLK1Config(RCC_HCLK_Div2); //选择PCLK1时钟源为HCLK(AHB)2分频
FLASH_SetLatency(FLASH_Latency_2); //设置Flash延时周期数为2
FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); //使能Flash预取缓存
//选择PLL时钟源为 HSE 1 分频,倍频数为9,则PLL=8MHz *9=72MHz
RCC_PLLConfig(RCC_PLLSource_HSE_Div1,RCC_PLLMul_9);
RCC_PLLCmd(ENABLE); //使能PLL
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY)==RESET); //等待PLL输出稳定
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); //选择SYSCLK时钟源为PLL
while(RCC_GetSYSCLKSource()!=0x08); //等待PLL成为SYSCLK时钟源
}
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1,ENABLE); //打开APB2总线上的GPIOA和USART1时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
}
设置各GPIO端口功能 GPIO_Configuration
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//设置USART1的Tx脚(PA.9)为第2功能推挽输出功能
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//设置USART1的Rx脚(PA.10)为浮空输入脚
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
设置NVIC参数 NVIC_Configuration
void NVIC_Configuration(void)
{
//定义NVIC初始化结构体NVIC_InitStructure
NVIC_InitTypeDef NVIC_InitStructure;
// #ifdef...#else...#endif结构的作用是根据预编译条件决定中断向量表起始地址
#ifdef
//中断向量表起始地址从0x20000000开始
NVIC_SetVectorTable(NVIC_VectTab_RAM,0x0);
#else
//中断向量表起始地址从0x80000000开始
NVIC_SetVectorTable(NVIC_VectTab_FLASH,0x0);
endif
//选择优先级分组0
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
//开启DMA6通道中断控制,0级先占优先级,0级次占优先级
NVIC_InitStructure.NVIC_IRQChannel=DMA1_Channel6_IRQChannel;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=0;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
设置DMA参数 DMA_Configuration
void DMA_Configuration(void)
{
//定义DMA初始化结构体DMA_InitStructure
DMA_InitTypeDef DMA_InitStructure;
//将DMA6通道的寄存器重设为默认值
DMA_DeInit(DMA1_Channel6);
// 外设地址:(u32)SRC_Const_Buffer;
内存地址:(u32)DST_Buffer;
外设作为数据传输的来源;
DMA缓存大小:BufferSize;
外设地址寄存器递增;
内存地址寄存器递增;
外设数据宽度为32位;
内存数据宽度为32位;
CAN工作在正常缓存模式;
设置DMA通道优先级为高;
DMA通道设置为内存到内存传输;
DMA_InitStructure.DMA_PeripheralBaseAddr=(u32)SRC_Const_Buffer;
DMA_InitStructure.DMA_MemoryBaseAddr=(u32)DST_Buffer;
DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize=BufferSize;
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_word;
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_word;
DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority=DMA_Priority_High;
DMA_InitStructure.DMA_M2M=DMA_M2M_Enable;
DMA_Init(DMA1_Channel6,&DMA_InitStructure);
DMA_ITConfig(DMA1_Channl6,DMA_IT_TC,ENABLE); //开启DMA传输完成中断
CurrDataCounter=DMA_GetCurrDataCounter(DMA_Channl6); //读出当前数据量计数值
}
设置Systick定时器,重装载时间为250ms Systick_Configuration
void Systick_Configuration(void)
{
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //选择HCLK/8为SysTick时钟源
SysTick_SetReload(9); //主频为72/8MHz,配置计数值为9可以得到1us定时间隔
SysTick_CounterCmd(SysTick_Counter_Enable); //启动SysTick计数
SysTick_ITConfig(ENABLE); //使能SysTick中断
}
设置USART1 USART_Configuration
void USART_Configuration(void)
{
USART_InitTypeDef USART_InitStructure; //定义USART初始化结构体USART_InitStructure
USART_ClockInitTypeDef USART_ClockInitStructure; //定义USART初始化结构体USART_ClockInitStructure
//波特率为9600bps;8位数据长度,1个停止位,无检验位;禁用硬件流控制;禁止USART时钟;时钟极性低;在第2个边沿捕获数据;最后一位数据的时钟脉冲不从SCLK输出
USART_InitStructure.USART_BaudRate=9600;
USART_InitStructure.USART_WordLength=USART_WordLength_8b;
USART_InitStructure.USART_StopBits=USART_StopBits_1;
USART_InitStructure.USART_Parity=USART_Parity_NO;
USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
USART_Init(USART1,&USART_InitStructure);
USART_Cmd(USART1,ENABLE); //使能USART1
}
将printf函数重定位到USART1 fputc
int fputc (int ch,FILE*f)
UASRT_SendData(USART1,(u8)ch);
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
return ch;
中断服务程序
头文件
#include"stm32f10x_it.h"
extern vu16 CurrDataCounter; //声明DMA传输数目变量
extern vu32 Tick; //计时变量
SysTick定时器中断服务函数 SysTickHandler
void SysTickHandler(void)
{
Tick++;
}
DMA1第六通道中断服务函数 DMA1_Channl6_IRQHandler
void DMA1_Channel6_IRQHandler(void)
{
CurrDataCounter=DMA_GetCurrDataCounter(DMA1_Channel6); //读取当前DMA数据数目
DMA_ClearITPendingBit(DMA_IT_GL6); //清除DMA全局中断挂起标志
}