4、任务调度与管理:任务优先级设计、任务间通信(队列、信号量、事件组)、看门狗与任务监控
各位同学,咱们今天聊点硬核的。任务调度与管理,说白了就是让芯片里的多个任务「有序地打架」。我做了这么多年嵌入式,见过太多系统因为优先级设计不合理,直接死机或者跑飞。嗯,这节课咱们就把这块彻底讲透。
4.1 任务优先级设计——别让高优先级任务「饿死」低优先级
任务优先级,是RTOS调度器的核心。我个人习惯把优先级分成三档:
- 紧急响应型(最高优先级):比如按键中断、通信接收。这类任务必须立刻响应,延迟几毫秒可能丢数据。
- 周期执行型(中等优先级):比如数据采集、显示刷新。可以容忍几十毫秒的抖动。
- 后台处理型(最低优先级):比如日志存储、统计计算。有空就跑,没空就等着。
我在项目中遇到过一个问题:一个电表里同时有脉冲采集(高优先级)和LCD刷新(低优先级)。结果脉冲任务一直占用CPU,LCD刷新任务永远得不到执行。你想想看,屏幕卡死,用户肯定投诉。
优先级设计还有一个原则:不要超过系统支持的最大优先级数。比如FreeRTOS默认支持32级,你硬要设成64级,调度器会直接崩溃。嗯,这里要注意,很多新手会犯这个错。
4.2 任务间通信——队列、信号量、事件组
任务之间怎么传数据?直接访问全局变量?别闹,那会引发资源竞争。RTOS提供了三种标准机制:
4.2.1 队列(Queue)——最常用的数据管道
队列就像一根水管,一个任务往里灌水,另一个任务从另一头接水。数据是拷贝传递的,安全可靠。
// 创建队列,每个元素4字节,队列深度10
QueueHandle_t xQueue = xQueueCreate(10, sizeof(uint32_t));
// 发送任务
uint32_t data = 100;
xQueueSend(xQueue, &data, portMAX_DELAY);
// 接收任务
uint32_t received;
xQueueReceive(xQueue, &received, portMAX_DELAY);
我个人习惯用队列传递结构体指针,而不是直接拷贝大数据块。比如电表里传递一个「计量数据包」,包含电压、电流、功率,用指针传递效率更高。
4.2.2 信号量(Semaphore)——轻量级同步工具
信号量分两种:二值信号量和计数信号量。二值信号量就像一把钥匙,谁拿到谁干活。计数信号量就像停车场空位,每来一辆车减一,每走一辆车加一。
我在电表项目里常用信号量做「事件通知」。比如:
- 计量任务完成一次采集后,释放信号量
- 通信任务等待信号量,收到后立即发送数据
这样通信任务不用轮询,省电又高效。
4.2.3 事件组(Event Group)——多条件同步利器
事件组可以同时等待多个事件。比如电表需要「电压稳定」和「电流正常」两个条件都满足,才启动计量。用事件组就非常方便:
EventGroupHandle_t xEventGroup = xEventGroupCreate();
// 任务A:检测电压稳定后,设置bit0
xEventGroupSetBits(xEventGroup, 0x01);
// 任务B:检测电流正常后,设置bit1
xEventGroupSetBits(xEventGroup, 0x02);
// 计量任务:等待两个条件都满足
EventBits_t uxBits = xEventGroupWaitBits(
xEventGroup, // 事件组句柄
0x03, // 等待bit0和bit1
pdTRUE, // 退出时清除这些位
pdTRUE, // 等待所有位都置位
portMAX_DELAY); // 超时时间
你想想看,如果用信号量实现,得搞两个信号量,还得加一个任务去轮询。事件组一行代码搞定,多清爽。
4.3 看门狗与任务监控——系统最后的防线
看门狗(Watchdog)是嵌入式系统的「最后一道防线」。一旦系统卡死,看门狗会强制复位。但问题来了:如果某个任务卡死了,但看门狗还在被其他任务喂,系统就永远不会复位。
所以,我建议用任务级看门狗,而不是简单的硬件看门狗。具体做法:
- 每个关键任务都有一个「心跳计数器」
- 一个监控任务定期检查所有任务的心跳
- 如果某个任务超过3个周期没更新心跳,就触发复位
我在一个智能电表项目里,用FreeRTOS的软件定时器实现了任务监控:
// 每个任务的心跳变量
volatile uint32_t g_task_heartbeat[TASK_COUNT] = {0};
// 任务主循环中更新心跳
void task_metrology(void *pvParameters) {
while(1) {
g_task_heartbeat[TASK_METROLOGY]++;
// ... 执行计量逻辑 ...
vTaskDelay(pdMS_TO_TICKS(100));
}
}
// 监控任务
void task_watchdog(void *pvParameters) {
uint32_t last_heartbeat[TASK_COUNT] = {0};
while(1) {
for(int i = 0; i < TASK_COUNT; i++) {
if(g_task_heartbeat[i] == last_heartbeat[i]) {
// 任务卡死了,触发复位
printf("Task %d is dead! Reset...\n", i);
NVIC_SystemReset();
}
last_heartbeat[i] = g_task_heartbeat[i];
}
vTaskDelay(pdMS_TO_TICKS(500));
}
}
4.4 实战经验总结
好了,讲了这么多,我总结几条铁律:
| 场景 | 推荐做法 | 常见坑 |
|---|---|---|
| 高优先级任务 | 只做最紧急的事,处理完就阻塞 | 高优先级任务里做耗时计算 |
| 队列通信 | 用指针传递大数据,用值传递小数据 | 队列深度设太小导致丢数据 |
| 信号量同步 | 中断中用FromISR版本 | 忘记检查返回值 |
| 事件组 | 多条件同步首选 | 事件位冲突(多个任务用同一位) |
| 看门狗 | 任务级监控 + 硬件看门狗双重保险 | 只喂狗不监控任务状态 |
最后说一句:任务调度没有银弹。每个项目都有自己的特点,你需要根据实际需求调整优先级和通信方式。我做了十几年嵌入式,每次新项目还是会花大量时间在任务设计上。嗯,这步走对了,后面就顺了。
下一章咱们聊聊「内存管理与堆栈优化」,这可是嵌入式开发的另一个大坑。到时候见。