《STM32从零开始学习历程》@EnzoReventon
DMA—直接存储区访问实验例程
本章节为DMA直接存储区访问的实验例程讲解,以“正点原子”的例程为基础进行讲解,如有不足之处还恳请各位大佬不吝赐教。
参考资料:
[野火EmbedFire]《STM32库开发实战指南——基于野火霸天虎开发板》
[正点原子]STM32F4开发指南-库函数版本_V1.2
[ST]《STM32F4xx中文参考手册》
1. DMA简介
DMA的详细介绍已经在上一讲中进行过详细的介绍:《STM32从零开始学习历程》——DMA直接存储区访问理论知识
2. 本实验历程实现功能介绍
根据《STM32从零开始学习历程》——DMA直接存储区访问理论知识的详细介绍,我们可以知道DMA是一种可以不通过CPU的直接进行数据传输的控制器。本例程主要功能为使用DMA串口通讯将一定量的数据发送出去,使用串口助手接收发送到的数据。程序功能要点如下:
(1). 通过DMA将数据发送到USART1,使用串口助手接收数据。
(2). 使用一个按键控制DMA发送,按下按钮就进行一次DMA数据发送操作。
(3). LCD屏幕显示发送状态与发送进度。(LCD的讲解将在后续blog中讲解,本文只要会用就行)
3. 实验准备
软件:Keil μVision5 v5.33(MDK5),串口助手XCOM V2.6
环境:Windows10 Enterprise x64
芯片:STM32F406ZGT6
设备:正点原子STM32F4探索者开发板,正点原子4.3寸 TFTLCD屏
仿真器:ST-Link
参考手册:
[野火EmbedFire]《STM32库开发实战指南——基于野火霸天虎开发板》
[正点原子]STM32F4开发指南-库函数版本_V1.2
[ST]《STM32F4xx中文参考手册》
[ST]《STM32F407xx》
4. 硬件设计
本实验中需要用到USART1,所以我们需要将USART1的TX与RX引脚与相应的GPIO引脚相连接,此处我们使用PB6/PB7引脚进行通讯,关于USART通讯串口的配置与选择问题可以看:《STM32从零开始学习历程》——USART串口通讯实验篇1——中断接收与发送,此处就不多赘述。
硬件连接呢我们还是通过使用杜邦线将USART TX/RX与PB6/PB7向连接,同时将USART1串口连接至电脑。如下图所示:
5. 程序设计流程
1.DMA配置程序过程
①使能DMA时钟
RCC_AHB1PeriphClockCmd();
② 初始化DMA通道参数
DMA_Init();
③使能串口DMA发送,串口DMA使能函数:
USART_DMACmd();
④查询DMA的EN位,确保数据流就绪,可以配置
DMA_GetCmdStatus();
⑤设置通道当前剩余数据量
DMA_SetCurrDataCounter();
⑥使能DMA1通道,启动传输。
DMA_Cmd();
⑤查询DMA传输状态
DMA_GetFlagStatus();
⑥获取/设置通道当前剩余数据量:
DMA_GetCurrDataCounter();
2. 相关函数介绍
1)使能 DMA2 时钟,并等待数据流可配置 。
DMA的时钟使能是通过 AHB1ENR 寄存器来控制的,这里我们要先使能时钟,才可以配置 DMA相关寄存器。所以先要使能 DMA2 的时钟。另外,要对配置寄存器( DMA_SxCR )进行设置,必须先等待其最低位为 0 (也就是 DMA 传输禁止了),才可以进行配置。
库函数使能DMA2 时钟的方法为:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE); //DMA2 时钟使能等待
DMA 可配置,也就是等待 DMA_SxCR 寄存器最低位为 0 的方法为:
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE) { } // 等待 DMA 可配置
2)初始化 DMA2 数据流 7 ,包括配置通道,外设地址,存储器地址,传输数据量等 。
DMA的某个数据流各种配置参数初始化是通过 DMA_Init 函数实现的:
void DMA_Init(DMA_Stream_TypeDef* DMAy_Streamx, DMA_InitTypeDef* DMA_InitStruct)
函数的第一个参数是指定初始化的 DMA 的数据流编号,这个很容易理解。入口参数范围为:
DMA x _Stream0 DMA x _Stream 7(x=1,2) 。
下面我们主要看看第二个参数。跟其他外设一样,同样是通过初始化结构体成员变量值来达到初始化的目的,下面我们来看看 DMA_InitTypeDef结构体的定义:
typedef struct
{
uint32_t DMA_Channel;
uint32_t DMA_PeripheralBaseAddr;
uint32_t DMA_Memory0BaseAddr;
uint32_t DMA_DIR;
uint32_t DMA_BufferSize;
uint32_t DMA_PeripheralInc;
uint32_t DMA_MemoryInc;
uint32_t DMA_Pe ripheralDataSize;
uint32_t DMA_MemoryDataSize;
uint32_t DMA_Mode;
uint32_t DMA_Priority;
uint32_t DMA_FIFOMode;
uint32_t DMA_FIFOThreshold;
uint32_t DMA_MemoryBurst;
uint32_t DMA_PeripheralBurst;
}
DMA_InitTypeDef;
1) DMA_Channel:
DMA 请求通道选择,可选通道0 至通道7,每个外设对应固定的通道,具体设置值需要查表DMA1 各个通道的请求映像和表DMA2 各个通道的请求映像。
2) DMA_PeripheralBaseAddr:
外设地址,设定DMA_SxPAR 寄存器的值;一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储区地址。
ADC3 的数据寄存器ADC_DR 地址为((uint32_t)ADC3+0x4C)。
3) DMA_Memory0BaseAddr:
存储器0 地址,设定DMA_SxM0AR 寄存器值;一般设置为我们自义存储区的首地址。我们程序先自定义一个16 位无符号整形数组ADC_ConvertedValue[4]用来存放每个通道的ADC 值, 所以把数组首地址(直接使用数组名即可) 赋值给DMA_Memory0BaseAddr。
4) DMA_DIR:
传输方向选择,可选外设到存储器、存储器到外设以及存储器到存储器。它设定DMA_SxCR 寄存器的DIR[1:0] 位的值。ADC 采集显然使用外设到存储器模式。
5) DMA_BufferSize:
设定待传输数据数目,初始化设定DMA_SxNDTR 寄存器的值。这里ADC是采集4 个通道数据,所以待传输数目也就是4。
6) DMA_PeripheralInc:
如果配置为DMA_PeripheralInc_Enable,使能外设地址自动递增功能,它设定DMA_SxCR 寄存器的PINC 位的值;一般外设都是只有一个数据寄存器,所以一般不会使能该位。ADC3 的数据寄存器地址是固定并且只有一个所以不使能外设地址递增。
7) DMA_MemoryInc:
如果配置为DMA_MemoryInc_Enable,使能存储器地址自动递增功能,它设定DMA_SxCR 寄存器的MINC 位的值;我们自定义的存储区一般都是存放多个数据的,所以使能存储器地址自动递增功能。我们之前已经定义了一个包含4 个元素的数字用来存放数据,使能存储区地址递增功能,自动把每个通道数据存放到对应数组元素内。
8) DMA_PeripheralDataSize:
外设数据宽度,可选字节(8 位)、半字(16 位) 和字(32 位),它设定DMA_SxCR 寄存器的PSIZE[1:0] 位的值。ADC 数据寄存器只有低16 位数据有效,使用半字数据宽度。
9) DMA_MemoryDataSize:
存储器数据宽度,可选字节(8 位)、半字(16 位) 和字(32 位),它设定DMA_SxCR 寄存器的MSIZE[1:0] 位的值。保存ADC 转换数据也要使用半字数据宽度,这跟我们定义的数组是相对应的。
10) DMA_Mode:
DMA 传输模式选择,可选一次传输或者循环传输,它设定DMA_SxCR 寄存器的CIRC 位的值。我们希望ADC 采集是持续循环进行的,所以使用循环传输模式。
11) DMA_Priority:
软件设置数据流的优先级,有4 个可选优先级分别为非常高、高、中和低,它设定DMA_SxCR 寄存器的PL[1:0] 位的值。DMA 优先级只有在多个DMA 数据流同时使用时才有意义,这里我们设置为非常高优先级就可以了。
12) DMA_FIFOMode:
FIFO 模式使能,如果设置为DMA_FIFOMode_Enable 表示使能FIFO 模式功能;它设定DMA_SxFCR 寄存器的DMDIS 位。ADC 采集传输使用直接传输模式即可,不需要使用FIFO 模式。
13) DMA_FIFOThreshold:
FIFO 阈值选择,可选4 种状态分别为FIFO 容量的1/4、1/2、3/4 和满;它设定DMA_SxFCR 寄存器的FTH[1:0] 位;DMA_FIFOMode 设置为DMA_FIFOMode_Disable,那DMA_FIFOThreshold 值无效。ADC 采集传输不使用FIFO 模式,设置改值无效。
14) DMA_MemoryBurst:
存储器突发模式选择,可选单次模式、4 节拍的增量突发模式、8 节拍的增量突发模式或16 节拍的增量突发模式,它设定DMA_SxCR 寄存器的MBURST[1:0] 位的值。ADC 采集传输是直接模式,要求使用单次模式。
15) DMA_PeripheralBurst:
外设突发模式选择,可选单次模式、4 节拍的增量突发模式、8 节拍的增量突发模式或16 节拍的增量突发模式,它设定DMA_SxCR 寄存器的PBURST[1:0] 位的值。
ADC 采集传输是直接模式,要求使用单次模式。
3)使能串口 1 的 DMA 发送
进行DMA 配置之后,我们就要开启串口的 DMA 发送功能,使用的函数是:
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口 1的 DMA发送
如果是要使能串口DMA 接受,那么第二个参数修改为 USART_DMAReq_Rx 即可。
4)使能 DMA 2 数据流 7 ,启动传输。
使能 DMA 数据流的函数为:
void DMA_Cmd(DMA_Stream_TypeDef* DMAy_Streamx, FunctionalState NewState)
使能 DMA2_Stream7 ,启动传输的方法为:
DMA_Cmd DMA2_Stream7 ENABLE
通过以上4 步设置,我们就可以启动一次 USART1 的 DMA 传输了。
5)查询 DMA 传输状态
在DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的函数是:
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG)
比如我们要查询DMA数据流 7传输是否完成,方法是:
DMA_GetFlagStatus(DMA2_Stream7,DMA_FLAG_TCIF7);
这里还有一个比较重要的函数就是获取当前剩余数据量大小的函数:
int16_t DMA_GetCurrDataCounter(DMA_Stream_TypeDef* DMAy_Streamx);
比如我们要获取DMA数据流 7还有多少个数据没有传输,方法是:
DMA_GetCurrDataCounter(DMA1_Channel4);
同样,我们也可以设置对应的DMA数据流传输的数据量大小 ,函数为:
void DMA_SetCurrDataCounter(DMA_Stream_TypeDef* DMAy_Streamx, uint16_t Counter);
6. 程序详解
USART.C
对于USART串口初始化的子程序我们仍然使用串口通讯中的程序代码。
//标准库需要的支持函数
struct __FILE
{
int handle;
};
FILE __stdout;
//定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
void uart_init(u32 bound){
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE); //使能GPIOA时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //使能USART1时钟
//串口1对应引脚复用映射
GPIO_PinAFConfig(GPIOB,GPIO_PinSource6,GPIO_AF_USART1); //GPIOB6复用为USART1
GPIO_PinAFConfig(GPIOB,GPIO_PinSource7,GPIO_AF_USART1); //GPIOB7复用为USART1
//USART1端口配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; //GPIOB6与GPIOB7
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOB,&GPIO_InitStructure); //初始化PB6,PB7
//USART1 初始化设置
USART_InitStructure.USART_BaudRate = bound; //波特率设置
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为8位数据格式
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); //初始化串口1
USART_Cmd(USART1, ENABLE); //使能串口1
}
//===============================================================================
///重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch , FILE *f)
{
USART_SendData(USART1,(uint8_t) ch );
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
return (ch);
}
//===============================================================================
///重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
return (int)USART_ReceiveData(USART1);
}
//===============================================================================
中断服务函数。
void USART1_IRQHandler(void) //串口1中断服务程序
{
uint8_t temp;
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) != RESET)
{
temp = USART_ReceiveData(USART1);
USART_SendData(USART1,temp);
}
DMA.C
DMA初始化函数。此函数有5个形参。
DMA_Stream_TypeDef *DMA_Streamx: 为选择数据流。
u32 chx: 为通道选择。
u32 par: 为外设地址。
u32 mar: 存储器地址。
u16 ndtr: 为数据传输量。
void MYDMA_Config(DMA_Stream_TypeDef *DMA_Streamx,u32 chx,u32 par,u32 mar,u16 ndtr)
{
DMA_InitTypeDef DMA_InitStructure;
if((u32)DMA_Streamx>(u32)DMA2) //得到当前stream是属于DMA2还是DMA1
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2,ENABLE); //DMA2时钟使能
}else
{
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1,ENABLE); //DMA1时钟使能
}
DMA_DeInit(DMA_Streamx);
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE) { } //等待DMA可配置
// 配置 DMA 初始化函数 //
DMA_InitStructure.DMA_Channel = chx; //通道选择
DMA_InitStructure.DMA_PeripheralBaseAddr = par; //DMA外设地址
DMA_InitStructure.DMA_Memory0BaseAddr = mar; //DMA 存储器0地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; //存储器到外设模式
DMA_InitStructure.DMA_BufferSize = ndtr; //数据传输量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器增量模式
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据长度:8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据长度:8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 使用普通模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //中等优先级
DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; //不使能FIFO模式
DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; //FIFO 阈值选择,此时无效
DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; //存储器突发单次传输
DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; //外设突发单次传输
DMA_Init(DMA_Streamx, &DMA_InitStructure); //初始化DMA Stream
}
开启一次DMA数据传输。
其中:DMA_Stream_TypeDef *DMA_Streamx 为DMA数据流;
u16 ndtr为为数据传输量。
void MYDMA_Enable(DMA_Stream_TypeDef *DMA_Streamx,u16 ndtr)
{
DMA_Cmd(DMA_Streamx, DISABLE); //关闭DMA传输
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){} //确保DMA可以被设置
DMA_SetCurrDataCounter(DMA_Streamx,ndtr); //数据传输量
DMA_Cmd(DMA_Streamx, ENABLE); //开启DMA传输
}
MAIN.C
①进行宏定义
#define SEND_BUF_SIZE 9600 //发送数据长度,最好等于sizeof(TEXT_TO_SEND)+2的整数倍.
u8 SendBuff[SEND_BUF_SIZE]; //发送数据缓冲区
const u8 TEXT_TO_SEND[]={"DMA串口实验 :Hello World!"};
int main(void)
{
u16 i;
u8 t=0;
u8 j,mask=0;
float pro=0; //进度
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置系统中断优先级分组2
delay_init(168); //初始化延时函数
uart_init(115200); //初始化串口波特率为115200
LED_Init(); //初始化LED
LCD_Init(); //LCD初始化
KEY_Init(); //按键初始化
MYDMA_Config(DMA2_Stream7,DMA_Channel_4,(u32)&USART1->DR,(u32)SendBuff,SEND_BUF_SIZE);
//DMA2,STEAM7数据流7,CH4通道4,外设为串口1,存储器为SendBuff,长度为:SEND_BUF_SIZE.
POINT_COLOR=RED;
LCD_ShowString(30,50,200,16,16,"STM32F407");
LCD_ShowString(30,70,200,16,16,"DMA");
LCD_ShowString(30,90,200,16,16,"==========");
LCD_ShowString(30,110,200,16,16,"2021/4/1");
LCD_ShowString(30,130,200,16,16,"KEY0:Start");
POINT_COLOR=BLUE; //设置字体为蓝色
//显示提示信息
j=sizeof(TEXT_TO_SEND);
for(i=0;i<SEND_BUF_SIZE;i++) //填充ASCII字符集数据
{
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按下
{
printf("\r\nDMA DATA:\r\n");
LCD_ShowString(30,150,200,16,16,"Start Transimit....");
LCD_ShowString(30,170,200,16,16," %") ; //显示百分号
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
MYDMA_Enable(DMA2_Stream7,SEND_BUF_SIZE); //开始一次DMA传输!
//等待DMA传输完成,此时我们来做另外一些事,点灯
//实际应用中,传输数据期间,可以执行另外的任务
while(1)
{
if(DMA_GetFlagStatus(DMA2_Stream7,DMA_FLAG_TCIF7)!=RESET) //等待DMA2_Steam7传输完成
{
DMA_ClearFlag(DMA2_Stream7,DMA_FLAG_TCIF7); //清除DMA2_Steam7传输完成标志
break;
}
pro=DMA_GetCurrDataCounter(DMA2_Stream7); //得到当前还剩余多少个数据
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;
}
}
}
//开启一次DMA传输
//DMA_Streamx:DMA数据流,DMA1_Stream0~7/DMA2_Stream0~7
//ndtr:数据传输量
void MYDMA_Enable(DMA_Stream_TypeDef *DMA_Streamx,u16 ndtr)
{
DMA_Cmd(DMA_Streamx, DISABLE); //关闭DMA传输
while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE){} //确保DMA可以被设置
DMA_SetCurrDataCounter(DMA_Streamx,ndtr); //数据传输量
DMA_Cmd(DMA_Streamx, ENABLE); //开启DMA传输
}
7. 效果展示
将程序下载进芯片后,屏幕将出现提示信息,按下按键0将执行一次DMA数据传输操作。
打开串口调试助手,设置好波特率,打开串口,将看到所有传输的数据。
注意事项:
在传输数据的过程中如果SEND_BUF_SIZE发送数据的长度没有定义准确将导致发送数据的不完整,如下图所示:
显然最后一行没有将“STM32F4 DMA”完整发送,如果需要完整发送需要对SEND_BUF_SIZE进行合理取值,最好等于sizeof(TEXT_TO_SEND)+2的整数倍。
8. 小结
本实验到此结束,其实实验不难,主要是一个实验的过程,需要对这个过程进行了解,还有就是相关函数的使用标志位的判定等。此外,多查手册仍然很重要。