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