4、任务调度与管理:任务优先级设计、任务间通信(队列/信号量/事件标志组)、看门狗策略
好,咱们接着聊任务调度与管理。这部分内容,说白了就是嵌入式系统的“交通指挥中心”。你想想看,一个电池化成设备里,几十个通道同时工作,有的在充电,有的在放电,有的在采集数据,有的在响应告警。如果调度不好,系统就会乱成一锅粥。
我个人习惯,在设计任务调度之前,先画一张“任务关系图”。把每个任务的功能、触发条件、执行周期、依赖关系都列清楚。这张图,比任何代码都重要。
4.1 任务优先级设计:别让“饿死”和“反转”找上门
优先级设计,是任务调度的核心。我见过不少新手,上来就把所有任务都设成同一个优先级,或者把计算密集型任务设成最高优先级。结果呢?系统要么响应迟钝,要么低优先级任务永远得不到执行。
我的设计原则是这样的:
- 实时性要求高的任务,优先级高:比如过压保护、过流保护、温度告警。这些任务必须在毫秒级内响应,否则电池会出问题。
- 周期性任务,优先级适中:比如电压采集、电流采集、SOC 计算。这些任务有固定的执行周期,优先级可以设在中档。
- 后台任务,优先级最低:比如日志记录、数据上传、UI 刷新。这些任务可以“有空再做”,优先级最低。
我常用的优先级分配表,大概是这样的:
| 优先级等级 | 任务类型 | 典型任务 | 响应时间要求 |
|---|---|---|---|
| 最高(0-1) | 紧急保护 | 过压保护、过流保护、温度告警 | < 1ms |
| 高(2-3) | 实时控制 | PID 调节、PWM 输出、通道切换 | < 10ms |
| 中(4-5) | 数据采集 | 电压/电流/温度采集、ADC 读取 | < 100ms |
| 低(6-7) | 通信与日志 | CAN/RS485 通信、SD 卡写入 | < 1s |
这里要注意一个经典问题:优先级反转。什么意思呢?就是低优先级任务占用了高优先级任务需要的资源,导致高优先级任务被阻塞。我遇到过最典型的情况:一个低优先级的日志任务正在写 SD 卡,占用了互斥锁。此时一个高优先级的保护任务也要写 SD 卡,结果被阻塞了。保护任务迟迟得不到执行,电池就危险了。
4.2 任务间通信:队列、信号量、事件标志组
任务之间怎么“说话”?这是嵌入式系统设计的另一个关键点。我常用的通信方式有三种:队列、信号量、事件标志组。每种方式都有自己的适用场景。
4.2.1 队列:数据传递的“快递员”
队列,说白了就是一个先进先出的缓冲区。一个任务往里放数据,另一个任务往外取数据。我特别喜欢用队列,因为它天然解决了“生产者-消费者”问题。
在电池化成设备里,队列的典型应用场景是:
- ADC 采集任务 → 数据处理任务:ADC 采集任务把原始电压/电流数据放入队列,数据处理任务从队列中取出数据,进行滤波、校准、计算。
- 通信接收任务 → 命令解析任务:通信接收任务把收到的 CAN 或 RS485 报文放入队列,命令解析任务从队列中取出报文,解析并执行。
我常用的队列 API 是这样的(以 FreeRTOS 为例):
// 创建队列,长度为 10,每个元素大小为 sizeof(ADC_Data_t)
QueueHandle_t xAdcQueue = xQueueCreate(10, sizeof(ADC_Data_t));
// 发送任务:将采集到的数据放入队列
ADC_Data_t adcData;
adcData.channel = 1;
adcData.voltage = 3.3;
adcData.current = 0.5;
xQueueSend(xAdcQueue, &adcData, portMAX_DELAY);
// 接收任务:从队列中取出数据
ADC_Data_t receivedData;
if (xQueueReceive(xAdcQueue, &receivedData, pdMS_TO_TICKS(100)) == pdPASS) {
// 处理数据
ProcessADCData(&receivedData);
}
4.2.2 信号量:资源管理的“红绿灯”
信号量,说白了就是一个计数器。它用来管理共享资源的访问权限。我常用的信号量有两种:
- 二值信号量:只有 0 和 1 两种状态。用来实现“互斥访问”或“任务同步”。比如,一个任务完成初始化后,释放信号量,另一个任务等待信号量,然后开始工作。
- 计数信号量:可以计数。用来管理多个相同资源。比如,系统有 4 个 DMA 通道,每个通道用一个信号量表示。任务申请 DMA 时,先获取信号量,用完后释放。
我遇到过的一个典型场景:电池化成设备有多个通道,每个通道的 PID 控制任务都需要访问同一个“电流基准值”变量。如果不加保护,多个任务同时写这个变量,数据就乱了。这时候,用二值信号量做互斥锁,是最简单的办法。
// 创建二值信号量
SemaphoreHandle_t xMutex = xSemaphoreCreateBinary();
xSemaphoreGive(xMutex); // 初始化为可用状态
// 任务 A:获取信号量,修改基准值
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(10)) == pdPASS) {
g_CurrentReference = 5.0; // 修改全局变量
xSemaphoreGive(xMutex); // 释放信号量
}
// 任务 B:获取信号量,读取基准值
if (xSemaphoreTake(xMutex, pdMS_TO_TICKS(10)) == pdPASS) {
float ref = g_CurrentReference; // 读取全局变量
xSemaphoreGive(xMutex);
}
4.2.3 事件标志组:多条件触发的“开关矩阵”
事件标志组,说白了就是一个位图。每个位代表一个事件。任务可以等待多个事件同时发生,或者任意一个事件发生。这种机制特别适合“多条件触发”的场景。
在电池化成设备里,事件标志组的典型应用是:
- 启动充电流程:需要同时满足“电池已连接”、“通信已建立”、“温度正常”三个条件。每个条件对应一个事件位。当三个位都置 1 时,充电任务才开始工作。
- 故障处理:过压、过流、过温,任何一个事件发生,都要触发保护动作。这时候用“或”模式等待,任何一个位置 1 就执行保护。
// 定义事件位
#define EVENT_BATTERY_CONNECTED (1 << 0)
#define EVENT_COMM_ESTABLISHED (1 << 1)
#define EVENT_TEMP_NORMAL (1 << 2)
// 创建事件标志组
EventGroupHandle_t xEventGroup = xEventGroupCreate();
// 充电任务:等待所有条件满足
EventBits_t uxBits = xEventGroupWaitBits(
xEventGroup,
EVENT_BATTERY_CONNECTED | EVENT_COMM_ESTABLISHED | EVENT_TEMP_NORMAL,
pdTRUE, // 退出时自动清除事件位
pdTRUE, // 等待所有位都置 1
portMAX_DELAY
);
if ((uxBits & (EVENT_BATTERY_CONNECTED | EVENT_COMM_ESTABLISHED | EVENT_TEMP_NORMAL)) ==
(EVENT_BATTERY_CONNECTED | EVENT_COMM_ESTABLISHED | EVENT_TEMP_NORMAL)) {
// 开始充电
StartCharging();
}
4.3 看门狗策略:系统的“最后一道防线”
看门狗,说白了就是一个“定时炸弹”。如果系统在规定时间内没有“喂狗”,看门狗就会复位系统。这是防止系统死锁或跑飞的最后一道防线。
我见过很多工程师,把看门狗当成“万能药”。不管什么问题,都靠看门狗复位解决。其实这是不对的。看门狗应该用来处理“预期之外的异常”,而不是“预期之内的错误”。
我的看门狗设计原则是:
- 独立看门狗(IWDG)用于硬件级保护:如果 CPU 完全死掉,连中断都进不去,独立看门狗还能工作。我一般把独立看门狗的喂狗周期设在 1-2 秒。
- 窗口看门狗(WWDG)用于软件级保护:窗口看门狗要求必须在特定时间窗口内喂狗。太早或太晚都不行。这可以检测出“任务执行时间异常”的情况。
- 多级看门狗:在一些高可靠性项目中,我会用两级看门狗。第一级是“任务级看门狗”,每个关键任务都有自己的超时计数器。第二级是“系统级看门狗”,如果任务级看门狗没有及时复位,系统级看门狗就会触发。
我常用的喂狗策略是这样的:
// 主循环中喂狗
void main_loop(void) {
while(1) {
// 执行所有任务
Task_ADC_Collect();
Task_PID_Control();
Task_Comm_Process();
Task_Log_Write();
// 所有任务执行完毕后,喂狗
HAL_IWDG_Refresh(&hiwdg);
}
}
// 或者,在空闲任务中喂狗(FreeRTOS)
void vApplicationIdleHook(void) {
HAL_IWDG_Refresh(&hiwdg);
}
还有一个细节要注意:调试时关闭看门狗。如果你在调试模式下还开着看门狗,单步执行时系统会不断复位,根本没法调试。我一般会在代码里加一个宏定义,调试时自动关闭看门狗。
#ifdef DEBUG_MODE
// 调试模式,关闭看门狗
#define DISABLE_WDT() do { /* 空操作 */ } while(0)
#else
// 发布模式,开启看门狗
#define DISABLE_WDT() HAL_IWDG_Refresh(&hiwdg)
#endif
好了,任务调度与管理这部分,我就讲这么多。总结一下:优先级设计要合理,避免反转;任务间通信要选对工具,队列传数据,信号量管资源,事件标志组做多条件触发;看门狗是最后防线,但别滥用。嗯,这些经验,都是我在项目里一点点踩坑踩出来的。希望对你有帮助。