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()一次就好了。

避坑指南: 队列和信号量的API调用,一定要检查返回值。我曾经因为队列满导致数据丢失,查了两天才发现是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.hport.cportmacro.h。我做过几次移植,总结下来就这几步:

  1. 配置时钟和中断:FreeRTOS需要一个定时器来产生系统节拍(tick)。一般用SysTick(ARM Cortex-M)或者芯片自带的通用定时器。节拍频率我习惯设成1000Hz,也就是1ms一个tick。
  2. 实现临界区保护:用portENTER_CRITICAL()portEXIT_CRITICAL()来开关中断。ARM芯片直接用__disable_irq()__enable_irq()就行。
  3. 实现任务切换:这是最核心的部分。需要写一个vPortSVCHandler()(SVC中断)和xPortPendSVHandler()(PendSV中断)。前者用于启动第一个任务,后者用于任务切换。
  4. 配置堆大小:在FreeRTOSConfig.h里设configTOTAL_HEAP_SIZE。蓝牙耳机一般给8KB到16KB就够了,但如果你要跑复杂的协议栈,建议给到32KB。

我记得第一次移植时,任务死活不切换。查了半天,发现是PendSV中断优先级没设成最低。FreeRTOS要求PendSV和SVC的中断优先级必须是最低值,否则会出问题。嗯,这个坑我替你们踩过了。

移植检查清单:
  • 系统节拍定时器是否正常工作?
  • 临界区开关中断是否生效?
  • 任务切换时,寄存器是否完整保存和恢复?
  • 堆空间是否足够?
  • 中断嵌套是否会导致优先级反转?

最后说一句:移植完成后,先跑一个简单的任务(比如LED闪烁),确认调度器能正常工作。然后再逐步加入蓝牙协议栈、音频处理等模块。别一上来就全上,出了问题你都不知道是RTOS的锅还是自己代码的锅。

好了,这一章就到这里。下一章我们聊聊蓝牙协议栈的初始化流程,到时候会用到今天讲的队列和信号量。做好准备。