基于STM32的DMA实验
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存在的意义。
关于DMA的疑问
“DMA循环模式”时,DMA传输时间取决于什么?
一般的,会有人询问:
ADC发送,启动DMA后,DMA多久会读取一次adc的数据寄存器。比如:假设ADC5ms采集一次,那么那么DMA在这5ms内发送几次ADC采集结果?
如果只发送一次,那么是不是可以理解为ADC数据寄存器更新后DAM才会发送数据,那么DAM怎么知道数据更新了呢?
答案:
DMA的时间由外设控制,所以多久传输一次,取决于你的触发源,以ADC外设为例:ADC转换完成就会有EOC,EOC会启动传输,如果ADC外设5ms采集一次模拟信号,那么就是5ms传输一次ADC外设转换后的数据。
请记住:每一个ADC(DMA对应通道的外设)都有对应的DMA通道,他们是一对CP有心灵感应。
什么情况下使用“外设地址增量模式”和“存储器增量模式”?
① 数据传输方向:存储器->外设
由于是从存储器读数据给外设,所以存储器设置为增量模式,这样的话,它地址可以自动增加;而外设因为是固定的地址,所以设为非增量模式。
② 数据传输方向:外设->存储器
DMA相较于CPU中断数据传输方式的优点
中断方式下,外设需与主机传输数据时要请求主给予中断服务,中断当前主程序的执行,自动转向对应的中断处理程序,控制数据的传输,过程始终是在处理器所执行的指令控制之下。
直接存储器访问(DMA)方式下,系统中有一个DMA控制器,它是一个可驱动总线的主控部件。当外设与主存储器之间需要传输数据时,外设向DMA控制器发出DMA请求,DMA控制器向中央处理器发出总线请求,取得总线控制权以后,DMA控制器按照总线时序控制外设与存储器间的数据传输而不是通过指令来控制数据传输,传输速度大大高于中断方式。
CPU在实现数据在“内存<->外设”之间交互的四种方法
CPU与外设之间数据传送都是通过内存实现的。外围设备和内存之间的常用数据传送控制方式有四种:
(1) 程序直接控制方式:就是由用户进程直接控制内存或CPU和外围设备之间的信息传送。这种方式控制者都是用户进程;
(2) 中断控制方式:被用来控制外围设备和内存与CPU之间的数据传送。这种方式要求CPU与设备(或控制器)之间有相应的中断请求线,而且在设备控制器的控制状态寄存器的相应的中断允许位;
(3) DMA方式:又称直接存取方式。其基本思想是在外围设备和内存之间开辟直接的数据交换通道;
(4) 通道方式:与DMA方式相类似,也是一种以内存为中心,实现设备和内存直接交换数据控制方式。
AHB总线与DMA
AHB总线与DMA之间的关系简介(文字简介)
首先,说点不靠谱的,APB和AHB总线,我个人感觉这个类似于个人PC系统里的北桥和南桥总线。南桥总线上挂接的都是鼠标、键盘这些慢速的设备,北桥上挂接显卡等高速设备。南桥频率低,北桥频率高。另外,南桥最后也要接到北桥上。这些感觉都类似于APB和AHB。
AHB,是Advanced High performance Bus的缩写,译作高级高性能总线,这是一种“系统总线”。
AHB主要用于高性能模块(如CPU、DMA和DSP等)之间的连接。AHB 系统由主模块、从模块和基础结构(Infrastructure)3部分组成,整个AHB总线上的传输都由主模块发出,由从模块负责回应。APB,是Advanced Peripheral Bus的缩写,这是一种外围总线。
APB主要用于低带宽的周边外设之间的连接,例如UART、1284等,它的总线架构不像 AHB支持多个主模块,在APB里面唯一的主模块就是APB 桥。再往下,APB2负责AD,I/O,高级TIM,串口1;APB1负责DA,USB,SPI,I2C,CAN,串口2345,普通TIM。
这两者都是总线,符合AMBA规范。
片上总线标准种类繁多,而由ARM公司推出的AMBA片上总线受到了广大IP开发商和SoC系统集成者的青睐,已成为一种流行的工业标准片上结构。AMBA规范主要包括了AHB(Advanced High performance Bus)系统总线和APB(Advanced Peripheral Bus)外围总线。二者分别适用于高速与相对低速设备的连接。
AHB总线与DMA之间的关系简介(从代码层面介绍)
// 大容量芯片中AHB对应的主模块
#define RCC_AHBPeriph_DMA1 ((uint32_t)0x00000001)
#define RCC_AHBPeriph_DMA2 ((uint32_t)0x00000002)
#define RCC_AHBPeriph_SRAM ((uint32_t)0x00000004)
#define RCC_AHBPeriph_FLITF ((uint32_t)0x00000010)
#define RCC_AHBPeriph_CRC ((uint32_t)0x00000040)
#define RCC_AHBPeriph_OTG_FS ((uint32_t)0x00001000)
#define RCC_AHBPeriph_ETH_MAC ((uint32_t)0x00004000)
#define RCC_AHBPeriph_ETH_MAC_Tx ((uint32_t)0x00008000)
#define RCC_AHBPeriph_ETH_MAC_Rx ((uint32_t)0x00010000)
以上是如下所示的AHB时钟使能函数中AHB总线上可选择的主模块:
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState)
使能DMA主模块时钟:
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输
DMA通道与外设对应的通道
DMA外设基地址如何计算?
通过访问ADCx的DR寄存器来间接获取
&(ADC3->DR) // ADC3的DR寄存器的地址
&(ADC2->DR) // ADC2的DR寄存器的地址
&(ADC1->DR) // ADC1的DR寄存器的地址
通过寄存器的基地址直接获取
外设基地址(Peripherial BaseAddress)[参考“STM32中文参考手册的28页存储器映像部分”]
寄存器地址偏移量(Address Offset)
最终,得到的ADC3的DR寄存器地址为:
0x40013C00+0x4C = 0x40013C4C
ADCx与内存使用DMA传输数据的过程
DMA相关库函数简介
DMA固件库函数
外设的固件库函数(以ADC为例)
代码示例(用ADC测量内部光敏传感器的模拟量的值,再将值通过DMA传输至内存中的数组中)
Main.c
#include "dma.h"
#include "adc.h"
#include "usart.h"
#include "delay.h"
#include "stm32f10x.h"
// DMA2_Channel5 -> ADC3_IN6 -> PF8
int main()
{
u16 Buffer[8] = {0};
u8 i = 0;
delay_init();
DMA_InitConfig((u32)&Buffer,8);
uart_init(115200);
ADC_InitConfig();
while(1)
{
ADC_DMACmd(ADC3,ENABLE);
while(1)
{
if(DMA_GetFlagStatus(DMA1_FLAG_TC5) == SET)
{
DMA_AgainEnable();
DMA_ClearFlag(DMA1_FLAG_TC5);
break; // 传输完毕,跳出子死循环
}
}
for(;i<8;i++)
{
printf("Buffer[%d] = %d\n\r",i,Buffer[i]);
}
i = 0;
}
}
Adc.c
#include "adc.h"
#include "stm32f10x.h"
void ADC_InitConfig()
{
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOF|RCC_APB2Periph_ADC3,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOF,&GPIO_InitStructure);
ADC_DeInit(ADC3);
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_Init(ADC3,&ADC_InitStructure);
ADC_DMACmd(ADC3,ENABLE);
ADC_Cmd(ADC3,ENABLE);
ADC_ResetCalibration(ADC3);
while(ADC_GetResetCalibrationStatus(ADC3) == SET);
ADC_StartCalibration(ADC3);
while(ADC_GetCalibrationStatus(ADC3) == SET);
}
u16 ADC_GetDigitalValue()
{
ADC_RegularChannelConfig(ADC3,ADC_Channel_6,1,ADC_SampleTime_28Cycles5);
ADC_SoftwareStartConvCmd(ADC3,ENABLE);
while(ADC_GetFlagStatus(ADC3,ADC_FLAG_EOC) == RESET);
return ADC_GetConversionValue(ADC3);
}
float ADC_GetAverageLight()
{
float temp = ADC_GetDigitalValue();
return temp/4096.0*100.0;
}
Adc.h
#ifndef _ADC_H
#define _ADC_H
#include "sys.h"
void ADC_InitConfig();
u16 ADC_GetDigitalValue();
float ADC_GetAverageLight();
#endif
Dma.c
#include "dma.h"
#include "stm32f10x.h"
#define ADC3_DR_ADDRESS 0x40013C4C
u16 _BufferSize = 0;
void DMA_InitConfig(u32 BufferAddress,u16 BufferSize)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2,ENABLE);
DMA_DeInit(DMA2_Channel5);
DMA_InitStructure.DMA_BufferSize = BufferSize;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_InitStructure.DMA_MemoryBaseAddr = BufferAddress;
DMA_InitStructure.DMA_MemoryDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)ADC3_BASE+0x4C;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_Init(DMA2_Channel5,&DMA_InitStructure);
_BufferSize = BufferSize;
DMA_Cmd(DMA2_Channel5,ENABLE);
}
void DMA_AgainEnable()
{
DMA_Cmd(DMA2_Channel5,DISABLE);
DMA_SetCurrDataCounter(DMA2_Channel5,_BufferSize);
DMA_Cmd(DMA2_Channel5,ENABLE);
}
Dma.h
#ifndef _DMA_H
#define _DMA_H
#include "sys.h"
void DMA_InitConfig(u32 BufferAddress,u16 BufferSize);
void DMA_AgainEnable();
#endif
正点原子源程序解析(内存->USART)
全部主程序示例
#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "lcd.h"
#include "usart.h"
#include "dma.h"
#define SEND_BUF_SIZE 8200 //发送数据长度,最好等于sizeof(TEXT_TO_SEND)+2的整数倍
u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区
const u8 TEXT_TO_SEND[]={"ALIENTEK WarShip STM32F1 DMA 串口实验"};
int main(void)
{
u16 i;
u8 t=0;
u8 j,mask=0;
float pro=0;//进度
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
uart_init(115200); //串口初始化为115200
LED_Init(); //初始化与LED连接的硬件接口
LCD_Init(); //初始化LCD
KEY_Init(); //按键初始化
MYDMA_Config(DMA1_Channel4,(u32)&USART1->DR,(u32)SendBuff,SEND_BUF_SIZE);//DMA1通道4,外设为串口1,存储器为SendBuff,长度SEND_BUF_SIZE.
POINT_COLOR=RED;//设置字体为红色
LCD_ShowString(30,50,200,16,16,"WarShip STM32");
LCD_ShowString(30,70,200,16,16,"DMA TEST");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,"2015/1/15");
LCD_ShowString(30,130,200,16,16,"KEY0:Start");
//显示提示信息
j=sizeof(TEXT_TO_SEND);
for(i=0;i<SEND_BUF_SIZE;i++)//填充数据到SendBuff
{
if(t>=j)//加入换行符
{
if(mask)
{
SendBuff[i]=0x0a;
t=0;
}
else
{
SendBuff[i]=0x0d;
mask++;
}
}
else//复制TEXT_TO_SEND语句
{
mask=0;
SendBuff[i]=TEXT_TO_SEND[t];
t++;
}
}
POINT_COLOR=BLUE;//设置字体为蓝色
i=0;
while(1)
{
t=KEY_Scan(0);
if(t==KEY0_PRES)//KEY0按下
{
LCD_ShowString(30,150,200,16,16,"Start Transimit....");
LCD_ShowString(30,170,200,16,16," %");//显示百分号
printf("\r\nDMA DATA:\r\n");
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
MYDMA_Enable(DMA1_Channel4);//开始一次DMA传输!
//等待DMA传输完成,此时我们来做另外一些事,点灯
//实际应用中,传输数据期间,可以执行另外的任务
while(1)
{
if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET) //判断通道4传输完成
{
DMA_ClearFlag(DMA1_FLAG_TC4);//清除通道4传输完成标志
break;
}
pro=DMA_GetCurrDataCounter(DMA1_Channel4);//得到当前还剩余多少个数据
pro=1-pro/SEND_BUF_SIZE;//得到百分比
pro*=100; //扩大100倍
LCD_ShowNum(30,170,pro,3,16);
}
LCD_ShowNum(30,170,100,3,16);//显示100%
LCD_ShowString(30,150,200,16,16,"Transimit Finished!");//提示传送完成
}
i++;
delay_ms(10);
if(i==20)
{
LED0=!LED0;//提示系统正在运行
i=0;
}
}
}
代码块1解析
⑴ 目的:填充SendBuff数组
⑵ 代码示例:
//显示提示信息
j=sizeof(TEXT_TO_SEND)/sizeof(u8);
for(i=0;i<SEND_BUF_SIZE;i++)//填充8200个字符到SendBuff字符串数组中
{
if(t>=j)//加入换行符
{
if(mask) // mask保证先进行回车后进行换行
{
SendBuff[i]=0x0a; // ‘\n’换行——另起一行
t=0;
}
else
{
SendBuff[i]=0x0d; //’\r’回车——回到行首
mask++;
}
}
else//复制TEXT_TO_SEND语句
{
mask=0;
SendBuff[i]=TEXT_TO_SEND[t];
t++;
}
}
i=0; // 清零i变量已被后面再次使用
0x0a与0x0d字符说明
① 回车代码:CR ASCII码:\ r ,十六进制,0x0d,回车的作用只是移动光标至该行的起始位置;
② 换行代码:LF ASCII码:\ n ,十六进制,0x0a,换行至下一行行首起始位置;
⑶ 逻辑解析
① 刚开始,未有数据存到SendBuff数组的时候,先回车换行将光标移至行首;
② 逐渐将TEXT_TO_SEND数组中的字符逐个转移至SendBuff数组中,当转移完成时,先回车后换行;
③ 再次将TEXT_TO_SEND数组中的字符逐个转移至SendBuff数组中,当转移完成时,先回车后换行。
⑷ 变量解析
⑸ 回车换行段程序逻辑简介
if(mask) // mask保证先进行回车后进行换行
{
SendBuff[i]=0x0a; // ‘\n’换行——另起一行
t=0;
}
else
{
SendBuff[i]=0x0d; //’\r’回车——回到行首
mask++;
}
注:将TEXT_TO_SEND数组装载进SendBuff数组一次应该循环“sizeof(TEXT_TO_SEND)/sizeof(unsigned char)+2”次,因为“\r\n”需要占用两个元素的位置。
代码块2解析
t=KEY_Scan(0);
if(t==KEY0_PRES)//KEY0按下
{
LCD_ShowString(30,150,200,16,16,"Start Transimit....");
LCD_ShowString(30,170,200,16,16," %");//显示百分号
printf("\r\nDMA DATA:\r\n");
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
MYDMA_Enable(DMA1_Channel4);//开始一次DMA传输!
//等待DMA传输完成,此时我们来做另外一些事,点灯
//实际应用中,传输数据期间,可以执行另外的任务
while(1)
{
if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET) //判断通道4传输完成
{
DMA_ClearFlag(DMA1_FLAG_TC4);//清除通道4传输完成标志
break;
}
pro=DMA_GetCurrDataCounter(DMA1_Channel4);//得到当前还剩余多少个数据
pro=1-pro/SEND_BUF_SIZE;//得到百分比
pro*=100; //扩大100倍
LCD_ShowNum(30,170,pro,3,16);
}
LCD_ShowNum(30,170,100,3,16);//显示100%
LCD_ShowString(30,150,200,16,16,"Transimit Finished!");//提示传送完成
}
这块代码实现的功能是:
① 每当按下KEY0,进行一次DMA传输,并进入子死循环;
② 传输过程中,(子死循环内)不断读取数据传输剩余量,并且转化为百分比在LCD上进行显示;
③ 同时不断判断传输是否完成,每当传输完成,将传输完成标志位软件清除并且退出子死循环。
实现简单的”内存->usart”数据的传输
Main.c
#include "dma.h"
#include "usart.h"
#include "delay.h"
#include "stm32f10x.h"
u8 Buffer[8] = {1,2,3,4,5,6,7,8};
int main()
{
delay_init();
DMA_InitConfig((u32)Buffer,8);
uart_init(115200);
while(1)
{
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); // 使能USART1_TX发送端的DMA传输接口
if(DMA_GetFlagStatus(DMA1_FLAG_TC4) == SET)
{
DMA_AgainEnable(); // 当传输完成,再次设置单次传输的数据总量
DMA_ClearFlag(DMA1_FLAG_TC4); // 软件清除DMA传输完成标志位
}
}
}
Dma.c
#include "dma.h"
#include "stm32f10x.h"
u8 _BufferSize = 0;
void DMA_InitConfig(u32 BufferAddress, u16 BufferSize)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
DMA_InitStructure.DMA_BufferSize = BufferSize;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_InitStructure.DMA_MemoryBaseAddr = BufferAddress;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&(USART1->DR);
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_Init(DMA1_Channel4,&DMA_InitStructure);
_BufferSize = BufferSize;
DMA_Cmd(DMA1_Channel4,ENABLE);
}
void DMA_AgainEnable()
{
DMA_Cmd(DMA1_Channel4,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel4,_BufferSize);
DMA_Cmd(DMA1_Channel4,ENABLE);
}
Dma.h
#ifndef _DMA_H
#define _DMA_H
#include "sys.h"
void DMA_InitConfig(u32 BufferAddress, u16 BufferSize);
void DMA_AgainEnable();
#endif