4. 嵌入式中间件概述

好,咱们进入RTOS的世界。说实话,我早年做嵌入式开发时,一直用裸机跑循环。后来项目越来越复杂,一个按键要响应、一个传感器要采集、还要同时处理通信协议……裸机真的扛不住了。那时候我才真正体会到,为什么需要RTOS。

这一章,咱们就啃FreeRTOS这块硬骨头。它轻量、开源、资料多,是入门RTOS的最佳选择。我会把任务管理、调度策略、任务间通信这三个核心模块讲透。

本章核心脉络: 任务(Task)是RTOS的基本执行单元 → 调度器决定哪个任务运行 → 任务间需要通信和同步(队列、信号量、互斥量)
FreeRTOS 核心知识体系 FreeRTOS 内核 任务管理 任务调度策略 任务间通信 创建/删除 挂起/恢复 任务优先级 任务状态 抢占式调度 时间片轮转 协作式调度 队列 信号量 互斥量 任务通知 💡 学习建议 先理解任务状态机,再掌握调度规则,最后用队列和信号量解决实际同步问题

4.1 任务管理——RTOS的基石

任务,说白了就是一个无限循环的函数。每个任务有自己的栈空间、优先级和状态。我刚开始用FreeRTOS时,犯过一个低级错误:任务函数里忘了加死循环,结果函数返回后系统直接崩溃。嗯,这个坑你们别踩。

4.1.1 任务创建与删除

创建任务用 xTaskCreate()。原型长这样:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度(单位:字)
    void *pvParameters,             // 传给任务的参数
    UBaseType_t uxPriority,         // 任务优先级(0~configMAX_PRIORITIES-1)
    TaskHandle_t *pxCreatedTask     // 返回的任务句柄
);

举个例子,创建一个LED闪烁任务:

void vLEDTask(void *pvParameters) {
    while(1) {
        GPIO_ToggleBits(GPIOA, GPIO_Pin_5);
        vTaskDelay(pdMS_TO_TICKS(500)); // 延时500ms
    }
}

TaskHandle_t xLEDTaskHandle = NULL;
xTaskCreate(vLEDTask, "LED", 128, NULL, 1, &xLEDTaskHandle);
个人经验: 栈大小别拍脑袋定。我习惯先给一个较大的值(比如256字),运行后用 uxTaskGetStackHighWaterMark() 查看实际使用量,再往下调。这样既省内存又不会溢出。

4.1.2 任务状态机

任务有四种状态:运行、就绪、阻塞、挂起。我画个简图帮大家理解:

        ┌──────────┐
        │  创建     │
        └────┬─────┘
             ↓
        ┌──────────┐
   ┌───→│  就绪     │←──────────┐
   │    └────┬─────┘           │
   │         │ 调度器选择       │
   │         ↓                 │
   │    ┌──────────┐  超时/事件 │
   │    │  运行     │───────────┤
   │    └────┬─────┘           │
   │         │ 等待事件        │
   │         ↓                 │
   │    ┌──────────┐           │
   │    │  阻塞     │───────────┘
   │    └──────────┘
   │
   └──── 挂起(vTaskSuspend)

这里有个关键点:只有就绪态的任务才能被调度器选中运行。阻塞态的任务在等待某个事件(延时、队列、信号量),事件没到就不参与调度。这设计很巧妙——避免了CPU空转。

4.2 任务调度策略——谁先跑?跑多久?

FreeRTOS支持三种调度策略。我一个个说。

调度策略 核心规则 适用场景
抢占式调度 高优先级任务就绪时,立即打断低优先级任务 实时性要求高的系统(默认配置)
时间片轮转 同优先级任务轮流运行固定时间片 多个同等重要的任务共享CPU
协作式调度 任务主动让出CPU(taskYIELD())才切换 对实时性要求不高的简单系统

4.2.1 抢占式调度——最常用

这是FreeRTOS的默认策略。优先级高的任务一旦就绪,立马抢走CPU。我在一个电机控制项目里就吃过亏:一个高优先级的中断服务任务把低优先级的通信任务饿死了,导致上位机收不到数据。后来我调整了优先级,把通信任务提到中等优先级,问题才解决。

避坑指南: 优先级别设太多。FreeRTOS支持最多32级,但我建议不超过5~8级。优先级太多,调度开销反而变大,而且容易出优先级反转问题。

4.2.2 时间片轮转——公平分配

当多个任务优先级相同时,调度器让它们轮流跑。每个任务跑一个时间片(通常1ms~10ms,由 configTICK_RATE_HZ 决定)。时间片到了,就切换到下一个同优先级任务。

