HAL库 串口空闲中断+DMA接收不定长数据 详解及踩坑

文章已更新,请移步新文章【STM32 HAL库实战】串口DMA + 空闲中断 实现不定长数据接收


前言

本文需要用到HAL库的HAL_UARTEx_ReceiveToIdle_DMA()函数,如果编辑器提示找不到函数,可以尝试更新HAL库至最新版本。

串口接收不定长数据是串口的常见应用。最近的项目需要用到modbus协议,由于不经常使用HAL库,配置串口接收时遇到了一些问题。在此记录一下,希望能帮助到一些人。

一、串口及DMA基础配置

串口的接收常见的方法有一下两种:
方法一:
传统方法。具体参考:STM32 HAL CubeMX 串口IDLE接收空闲中断+DMA_Z小旋的博客
方法二:
利用HAL库的 HAL_UARTEx_ReceiveToIdle_DMA()函数,代码简洁。
本文采用的是方法二。
打开STM32CubeMX,开始配置程序。

注意要打开串口的全局中断
在这里插入图片描述

打开串口接收DMA,模式选择Normal。

在这里插入图片描述

点击侧边栏的NVIC选项
取消选中 Force DMA channels Interrupts,否则DMA不可自定义DMA中断优先级。

我这里配置成14,防止抢占其他更重要的中断。如果没有这个需要保持默认的0即可。

在这里插入图片描述
后续只需正确配置时钟树,然后即可生成代码。

二、HAL_UARTEx_ReceiveToIdle_DMA()函数功能

函数中会把接收类型设置成HAL_UART_RECEPTION_TOIDLE,然后开启DMA接收,清除一次IDLEF标志位,重新开启IDLEF标志位
在这里插入图片描述

开启标志位后,如果串口中断来临就会执行中断处理函数void USART1_IRQHandler(void)

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
	HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);	//重新开启串口空闲中断和DMA接收,一定要放在这里
  /* USER CODE END USART1_IRQn 1 */
}

HAL_UART_IRQHandler(&huart1)中会判断各种中断类型,并执行对应的操作

主要关注其中关于IDLE中断的部分
首先判断串口的接收类型 ReceptionType,在HAL_UARTEx_ReceiveToIdle_DMA()中已经被设为HAL_UART_RECEPTION_TOIDLE
在这里插入图片描述

清除相关中断的标志位(标志位具体功能对照参考手册查看)

在这里插入图片描述
最后调用事件回调函数HAL_UARTEx_RxEventCallback();

在这里插入图片描述

这个函数默认被定义成__weak 需要我们重新实现

__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size);

三、使用HAL_UARTEx_ReceiveToIdle_DMA()函数

1. 重新实现回调函数HAL_UARTEx_RxEventCallback

这里为了方便演示在main.c中重新实现
uint8_t rx_buffer[BUF_SIZE];  // 创建接收缓存,大小为BUF_SIZE
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1)
    {
        cnt = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
        HAL_UART_Transmit(&huart1, rx_buffer, cnt, 0xffff);	//将接受到的数据再发回上位机
        memset(rx_buffer, 0, cnt);
    }
}

2.调用接收函数

在main函数中,while循环前加入函数 HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);否则无法完成第一次接收。
测试结果如下:
单片机发回了相同的数据。
在这里插入图片描述

四、踩坑

大部分文章中,习惯把HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE); //重新开启串口空闲中断和DMA接收
放在重新实现的函数void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)中,
而不是本文中放在void USART1_IRQHandler(void)中的方式。
也就是
本文:

void USART1_IRQHandler(void)
{
    /* USER CODE BEGIN USART1_IRQn 0 */
    /* USER CODE END USART1_IRQn 0 */
    HAL_UART_IRQHandler(&huart1);
    /* USER CODE BEGIN USART1_IRQn 1 */
    HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, BUF_SIZE);	//本文中选择放置在此处
    /* USER CODE END USART1_IRQn 1 */
}

区别于:

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART1)
    {
        cnt = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
        HAL_UART_Transmit(&huart1, rx_buffer, cnt, 0xffff);
        memset(rx_buffer, 0, cnt);
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);	//其他大部分文章放置于此处
    }
}

放置在回调函数HAL_UARTEx_RxEventCallback中似乎也说的通:产生串口空闲中断时,会调用回调函数,并在其中重新开启串口空闲中断和DMA,等待下一次串口空闲中断来临。但是这样做会不会有bug?
实际测试一下:
首先,串口初始化时波特率配置成 9600bps

串口初始化

测试流程

第一次发送:波特率正确 9600bps 发送消息后收到正确的回复

在这里插入图片描述

第二次发送: 波特率错误 115200bps 发送消息后收不到回复

在这里插入图片描述

第三次发送 波特率改回正确的9600bps 发送后依然收不到回复

在这里插入图片描述

以上就是模拟波特率不小心设置错误的情况,居然产生了严重的问题,即使改回正确的波特率依然无法收到回复。
说明这种做法存在一定的风险。
经过大量的排查,最后确定问题就出在函数 HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE)放置的位置

波特率正确的正常情况下
USART的CR1寄存器中,IDLEIE位打开,ISR寄存器中IDLE位关闭

在这里插入图片描述

在这里插入图片描述

同时对应DMA通道寄存器的情况如下

在这里插入图片描述

目前收发正常

在这里插入图片描述

也能正常进入回调函数

在这里插入图片描述

