请保留-> 【原文: https://blog.csdn.net/luobing4365 和 http://yiiyee.cn/blog/author/luobing/】
(代码仓库地址:https://gitee.com/luobing4365/yie002-explorer
具体参考博客:YIE002开发探索-Gitee代码仓库说明)
在实际应用中,单片机的CPU是最“忙”的,需要完成的任务非常多。因此,CPU资源是非常宝贵的,能够少用就尽量少用,这也能很大程度上提高系统的稳定性。
DMA(直接存储器访问)就是用来解决类似问题的。它用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输,无须CPU干预,就可以实现数据的快速移动。
我曾和朋友开发过一款DTU的产品,是使用STM32单片机和FreeRTOS构建的系统。系统中有三个串口都需要发挥作用,如果只用中断的话,CPU频繁被中断跳转,效率是很低的,根本无法满足现场的数据需求。因此,我们根据应用需求,使用DMA构建了环状的串口数据接收和发送架构,产品最终表现很好。
本篇准备尝试在新的开发方式下,实现串口的DMA数据传输。
1 STM32的DMA
DMA全称为Direct Memory Access,也即直接存储器访问。CPU完成初始化动作,传输的动作则是由DMA控制器来实现完成的。
典型的例子是移动外设的数据到内存区,这样的操作不需要处理器干预,因此可以让处理器去排程做其他工作。
DMA传输对于高效能嵌入式系统算法和网络很重要,它没有中断处理的现场保留和恢复的过程,通过硬件为内存和外部I/O设备构建直接传输数据的通道,大大提高了CPU的效率。
1.1 STM32的DMA功能
STM32 最多有 2 个 DMA 控制器(DMA2 仅存在大容量产品中), DMA1 有 7 个通道。 DMA2 有 5个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁起来协调各个 DMA 请求的优先权。
STM32的DMA主要特性如下:
● 12个独立的可配置的通道(请求): DMA1有7个通道, DMA2有5个通道;
● 每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置;
● 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推) ;
● 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐;
● 支持循环的缓冲器管理;
● 每个通道都有3个事件标志(DMA半传输、 DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求;
● 支持存储器和存储器间的传输;
● 支持外设和存储器、存储器和外设之间的传输;
● 闪存、 SRAM、外设的SRAM、 APB1、 APB2和AHB外设均可作为访问的源和目标;
● 可编程的数据传输数目:最大为65535。
从外设(TIMx、 ADC、 SPIx、 I2Cx 和 USARTx)产生的 DMA 请求,通过逻辑或输入到DMA 控制器,这就意味着同时只能有一个请求有效。外设的 DMA 请求,可以通过设置相应的外设寄存器中的控制位,被独立地开启或关闭。
如图1,给出各外设的DMA对应通道。
![](http://yiiyee.cn/blog/wp-content/uploads/2021/08/YIE002EXPLORER08-001-1024x400.png)
当然,DM2也有对应的通道览表。本篇使用的例子中,用的是串口1,其DMA通道是在DMA1下的。想查看更多的信息,请查阅《STM32参考手册》。
对于DMA的寄存器,整理的思维导图如图2所示。
![](http://yiiyee.cn/blog/wp-content/uploads/2021/08/YIE002EXPLORER08-002.png)
阅读DMA相关的代码时,对寄存器的理解是必不可少的。上一篇中对串口中断的Cube Library的代码进行了跟踪,DMA的处理过程也差不多,就不再细致地跟踪阅读了。
1.2 STM32串口的DMA
YIE002开发探索05中,曾经列出了与串口相关的Cube Library的API函数。其中就包含了与DMA相关的两个函数,HAL_UART_Transmit_DMA()(串口DMA模式发送)和HAL_UART_Receive_DMA()(串口DMA模式接收)。
这两个函数的使用方法,与上一篇介绍的HAL_UART_Tramsit_IT()和HAL_UART_Receive_IT()类似。在1.1节中,我们知道,USART1_Tx使用DMA1的通道4,USART1_Rx使用DMA1的通道5。使用过程中,有以下问题需要先搞清楚:
1) USART采用DMA接收时,如何读取当前接收到的字节数?
2) USART有中断函数USARTx_IRQHandler,串口DMA使用中断的时候,也有DMA1_Channelx_IRQHandler()(DMA接收或发送中断),这几个中断在数据传输的时候,顺序如何?
先回答第2个问题。
以USART1为例,在startup_stm32f103xb.s中,给出了DMA相关的中断函数:
EXPORT DMA1_Channel4_IRQHandler [WEAK]
EXPORT DMA1_Channel5_IRQHandler [WEAK]
其中,DMA1_Channel4_IRQHandler()为数据发送中断函数(数据从内存发往串口设备);DMA1_Channel5_IRQHandler()为数据接收中断函数(从串口设备接收数据,发往内存)。
而除了USART1的串口中断函数为USART1_IRQHandler()。包括DMA的中断函数在内,这三个函数均定义在stm32f1xx_it.c源文件中。
每个DMA通道都可以在DMA传输过半、传输完成和传输错误时产生中断,可通过设置寄存器的不同位来打开这些中断。
在本篇的示例中,使用的是传输完成时产生中断。而字符串传输完成,是通过串口的IDLE中断来判断的。
也就是说,基本的程序设计为:
1) 设置USART参数,启动DMA接收中断;
2) 在接收到完整的数据包后,使用串口DMA发送函数,将串口数据发往PC。
在这种情况下,上述三个中断被调用的顺序依次为:
USART1_IRQHandler()(实际上是IDLE中断被触发了)->
DMA1_Channel5_IRQHandler()(通过DMA把串口数据接收完成)->
DMA1_Channel4_IRQHandler()(通过DMA发送串口数据)
下面回答本节的第1个问题。
USART采用DMA接收时,并没有全局变量来表示接收到数据的长度。可以通过DMA通道x传输数量寄存器(DMA_CNDTRx)中包含的数据传输数量,来进行判断。此寄存器中包含了剩余待传输字节数目,在每次DMA传输后递减。
可通过宏__HAL_DMA_GET_COUNTER来实现,其定义如下:
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNDTR);
将预先定义的接收总字节数,减去剩余待传输字节数目,就得到了目前接收到的数据字节数了。
对DMA进行了上述了解后,我们进入编程环节。
2 YIE002-STM32的串口编程(DMA)
本篇的示例,在上一篇串口(中断)的示例代码基础上进行修改。对于字符串结束的判断(也即数据帧的结束),仍旧通过IDLE中断函数来处理。而串口数据的接收和发送,则改用DMA来实现了。
2.1 串口(DMA)的Cube MX图形配置
在Pinout&Configuration栏的Connectivity中,选择USART1的配置界面。之前设置的参数不用修改,主要是添加DMA的处理。USART1的配置界面中,DMA Setting对话框提供了“Add”和“Delete”按钮,可以使用“Add”按钮添加串口的DMA接收和发送,如图3所示。
![](http://yiiyee.cn/blog/wp-content/uploads/2021/08/YIE002EXPLORER08-003.png)
添加了USART1_RX和USART1_TX两个DMA请求,两者的模式都设置为Normal,数据宽度为字节,其相关的内存地址选择了自动增加。
在添加的过程中,主要它们所用的DMA通道,以及方向就可以了。
对于DMA中断优先级,仍旧可以在System Core的NVIC配置界面上修改。我修改的内容如图4所示。
![](http://yiiyee.cn/blog/wp-content/uploads/2021/08/YIE002EXPLORER08-004.png)
完成上述配置后,选择GENERATE CODE按钮,生成代码。
2.2 添加应用代码
代码的架构,沿用的上一篇的代码。由于采用的DMA传输,串口回调函数HAL_UART_RxCpltCallback()不必编写。主要修改的地方如下:
1) 全局变量
/* USER CODE BEGIN 0 */
#define USART1_REC_LEN 200 //接收的最大字符串长度
uint8_t Usart1PackageFlag; //接收到完整的包的标志,即一串数据接收完毕;
uint8_t bUsart1RxBuff[USART1_REC_LEN]; //接收缓冲,最大USART1_REC_LEN个字节
uint16_t wUsart1RxNumber=0; //接收到的数据字节数
uint8_t bUsart1Buffer[1]; //接收用的暂存缓冲区
//IDLE防抖使用的标志:只有接收到数据后,IDLE才有效,防止抖动
//1:数据接收到了;
uint8_t Usart1DataFlag; //本实例中不使用此变量
2) 修改主函数main()中的代码
//….前略
/* USER CODE BEGIN 2 */
//DMA接收函数,启动了DMA的接收
HAL_UART_Receive_DMA(&huart1,bUsart1RxBuff,USART1_REC_LEN);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
//robin 20210803
if(Usart1PackageFlag==1) //接收到数据包
{
Usart1PackageFlag=0;
rUsart_len=wUsart1RxNumber;
wUsart1RxNumber=0;
for(i=0; i<rUsart_len; i++)
rUsartData[i]=bUsart1RxBuff[i];
if(HAL_UART_Transmit_DMA(&huart1,rUsartData,rUsart_len)!=HAL_OK)
{
Error_Handler();
}
HAL_UART_Receive_DMA(&huart1,bUsart1RxBuff,USART1_REC_LEN); //重新打开DMA接收
}
}
/* USER CODE END 3 */
}
3) 修改IDLE中断处理函数
void MyUser_UART_IDLE_IRQHandler(UART_HandleTypeDef *huart)
{
uint16_t ResLen;
if(huart== &huart1) //串口1的处理
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_13); //让灯亮灭,方便判断触发了中断
HAL_UART_DMAStop(&huart1);
ResLen = (uint16_t)__HAL_DMA_GET_COUNTER(&hdma_usart1_rx);// 获取DMA中未传输的数据个数
wUsart1RxNumber = USART1_REC_LEN-ResLen; //得到已经接收的数据个数
if(wUsart1RxNumber)
Usart1PackageFlag=1;
}
}
本篇代码的功能,实际上与上一篇是一样的,测试方法和前两篇完全一样。如本篇开始所说,我曾经使用串口DMA构建了ringbuff的串口环形缓冲区。构建环形缓冲区的基本知识,在本篇中都已经描述过了,有兴趣的话试试吧。
1,175 total views, 1 views today