第三章:嵌入式实时操作系统(RTOS)基础
3.1 FreeRTOS任务创建与管理
做BMS开发,我个人习惯是从任务设计开始的。你想想看,一个电池管理系统要同时做电压采集、温度监控、均衡控制、通信上报……如果不用RTOS,光靠一个大循环来回跑,代码很快就变成一锅粥了。
FreeRTOS里,任务就是一个无限循环的函数。创建任务用xTaskCreate(),这个函数我闭着眼睛都能写出来:
TaskHandle_t xTaskVoltageHandle = NULL;
void vTaskVoltage(void *pvParameters)
{
// 任务初始化
ADC_Init();
for( ;; )
{
// 采集电压
ReadCellVoltages();
// 处理数据
ProcessVoltages();
// 延时,让出CPU
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 在main函数中创建
xTaskCreate(
vTaskVoltage, // 任务函数
"Voltage Task", // 任务名称,调试用
256, // 栈深度,单位是字
NULL, // 参数
3, // 优先级
&xTaskVoltageHandle // 任务句柄
);
嗯,这里要注意栈大小。我在项目中遇到过,栈给太小,任务跑着跑着就崩了。BMS里任务多,每个任务至少给256字,通信任务我建议给512字以上。
避坑指南:我曾经在一个量产项目里,因为栈溢出导致系统偶尔死机。排查了三天才发现是某个任务里临时变量太多,栈不够用。后来我养成了习惯——每个任务创建后先跑压力测试,看栈使用峰值。
3.2 任务优先级与调度策略
FreeRTOS用的是抢占式调度。说白了,高优先级的任务随时可以打断低优先级的任务。这个机制在BMS里特别重要。
我一般把任务分成三个等级:
| 优先级 | 任务类型 | 典型任务 |
|---|---|---|
| 高(4-5) | 安全关键型 | 过压保护、过温保护、硬件故障检测 |
| 中(2-3) | 数据采集型 | 电压采集、电流采集、温度采集 |
| 低(1) | 非实时型 | 日志记录、状态上报、UI刷新 |
为什么会这样分?你想想看,电池过压了,必须在几毫秒内切断回路。这个任务优先级必须最高。而日志记录晚个几百毫秒,完全没问题。
关键点:优先级不是越高越好。优先级反转了解一下?高优先级任务等低优先级任务释放资源,结果中间优先级任务插进来,高优先级反而被"饿死"。我在BMS里遇到过这种情况,后来用互斥锁解决了。
3.3 信号量与互斥锁
信号量,说白了就是个计数器。二值信号量像一把钥匙,谁拿到谁干活。计数信号量像停车场,有几个空位就能停几辆车。
互斥锁是信号量的特殊形式,它带优先级继承机制。这个机制能解决我刚才说的优先级反转问题。
// 创建互斥锁
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// 任务A:写数据到共享缓冲区
void vTaskWrite(void *pvParameters)
{
for( ;; )
{
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE)
{
// 写数据
WriteToBuffer();
// 释放锁
xSemaphoreGive(xMutex);
}
}
}
// 任务B:读数据
void vTaskRead(void *pvParameters)
{
for( ;; )
{
if(xSemaphoreTake(xMutex, portMAX_DELAY) == pdTRUE)
{
// 读数据
ReadFromBuffer();
xSemaphoreGive(xMutex);
}
}
}
我的经验:在BMS里,电池电压数据是多个任务共享的。采集任务写,均衡任务读,保护任务也读。我建议用互斥锁保护这个数据区。曾经有人用二值信号量,结果优先级反转导致保护动作延迟了50ms——这在BMS里是致命的。
3.4 消息队列与事件组
消息队列,就是任务之间的"快递员"。一个任务发消息,另一个任务收消息。数据不会乱,顺序也不会错。
// 创建队列,每个消息10个字节,队列深度5
QueueHandle_t xQueue = xQueueCreate(5, 10);
// 发送任务
void vTaskSend(void *pvParameters)
{
uint8_t data[10] = {0x01, 0x02, 0x03};
for( ;; )
{
// 发送数据
xQueueSend(xQueue, data, portMAX_DELAY);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 接收任务
void vTaskReceive(void *pvParameters)
{
uint8_t recvData[10];
for( ;; )
{
// 接收数据,阻塞等待
if(xQueueReceive(xQueue, recvData, portMAX_DELAY) == pdTRUE)
{
// 处理接收到的数据
ProcessCommand(recvData);
}
}
}
事件组就更有意思了。它像一面旗子,多个任务可以同时等待多个事件。比如BMS里,我要等"电压采集完成"和"温度采集完成"两个事件都到了,才能开始做SOC估算。
// 创建事件组
EventGroupHandle_t xEventGroup = xEventGroupCreate();
#define BIT_VOLTAGE_DONE (1 << 0)
#define BIT_TEMP_DONE (1 << 1)
#define BIT_CURRENT_DONE (1 << 2)
// SOC估算任务,等待三个事件
void vTaskSOC(void *pvParameters)
{
EventBits_t uxBits;
for( ;; )
{
// 等待三个事件都发生
uxBits = xEventGroupWaitBits(
xEventGroup,
BIT_VOLTAGE_DONE | BIT_TEMP_DONE | BIT_CURRENT_DONE,
pdTRUE, // 清除事件位
pdTRUE, // 等待所有事件
portMAX_DELAY
);
// 三个数据都齐了,开始算SOC
CalculateSOC();
}
}
实用建议:消息队列适合点对点通信,事件组适合多任务同步。我在BMS里,采集任务完成后设置事件位,保护任务、均衡任务、SOC任务各自等待自己关心的事件。这样代码结构清晰,也容易调试。
好了,RTOS基础就讲到这里。记住一句话:RTOS不是银弹,但它能让你的BMS代码从"一团乱麻"变成"井井有条"。下一章我们开始讲状态机,那才是BMS软件架构的精髓。