2. 嵌入式实时操作系统基础:FreeRTOS任务调度、中断管理、信号量与互斥锁在协议栈中的应用

各位同学,咱们直接进入正题。这一章是协议栈的“地基”——FreeRTOS。你想想看,一个医疗设备里,采集数据、处理算法、通信收发、界面刷新,这些活儿要是都挤在一个大循环里,迟早得出事。我见过不少工程师,协议栈写得挺漂亮,一上RTOS就崩,为什么?说白了,就是对任务调度和资源竞争的理解还停留在“裸机思维”上。

我个人习惯,在讲任何协议栈之前,先把RTOS的这几个核心机制掰开揉碎。今天咱们就重点聊四个东西:任务调度、中断管理、信号量和互斥锁。它们在协议栈里怎么用,有哪些坑,我一个个说。

FreeRTOS 核心机制与协议栈关系图 FreeRTOS 内核 任务调度 中断管理 信号量 互斥锁 医疗通信协议栈(应用层) 图:FreeRTOS 四大核心机制与协议栈的协作关系。每个机制都是协议栈稳定运行的基石。

2.1 任务调度:别让CPU闲着,也别让它乱抢

FreeRTOS 是抢占式调度。什么意思?高优先级的任务就绪了,低优先级的立马靠边站。这在协议栈里特别重要——比如接收中断来了,你得赶紧把数据从缓冲区搬走,否则下一个包就把你覆盖了。

我一般把协议栈拆成三个任务:

  • 接收任务(高优先级):从硬件缓冲区取数据,做帧校验。
  • 处理任务(中优先级):解析协议、执行命令、组响应包。
  • 发送任务(低优先级):把待发送的数据通过DMA或中断发出去。

这里有个关键点:优先级不要乱设。我在项目中遇到过,有人把发送任务设成最高优先级,结果接收中断被阻塞,丢包率飙升。你想想看,数据都没收进来,你发什么?

经验法则:中断服务程序(ISR)的优先级 > 接收任务 > 处理任务 > 发送任务。发送任务可以靠DMA完成,CPU不用死等。

2.2 中断管理:ISR 里别干重活

这是新手最容易翻车的地方。ISR 里做复杂计算、调用阻塞函数、甚至打印日志——我当年也干过这种傻事。结果系统响应越来越慢,最后看门狗复位。

正确的做法是:ISR 只做最轻量的操作。比如:

  1. 从硬件寄存器读一个字节,放入环形缓冲区。
  2. 给接收任务发一个信号量,告诉它“有数据了”。
  3. 清中断标志,退出。

FreeRTOS 提供了 xSemaphoreGiveFromISR() 和 xQueueSendFromISR() 这类“FromISR”函数,专门给中断里用。记住,绝对不要在 ISR 里调用 vTaskDelay() 或 printf()。我曾经见过一个同事,在 ISR 里调了 printf,结果串口中断嵌套,直接死锁。

警告:ISR 的执行时间必须控制在微秒级。如果协议栈的硬件中断频率是 1kHz,那你的 ISR 必须在 1ms 内完成所有操作,否则下一个中断就丢了。

2.3 信号量:任务间的“传话筒”

信号量在协议栈里最常见的用法就是同步。比如接收 ISR 通知接收任务:

// 接收 ISR
void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t byte = UART->DR;  // 读数据
    // 放入环形缓冲区(略)
    xSemaphoreGiveFromISR(xRxSemaphore, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 接收任务
void vRxTask(void *pvParameters) {
    while(1) {
        xSemaphoreTake(xRxSemaphore, portMAX_DELAY);
        // 从缓冲区取数据,做帧校验
        // 如果校验通过,发给处理任务
    }
}

你看,ISR 只负责“给信号”,真正的数据处理在任务里。这样既保证了实时性,又不会阻塞中断。

还有一种叫计数信号量。比如你的硬件缓冲区能存 10 个包,那信号量初始值就设成 10。每收一个包,信号量减 1;每处理一个包,信号量加 1。这样就能精确控制缓冲区水位。

小技巧:调试时可以用 uxSemaphoreGetCount() 查看信号量当前值,判断任务是否被阻塞。我经常在串口调试里打印这个值,排查死锁问题。

2.4 互斥锁:别让两个任务同时改同一个变量

互斥锁和信号量有点像,但有个本质区别:互斥锁自带优先级继承。什么意思?假设低优先级任务拿了锁,高优先级任务在等这把锁,那系统会临时提升低优先级任务的优先级,防止它被中优先级任务抢占,导致高优先级任务无限等待——这就是经典的优先级反转。

在协议栈里,互斥锁通常用来保护共享数据结构。比如一个全局的“设备状态结构体”,接收任务在写,处理任务在读。不加锁的话,读到的数据可能是一半新的、一半旧的。

特性 二进制信号量 互斥锁
用途 任务同步(通知事件) 资源互斥(保护共享数据)
优先级继承 不支持 支持
谁可以释放 任何任务/ISR 只能由持有者释放
典型场景 ISR 通知任务 保护全局变量、缓冲区

我个人的习惯是:能用互斥锁的地方,尽量不用信号量做互斥。因为优先级继承这个特性,在医疗设备这种对实时性要求极高的场景里,能避免很多隐蔽的时序问题。

避坑指南:我曾经在一个心电监护仪的项目里,用二进制信号量保护共享缓冲区。结果一个中优先级的显示任务频繁抢占,导致高优先级的通信任务拿不到锁,数据一直积压。换成互斥锁后,问题立刻解决。记住,信号量是“通知”,互斥锁是“保护”,别混用。

2.5 综合应用:一个简单的协议栈任务模型

最后,我画一个实际项目中常用的任务模型,你们可以参考:

// 任务定义
TaskHandle_t xRxTaskHandle;
TaskHandle_t xProcessTaskHandle;
TaskHandle_t xTxTaskHandle;

// 信号量与互斥锁
SemaphoreHandle_t xRxSemaphore;      // 二进制信号量,通知接收任务
SemaphoreHandle_t xTxSemaphore;      // 二进制信号量,通知发送任务
SemaphoreHandle_t xBufferMutex;      // 互斥锁,保护共享缓冲区

void main(void) {
    // 创建信号量和互斥锁
    xRxSemaphore = xSemaphoreCreateBinary();
    xTxSemaphore = xSemaphoreCreateBinary();
    xBufferMutex = xSemaphoreCreateMutex();

    // 创建任务
    xTaskCreate(vRxTask, "Rx", 256, NULL, 3, &xRxTaskHandle);
    xTaskCreate(vProcessTask, "Process", 512, NULL, 2, &xProcessTaskHandle);
    xTaskCreate(vTxTask, "Tx", 256, NULL, 1, &xTxTaskHandle);

    vTaskStartScheduler();
}

这个模型里,接收任务优先级最高(3),处理任务次之(2),发送任务最低(1)。互斥锁保护缓冲区,信号量做任务间同步。结构清晰,不容易出问题。

核心总结:FreeRTOS 在协议栈里的角色,就是帮你管好“谁先做、谁后做、谁不能同时做”。任务调度定优先级,中断管理保实时,信号量做通知,互斥锁防冲突。把这四个东西用熟了,协议栈的稳定性会上一个大台阶。