第2章:RTOS入门:FreeRTOS任务调度、信号量、消息队列,从裸机过渡到多任务系统

说实话,很多做嵌入式开发的朋友,一开始都是从裸机(bare-metal)起步的。一个大循环,轮询各种标志位,或者靠中断来响应紧急事件。这种模式简单直接,但一旦项目复杂起来,你就会发现——代码越来越难维护,实时性也越来越难保证。

我记得我第一次接手一个需要同时处理传感器采集、LCD显示、按键响应和无线通信的项目时,裸机代码已经写到了上千行。一个中断里嵌套另一个中断,稍不留神就出bug。那时候我就想:有没有一种方式,能让这些任务各干各的,互不干扰?

嗯,答案就是RTOS(实时操作系统)。今天我们就从FreeRTOS入手,聊聊怎么从裸机思维过渡到多任务系统。

为什么需要RTOS?

裸机编程的核心是「超级循环」:

void main(void)
{
    while(1)
    {
        do_sensor_read();
        do_lcd_display();
        do_key_scan();
        do_uart_send();
    }
}

你想想看,如果do_sensor_read()里有个延时等待,那整个循环都得卡住。按键响应就会变慢,LCD刷新也会延迟。说白了,裸机是「串行」的,所有事情排着队做。

而RTOS呢?它把每个功能拆成独立的任务(Task),由调度器决定谁先运行、运行多久。看起来就像多个任务在「同时」执行——这就是多任务系统的魅力。

核心区别一句话:裸机是「我主动去检查」,RTOS是「事件来了通知我」。

FreeRTOS的任务调度

FreeRTOS的任务调度,说白了就是「谁优先级高谁先跑,同优先级轮流跑」。我习惯把它想象成一个会议室:高优先级的任务就像大领导,随时可以插队;同优先级的同事就按顺序轮流发言。

创建一个任务很简单:

void vTask1(void *pvParameters)
{
    while(1)
    {
        // 任务代码
        vTaskDelay(pdMS_TO_TICKS(1000)); // 延时1秒
    }
}

void vTask2(void *pvParameters)
{
    while(1)
    {
        // 任务代码
        vTaskDelay(pdMS_TO_TICKS(500)); // 延时0.5秒
    }
}

void main(void)
{
    xTaskCreate(vTask1, "Task1", 128, NULL, 1, NULL);
    xTaskCreate(vTask2, "Task2", 128, NULL, 2, NULL);
    vTaskStartScheduler(); // 启动调度器
}

这里要注意:Task2的优先级是2,Task1是1。所以Task2会优先运行。当Task2调用vTaskDelay()进入阻塞状态时,Task1才有机会执行。

我的经验:任务优先级不要设太多,3-5级就够用了。我曾经在一个项目里设了10级优先级,结果调试时自己都搞不清谁先谁后。后来简化成3级:紧急(中断级)、普通(主循环级)、后台(空闲级),反而更清晰。

信号量:任务间的「红绿灯」

多任务系统里,最头疼的问题就是资源共享。比如两个任务都要往同一个串口发数据,如果不加控制,数据就会混在一起。

信号量就是解决这个问题的。它像一个令牌,谁拿到令牌谁就能使用资源,用完再还回来。

SemaphoreHandle_t xSemaphore;

