第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的内存管理和中断管理,到时候再细说。