目录
- 一、前言
- 二、三种串口通信方式的局限性
- [三、IDLE 空闲中断原理与对应函数](#三、IDLE 空闲中断原理与对应函数)
- 四、完整功能代码开发与解析
- 五、两种数据接收写法对比
- 六、运行现象
- 七、总结
- 八、结尾
一、前言
本篇笔记将介绍串口 UART 开发中效率最优的编程方法 ------IDLE 空闲中断。前文我们依次讲解并实现了串口的查询、中断、DMA 三种通信方式,三种方式各有适配场景但也存在相应的短板,而 IDLE 空闲中断的引入,能完美解决串口数据传输的核心痛点,搭配 DMA 使用更是能将串口接收的稳定性与程序运行效率拉满。本次依旧基于 FreeRTOS 多任务完成开发,结合队列实现数据的安全中转,延续 RS485 双串口通信 + LCD 实时显示的核心功能。
二、三种串口通信方式的局限性
在实际的项目开发中,三种基础串口通信方式都存在各自的局限性,也是我们需要引入 IDLE 空闲中断的核心原因:
- 采用查询方式时,若程序执行耗时较长的指令,CPU 无法及时读取接收寄存器的数据,会导致寄存器数据爆满、新数据覆盖旧数据,最终造成数据丢失,这也是多任务系统中极少使用查询方式的核心原因。
- 采用中断与 DMA 方式时,虽然能通过中断触发数据接收,无需 CPU 轮询,但我们仅在程序初始化时手动开启一次 IT/DMA 接收;若后续 CPU 被长耗时指令占据,串口持续接收的数据依然无法被及时处理,依旧会出现数据丢失的问题。
常规的解决办法:对中断 / DMA 方式做简单改进,初始化时启动 IT/DMA,在每次接收完成的回调函数中,重新启动对应的 IT/DMA 接收,循环往复保证接收不中断。
而IDLE 空闲中断,则是从底层机制上解决问题的最优解:它是一种专门判断「发送方数据是否全部发送完毕」的硬件机制,当接收方的串口总线上长时间无数据传输时,会自动触发一次空闲中断,精准判定一帧数据接收完成。
三、IDLE 空闲中断原理与对应函数
针对 IDLE 空闲中断,HAL 库提供了专属的增强型串口收发函数,不同串口工作方式对应不同的函数与回调函数,各类方式的函数匹配关系整理如下,也是本次开发的核心函数:
| 方式 | 核心函数 | 对应回调函数说明 |
|---|---|---|
| 查询 | HAL_UARTEx_ReceiveToIdle | 根据返回参数 RxLen 判断:接收缓冲区满完成接收 / 空闲中断触发中止接收 |
| 中断 | HAL_UARTEx_ReceiveToIdle_IT | 缓冲区满完成:HAL_UART_RxCpltCallback;空闲中止:HAL_UARTEx_RxEventCallback |
| DMA | HAL_UARTEx_ReceiveToIdle_DMA | 半满回调:HAL_UART_RxHalfCpltCallback;满帧完成:HAL_UART_RxCpltCallback;空闲中止:HAL_UARTEx_RxEventCallback |
| 错误 | - | HAL_UART_ErrorCallback |
补充:对于纯中断方式而言,引入 IDLE 空闲中断的提升效果有限。因为纯中断接收每收到 1 字节数据就会触发一次中断,该机制本身就相当于替代了空闲中断的作用,能实时响应每一个字节的接收。
对于数据接收场景,DMA+IDLE 空闲中断 的组合能最大化提升程序效率与稳定性。为彻底防止接收数据拥塞、丢失,在对应的回调函数HAL_UART_RxCpltCallback或HAL_UARTEx_RxEventCallback内,必须完成两个核心操作:
- 将 DMA 接收缓存区的有效数据,存入内存 buf(裸机开发)或 FreeRTOS 队列(多任务开发);
- 重新使能 DMA+IDLE 的串口接收,保证下一帧数据能正常接收。
四、完整功能代码开发与解析
基于上述的开发逻辑,我们从代码层面实现 DMA+IDLE 空闲中断的串口接收功能,核心开发思路明确:编写串口接收启动函数(创建队列 + 使能 DMA+IDLE)、定义接收缓存区与队列句柄、完善各类中断回调函数(写队列 + 重新使能 DMA)、编写队列读数据函数,发送任务沿用此前的逻辑无需修改。
核心开发步骤梳理
- 编写串口 4 接收启动函数:创建 FreeRTOS 队列、初始化 DMA+IDLE 接收;
- 定义全局缓存区、队列句柄、状态标志位,供函数间调用;
- 完善接收完成回调、空闲中断回调、错误回调:均实现「标志位拉高 + 数据入队 + 重新使能 DMA」;
- 编写读队列函数:从队列中取出接收数据,供业务任务调用;
- 发送任务函数保持不变,接收任务通过读队列函数获取数据。
完整代码(usart.c 用户代码区)
c
static uint8_t g_uart2_tx_cplt = 0;
static uint8_t g_uart4_rx_cplt = 0;
static QueueHandle_t g_Uart4_Rx_Queue;
static uint8_t g_uart4_rx_buf[100]; // 串口4 DMA接收缓存区
// 串口2发送完成回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart2)
{
g_uart2_tx_cplt = 1;
}
}
// 串口2发送完成等待函数
int wait_uart2_tx_cplt(int timeout)
{
//wait for completed
while(g_uart2_tx_cplt == 0 && timeout)
{
vTaskDelay(1);
timeout--;
}
if(timeout == 0)
return -1;
else
{
g_uart2_tx_cplt = 0;
return 0;
}
}
// 串口4 DMA接收满帧完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart4)
{
g_uart4_rx_cplt = 1;
for(int i = 0; i < 100; i++)
{
xQueueSendFromISR(g_Uart4_Rx_Queue, (const void*)&g_uart4_rx_buf[i], NULL);
}
HAL_UARTEx_ReceiveToIdle_DMA(&huart4, g_uart4_rx_buf, 100); // 重新使能DMA+IDLE
}
}
// 串口4接收完成等待函数
int wait_uart4_rx_cplt(int timeout)
{
//wait for completed
while(g_uart4_rx_cplt == 0 && timeout)
{
vTaskDelay(1);
timeout--;
}
if(timeout == 0)
return -1;
else
{
g_uart4_rx_cplt = 0;
return 0;
}
}
// 串口4 IDLE空闲中断回调函数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart == &huart4)
{
g_uart4_rx_cplt = 1;
for(int i = 0; i < Size; i++)
{
xQueueSendFromISR(g_Uart4_Rx_Queue, (const void*)&g_uart4_rx_buf[i], NULL);
}
HAL_UARTEx_ReceiveToIdle_DMA(&huart4, g_uart4_rx_buf, 100); // 重新使能DMA+IDLE
}
}
// 串口4接收错误回调函数
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart4)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart4, g_uart4_rx_buf, 100); // 异常后重新使能
}
}
// 串口4 DMA+IDLE接收启动初始化函数
void UART4_Rx_Start(void)
{
g_Uart4_Rx_Queue = xQueueCreate(200, 1); // 创建队列:200个节点,每个节点1字节
HAL_UARTEx_ReceiveToIdle_DMA(&huart4, g_uart4_rx_buf, 100); // 使能DMA+IDLE接收
}
// 串口4读队列获取数据函数
int UART4_GetData(uint8_t *pData)
{
xQueueReceive(g_Uart4_Rx_Queue, pData, portMAX_DELAY);
return 0;
}
补充:中断上下文的队列写入,必须使用带
FromISR的 FreeRTOS 队列函数xQueueSendFromISR,不可使用普通的xQueueSend,否则会导致系统异常。
app_freertos.c 任务函数适配
发送任务函数无需修改,接收任务仅需增加调用串口 4 启动函数,并将原 DMA 接收函数替换为自定义的读数据函数即可,核心逻辑不变。
c
extern UART_HandleTypeDef huart4;
extern UART_HandleTypeDef huart2;
extern void UART4_Rx_Start(void);
extern int UART4_GetData(uint8_t *pData);
extern int wait_uart2_tx_cplt(int timeout);
static void CH1_UART2_TxTaskFunction(void *pvParameters)
{
uint8_t c = 0;
while(1)
{
HAL_UART_Transmit_DMA(&huart2, &c, 1);
wait_uart2_tx_cplt(100);
vTaskDelay(1000);
c++;
}
}
static void CH2_UART4_RxTaskFunction(void *pvParameters)
{
uint8_t c = 0;
int cnt = 0;
char buf[100];
HAL_StatusTypeDef err;
UART4_Rx_Start();
while(1)
{
err = UART4_GetData(&c);
if(err == 0)
{
sprintf(buf, "Recv Data : 0x%02x, Cnt : %d", c, cnt++);
Draw_String(0, 0, buf, 0x0000ff00, 0);
}
else
{
HAL_UART_DMAStop(&huart4);
}
}
}
五、两种数据接收写法对比
本次开发中,针对串口 4 的接收功能有两种可用的写法,两种写法均能实现功能,各有优劣,可根据实际项目需求选择,也是嵌入式开发中很重要的选型思路:
✅ 写法一:UART4_GetData + FreeRTOS 队列 【间接接收】
- 核心逻辑:DMA 接收数据 → 中断回调中
xQueueSendFromISR数据入队 → 业务任务中xQueueReceive数据出队 → LCD 显示; - 核心优点:中断上下文仅执行快速入队操作 ,耗时的 LCD 显示等业务逻辑全部放在任务中执行,中断执行时间极短,不会阻塞其他中断响应,系统稳定性拉满,是工业级项目的标准规范写法;
- 核心缺点:代码量稍多,多了一层队列中转的逻辑。
✅ 写法二:直接调用 HAL_UART_Receive_DMA 【直接接收】
- 核心逻辑:DMA 接收数据 → 任务中等待接收完成 → 直接读取变量数据 → LCD 显示;
- 核心优点:代码极简、逻辑直观、改动量小,无需创建队列与编写读写函数,适合简单的功能开发;
- 核心缺点:任务中的
wait_uart4_rx_cplt属于阻塞等待,会占用当前任务的 CPU 时间片;但对于本项目无任何影响(当前任务优先级不高,无高优先级任务抢占需求)。
选型建议
- 追求「极致稳定性、工业级规范、项目可拓展性」→ 保留队列写法;
- 追求「代码简洁、逻辑简单、快速实现功能」→ 直接用
HAL_UART_Receive_DMA,删除队列相关代码即可。
六、运行现象
将编写完成的代码烧录至开发板后,实测运行现象如下,功能正常且数据收发稳定无丢失:

