FreeRTOS中的非阻塞串口实现
在 FreeRTOS 中,如何实现一个 thread-safe 的 printf?
本文实现了一个线程安全的 printf,同时不用阻塞方式 (临界区, 暂停中断等) 挂起其他任务的架构。
同时,usartTx 使用 dma 方式发送,尽可能地降低阻塞。
usartRx 使用了不定长 dma+Idle 中断的方式。
任务架构
为了不挂起其它任务,最有效的方式当然是
- 创建一个串口发送任务
usartTxTask
; - 任务使用消息队列接收打印任务;
- 其它任务需要打印时,向队列中发送数据;
- 由
usartTxTask
直接调用 DMA 发送队列中的数据。
Printf 实现
在 FreertosConfig.h
中提供了定义 configPRINTF( x )
,自行提供
1 | void MyPrintFunction(const char *pcFormat, ... ); |
即可使用 configPRINTF( ("Format") )
打印。
注意由于是宏定义展开,需要使用双括号包裹参数。
MyPrintFunction
的实现依赖可变参数,这里不做展开。
由于 CubeMX 的问题,实现中依赖的 vsnprintf()
似乎是非线程安全的,所以这里添加 mutex 来保证字符串格式化不会出现问题。
详细可见:newlib and FreeRTOS
1 | void MyPrintFunction(const char *pcFormat, ...) |
在 MyPrintFunction
格式化以后,usartTxTask
发送队列中的数据:
1 | void usartTxTask(void *pvParameters) { |
注意这里的 while (HAL_UART...)
等待 DMA 发送,由于 Rx 的 DMA 需要配置为 Circular
模式,所以 HAL_UART_GetState(&hlpuart1)
一定会返回 Rx_Busy
。初始化时也需要注意使用 HAL_UART_AbortReceive(&hlpuart1);
来避免 HAL_UARTEx_ReceiveToIdle_DMA
返回 HAL_Error
。
串口配置
本文使用了 HAL_UARTEx_RxEventCallback
与 Idle
中断,实现了一个双缓冲的不定长串口接收。原始代码来自官方例程 UART_ReceptionToIdle_CircularDMA
。
Idle
中断
DMA 的 Circular 模式会重复搬运 Rx 寄存器的值到内存中,此时只能通过 Rx 的 Idle 状态来判断当前是否接收完成。Idle 中断触发就在 Rx 为空闲时。HAL_UARTEx_RxEventCallback()
这似乎是 Idle 中断专用的回调函数,官方文档没有太多介绍,只有这个例程中有一些介绍。
需要注意的是,HAL_UARTEx_RxEventCallback
的触发时机有三个,需要手动使用 size 来判断当前是什么中断。
详细在例程的 readme 中有写:
1 | Example : case of a reception of 22 bytes before Idle event occurs, using Circular DMA and a Rx buffer |
请更新到 HAL
1.6.1
.1.6.0
的HAL_UART_AbortReceive()
有 Bug,会导致中断回调无法进入。
上板!
Bugs
过程中其实遇到了很多问题:
UART_ReceptionToIdle_CircularDMA
失败。
最后发现是RxBusy
的问题。HAL_UARTEx_RxEventCallback
不触发。
打着断点终于发现HAL_UART_AbortReceive()
会直接更改huart.ReceptionType
到 normal.HAL 1.6.1
版本修复了这个问题,让 Idle 中断能正常触发。Transmit 函数跑飞到
configASSERT( ( portAIRCR_REG & portPRIORITY_GROUP_MASK ) <= ulMaxPRIGROUPValue );
按照prot.c line 766
的指示,调用NVIC_SetPriorityGrouping( 0 );
解决。似乎是 RTOS 和外设直接管理的中断有冲突。改用 G474 的
lpuart1
,串口不响应。
参照手册,lpuart1
硬连接到板载 STLINK,此时需要使用 VCOM 模式让 STLINK 模拟串口。也就是,在 CubeMX 中开启 Human Machine Interface 的 VCOM ,然后当作普通串口使用即可。任务占用的 Heap 异常高。
这个问题还没有解决,似乎是打印时使用的vsnprintf()
占用了大量 heap。一个stack==128
的任务居然占用了 912 字节。Rx 任务 Buffer 在接收慢一次
似乎是中断中vTaskNotifyGiveFromISR()
函数执行后中断函数会继续执行,导致 RxTask 中得到的是交换后的旧 Buffer。重写了一下HAL_UARTEx_RxEventCallback()
的缓冲交换逻辑解决。
验证
串口最后打印
1 | LED2 Walking! |
分别来自
1 | void ledToggle(void* x)//tskIDLE_PRIORITY |
完整的配置代码会在某个时间之后开源在 singledog957/NUEDC2025
使用了不同的字符串,因为之前 DMA 混合了发送数据,导致 Debug 时的 LED1 Init
和 LED2 Init
都被混合成了 LED2 Init
其它
这是第一次实际上板使用 FreeRTOS
,其实一开始还遇到了很多问题,比如 CMSIS FreeRTOS 的封装函数和原生函数的混用、Heap 不够分配失败等等。
在操作系统中,实时性被放大,任务间的同步和异步需要更谨慎地考虑。怎么在有限的资源内发挥出极致的性能,需要的是设计者对全局的把握,也如艺术一般,象征意义远大于实际意义。
再有就是不要对库函数抱有绝对地信任或者畏惧。代码都是人写的,机器永远是对的。
- Title: FreeRTOS中的非阻塞串口实现
- Author: SingleDog
- Created at : 2025-07-09 16:57:00
- Updated at : 2025-07-09 22:16:55
- Link: https://www.singledog233.top/RTOS_USART/
- License: This work is licensed under CC BY-NC-SA 4.0.