void vTaskSender1(void *pvParameters)
{
    while(1)
    {
        if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
        {
            // 使用串口发送数据
            UART_Send("Hello from Task1\n");
            xSemaphoreGive(xSemaphore); // 释放信号量
        }
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void vTaskSender2(void *pvParameters)
{
    while(1)
    {
        if(xSemaphoreTake(xSemaphore, portMAX_DELAY) == pdTRUE)
        {
            UART_Send("Hello from Task2\n");
            xSemaphoreGive(xSemaphore);
        }
        vTaskDelay(pdMS_TO_TICKS(150));
    }
}

void main(void)
{
    xSemaphore = xSemaphoreCreateBinary();
    xTaskCreate(vTaskSender1, "Sender1", 128, NULL, 1, NULL);
    xTaskCreate(vTaskSender2, "Sender2", 128, NULL, 1, NULL);
    vTaskStartScheduler();
}

避坑指南:我曾经犯过一个错误——在中断服务函数里调用xSemaphoreGive(),但忘了检查返回值。结果信号量被多次释放,导致任务误以为资源可用。记住:中断中释放信号量要用xSemaphoreGiveFromISR(),并且要判断是否需要触发上下文切换。

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

信号量适合做同步和互斥,但如果你需要传递数据呢?比如一个任务采集温度数据,另一个任务负责显示。这时候就需要消息队列了。

消息队列就像一个邮箱:发送方把数据放进去,接收方从里面取出来。如果队列满了,发送方可以选择等待;如果队列空了,接收方也可以选择等待。

QueueHandle_t xQueue;

void vTaskSensor(void *pvParameters)
{
    int32_t temperature = 0;
    while(1)
    {
        temperature = read_temperature(); // 读取温度
        xQueueSend(xQueue, &temperature, portMAX_DELAY);
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

void vTaskDisplay(void *pvParameters)
{
    int32_t received_temp = 0;
    while(1)
    {
        if(xQueueReceive(xQueue, &received_temp, portMAX_DELAY) == pdTRUE)
        {
            LCD_ShowNumber(received_temp); // 显示温度
        }
    }
}

void main(void)
{
    xQueue = xQueueCreate(5, sizeof(int32_t)); // 队列长度5,每个元素4字节
    xTaskCreate(vTaskSensor, "Sensor", 128, NULL, 1, NULL);
    xTaskCreate(vTaskDisplay, "Display", 128, NULL, 1, NULL);
    vTaskStartScheduler();
}

这里队列长度设为5,意味着最多可以缓存5个温度值。如果显示任务处理得慢,传感器任务还能继续采集,数据不会丢失——这就是队列的缓冲作用。

我的建议:队列长度要根据实际场景来定。数据产生快、消费慢,就设大一点;反之设小一点。我一般会留20%的余量,防止突发数据导致丢包。

从裸机到RTOS的思维转变

说实话,刚开始用RTOS时,我最大的障碍不是API怎么调用,而是思维没转过来。裸机时代,我习惯用全局变量传数据,用延时函数控制时序。到了RTOS,这些做法反而成了隐患。

我总结了几点关键转变:

  • 别用全局变量传数据——用消息队列或共享内存加信号量保护
  • 别用延时函数做时序控制——用任务延时或定时器
  • 别在中断里做复杂处理——中断只做标记,具体处理交给任务
  • 别让任务空转等待——用信号量或队列的阻塞机制,让出CPU

举个例子,裸机里你可能会这样写:

// 裸机风格:轮询等待按键
while(1)
{
    if(KEY_Read() == KEY_PRESSED)
    {
        do_something();
    }
}

这种写法在RTOS里就是灾难——它会一直占用CPU,其他任务根本没机会运行。正确的做法是:

// RTOS风格:等待信号量
void vTaskKey(void *pvParameters)
{
    while(1)
    {
        if(xSemaphoreTake(xKeySemaphore, portMAX_DELAY) == pdTRUE)
        {
            do_something(); // 按键被按下时才执行
        }
    }
}

你看,任务在等待信号量时会进入阻塞状态,CPU可以去做别的事。这才是多任务系统的精髓。

总结

FreeRTOS其实没那么神秘。它就是把我们平时在裸机里手动做的事情——任务切换、资源保护、数据传递——用一套标准化的机制来实现。你只要记住三个核心概念:

  • 任务调度:谁优先级高谁先跑,同优先级轮流跑
  • 信号量:保护共享资源,防止数据冲突
  • 消息队列:任务间传递数据,解耦生产者和消费者

嗯,掌握了这些,你就已经迈出了从裸机到RTOS的第一步。下一章我们会深入聊聊FreeRTOS的内存管理和中断管理,到时候再细说。