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喘口气用的,不是让你加班的。

最后说一句,调度器配置没有标准答案。每个起搏器项目都有自己的特点。我的建议是:先按我说的原则搭框架,然后根据实际测试数据微调。别怕改,就怕不改。