4. 任务调度器配置:任务优先级分配策略、时间片轮转与抢占式调度、空闲任务钩子函数
好,咱们接着聊调度器。说实话,调度器是RTOS的灵魂,也是起搏器软件里最容易出问题的地方。我见过不少同行,把调度器配置想得太简单,结果在临床测试阶段翻车。今天我把这块掰开揉碎了讲,你听完就能上手。
4.1 任务优先级分配策略
优先级分配,说白了就是给每个任务排个队。谁先跑,谁后跑,全看优先级数字。但这里有个坑——起搏器里任务优先级不能随便拍脑袋。
我个人的习惯是:先画一张任务紧急程度表。比如起搏器里,感知心电信号的任务必须最高优先级,因为漏掉一次心跳可能出大事。而日志记录任务,优先级就可以放低。
核心原则:优先级与任务的实时性要求严格绑定。越紧急,优先级越高。
举个例子,我在一个项目中遇到过这样的情况:把电池管理任务的优先级设得比心电采样还高。结果呢?电池状态倒是监控得死死的,但心电信号采样被频繁打断,导致起搏脉冲发放时机偏差。嗯,这问题排查了整整两天。
这里我总结了几条经验:
- 硬实时任务(比如起搏脉冲发放)给最高优先级,通常0-2级
- 软实时任务(比如心电信号滤波)给中间优先级,3-5级
- 后台任务(比如数据记录、自检)给最低优先级,6-7级
注意:优先级不要超过8级。为什么?因为起搏器MCU资源有限,优先级太多反而增加调度开销。我曾经见过一个项目用了32级优先级,结果上下文切换时间长了30%,得不偿失。
4.2 时间片轮转与抢占式调度
这两种调度方式,你想想看,其实就像两种不同的工作模式。
抢占式调度:高优先级任务来了,低优先级任务立刻让路。这在起搏器里是必须的。比如心电检测到异常节律,必须立刻抢占当前任务,启动紧急起搏程序。
时间片轮转:同等优先级的任务,每人分一个时间片轮流跑。这个在起搏器里用得少,但也不是没用。比如多个后台任务,大家轮流干活,谁也不饿着。
我记得有一次调试,发现一个低优先级的通信任务老是跑不完。排查下来,原来是抢占式调度太频繁,通信任务每次刚拿到CPU就被抢走。后来我给通信任务和其他几个后台任务设了相同优先级,用时间片轮转,问题就解决了。
我的建议:起搏器里,关键任务用抢占式,非关键任务用时间片轮转。混合使用效果最好。
配置代码大概长这样:
/* 任务配置结构体 */
typedef struct {
uint8_t priority; /* 优先级 */
uint32_t time_slice; /* 时间片大小,0表示不使用时间片 */
uint8_t preemptive; /* 是否允许抢占 */
} TaskConfig_t;
/* 起搏器任务配置示例 */
TaskConfig_t pacing_task = {
.priority = 0, /* 最高优先级 */
.time_slice = 0, /* 不使用时间片 */
.preemptive = 1 /* 允许抢占 */
};
TaskConfig_t comm_task = {
.priority = 5, /* 中等优先级 */
.time_slice = 10, /* 10个tick的时间片 */
.preemptive = 0 /* 不允许抢占,使用时间片轮转 */
};
4.3 空闲任务钩子函数
空闲任务,就是CPU没事干的时候跑的那个任务。很多人觉得它不重要,其实不然。
空闲任务钩子函数,说白了就是你在空闲任务里塞一段自定义代码。起搏器里,我通常用它做三件事:
- 低功耗模式切换:CPU空闲时进入睡眠模式,省电
- 系统健康检查:趁空闲跑一跑内存检测、堆栈溢出检查
- 看门狗喂狗:确保系统不会因为空闲而复位
我曾经犯过一个错误:在空闲任务钩子里放了一个延时函数。结果呢?空闲任务被阻塞,系统以为CPU一直忙,功耗飙升。嗯,从那以后我规定:空闲任务钩子里绝对不能有阻塞调用。
经验之谈:空闲任务钩子函数要轻量、快速、无阻塞。最好控制在100微秒以内执行完毕。
代码示例:
/* 空闲任务钩子函数 */
void vApplicationIdleHook(void)
{
/* 1. 喂看门狗 */
watchdog_refresh();
/* 2. 检查堆栈使用情况 */
static uint32_t last_check = 0;
if (xTaskGetTickCount() - last_check > 1000)
{
check_stack_usage();
last_check = xTaskGetTickCount();
}
/* 3. 进入低功耗模式 */
__WFI(); /* Wait For Interrupt */
}
警告:不要在空闲任务钩子里做复杂计算或I/O操作。我曾经见过有人在里面写日志到Flash,结果导致系统响应变慢。记住,空闲任务是给CPU喘口气用的,不是让你加班的。
最后说一句,调度器配置没有标准答案。每个起搏器项目都有自己的特点。我的建议是:先按我说的原则搭框架,然后根据实际测试数据微调。别怕改,就怕不改。