第4章:实时操作系统(RTOS)基础:FreeRTOS任务创建与管理、任务优先级与调度策略、信号量与互斥锁、消息队列与事件标志组
各位同学,大家好。今天我们来聊聊嵌入式系统里一个绕不开的话题——实时操作系统,也就是RTOS。我做了这么多年储能控制器,说句实话,没有RTOS的项目,我反而觉得心里没底。尤其是FreeRTOS,轻量、开源、生态好,在储能BMS、逆变器控制里用得特别多。
这一章,我会把FreeRTOS最核心的几个概念掰开揉碎了讲。任务怎么创建、优先级怎么定、任务之间怎么通信、怎么同步。嗯,咱们一个一个来。
4.1 任务创建与管理
任务,说白了就是一个无限循环的函数。它有自己的栈空间、优先级、状态。FreeRTOS里创建任务用xTaskCreate()这个API。我个人习惯把任务创建放在main()函数里,启动调度器之前。
核心API:
BaseType_t xTaskCreate(
TaskFunction_t pvTaskCode, // 任务函数指针
const char * const pcName, // 任务名称(调试用)
configSTACK_DEPTH_TYPE usStackDepth, // 栈深度,单位是字
void *pvParameters, // 传给任务的参数
UBaseType_t uxPriority, // 任务优先级
TaskHandle_t *pxCreatedTask // 任务句柄
);
我在项目中遇到过一个问题:有个同事把栈深度设得太小,结果任务跑着跑着就崩了。你想想看,栈溢出是RTOS里最难排查的问题之一。所以我建议,初期调试时栈深度给大一点,比如512字甚至1024字,等稳定了再优化。
避坑指南:我曾经在一个BMS项目中,因为任务栈溢出导致系统随机死机。排查了整整两天,最后用uxTaskGetStackHighWaterMark()才发现某个任务的栈只剩4个字了。所以,记得在调试阶段监控栈使用量。
任务的状态有四种:运行态、就绪态、阻塞态、挂起态。调度器会从就绪态的任务里挑一个优先级最高的来执行。如果优先级相同,就轮着来——这叫时间片轮转。
4.2 任务优先级与调度策略
FreeRTOS用的是抢占式调度。什么意思?就是高优先级的任务一旦就绪,立马打断低优先级任务的执行。这个机制在储能控制器里特别重要。比如,过流保护任务必须比通信任务优先级高,否则保护来不及响应,硬件就烧了。
我一般把任务分成三个等级:
| 优先级等级 | 典型任务 | 说明 |
|---|---|---|
| 高(4-5) | 过流保护、温度保护 | 实时性要求极高,毫秒级响应 |
| 中(2-3) | 电压采样、SOC计算 | 周期性任务,允许少量延迟 |
| 低(1) | 日志记录、人机交互 | 可以容忍较大延迟 |
这里有个坑:优先级不要设太多。FreeRTOS支持几十个优先级,但实际项目中我建议只用5-7个。优先级太多,调度逻辑会变得复杂,而且容易出优先级反转的问题。
注意:优先级反转是RTOS里一个经典问题。低优先级任务持有高优先级任务需要的资源,导致高优先级任务被阻塞。解决办法是使用互斥锁(带优先级继承的版本),而不是普通信号量。
4.3 信号量与互斥锁
信号量,说白了就是一个计数器。用来做任务同步或者资源管理。二值信号量只有0和1,适合做“事件通知”。计数信号量可以管理多个资源,比如一个缓冲池里有5个缓冲区,信号量初值就是5。
互斥锁是信号量的一种特殊形式。它带了优先级继承机制,能解决优先级反转问题。我强烈建议:如果只是保护共享资源,用互斥锁,别用二值信号量。
// 创建互斥锁
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// 任务A获取锁
if (xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE) {
// 访问共享资源
// ...
// 释放锁
xSemaphoreGive(xMutex);
}
我在一个储能逆变器项目里,就遇到过因为没用互斥锁导致的数据竞争。两个任务同时修改一个全局变量,结果电压值偶尔跳变,保护逻辑误动作。后来改成互斥锁,问题就解决了。
4.4 消息队列与事件标志组
消息队列是任务间传递数据的标准方式。它像一个管道,一个任务往里写,另一个任务往外读。队列里的数据是拷贝传递的,所以要注意数据大小。我一般传指针,而不是传整个结构体。
经验之谈:我曾经传一个256字节的结构体,队列深度设了10个,结果RAM直接爆了。后来改成传指针,RAM占用降了90%。记住,队列里存的是数据的副本,不是引用。
事件标志组适合做“多条件同步”。比如,一个任务要等三个传感器都采集完毕才能开始处理。用事件标志组,每个传感器采集完就设置一个位,任务等所有位都置1了再执行。
// 创建事件标志组
EventGroupHandle_t xEventGroup = xEventGroupCreate();
// 传感器1采集完成
xEventGroupSetBits(xEventGroup, BIT_0);
// 传感器2采集完成
xEventGroupSetBits(xEventGroup, BIT_1);
// 处理任务等待所有位
EventBits_t uxBits = xEventGroupWaitBits(
xEventGroup,
BIT_0 | BIT_1, // 等待的位
pdTRUE, // 退出时清除位
pdTRUE, // 等待所有位
portMAX_DELAY // 无限等待
);
嗯,这里要注意:事件标志组适合事件数量不多的情况。如果事件超过24个,就得考虑用多个标志组或者换别的机制了。
4.5 知识体系总览
为了让大家更直观地理解FreeRTOS的核心机制,我画了一张图。这张图展示了任务、同步、通信之间的关系。
这张图把FreeRTOS的核心机制串起来了。任务创建后,调度器决定谁跑;任务之间通过信号量、互斥锁、事件标志组来同步;数据传递则靠消息队列。说白了,这就是一个完整的任务协作体系。
好了,这一章的内容就到这里。FreeRTOS的基础概念其实不难,难的是在实际项目中用好它们。我建议大家动手写几个小demo,比如创建两个任务,一个发消息,一个收消息,跑通了再往项目里加。嗯,实践出真知。
公众号:蓝海资料掘金营,微信deep3321