接着改成错误的波特率,收不到回复

在这里插入图片描述

并且仿真未在回调函数中的断点处停下,说明不再进入回调函数

在这里插入图片描述

查看DMA寄存器,发现DMA没有正确打开(与之前的截图对比)

在这里插入图片描述

查看USART寄存器发现也未正确开启

在这里插入图片描述

在这里插入图片描述

原因

串口波特率错误时,不再进入回调函数。一轮接收结束时,串口空闲中断与DMA均被关闭,而两者的重启在回调函数中通过
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);开启。这就导致波特率错误时,无法接收后续的新数据。

总结

HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);必须放在void USART1_IRQHandler(void)中。
这样即使接收错误,也能重新开启串口空闲中断和DMA,不影响下次接收。

交流探讨

有问题可以在QQ交流群里找我,源代码也放在群文件里。

648551442

### STM32 HAL中实现串口空闲中断并结合DMA进行接收 #### 一、硬件与软件环境配置 为了在STM32 HAL中实现串口空闲中断并结合DMA进行不定长数据接收,需完成以下基础设置: 1. **使能相关外设时钟** 需要通过`__HAL_RCC_USARTx_CLK_ENABLE()`函数启用USART外设的时钟。 2. **初始化GPIO引脚** 将UART/USART的TX和RX引脚配置为复用功能模式,并设置合适的上拉电阻[^1]。 3. **配置串口参数** 使用`MX_USARTx_Init()`函数初始化串口通信参数(波特率、字长、停止位等)。这些参数通常由CubeMX工具自动生成。 4. **开启DMA通道** 设置用于接收数据DMA流或通道。例如,在DMA控制器中分配一个专用的传输请求给USART RX线程。 5. **激活空闲检测机制** 调用宏定义命令如 `__HAL_UART_ENABLE_IT(&huartx, UART_IT_IDLE)` 来启动IDLE状态下的事件触发器[^2]。 --- #### 二、具体实现流程 以下是基于上述理论的具体实施步骤及其对应代码片段: 1. **创建全局变量存储缓冲区地址** 定义两个指针分别指向当前正在使用的DMA缓存区域以及最终保存有效载荷的位置。 ```c uint8_t aRxBuffer[RECEIVE_BUFFER_SIZE]; // 接收缓冲区大小可根据实际需求调整 uint16_t uwReceivedLength; // 记录已接收到的有效字符数量 ``` 2. **编写初始化函数** 下面展示了一个简化版的初始化过程: ```c void MX_DMA_Init(void){ __HAL_RCC_DMAx_CLK_ENABLE(); // 开启DMA模块供电开关 hdma_usart_rx.Instance = DMA_INSTANCE; hdma_usart_rx.Init.Request = DMA_REQUEST_0; hdma_usart_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_usart_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart_rx.Init.PeriphDataAlignment= DMA_PDATAALIGN_BYTE; hdma_usart_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart_rx.Init.Mode = DMA_CIRCULAR; // 循环模式适合连续采集场景 if (HAL_DMA_Init(&hdma_usart_rx) != HAL_OK){ Error_Handler(); } __HAL_LINKDMA(huartx, hdmarx, hdma_usart_rx); // 关联DMA实例到指定串口号 } void MX_USARTx_Init(void){ huartx.Instance = USARTx; huartx.Init.BaudRate = BAUD_RATE; huartx.Init.WordLength = UART_WORDLENGTH_8B; huartx.Init.StopBits = UART_STOPBITS_1; huartx.Init.Parity = UART_PARITY_NONE; huartx.Init.HwFlowCtl = UART_HWCONTROL_NONE; huartx.Init.Mode = UART_MODE_TX_RX; if(HAL_UART_Init(&huartx)!= HAL_OK ){ Error_Handler(); } /* 启动DMA */ HAL_UART_Receive_DMA(&huartx,(uint8_t*)aRxBuffer,sizeof(aRxBuffer)); /* 注册空闲中断服务程序 */ __HAL_UART_ENABLE_IT(&huartx,UART_IT_IDLE); } ``` 3. **设计回调处理逻辑** 当发生空闲信号时会跳转至相应的ISR入口点执行特定操作序列如下所示: ```c void USARTx_IRQHandler(void){ if(__HAL_UART_GET_FLAG(&huartx,UART_FLAG_IDLE)){ // 清除标志位以防误判 __HAL_UART_CLEAR_IDLEFLAG(&huartx); // 停止DMA工作防止覆盖尚未读取的内容 HAL_UART_DMAStop(&huartx); // 获取实际接收到的数据长度 uwReceivedLength=__HAL_DMA_GET_COUNTER(&hdma_usart_rx); uwReceivedLength=(sizeof(aRxBuffer)-uwReceivedLength); // 数据解析部分可以移交给主循环或者单独的任务队列去完成 Process_Data((char *)aRxBuffer,uwReceivedLength); // 继续监听新的消息到来 HAL_UART_Receive_DMA(&huartx,(uint8_t*)aRxBuffer,sizeof(aRxBuffer)); } } ``` 以上即完成了整个系统的搭建框架图解说明. --- ### 注意事项 - 如果项目中有多个优先级不同的任务运行在同一MCU平台上,则建议把耗时较长的操作放到较低级别的调度单元里去做以免影响实时响应性能. - 对于超长时间未结束的情况要考虑加入看门狗定时重启保护措施. ---
评论 66
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值