前言
本文的参考资料
感谢提供标准库版本的CSDN同学:这两篇文章至少是我看过的最详细的标准库配置DMA版本。而且代码实测稳定能用。
STM32 | DMA配置和使用如此简单(超详细)_。。。| 。。。的博客-CSDN博客_stm32dma配置
STM32 | 串口DMA很难?其实就是如此简单!(超详细、附代码)_。。。| 。。。的博客-CSDN博客
感谢这些同学提供的HAL库版本参考资料:
STM32 串口实现不定长数据接收(亲测有效,附代码)_不如去睡觉的博客-CSDN博客_stm32串口不定长数据接收
STM32 HAL UART DMA不通的问题解决及注意事项_PegasusYu的博客-CSDN博客_hal_uart_transmit_dma发不出去
HAL库的DMA发送问题_三境界的博客-CSDN博客_hal库dma发送
STM32F4 HAL库 串口 DMA正常模式仅发一次问题?_KK.m的博客-CSDN博客_stm32串口dma只能发送一次
阅读须知
- 在阅读本文之前,建议参照标准库参考链接第一个认真理解DMA串口收发的原理(因为作者的代码就是从标准库到HAL库移植的),本文因为篇幅有限恕不详述,重点放在介绍HAL库下DMA的配置使用。如果有条件的同学可以认真学习标准库参考链接第二个先学习如何使用标准库函数完整实现DMA串口配置,再来阅读本文会舒服很多。
- 意法半导体在DMA功能上对HAL库的封装并不如标准库那么简单明了,效果也比标准库逊色一些。有的时候遇到数据发不出去或者其他令人抓狂的情况,建议利用好身边的在线调试器,在出现故障的地方设下断点反复调试,总结经验并订正导致代码不稳定的地方。
配置环境
编译器:Keil uVision 5.29
调试用平台:正点原子mini开发板(stm32F103RCT6)
低代码框架生成:STM32CubeMX 6.4.0
关键词:串口+DMA;不定长数据传输;中断
预备工作
DMA硬件理论
我们究竟为啥要用DMA,正常的串口中断接收不好么?我自己的理解是这样子的:
- 正常情况下,像正点原子的官方例程,在没有DMA的时候,他的不定长数据接收逻辑是在上位机发送的数据最后一定加上一个回车符(所谓的\r\n),通过回车符来判断是不是到了数据末尾。后面做项目的时候,你会发现许多的数据(比如一些外接设备的浮点数收发)最后是不带回车符的,或者带多个回车符,那怎么办?
- 轮询串口内容?我都为CPU感到疲劳。而且轮询是定时周期的,也就意味着数据的收发周期无法改变(,可能还需要占用一个宝贵的定时器)。那遇到一些会改变数据发送周期的外接设备怎么办?
- 使用串口空闲中断判断。你说的没错,其实DMA实现不定长接收也是通过串口空闲中断来实现的。那就要说出DMA另一个优势了:
- 俗话说:“条条大路通罗马。”但不是每条道路(数据处理的指令)都是一路畅通到罗马(CPU)。通向CPU的道路有的十字路口众多(总线),当然有人在负责仲裁(CPU负责总线仲裁的部分),有的在不定期施工(其他中断),有的尚未开放。CPU搬运数据,一般来说,要经过十字路口,在等完比自己优先级高的货物走完(其他外设和内存的读写)后的情况下达“你过来呀”的指令,然后从串口开始搬东西,期间还要等一些优先级比自己高的施工完毕,才能将数据搬到你要的位置(数组或者其他)。然后才是你读取那个位置的数据。
- 直接存储器访问(DMA)提供了另一种思路。开辟一条道路给串口和你要的位置直连,不用经过CPU仲裁,也不用在中断里面搬运数据,而是让串口直接“装”数据到你要的位置,“串口-CPU-中断-你要的位置”删掉了两个环节,也防止了中断嵌套导致的潜在不稳定性(打断数据传输导致数据丢失,或者程序直接跑飞)。
- 总结起来,就是节省时间,提高稳定性。十分官方的解释就是(摘抄自正点原子HAL库手册):DMA,全称为 Direct Memory Access ,即 直接存储器访问 。 DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路, 能 使 CPU 的效率大为提高。
这段内容我个人推荐你阅读标准库参考链接第一个红圈部分里面的内容,因为不管是HAL库还是标准库,说到底都是操作stm32的寄存器来实现功能,作为DMA知识的引入的话,这篇文章讲的已经足够详细了。
![](https://i-blog.csdnimg.cn/blog_migrate/d2afcf0ad79bca13a292c86ee2059e52.png)
DMA的硬件配置主要就是要注意下DMA各个通道与外设的对应关系。我东施效颦,贴几张别人的图片,简略的讲解一下。
(1)DMA1控制器
从外设(TIMx[x=1、2 、3、4] 、ADC1 、SPI1、SPI/I2S2、I2Cx[x=1、2]和USARTx[x=1、2、3])产生的7个请求,通过逻辑或输入到DMA1控制器,这意味着同时只能有一个请求有效。参见下图的DMA1请求映像。
外设的DMA请求,可以通过设置相应外设寄存器中的控制位,被独立地开启或关闭。
DMA1 请求映像
各个通道的DMA1请求一览
(2)DMA2控制器
从外设(TIMx[5、6、7、8]、ADC3、SPI/I2S3、UART4、DAC通道1、2和SDIO)产生的5个请求,经逻辑或输入到DMA2控制器,这意味着同时只能有一个请求有效。参见下图的DMA2请求映像。
外设的DMA请求,可以通过设置相应外设寄存器中的DMA控制位,被独立地开启或关闭。
注意: DMA2控制器及相关请求仅存在于大容量产品和互联型产品中。
DMA2 请求映像
各个通道的DMA2请求一览
要想让不同的外设能够使用DMA方式来处理数据,要根据这个表格使能对应的DMA通道才行。好消息是意法半导体也想到了这一点,所以在下文的配置中你可以惊喜的发现只需要配置外设并在DMA setting中使能DMA就可以了,通道由CubeMX自动设定,不需要再查这张表。
配置工作
配置的基本流程其实和标准库的流程相差无几。
- 配置串口各项参数,配置DMA
- 打开UART串口全局中断和打开DMA全局中断
- ///我是在cubemx配置和点击代码生成,在生成后的代码写功能实现的分界线///
- 封装dma发送功能并加入等待逻辑
- dma发送完成中断中实现等待状态解除功能(别忘了发送完成也算串口空闲哦~)
- 在串口全局中断函数里面实现空闲中断判定,停止DMA接收后实现双缓存轮流存储串口的不定长数据
- 数据处理完之后重新启用DMA接收
- 在主函数里面启用上面提及的所需中断和第一次DMA接收
- 烧录板子验证效果
过程详解
CubeMX生成
在配置DMA和串口前的准备工作(给从标准库刚刚迁移到HAL库的同学看的)
正常情况下的开发板都配置了一颗高速度的外部晶振,需要你在RCC选项卡手动打开,这和正点原子有一些不同。HSE(高速外部时钟):石英晶振。这样子才能让开发板工作在类似于正点原子所有例程的72MHz的系统时钟频率下。
然后在时钟树里面,更改系统时钟为72MHZ(如果刚才没改的话,那么极限就是64MHZ),更改时钟频率会有提示说是否让CubeMX决定时钟路径,点是即可。
记得把SYS选项卡中的Debug改成Serial Wire(如果你用的调试器是SWD的话),至少不能说禁用,不然程序写上去就不能调试和重新写数据咯~
定义自己uvision工程的名字。选择文件存储的路径。注意这两者都要避免有任何中文。
将IDE改成MDK-ARM,版本改成你用的(最接近的)Keil那个版本。
串口与DMA配置
串口在Connectivity(通讯)分支下面。此次以串口1为例子。模式调节为异步(两线)通讯,参数设置从上到下依次是波特率,字节长度,校验和,停止位,按照你正常的习惯设置即可。
切换到DMA配置选项卡,刚开始的时候这里是一片空白,点击添加并修改DMA请求的类别。
添加完Rx Tx两个通道之后,点一下其中一个。我们可以看到这些选项。
DMA模式(Mode): 分为两个。两个通道都选择Normal正常模式即可,因为我们收发数据都是处理完再准备下一次。
- DMA_Mode_Normal(正常模式)
一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次- DMA_Mode_Circular(循环传输模式)
当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。 也就是多次传输模式
自增地址(Increment Address): Peripheral外设和Memory内存只有一个是可以更改的,两个通道都是这样。记得勾选上。我们发送串口数据的时候,发送完一个字节,DMA位置的地址交给硬件向前移动就可以了。
指针递增模式
外设和存储器指针在每次传输后可以自动向后递增或保持常量。当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值
数据长度(Data Width): 每次操作的数据长度。两个通道的Peripheral外设和Memory内存都是Byte字节。
这是两个通道的DMA请求优先级。建议是可以提高一些(虽然就启用了两个DMA,没啥鸟用)优先级,两个通道保持一致。
优先级管理采用软件+硬件:
- 软件:每个通道的优先级可以在DMA_CCRx寄存器中设置,有4个等级
最高级>高级>中级>低级- 硬件:如果2个请求,它们的软件优先级相同,则较低编号的通道比较高编号的通道有较高的优先权。比如:如果软件优先级相同,通道2优先于通道4
配置完之后切换到NVIC设置中。可以看到DMA全局的中断默认勾选且不可以关闭。我们只要打开串口全局中断即可。
切到NVIC选项卡。和标准库的参考文章一样,这里我们需要注意一下DMA的中断优先级是要高于串口中断的优先级的,所以记得在优先级里面改过来。
PS1:勾线根据主/副优先级排序,可以更直观的看到各个中断的优先级情况。
PS2:我这个是调了4位主优先级的情况(给FreeRTOS用的),如果是别的中断分组记得根据自己设置的中断分组来自己调节顺序就好。
呼,设置完了。可我们的工作才刚刚开始~点击生成代码吧。
DMA发送
在写入收发逻辑之前,我们需要一些准备工作。收发部分是完整的从标准库参考链接第二个移植过来的,讲解的顺序也会按照这个顺序来。
我们主要在stm32f1xx_it.h/c(官方代码框架的中断逻辑部分)完成我们的工作。
首先,我们要先定义三个缓冲区(作全局定义),一个发送缓冲区,两个接收缓冲区,两个接收缓冲区是为了做双缓冲区,目的是为了防止后一次传输的数据覆盖前一次传输的数据,并且留出足够的时间让CPU处理缓冲区数据。双缓冲在串口DMA中有着很重要的意义并起着很大的作用!
在main.c里面,CubeMX已经定义好了UART和DMA的句柄。
UART_HandleTypeDef huart1;//这个不用我说吧;-)
DMA_HandleTypeDef hdma_usart1_tx;//DMA用于串口发送的通道句柄。相比记忆通道编号而言,记忆句柄就方便多了。
DMA_HandleTypeDef hdma_usart1_rx;//DMA接收句柄。
下面的代码声明了我们要用的一些全局变量。记得是在stm32f1xx_it.c的USER code定义区域定义哦~
/* USER CODE BEGIN 0 */
uint8_t USART1_TX_BUF[MAX_TX_LEN]; // my_printf的发送缓冲,下文详述其作用。
volatile uint8_t USART1_TX_FLAG = 0; // USART发送标志,启动发送时置1,加volatile防编译器优化
uint8_t u1rxbuf[MAX_RX_LEN]; // 数据接收缓冲1
uint8_t u2rxbuf[MAX_RX_LEN]; // 数据接收缓冲2
uint8_t WhichBufIsReady = 0; // 双缓存指示器。
// 0:u1rxbuf 被DMA占用接收, u2rxbuf 可以读取.
// 0:u2rxbuf 被DMA占用接收, u1rxbuf 可以读取.
uint8_t *p_IsOK = u2rxbuf; // 指针——指向可以读取的那个缓冲
uint8_t *p_IsToReceive = u1rxbuf; // 指针——指向被占用的那个缓冲
//注意定义的时候要先让这两个指针按照WhichBufIsReady的初始状态先初始化一下。下文详述为什么要这样子。
/* USER CODE END 0 */
你需要在stm32f1xx_it.h补充相关的宏定义,要包含的头文件,需要extern的变量和我们要用的函数声明。
/* USER CODE BEGIN Includes */
#define MAX_RX_LEN (256U) // 一次性可以接受的数据字节长度,你可以自己定义。U是Unsigned的意思。
#define MAX_TX_LEN (512U) // 一次性可以发送的数据字节长度,你可以自己定义。
#include "stdio.h"
#include "string.h"
#include <stdarg.h>
//包含仿printf需要的头文件
/* USER CODE END Includes */
/* Exported types ------------------------------------------------------------*/
/* USER CODE BEGIN ET */
/* USER CODE END ET */
/* Exported constants --------------------------------------------------------*/
/* USER CODE BEGIN EC */
extern uint8_t *p_IsOK;
extern uint8_t *p_IsToReceive;
/* USER CODE END EC */
/* Exported macro ------------------------------------------------------------*/
/* USER CODE BEGIN EM */
/* USER CODE END EM */
/* Exported functions prototypes ---------------------------------------------*/
//此处省略CubeMX输出的中断函数声明……
/* USER CODE BEGIN EFP */
void DMA_USART1_Tx_Data(uint8_t *buffer, uint16_t size);//数组发送串口数据
void my_printf(char *format, ...);//仿制printf发送串口数据
void USART1_TX_Wait(void);//发送等待函数
/* USER CODE END EFP */
需要在main.h补充一下这个:
/* USER CODE BEGIN Includes */
#include "stm32f1xx_it.h"//包含上面的东西,不然主函数用到*p_IsToReceive会报错。
/* USER CODE END Includes */
发送数据上有两种形式,一种是以数组的形式发送,此情况下要知道数组有效元素的个数;另一种就是类似“printf”的形式,此形式可以基于第一种情况稍作修改。在标准库里面,我们需要进行这样子的操作:
但在HAL里面,意法半导体“贴心”地给我们直接准备了一个函数。(为什么是打了引号,下文会讲……)
HAL_UART_Transmit_DMA(&huart1, buffer, size)
从左到右分别是串口HAL句柄,接收数据用的数组,一次性要发送的字节数目。
普通数组发送模式
在标准库函数里面,代码是这样子的。
void DMA_USART2_Tx_Data(u8 *buffer, u32 size)
{
while(USART2_TX_FLAG); //等待上一次发送完成(USART2_TX_FLAG为1即还在发送数据)
USART2_TX_FLAG=1; //USART2发送标志(启动发送)
DMA1_Channel7->CMAR = (uint32_t)buffer; //设置要发送的数据地址
DMA1_Channel7->CNDTR = size; //设置要发送的字节数目
DMA_Cmd(DMA1_Channel7, ENABLE); //开始DMA发送
}
但在我们这里,画风突变:
void DMA_USART1_Tx_Data(uint8_t *buffer, uint16_t size)
{
USART1_TX_Wait(); // 等待上一次发送完成(USART1_TX_FLAG为1即还在发送数据)
USART1_TX_FLAG = 1; // USART1发送标志(启动发送)
HAL_UART_Transmit_DMA(&huart1, buffer, size); // 发送指定长度的数据
}
有标准库的同学会问了:为什么不用开关DMA呀?HAL库帮我们封装了DMA的使能和失能函数在发送函数里面了,所以这些交给HAL去处理就可以了。回想当时我刚开始移植的时候还傻傻的用上了这两个函数,结果发现就是画蛇添足。
__HAL_DMA_DISABLE(&hdma_usart1_rx);
__HAL_DMA_ENABLE(&hdma_usart1_rx);
DMA的开关是简化了,但这为后面遇到的一个bug埋下了伏笔。
细心的同学会发现我在发送之前定义了一个等待函数,而且等待的方式是重新定义的,和标准库函数不一样。为什么不直接用官方的HAL函数,而是要重新封装一个发送函数呢?
在某些场合,你可能需要用到这样子:
DMA_USART1_Tx_Data("A!", strlen("A!"))//发送一个 A! 给上位机
DMA_USART1_Tx_Data("B!", strlen("B!"))//发送一个 B! 给上位机
假如你使用了官方的HAL函数,而不是重新封装一个带等待的发送函数,那么你会惊喜地发现你只能发送一个 A! 出去,只要不用其他把这两个函数隔开,你就甭想发出去 B! 。道理很简单,意法半导体在封装HAL的时候同时考虑了此次发送的时候上一次发送有没有完成的判断逻辑。如果上一次没有发送完就再发送一次,这一次的发送请求会被直接忽略掉。
那么我们的USART1_TX_FLAG就派上用场辽。发送的时候先置个1,发送完了在DMA发送完成中断里面把他变回0不就可以啦~这样子就能保证每次发送都是在通道空闲的情况下进行的。stm32f1xx_it.c里面找到DMA通道4的中断函数:
/**
* @brief This function handles DMA1 channel4 global interrupt.
*/
void DMA1_Channel4_IRQHandler(void)//嘿嘿,发送通道对应DMA4,表格还是要好好记一记的
{
/* USER CODE BEGIN DMA1_Channel4_IRQn 0 */
if (__HAL_DMA_GET_FLAG(&hdma_usart1_tx, DMA_FLAG_TC4) != RESET) //数据发送完成中断
{
// __HAL_DMA_CLEAR_FLAG(&hdma_usart1_tx, DMA_FLAG_TC4);
// 这一部分其实在 HAL_DMA_IRQHandler(&hdma_usart1_tx) 也完成了。
__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除串口空闲中断标志位,发送完成那么串口也是空闲态哦~
USART1_TX_FLAG = 0; // 重置发送标志位
huart1.gState = HAL_UART_STATE_READY;
hdma_usart1_tx.State = HAL_DMA_STATE_READY;
__HAL_UNLOCK(&hdma_usart1_tx);
// 这里疑似是HAL库函数的bug,具体可以参考我给的链接
// huart1,hdma_usart1_tx 的状态要手动复位成READY状态
// 不然发送函数会一直以为通道忙,就不再发送数据了!
}
/* USER CODE END DMA1_Channel4_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_usart1_tx);
/* USER CODE BEGIN DMA1_Channel4_IRQn 1 */
/* USER CODE END DMA1_Channel4_IRQn 1 */
}
其中把句柄状态还原为ready那一部分代码要好好注意一下。HAL库发送函数在发送之前检查通道是否忙是通过检查句柄里面定义的state成员元素来实现的。因为不明原因在发送前state成员元素会被变成busy,但发送后并不会自动回位,需要用户自己手动操作一下。
那么为啥我和标准库版本的等待逻辑是不一样的呢?其实USART1_TX_Wait() 的定义是这样子的(记得自己在USER CODE自己加上这段代码):
void USART1_TX_Wait(void)
{
uint16_t delay = 20000;
while (USART1_TX_FLAG)
{
delay--;
if (delay == 0)
return;
}
}
如果接触过郭老师51单片机的同学可能知道,这是等待的超时机制,超时自动退出等待并强制执行。在极端条件测试的时候,如果单纯只是等待,每次发送的时候都会有一定的延时,延时不断的累加,一旦延时严重到发送完成还没来得及复位USART1_TX_FLAG=0就被拉去再发一次数据,程序就会死在while (USART1_TX_FLAG)直接不动弹了。解决的方法要么是上操作系统确保任务调配的顺序合理,要么就是设置超时退出机制,当然这是以偶尔的数据传输失败为代价的,但保证了整个程序的稳定性。
类似printf形式发送数据
自己定义带别名的printf最大的好处是可以同时多个串口使用printf方式发送,而不会局限于fput单个定义的printf之中。
这一段和标准库函数参考资料的差不多,其实就是直接移植过来的,改了一下标准库函数而已。记得放在stm32f1xx_it.c的USER code里面。
void my_printf(char *format, ...)
{
//VA_LIST 是在C语言中解决变参问题的一组宏,
//所在头文件:#include <stdarg.h>,用于获取不确定个数的参数。
va_list arg_ptr;//实例化可变长参数列表
USART1_TX_Wait(); //等待上一次发送完成(USART1_TX_FLAG为1即还在发送数据)
va_start(arg_ptr, format);//初始化可变参数列表,设置format为可变长列表的起始点(第一个元素)
// MAX_TX_LEN+1可接受的最大字符数(非字节数,UNICODE一个字符两个字节), 防止产生数组越界
vsnprintf((char *)USART1_TX_BUF, MAX_TX_LEN + 1, format, arg_ptr);
//从USART1_TX_BUF的首地址开始拼合,拼合format内容;MAX_TX_LEN+1限制长度,防止产生数组越界
va_end(arg_ptr); //注意必须关闭
DMA_USART1_Tx_Data(USART1_TX_BUF, strlen((const char *)USART1_TX_BUF));
// 记得把buf里面的东西用HAL发出去
}
DMA接收(带双缓冲)
说到接收数据,大家应该知道定长数据和不定长数据吧。实际应用中,如果你使用某传感器模块,一般传感器输出的数据包长度是固定,这就是定长数据;但使用中,我们也可能接收不定长数据,而且是很大可能,正如前面我介绍发送数据一样,我们我们输出的数据长度随时都会变化,这时候就是不定长数据了。本文限于篇幅只讲解不定长数据接收的工作,相信你在读懂全文之后,也能根据我给的标准库参考资料移植得到定长数据的接收函数。
下面一段话几乎照搬原文:
介绍如何使用串口DMA接收数据前,先得讲解双缓冲!双缓冲非常重要,如果接收中断间隔时间非常短(即发送数据帧的速率很快),MCU来不及处理此次接收到的数据,又产生中断,这时不能直接开启DMA通道,否则数据会被覆盖。有2种方式解决。
在重新开启接收DMA通道之前,将DMA_Rx_Buf缓冲区里面的数据复制到另外一个数组中,然后再开启DMA,然后马上处理复制出来的数据。
建立双缓冲,设置一个缓冲区标志(用来指示当前处在哪个缓冲区),每完成一次传输就切换一下被占用地址和就绪地址指针指向的实际数据缓冲数组,下次传输数据就会保存到新的缓冲区中,可以通过自定义缓存区标志来判断和切换,这样可以避免缓冲区数据来不及处理就被覆盖的情况,也能为处理数据留出更多地时间(指到下次传输完成)。
话不多说,先上代码。
/**
* @brief This function handles USART1 global interrupt.
*/
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
if (RESET != __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
{ // 我记得好像HAL库里面没有给串口空闲中断预留专用的回调函数 qaq
// __HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 这一部分其实在 HAL_UART_IRQHandler(&huart1) 也完成了。
HAL_UART_DMAStop(&huart1); // 把DMA接收停掉,防止速度过快导致中断重入,数据被覆写。
uint32_t data_length = MAX_RX_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
// 数据总长度=极限接收长度-DMA剩余的接收长度
if (WhichBufIsReady) //WhichBufIsReady=1
{
p_IsOK = u2rxbuf; // u2rxbuf 可以读取,就绪指针指向它。
p_IsToReceive = u1rxbuf; // u1rxbuf 作为下一次DMA存储的缓冲,占用指针指向它。
WhichBufIsReady = 0; //切换一下指示器状态
}
else //WhichBufIsReady=0
{
p_IsOK = u1rxbuf; // u1rxbuf 可以读取,就绪指针指向它。
p_IsToReceive = u2rxbuf; // u2rxbuf 作为下一次DMA存储的缓冲,占用指针指向它。
WhichBufIsReady = 1; //切换一下指示器状态
}
从下面开始可以处理你接收到的数据啦!举个栗子,把你收到的数据原原本本的还回去
DMA_USART1_Tx_Data(p_IsOK,data_length);//数据打回去,长度就是数据长度
///不管是复制也好,放进去队列也罢,处理你接收到的数据的代码建议从这里结束
memset((uint8_t *)p_IsToReceive, 0, MAX_RX_LEN); // 把接收数据的指针指向的缓冲区清空
}
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN); //数据处理完毕,重新启动接收
/* USER CODE END USART1_IRQn 1 */
}
就连怎么计算数据长度都是标准库函数移植的:
因为接收的是不定长数据,所以必须求出数据长度,这里就用了个很巧妙的方法!DMA通道x传输数量寄存器(DMA_CNDTRx)在通道开启后该寄存器变为只读,指示剩余的待传输字节数目。寄存器内容在每次DMA传输后递减。所以用总缓冲区大小 - 剩下缓冲区大小即可求出使用掉的缓冲区大小,也就是接收数据的长度。注意标准库函数返回剩余缓冲区大小的函数是DMA_GetCurrDataCounter(),而HAL库是使用*__HAL_DMA_GET_COUNTER(&hdma_usart1_rx)*罢了,本质都是读取DMA_CNDTRx。
这段代码和标准库函数最大的区别就是DMA的失能和重新使能不是使用DMA_Cmd(XXX, XXX )而是使用了HAL_UART_DMAStop(&huart1)和HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN) ,HAL的receive函数兼有切换接收缓冲和接收使能的作用,这点要注意。
细心的同学可能发现我用的指针都是全局变量,而标准库函数版本是用的一个局部变量,这是因为我们在main() 里面还会用到一次占用指针来初始化函数HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN)。第二个是我们并不需要手动清除IDLE标志位,USART_ReceiveData(USART2) 也不用(其实就是库函数里面通过读一次串口来消除标志位,详见参考资料),因为HAL库中HAL_UART_IRQHandler(&huart1) 会帮我们处理掉串口的所有标志位。第三个是其实我在DMA通道发送完成中断中手动清除了IDLE标志位,因为发送完成,串口也是空闲态哦~但这个时候可不是完整收到数据的时候。
最后在main函数里头,我们要做最后的初始化工作:
/* USER CODE BEGIN 2 */
//__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
// 这一段其实是有争议的,有人说手册讲了如果RXNE接收非空中断没有使能,那么IDLE中断无效
// 但我试了一下关掉,不会这样子,所以就没鸟他
// 开启串口1空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
// 开启DMA发送通道的发送完成中断,才能实现封装发送函数里面的等待功能
__HAL_DMA_ENABLE_IT(&hdma_usart1_tx, DMA_IT_TC);
// 清除空闲标志位,防止中断误入
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 立即就要打开DMA接收
// 不然DMA没有提前准备,第一次接收的数据是读取不出来的
HAL_UART_Receive_DMA(&huart1, p_IsToReceive, MAX_RX_LEN);
/* USER CODE END 2 */
问题解析
Q1:为什么我的串口压根就没有反应?
- 认真检查串口中断,DMA中断有没有打开,在main函数里面有没有加上中断使能代码。
- 检查一下main函数里面串口的初始化程序的这一部分:
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
……
如果你发现代码和我的不一样,DMA初始化放在了UART串口初始化的后面,恭喜你又踩到了HAL的一个bug。DMA必须先于UART初始化才能成功,虽然我也不知道为什么。
偷懒的方法是在main函数里面直接位置对调一下,一劳永逸的方法是在这里修改一下。
点击初始化函数对应的那一行,然后用上移键和下移键把DMA调到UART前面即可。
Q2:有时候会出现串口信息发送不全的情况。
检查两个地方:宏定义和等待函数。
宏定义有问题一般表现为发送的数据末尾丢失。
#define MAX_RX_LEN (256U) // 接收的最长限制,如果你是接收完之后立马返回给上位机,这里要看一看,特别是测试的时候喜欢搞巨长无比的字符串的同学。
#define MAX_TX_LEN (512U) // 发送的最长限制,如果发送的数据太长这里就要改大
等待函数有问题一般表现为发送的数据中间或者开头丢失,末尾却好好的。
void USART1_TX_Wait(void)
{
uint16_t delay = 20000;
//这里的delay可以根据你发送的数据长度动态调节,如果中间断片建议让delay数值更大,
//给更多的时间进行发送。只要最后系统不会卡死就好。
while (USART1_TX_FLAG)
{
delay--;
if (delay == 0)
return;
}
}
后记
- 如果不是使用自定义的标志位来操作,而是使用DMA自带的标志位来判断,效果可能会好一些。
- 数据吞吐量大的场合要么上操作系统,要么搞DMA降低压力,要么用网口之类有更成熟接收协议的通讯渠道
- 参考程序代码下载链接: STM32DMA串口不定长数据收发+FreeRTOS操作系统参考代码
- 看完之后能用上,点赞收藏是美德~