4. 嵌入式RTOS基础:FreeRTOS任务管理、消息队列与信号量、任务间通信机制

各位同学,咱们今天聊聊FreeRTOS。说实话,我刚开始做嵌入式开发那会儿,用的还是裸机跑循环。后来项目越来越复杂,一个按键要响应好几个功能,中断里还要处理网络数据包……那叫一个头疼。直到我真正把FreeRTOS用起来,才感觉打开了新世界的大门。

这一节,我会把任务管理、消息队列、信号量这些核心概念,结合我踩过的坑,给大家掰开揉碎了讲清楚。

4.1 任务管理:别让你的CPU闲着

任务,说白了就是一段独立执行的函数。在FreeRTOS里,每个任务都有自己的栈空间和优先级。我习惯把任务想象成一个个独立的小工人,CPU就是那个车间主任,谁优先级高谁先干活。

核心概念:任务状态机。一个任务有四种状态:运行态、就绪态、阻塞态、挂起态。你想想看,如果任务一直在运行,那其他任务怎么办?所以任务需要主动让出CPU。

创建任务的API很简单:

// 创建一个LED闪烁任务
xTaskCreate(
    vLEDTask,           // 任务函数
    "LED Task",         // 任务名称(调试用)
    128,                // 栈深度(单位:字)
    NULL,               // 参数
    1,                  // 优先级(数字越小优先级越低)
    NULL                // 任务句柄
);

嗯,这里要注意栈深度。我遇到过好几次任务莫名其妙跑飞,查了半天发现是栈溢出了。FreeRTOS提供了uxTaskGetStackHighWaterMark()这个函数,可以查看任务栈的剩余空间。我个人习惯在调试阶段,每个任务都打印一下栈使用情况。

避坑指南:我曾经在一个项目中,把某个任务的栈设成了64个字,结果任务里调用了sprintf,直接栈溢出。建议任务栈至少128字起步,如果用到printf这类函数,256字起步。

4.2 消息队列:任务间的快递员

任务之间怎么传数据?用全局变量?别闹了,那会引发资源竞争。FreeRTOS提供了消息队列,说白了就是一个先进先出的缓冲区。一个任务往里扔数据,另一个任务从里面取数据,安全又高效。

我举个例子。在一个智能照明项目中,按键检测任务检测到按键按下,需要通知LED控制任务改变灯光模式。这时候消息队列就派上用场了:

// 创建队列,每个元素4字节,队列深度10
QueueHandle_t xKeyQueue = xQueueCreate(10, sizeof(uint32_t));

// 按键任务:发送数据
void vKeyTask(void *pvParameters) {
    uint32_t keyValue = 0;
    while(1) {
        keyValue = readKey();
        if(keyValue != 0) {
            // 发送到队列,超时时间设为0
            xQueueSend(xKeyQueue, &keyValue, 0);
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// LED任务:接收数据
void vLEDTask(void *pvParameters) {
    uint32_t receivedKey = 0;
    while(1) {
        // 阻塞等待队列数据
        if(xQueueReceive(xKeyQueue, &receivedKey, portMAX_DELAY) == pdPASS) {
            switch(receivedKey) {
                case KEY_MODE: toggleMode(); break;
                case KEY_BRIGHT: adjustBrightness(); break;
            }
        }
    }
}

你看,这样设计后,两个任务完全解耦了。按键任务只管发,LED任务只管收。我在实际项目中,经常用队列来传递传感器数据、网络命令等。说白了,队列就是任务间的快递员,你只管把包裹交给它,它负责安全送达。

注意:队列操作是线程安全的,但队列里的数据是拷贝传递的。如果传递结构体,注意结构体大小不要太大,否则会影响性能。我一般控制在64字节以内。

4.3 信号量:资源的通行证

信号量,你可以把它想象成停车场的车位计数器。二值信号量就是只有一个车位,互斥信号量就是带优先级的车位,计数信号量就是多个车位。

为什么需要信号量?我举个真实案例。在一个OTA升级项目中,Flash写入操作是独占资源。如果两个任务同时写Flash,数据就全乱了。这时候就需要互斥信号量来保护:

// 创建互斥信号量
SemaphoreHandle_t xFlashMutex = xSemaphoreCreateMutex();

// 任务A:写Flash
void vTaskA(void *pvParameters) {
    while(1) {
        // 获取信号量,如果被占用则阻塞
        if(xSemaphoreTake(xFlashMutex, portMAX_DELAY) == pdPASS) {
            // 安全地写Flash
            writeFlash(dataA);
            // 释放信号量
            xSemaphoreGive(xFlashMutex);
        }
    }
}

// 任务B:写Flash
void vTaskB(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xFlashMutex, portMAX_DELAY) == pdPASS) {
            writeFlash(dataB);
            xSemaphoreGive(xFlashMutex);
        }
    }
}

这里有个坑,我必须要说。互斥信号量有优先级继承机制,能防止优先级反转。但二值信号量没有这个机制。所以,如果多个任务竞争同一个资源,且任务优先级不同,一定要用互斥信号量。

经验之谈:我曾经在一个项目中,用二值信号量保护一个串口。结果低优先级任务拿着信号量不放,高优先级任务一直等,系统响应变得极慢。后来换成互斥信号量,问题立刻解决。记住:保护共享资源,用互斥信号量;单纯的任务同步,用二值信号量。

4.4 任务间通信机制:选对工具很重要

FreeRTOS提供了多种通信方式,我给大家整理了一个表格,方便对比:

通信方式 适用场景 数据量 特点
消息队列 传递数据 小到中等 先进先出,支持多生产者多消费者
二值信号量 任务同步 无数据 类似标志位,适合事件通知
计数信号量 资源计数 无数据 记录可用资源数量
互斥信号量 保护共享资源 无数据 有优先级继承,防止反转
任务通知 轻量级通信 32位数据 比信号量快,但功能有限

我个人比较喜欢用任务通知。为什么?因为它轻量啊!不需要创建队列或信号量,直接给任务发一个32位的值。但要注意,任务通知只能一对一通信,而且接收方只能有一个任务在等。

小技巧:如果你的通信场景是「一个任务通知另一个任务做某事」,且不需要传递复杂数据,用任务通知比用信号量快30%左右。我在智能照明项目中,按键通知LED切换模式,用的就是任务通知。

好了,这一节的内容就到这里。总结一下:任务管理要关注栈大小和优先级;消息队列适合传数据;信号量适合同步和保护;任务通知是轻量级选择。下一节我们会讲中断管理,到时候再聊聊怎么在中断里安全地使用这些机制。