串口 2 依旧按每秒 1 次的频率发送自增字节数据,串口 4 通过 DMA+IDLE 空闲中断稳定接收数据,LCD 屏幕实时打印接收的十六进制数据与计数cnt;由于发送数据与计数均从 0 开始且每秒自增 1 次,因此数据值与计数数值保持同步增长,现象与此前的版本一致,且数据传输无任何丢失、乱码问题。
七、总结
- IDLE 空闲中断是判定串口一帧数据接收完成的硬件机制,完美解决串口数据丢失的核心痛点;
- DMA+IDLE是串口接收的最优组合,解放 CPU 资源的同时保证数据传输稳定,是项目首选方案;
- 中断回调中必须重新使能 DMA 接收,同时配合队列完成数据中转,规避中断耗时;
- 中断上下文操作队列,需使用带
FromISR的专用函数,是 FreeRTOS 的硬性规范; - 三种串口通信方式各有优劣,结合 IDLE 中断优化后,能适配所有串口开发场景。
八、结尾
至此,我们完成了串口 RS485 通信所有最优方案的闭环学习,从基础的查询、中断、DMA,到最终的 DMA+IDLE 空闲中断,每一次优化都是对 CPU 资源利用与系统稳定性的双重提升,也是嵌入式开发的核心优化思路。这些串口开发的核心逻辑可通用至所有外设,是嵌入式工程师必备的基础能力。感谢各位的阅读,持续关注本系列笔记,后续将带来更多项目实战干货与技术优化技巧,一起夯实技术基础,稳步进阶!