RS485 双串口通信 + LCD 实时显示(DMA+IDLE 空闲中断版)

目录

一、前言

本篇笔记将介绍串口 UART 开发中效率最优的编程方法 ------IDLE 空闲中断。前文我们依次讲解并实现了串口的查询、中断、DMA 三种通信方式,三种方式各有适配场景但也存在相应的短板,而 IDLE 空闲中断的引入,能完美解决串口数据传输的核心痛点,搭配 DMA 使用更是能将串口接收的稳定性与程序运行效率拉满。本次依旧基于 FreeRTOS 多任务完成开发,结合队列实现数据的安全中转,延续 RS485 双串口通信 + LCD 实时显示的核心功能。

二、三种串口通信方式的局限性

在实际的项目开发中,三种基础串口通信方式都存在各自的局限性,也是我们需要引入 IDLE 空闲中断的核心原因:

  1. 采用查询方式时,若程序执行耗时较长的指令,CPU 无法及时读取接收寄存器的数据,会导致寄存器数据爆满、新数据覆盖旧数据,最终造成数据丢失,这也是多任务系统中极少使用查询方式的核心原因。
  2. 采用中断与 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_RxCpltCallbackHAL_UARTEx_RxEventCallback内,必须完成两个核心操作:

  1. 将 DMA 接收缓存区的有效数据,存入内存 buf(裸机开发)或 FreeRTOS 队列(多任务开发);
  2. 重新使能 DMA+IDLE 的串口接收,保证下一帧数据能正常接收。

四、完整功能代码开发与解析

基于上述的开发逻辑,我们从代码层面实现 DMA+IDLE 空闲中断的串口接收功能,核心开发思路明确:编写串口接收启动函数(创建队列 + 使能 DMA+IDLE)、定义接收缓存区与队列句柄、完善各类中断回调函数(写队列 + 重新使能 DMA)、编写队列读数据函数,发送任务沿用此前的逻辑无需修改。

核心开发步骤梳理

  1. 编写串口 4 接收启动函数:创建 FreeRTOS 队列、初始化 DMA+IDLE 接收;
  2. 定义全局缓存区、队列句柄、状态标志位,供函数间调用;
  3. 完善接收完成回调、空闲中断回调、错误回调:均实现「标志位拉高 + 数据入队 + 重新使能 DMA」;
  4. 编写读队列函数:从队列中取出接收数据,供业务任务调用;
  5. 发送任务函数保持不变,接收任务通过读队列函数获取数据。

完整代码(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 次,因此数据值与计数数值保持同步增长,现象与此前的版本一致,且数据传输无任何丢失、乱码问题。

七、总结

  1. IDLE 空闲中断是判定串口一帧数据接收完成的硬件机制,完美解决串口数据丢失的核心痛点;
  2. DMA+IDLE是串口接收的最优组合,解放 CPU 资源的同时保证数据传输稳定,是项目首选方案;
  3. 中断回调中必须重新使能 DMA 接收,同时配合队列完成数据中转,规避中断耗时;
  4. 中断上下文操作队列,需使用带FromISR的专用函数,是 FreeRTOS 的硬性规范;
  5. 三种串口通信方式各有优劣,结合 IDLE 中断优化后,能适配所有串口开发场景。

八、结尾

至此,我们完成了串口 RS485 通信所有最优方案的闭环学习,从基础的查询、中断、DMA,到最终的 DMA+IDLE 空闲中断,每一次优化都是对 CPU 资源利用与系统稳定性的双重提升,也是嵌入式开发的核心优化思路。这些串口开发的核心逻辑可通用至所有外设,是嵌入式工程师必备的基础能力。感谢各位的阅读,持续关注本系列笔记,后续将带来更多项目实战干货与技术优化技巧,一起夯实技术基础,稳步进阶!

相关推荐
徐子元竟然被占了!!2 小时前
常用端口学习
运维·网络·学习
小乔的编程内容分享站2 小时前
C语言指针相关笔记
c语言·笔记
__万波__2 小时前
STM32L475基于完全空白的项目,完成时钟树初始化配置并验证
单片机·嵌入式硬件
XH华2 小时前
数据结构第九章:树的学习(上)
数据结构·学习
逐步前行2 小时前
SolidWorks2024_装配体实例(桌下抽屉)
笔记
_ziva_2 小时前
Miniconda 下载 + 安装 + VS Code 集成使用教程
笔记
tq10863 小时前
商业环境中的三重生命:自然、职业与组织的平衡模型
笔记
行业探路者3 小时前
健康宣教二维码是什么?主要有哪些创新优势?
人工智能·学习·音视频·二维码·产品介绍
良许Linux3 小时前
STM32F103每个符号的意思是什么?
stm32·单片机·嵌入式硬件