第三章 RTOS基础与任务调度:FreeRTOS移植、任务创建与删除、任务优先级与调度策略、任务间通信
各位同学,欢迎来到第三章。这一章我们聊聊RTOS,特别是FreeRTOS。说实话,在TWS耳机这种资源受限的芯片上,没有RTOS,你根本玩不转。裸机跑轮询?那延迟和功耗会让你崩溃的。
我个人习惯,拿到一个新芯片,第一件事就是把FreeRTOS跑起来。它就像地基,地基不稳,上面盖什么楼都得塌。这一章,我会把移植、任务管理、调度策略和通信机制,掰开了揉碎了讲给你听。
3.1 FreeRTOS移植:从零开始跑起来
移植FreeRTOS,说白了就是让这个“小操作系统”能认识你的芯片硬件。我刚开始做移植时,总觉得很神秘,后来发现,核心就三件事:配置时钟、实现中断、修改堆栈。
第一步:拿到源码,找到核心文件
FreeRTOS的源码结构很清晰。你只需要关注这几个文件:
tasks.c、queue.c、list.c—— 核心代码,不用动。portable文件夹 —— 这才是你干活的地方。找到对应你MCU架构的文件夹,比如ARM Cortex-M系列。FreeRTOSConfig.h—— 配置文件,重中之重。
第二步:配置FreeRTOSConfig.h
这个文件决定了你的RTOS行为。我见过很多新手,直接复制别人的配置,结果系统跑飞了都不知道为什么。记住,每个项目都要自己配。
/* FreeRTOSConfig.h 关键配置示例 */
#define configUSE_PREEMPTION 1 // 使用抢占式调度
#define configCPU_CLOCK_HZ ( ( unsigned long ) 48000000 ) // 你的MCU主频
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) // 系统时钟节拍,1ms
#define configMAX_PRIORITIES ( 5 ) // 最大优先级数,够用就行,别贪多
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 ) // 最小任务栈大小
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 8 * 1024 ) ) // 总堆大小,TWS耳机通常8-16KB
configTICK_RATE_HZ 时,为了追求高精度设成了10000(0.1ms)。结果系统大部分时间都在处理时钟中断,任务反而跑不动了。TWS耳机里,1000Hz(1ms)是黄金平衡点。
第三步:实现滴答定时器和PendSV中断
这是移植的核心。滴答定时器(SysTick)为RTOS提供心跳,PendSV用于上下文切换。在ARM Cortex-M上,通常这样实现:
/* 滴答定时器中断服务函数 */
void SysTick_Handler(void)
{
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler(); // FreeRTOS的滴答处理函数
}
}
/* PendSV中断,用于任务切换 */
void PendSV_Handler(void)
{
vPortSVCHandler(); // 调用FreeRTOS的上下文切换函数
}
嗯,这里要注意:PendSV_Handler 的优先级一定要设为最低。为什么?因为上下文切换不能被其他中断打断,否则会导致竞态条件。我见过有人把PendSV优先级设得很高,结果系统调度乱成一锅粥。
3.2 任务创建与删除:让代码“活”起来
任务,就是RTOS里的“线程”。在TWS耳机里,一个任务处理音频流,一个任务处理蓝牙协议栈,一个任务处理按键和触摸。任务创建很简单,但用好它需要经验。
任务创建:xTaskCreate
我个人习惯,每个任务都给它起个有意义的名字,方便调试。
/* 任务函数原型 */
void vAudioTask(void *pvParameters);
void vBLEStackTask(void *pvParameters);
void vUserInputTask(void *pvParameters);
/* 创建任务 */
xTaskCreate(
vAudioTask, // 任务函数
"Audio Task", // 任务名字,调试用
256, // 栈深度,单位是字(4字节)
NULL, // 参数
3, // 优先级,数值越大优先级越高
NULL // 任务句柄
);
xTaskCreate(vBLEStackTask, "BLE Stack", 512, NULL, 4, NULL);
xTaskCreate(vUserInputTask, "User Input", 128, NULL, 2, NULL);
uxTaskGetStackHighWaterMark() 查看剩余栈空间。如果剩余空间小于20%,我就加大栈大小。别一开始就给太大,浪费RAM。
任务删除:vTaskDelete
任务删除很简单,但要注意:删除自己时,要传NULL。删除别人时,要传任务句柄。
/* 任务内部删除自己 */
void vSomeTask(void *pvParameters)
{
// ... 做一些初始化工作 ...
vTaskDelete(NULL); // 任务结束,自我了断
}
/* 外部删除任务 */
TaskHandle_t xTaskHandle = NULL;
// ... 创建任务时保存句柄 ...
vTaskDelete(xTaskHandle); // 干掉它
3.3 任务优先级与调度策略:谁先跑,谁后跑
FreeRTOS支持抢占式调度和协作式调度。在TWS耳机里,我们几乎只用抢占式。为什么?因为音频流必须实时处理,不能被低优先级的按键任务拖累。
优先级设计原则
我总结了一个“三区优先级”原则,在TWS耳机里特别好用:
| 优先级区间 | 典型任务 | 说明 |
|---|---|---|
| 高优先级(4-5) | 音频处理、蓝牙协议栈 | 实时性要求极高,不能被打断 |
| 中优先级(2-3) | 编解码、音量控制 | 有一定实时性,可以容忍少量延迟 |
| 低优先级(1) | 按键扫描、LED控制、电量检测 | 不着急,有空再处理 |
调度策略:抢占式 vs 时间片
FreeRTOS默认是抢占式调度。高优先级任务就绪时,会立即抢占低优先级任务。同优先级任务之间,则通过时间片轮转。
你想想看,如果音频任务和蓝牙任务优先级相同,会发生什么?它们会轮流跑,每个任务跑一个时间片。但音频处理需要连续的数据流,时间片切换会导致音频卡顿。所以,我通常把音频任务优先级设得比蓝牙任务高一级。
3.4 任务间通信:队列、信号量与互斥锁
任务之间怎么说话?不能直接调用对方的函数,那样会破坏RTOS的独立性。FreeRTOS提供了三种“信使”:队列、信号量、互斥锁。
3.4.1 队列:数据搬运工
队列用于任务间传递数据。比如,按键任务检测到用户按下了“音量+”,就把这个事件通过队列发给音量控制任务。
/* 创建队列,最多存放5个消息,每个消息4字节 */
QueueHandle_t xVolumeQueue = xQueueCreate(5, sizeof(uint32_t));
/* 按键任务:发送数据 */
void vKeyScanTask(void *pvParameters)
{
uint32_t ulEvent = KEY_VOLUME_UP;
xQueueSend(xVolumeQueue, &ulEvent, portMAX_DELAY);
}
/* 音量控制任务:接收数据 */
void vVolumeControlTask(void *pvParameters)
{
uint32_t ulReceivedEvent;
while(1)
{
if(xQueueReceive(xVolumeQueue, &ulReceivedEvent, pdMS_TO_TICKS(100)) == pdPASS)
{
// 处理音量事件
AdjustVolume(ulReceivedEvent);
}
}
}
portMAX_DELAY,除非你确定发送方一定会发数据。否则,接收任务会永久阻塞。我一般设成 pdMS_TO_TICKS(100),超时了就做点别的事,或者打印个日志。
3.4.2 信号量:事件通知器
信号量用于同步。比如,音频解码任务等待DMA传输完成,DMA完成后通过信号量通知解码任务。
/* 创建二值信号量 */
SemaphoreHandle_t xDMASemaphore = xSemaphoreCreateBinary();
/* DMA中断服务函数 */
void DMA_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xDMASemaphore, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/* 音频解码任务 */
void vAudioDecodeTask(void *pvParameters)
{
while(1)
{
// 等待DMA传输完成
if(xSemaphoreTake(xDMASemaphore, portMAX_DELAY) == pdPASS)
{
// 处理解码后的音频数据
ProcessAudioData();
}
}
}
xSemaphoreGiveFromISR 时,一定要检查 xHigherPriorityTaskWoken。如果它为 pdTRUE,说明有一个更高优先级的任务被唤醒了,你需要在中断末尾调用 portYIELD_FROM_ISR 来触发一次上下文切换。我曾经忘了这个,导致高优先级任务迟迟得不到执行。
3.4.3 互斥锁:资源保护神
互斥锁用于保护共享资源。比如,两个任务都要写同一个I2C总线,如果不加锁,数据会乱掉。
/* 创建互斥锁 */
SemaphoreHandle_t xI2CMutex = xSemaphoreCreateMutex();
/* 任务A:写I2C */
void vTaskA(void *pvParameters)
{
xSemaphoreTake(xI2CMutex, portMAX_DELAY);
// 写I2C操作
I2C_Write(slaveAddr, regAddr, data);
xSemaphoreGive(xI2CMutex);
}
/* 任务B:写I2C */
void vTaskB(void *pvParameters)
{
xSemaphoreTake(xI2CMutex, portMAX_DELAY);
// 写I2C操作
I2C_Write(slaveAddr, regAddr, data);
xSemaphoreGive(xI2CMutex);
}
互斥锁有个好特性:优先级继承。如果低优先级任务持有锁,高优先级任务在等锁,系统会临时提升低优先级任务的优先级,防止“优先级反转”。
好了,这一章的内容就到这里。RTOS是TWS耳机固件的灵魂,任务调度和通信机制是它的骨架。下一章,我们会深入音频数据流处理,看看数据是怎么从蓝牙芯片流到喇叭的。到时候,你会更深刻地理解为什么任务优先级和通信机制这么重要。