举个例子:

// 两个任务优先级都是2
xTaskCreate(vTaskA, "TaskA", 128, NULL, 2, NULL);
xTaskCreate(vTaskB, "TaskB", 128, NULL, 2, NULL);
// 调度器会让TaskA和TaskB轮流运行,每个跑一个时间片

4.2.3 协作式调度——手动让权

这个用得少,但在某些低功耗场景下很实用。任务不主动调用 taskYIELD() 或阻塞API,调度器就不会切换。好处是上下文切换次数少,功耗低;坏处是一个任务死循环,整个系统就卡死了。

4.3 任务间通信——让任务们好好说话

任务之间不能直接访问对方的局部变量。那怎么传数据?怎么同步?FreeRTOS提供了队列、信号量、互斥量、任务通知等机制。我挑最常用的三个讲。

4.3.1 队列——数据搬运工

队列是FIFO结构,一个任务往里写,另一个任务往外读。数据是拷贝传递的,不是指针传递。这一点要注意——如果数据量大,拷贝开销会很高。

创建队列:

QueueHandle_t xQueue;
xQueue = xQueueCreate(10, sizeof(uint32_t)); // 队列长度10,每个元素4字节

发送数据:

uint32_t data = 100;
xQueueSend(xQueue, &data, portMAX_DELAY); // 队列满则永久等待

接收数据:

uint32_t received;
xQueueReceive(xQueue, &received, pdMS_TO_TICKS(100)); // 等待100ms
我的习惯: 队列长度别设太大。我曾经设了个100长度的队列,结果内存直接爆了。一般根据数据产生速率和消费速率来算,留20%余量就够了。

4.3.2 信号量——资源计数器

信号量分两种:二值信号量和计数型信号量。二值信号量像一把钥匙,只能一个人用;计数型信号量像停车场空位,可以多个。

典型用法——中断中释放信号量,任务中获取:

SemaphoreHandle_t xSemaphore;

// 创建二值信号量
xSemaphore = xSemaphoreCreateBinary();

// 中断服务函数中释放
void EXTI0_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务中等待
void vTask(void *pvParameters) {
    while(1) {
        if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE) {
            // 处理中断事件
        }
    }
}

这个模式我用了无数次。中断只做标记,具体处理交给任务,既快又安全。

4.3.3 互斥量——解决资源争抢

互斥量是二值信号量的升级版,多了优先级继承机制。啥意思?就是当低优先级任务持有互斥量时,高优先级任务来抢,系统会临时提升低优先级任务的优先级,防止优先级反转。

我举个例子说明优先级反转有多坑:

任务A(优先级3)等待互斥量 → 任务B(优先级2)抢占CPU → 任务C(优先级1)持有互斥量
结果:A被B和C一起拖死,实时性全无

互斥量的使用和信号量类似,但注意:不能在中断中使用互斥量。因为优先级继承在中断上下文里没法实现。

机制 用途 能否在中断中使用 特点
队列 传递数据 可以(用FromISR版本) 数据拷贝,适合小数据量
二值信号量 事件通知/同步 可以 简单高效,无优先级继承
计数信号量 资源计数 可以 可记录可用资源数量
互斥量 保护共享资源 不可以 有优先级继承,防反转
任务通知 轻量级信号量/消息 可以 更快,但只能点对点
选型建议: 传数据用队列,做同步用二值信号量,保护临界区用互斥量。任务通知虽然快,但只能一对一,别滥用。

4.4 实战中的几个坑

最后,我分享几个自己踩过的坑,你们遇到类似情况可以少走弯路。

  • 栈溢出: 任务栈设太小,运行一段时间后莫名其妙死机。用 configCHECK_FOR_STACK_OVERFLOW 开启检测,或者用 uxTaskGetStackHighWaterMark() 监控。
  • 优先级反转: 我前面提过。用互斥量代替二值信号量保护共享资源,或者设计时让持有资源的任务优先级尽量高。
  • 死锁: 两个任务互相等待对方释放资源。我习惯用 xSemaphoreTake() 加超时参数,别用 portMAX_DELAY 死等。
  • 中断中调用非FromISR版本API: 这会导致系统崩溃。记住,中断里只能用 xQueueSendFromISR()、xSemaphoreGiveFromISR() 这些带FromISR后缀的函数。

嗯,这一章的内容就到这里。FreeRTOS的任务管理、调度策略和通信机制,是后续所有中间件集成的基础。把这些搞透了,后面讲消息队列、内存管理、驱动框架时,你会觉得轻松很多。