freertos开发空气检测仪之串口驱动与单元测试实践
前言
在本次空气空气检测仪项目中,有使用到串口。使用ai助手完成本篇博文,方便后期复盘回顾。
背景与目标
- 工程:Air_check_App(GD32,FreeRTOS)
- 目标:设计一套可跨芯片复用的串口驱动,快速适配不同 UART 外设与引脚,并通过应用层的单元测试验证可靠性与易用性
- 设计原则:分层清晰、最小可移植面、生产者-消费者模型、FreeRTOS 中断优先级安全
分层设计总览
- CAL(Chip Abstraction Layer) :面向芯片的最小硬件抽象
- 负责时钟/GPIO/USART参数初始化、中断标志处理、可选 DMA 配置
- 代码: cal_uart.c cal_uart.h
- Device Core(设备核心层) :面向上层的统一接口
- 提供 UART_Device 抽象,内部用 FreeRTOS Queue/Mutex 管理数据与并发
- 代码:uart_device.c uart_device.h
- Driver Config(驱动配置层) :设备实例与中断绑定
- 针对具体外设与引脚,配置 CalUartConfig,注册设备,编写 ISR
- 代码: [driver_uart.c](file:///d:/freertos_item/air_check/Air_check_App/User/air_check_device/ModuleDrivers/driver_uart.c)、[driver_uart.h](file:///d:/freertos_item/air_check/Air_check_App/User/air_check_device/ModuleDrivers/driver_uart.h)
- Application/Test(应用/单元测试层) :执行用例、业务验证
- 以任务形式与设备交互,进行 Echo/吞吐/稳定性测试
- 代码: [uart_test.c](file:///d:/freertos_item/air_check/Air_check_App/User/air_check_device/unittest/uart_test.c)、[main.c](file:///d:/freertos_item/air_check/Air_check_App/User/main.c)
关键接口与代码走读
CAL 层
- 初始化:
CAL_UART_Init(cfg, baudrate)- 时钟/GPIO/USART参数/NVIC优先级设置
- 中断使能按是否使用 DMA 区分 RBNE/IDLE
- 中断标志处理:
CAL_UART_IRQHandler(cfg, rx_byte)- 返回 1 表示 RBNE 收到字节;返回 2 表示 IDLE(配合 DMA);返回 0 表示无事件
- 代码参考:cal_uart.c
Device Core 层
- 设备抽象:
struct UART_Device { name, priv_cfg, priv_data, Init/Send/Recv } - 初始化:
UART_Dev_Init- 自动分配运行时数据(Queue/Mutex),调用 CAL 初始化
- 发送:
UART_Dev_Send- Mutex 保护,调用 CAL 层轮询发送
- 接收:
UART_Dev_Recv(首字节阻塞 + 后续短超时批量读)- 先等待首字节(默认 100ms),随后以短等待(默认 5ms)尽可能多地读入剩余字节,返回实际字节数
- 这样既避免"等满固定长度"的假阻塞,又能高效收集成帧数据
- ISR 推送:
UART_Dev_PushRxByte- 在中断中将字节放入队列,并
portYIELD_FROM_ISR
- 在中断中将字节放入队列,并
Driver Config 层
- 示例一:
USART1 (PA2/PA3)设备wifi_uart - 示例二:
UART3 (PC10/PC11)设备uart3 - 注册:
Driver_UART_Init()中统一UART_Device_Register - ISR:
USART1_IRQHandler/UART3_IRQHandler调用 CAL 解析,再把字节推到设备队列 - 代码参考: [driver_uart.c](file:///d:/freertos_item/air_check/Air_check_App/User/air_check_device/ModuleDrivers/driver_uart.c#L12-L29)、[driver_uart.c:UART3 cfg](file:///d:/freertos_item/air_check/Air_check_App/User/air_check_device/ModuleDrivers/driver_uart.c#L31-L51)、[driver_uart.c:ISRs](file:///d:/freertos_item/air_check/Air_check_App/User/air_check_device/ModuleDrivers/driver_uart.c#L79-L99)
Application/Test 层
- 测试任务:
uart_test_task- 获取设备(如
"uart3"),初始化 115200 - 发送欢迎语,循环接收并回显
- 获取设备(如
- 任务创建:
uart_test_start()设置优先级为高优先级(3),保证通信任务可及时运行 - 代码参考: [uart_test.c](file:///d:/freertos_item/air_check/Air_check_App/User/air_check_device/unittest/uart_test.c#L12-L66)
中断优先级与 FreeRTOS 配置要点
- Cortex-M 优先级数值越大,优先级越低
- 设置 NVIC 分组为 Group 4 (抢占4位,子优先级0位),务必在所有 NVIC 配置前调用
- 代码: [main.c](file:///d:/freertos_item/air_check/Air_check_App/User/main.c#L69-L75)
- FreeRTOS 配置需明确
configPRIO_BITS=4,并设置configMAX_SYSCALL_INTERRUPT_PRIORITY = (11 << (8-4)) = 0xB0- 所有调用
FromISRAPI 的中断优先级必须 ≥ 11(如设为 12 更安全)
- 若分组或优先级错误会卡死于
configASSERT( ucCurrentPriority >= ucMaxSysCallPriority ) - 配置参考: [FreeRTOSConfig.h](file:///d:/freertos_item/air_check/Air_check_App/User/FreeRTOSConfig.h#L76-L100)
"接收不到数据"问题复盘
- 现象:中断已触发、日志有欢迎语,但应用层收不到
- 根因:原
UART_Dev_Recv使用"固定长度全阻塞"策略,短报文场景会长期等待凑满 - 修复:改为"首字节阻塞 + 后续短超时快速读",返回当前实际长度,兼顾交互与吞吐
- 代码: [uart_device.c:Recv](file:///d:/freertos_item/air_check/Air_check_Ap/User/air_check_device/device/uart_device.c#L111-L126)
- 备选方案:使用 FreeRTOS StreamBuffer;或提供带超时参数的
RecvWithTimeout
UART3 适配流程
- 新建
CalUartConfig,映射 UART3 与 PC10/PC11 引脚 - 添加设备实例
"uart3",注册于Driver_UART_Init - 编写
UART3_IRQHandler,沿用通用的 CAL/D evice 逻辑 - 测试任务切换设备名为
"uart3"即完成适配
测试步骤
- 根据硬件原理连接:PC10(TX),PC11(RX),波特率 115200
- 运行:看到 "UART Test Start",发送任意数据应回显
- 压测:连续发送数据观察稳定性与丢包率(可扩展统计)
实验现象

现象代码片段
static void uart_test_task(void *pvParameters)
{
struct UART_Device *pDev = GetUARTDevice("uart3");
uint8_t rx_buf[128];
int len;
if (pDev == NULL) {
DBG_log("[UART TEST] Failed to get uart3 device!\n");
vTaskDelete(NULL);
return;
}
/* 初始化: 波特率 115200 */
if (pDev->Init(pDev, 115200) != 0) {
DBG_log("[UART TEST] Failed to init uart3 device!\n");
vTaskDelete(NULL);
return;
}
DBG_log("[UART TEST] Device initialized. Starting loopback test...\n");
/* 发送欢迎信息 */
char *welcome = "UART Test Start\r\n";
pDev->Send(pDev, (uint8_t *)welcome, strlen(welcome));
while (1)
{
/* 尝试接收数据 */
len = pDev->Recv(pDev, rx_buf, sizeof(rx_buf) - 1);
if (len > 0) {
rx_buf[len] = '\0';
DBG_log("[UART TEST] Recv: %s", rx_buf);
/* 回显 */
pDev->Send(pDev, rx_buf, len);
}
/* 稍微延时,避免空转过快 (虽然 Recv 是阻塞的,但如果 Queue 空会立即返回还是阻塞?) */
/* Recv 实现中是阻塞 portMAX_DELAY,所以这里不需要延时,除非 Recv 返回错误 */
/* 但为了保险,如果 len == 0 (超时或其他),我们延时一下 */
if (len <= 0) {
vTaskDelay(pdMS_TO_TICKS(100));
}
}
}
常见坑与建议
- NVIC 分组必须在所有外设初始化前设置为 Group 4
- 中断优先级必须 ≥
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY(推荐 12) - IDLE 中断清除需按数据手册的"读 STAT 后读 DATA"顺序
- 队列长度按峰值流量评估,必要时改用 StreamBuffer/RingBuffer
- 发送用 Mutex 保证线程安全;ISR 推送后注意
portYIELD_FROM_ISR - 任务优先级需要体现"实时性梯队",日志类任务设为低优先级
复盘与经验
- 分层设计降低了移植成本:适配新串口仅需在驱动配置层"加设备"
- 通用的 Device Core 让上层应用不感知芯片差异,专注于业务逻辑
- FreeRTOS 的中断优先级与分组是稳定运行的关键前提
- 面向流式数据的接收策略应避免"固定长度硬阻塞",兼顾交互反馈