上一篇,实验了中断方式的USART接收,其过程是接收缓冲区接收到的的数据长度达到设定字节后调用回调函数进行处理,并开启下一次接收。在实际的工程中很多通信数据的帧长度并不确定,本篇就是要解决这个问题。
USART接收不定长数据的方法主要有两种:
方法1:每次接收一个字节,根据特定字符判断是否接收完成
核心思路
- 单次接收 1 字节:通过
HAL_UART_Receive_IT()
每次只接收 1 个字节,接收完成后进入中断回调函数。 - 循环触发中断:在中断回调中处理当前字节后,重新开启接收中断,实现连续接收。
- 结束标志判断:通过约定的结束符(如
\n
、\r
或特定帧尾)判断数据是否接收完成。 - 缓冲区管理:使用数组缓存接收数据,通过索引跟踪接收位置,防止溢出。
关于方法1的文章很多,不再重复。
方法2:“空闲中断”(IDLE Line Interrupt)配合DMA的接收中断机制
1. 空闲中断(IDLE Line Interrupt)的作用
idle:空闲的。
当串口接收数据时,若连续一段时间(通常为一个字符传输时间的 1.5 倍以上)没有新数据到来,硬件会触发 “空闲中断”。其核心功能是判断一段完整数据帧的结束—— 例如,上位机发送一帧数据后暂停,空闲状态的出现意味着当前帧传输完成。
2. DMA 的接收机制
DMA(直接存储器访问)可在不占用 CPU 的情况下,将串口接收寄存器的数据直接搬运到内存缓冲区。此时,DMA 的作用是自动完成数据的连续接收和存储,避免 CPU 频繁中断来读取单字节数据,极大提升效率。
3. 两者配合的工作流程
- DMA 持续接收:串口启动后,DMA 被配置为循环或单次模式,持续将接收数据写入缓冲区,并记录接收长度(可通过 DMA 计数器或自定义变量实现)。
- 空闲中断触发帧结束:当数据帧传输完毕,串口进入空闲状态,触发空闲中断。此时 CPU 响应中断,通过读取 DMA 记录的接收长度,即可获取当前完整帧的数据量。
- 处理与复位:CPU 在中断服务程序中处理缓冲区中的完整数据帧,处理完成后复位 DMA 和空闲中断标志,准备接收下一帧数据。
4. 优势
- 高效性:DMA 负责数据搬运,CPU 仅在帧结束时被中断,减少 CPU 占用。
- 完整性:空闲中断确保能准确捕获一帧数据的边界,避免数据拆分或拼接错误。
这种机制广泛用于串口通信(如 RS232/485)中,尤其适合不定长数据帧的接收场景。
下面是方法2的实现步骤:
-
设定GPIO和时钟、通信等
-
USART和时钟
在之前的SYM32模板里面,已经设定了USART的管脚、波特率等,时钟也进行了设定,这里不再重复。
-
打开DMA
-
切到 DMA Settings 标签页
-
点击 Add 按钮
-
在弹出的列表里选
- USART1_RX
- Direction = Peripheral To Memory
- Priority = Low(或 Medium,随意)
- Mode = Circular(推荐,防止溢出)
- Data Width = Byte
- Increment = Memory(√)
- 其余默认即可
点 OK 后,会看到列表里出现一行 USART1_RX DMA1 Channel5
(不同芯片通道号不同)。
4 .同样的方法,add一个USART1_TX的DMA channel:
-
打开 USART1 全局中断
确保UART1的全局中断优先级高于DMA。
黄齿轮生成代码。
以下为编程阶段。
-
新建关于串口通信的相关文件
由于之前在创建项目的时候选择了为每个外设生成.c和.h文件:
配置了usart功能后,系统会自动生成相关的文件,在usart.s和usart.h中有关于usart外设的配置,这一点还是比较方便的。
上面的usart.h和usart.c是底层驱动代码,是由STM32CubeIDE托管的文件,一般不用来存放应用层的代码。所以,新建uart_app.c和uart_app.h,所有与uart通信相关的应用层的各种定义和功能函数都放在这两个文件内。
-
在uart_app.h中添加代码:
#ifndef __UART_APP_H
#define __UART_APP_H
#include "stm32f1xx_hal.h" // 根据芯片换成对应 hal 头
#define UART_RX_BUF_LEN 256 // 最大一次接收字节数
extern UART_HandleTypeDef huart1; // 在 usart.h 中声明
void UART_APP_Init(void); // 供 main.c 调用
void UART_APP_IdleCallback(void); // 在 USART1_IRQHandler 里调用
#endif
-
在uart_app.c中添加代码:
#include "uart_app.h"
#include <string.h>
/* 接收缓冲区 -------------------------------------------------------------*/
static uint8_t RxBuf[UART_RX_BUF_LEN];
static uint16_t RxLen = 0; // 当前帧长度
/* 初始化:打开 DMA 接收 + 空闲中断 --------------------------------------*/
void UART_APP_Init(void)
{
/* 使能空闲中断 */
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
/* 启动 DMA 循环接收 */
HAL_UART_Receive_DMA(&huart1, RxBuf, UART_RX_BUF_LEN);
}
/* USART1_IRQHandler 里调用的空闲中断处理 --------------------------------*/
void UART_APP_IdleCallback(void)
{
/* 1. 清 IDLE 标志 */
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
/* 2. 关闭 DMA,计算本次实际接收字节数 */
HAL_DMA_Abort(huart1.hdmarx); // 立即停止 DMA
RxLen = UART_RX_BUF_LEN - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
/* 3. 把收到的数据原样发回去 */
//这里可以是其他任意功能,比如说数据存储、处理、操作GPIO等等,也可以在这里调用自定义的应用层功能函数
if (RxLen > 0)
{
HAL_UART_Transmit_DMA(&huart1, RxBuf, RxLen);
}
/* 4. 重新打开 DMA 接收,继续监听下一帧 */
HAL_UART_Receive_DMA(&huart1, RxBuf, UART_RX_BUF_LEN);
}
-
在stm32f1xx_it.c中添加代码:
-
添加用户include
#include "uart_app.h"
-
修改stm32f1xx_it.c中的USART1_IRQHandler()函数
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 */
/* 如果产生了 IDLE 中断,就调用在uart_app.c中定义的的回调函数 */
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
UART_APP_IdleCallback();
}
/* USER CODE END USART1_IRQn 1 */
}
-
在main.c中添加代码:
-
添加用户include
#include "uart_app.h"
-
在main()函数内添加uart_app的初始化:
UART_APP_Init(); // 打开接收
- 总结一下整体思路:
- uart_app.h内声明了uart初始化函数:UART_APP_Init()和空闲中断回调函数UART_APP_IdleCallback()原型。
- 在uart_app.c中定义了上面的初始化函数和回调函数。
- main.c中运行初始化函数:UART_APP_Init()。
- 在stm32f1xx_it.c中,当检测到了USART1中断,在USART1_IRQHandler()函数(USART1中断的回调函数)中判断当前是否空闲中断(IDLE Line Interrupt),如果是,就调用回调函数UART_APP_IdleCallback()。
- 在回调函数UART_APP_IdleCallback()定义应用函数和实现功能。
-
生成代码和调试,并用仿真软件进行定时发送: