4. RTOS基础与移植:FreeRTOS任务调度、队列与信号量、内存管理、移植到目标芯片
好,咱们进入第四章。这一章我打算聊聊RTOS,具体说就是FreeRTOS。很多做蓝牙耳机的朋友,一开始都是跑裸机,一个超级循环搞定所有事。但做到后面你会发现,蓝牙协议栈、音频编解码、按键处理、LED指示……这些东西搅在一起,裸机根本扛不住。这时候,你就需要一个RTOS来帮你理清头绪。
我个人习惯,做蓝牙耳机项目,首选就是FreeRTOS。为什么?因为它轻量、稳定、文档全,而且市面上绝大多数蓝牙芯片都支持它。说白了,它就是嵌入式界的Linux——虽然不是最炫的,但绝对是最靠谱的。
4.1 任务调度:别让你的CPU闲着
任务调度是RTOS的核心。FreeRTOS用的是抢占式调度,优先级高的任务先跑。你想想看,蓝牙协议栈的中断服务必须及时响应,音频数据流不能断,按键消抖又不能太急……这些任务优先级怎么定?
我在项目中遇到过一个问题:蓝牙连接事件优先级设得太高,结果音频解码任务一直被抢,声音断断续续。后来我把音频解码任务的优先级提了一级,问题就解决了。嗯,这里要注意:优先级不是越高越好,够用就行。
任务创建很简单,看代码:
// 创建一个蓝牙协议栈处理任务
xTaskCreate(
bluetooth_task, // 任务函数
"BT Task", // 任务名称
512, // 栈深度(单位:字)
NULL, // 参数
3, // 优先级(数值越大优先级越高)
NULL // 任务句柄
);
栈深度怎么定?我一般先给个512,跑起来后用uxTaskGetStackHighWaterMark()看一下剩余栈空间,再调整。别一开始就给很大,浪费RAM。
4.2 队列与信号量:任务间的“传话筒”
任务之间怎么通信?队列和信号量就是干这个的。
队列用来传递数据。比如蓝牙协议栈收到一个音频包,通过队列发给音频解码任务。我习惯把队列长度设成2的幂次,比如8、16、32,这样内部实现效率更高。
// 创建一个队列,每个元素32字节,最多存10个
QueueHandle_t audio_queue = xQueueCreate(10, 32);
// 发送端(蓝牙协议栈任务)
xQueueSend(audio_queue, &audio_data, portMAX_DELAY);
// 接收端(音频解码任务)
xQueueReceive(audio_queue, &audio_data, portMAX_DELAY);
信号量用来做同步。比如按键按下,你要通知某个任务去处理。二值信号量最常用,就像一个“有人按了按钮”的旗子。
我曾经踩过一个坑:信号量创建时用了semaphoreCreateBinary(),结果发现第一次释放后,任务收不到第二次信号。后来查文档才知道,FreeRTOS的二值信号量默认是“已给”状态,需要先xSemaphoreTake()一次才能用。改成xSemaphoreCreateBinary()并手动xSemaphoreGive()一次就好了。
xQueueSend()返回了errQUEUE_FULL。加个超时重试机制就稳了。
4.3 内存管理:别让堆碎片害了你
FreeRTOS提供了5种内存管理方案,从heap_1到heap_5。做蓝牙耳机,我推荐用heap_4。它支持动态分配和释放,而且能合并相邻空闲块,减少碎片。
为什么不用heap_3?它包装了标准库的malloc/free,但标准库的malloc在嵌入式环境里容易产生碎片。heap_4是专为RTOS设计的,更靠谱。
内存分配的大小,我建议用pvPortMalloc()代替标准malloc。这样分配的内存来自FreeRTOS的堆,不会被其他模块污染。
// 分配一块内存给音频缓冲区
uint8_t *audio_buf = (uint8_t *)pvPortMalloc(1024);
if (audio_buf == NULL) {
// 处理内存不足的情况
vTaskDelay(pdMS_TO_TICKS(100));
// 重试或报错
}
xPortGetFreeHeapSize()查看剩余堆空间。如果发现内存泄漏,就在任务退出前加个vPortFree(),养成好习惯。
4.4 移植到目标芯片:别怕,有套路
移植FreeRTOS到新芯片,说白了就是改三个文件:FreeRTOSConfig.h、port.c、portmacro.h。我做过几次移植,总结下来就这几步:
- 配置时钟和中断:FreeRTOS需要一个定时器来产生系统节拍(tick)。一般用SysTick(ARM Cortex-M)或者芯片自带的通用定时器。节拍频率我习惯设成1000Hz,也就是1ms一个tick。
- 实现临界区保护:用
portENTER_CRITICAL()和portEXIT_CRITICAL()来开关中断。ARM芯片直接用__disable_irq()和__enable_irq()就行。 - 实现任务切换:这是最核心的部分。需要写一个
vPortSVCHandler()(SVC中断)和xPortPendSVHandler()(PendSV中断)。前者用于启动第一个任务,后者用于任务切换。 - 配置堆大小:在
FreeRTOSConfig.h里设configTOTAL_HEAP_SIZE。蓝牙耳机一般给8KB到16KB就够了,但如果你要跑复杂的协议栈,建议给到32KB。
我记得第一次移植时,任务死活不切换。查了半天,发现是PendSV中断优先级没设成最低。FreeRTOS要求PendSV和SVC的中断优先级必须是最低值,否则会出问题。嗯,这个坑我替你们踩过了。
- 系统节拍定时器是否正常工作?
- 临界区开关中断是否生效?
- 任务切换时,寄存器是否完整保存和恢复?
- 堆空间是否足够?
- 中断嵌套是否会导致优先级反转?
最后说一句:移植完成后,先跑一个简单的任务(比如LED闪烁),确认调度器能正常工作。然后再逐步加入蓝牙协议栈、音频处理等模块。别一上来就全上,出了问题你都不知道是RTOS的锅还是自己代码的锅。
好了,这一章就到这里。下一章我们聊聊蓝牙协议栈的初始化流程,到时候会用到今天讲的队列和信号量。做好准备。