4. 任务调度与实时性设计:任务优先级分配、时间片轮转、看门狗策略、低功耗模式切换

好,咱们进入第四章。这一章我打算聊聊任务调度和实时性。说实话,这是理疗仪软件架构里最核心、也最容易出问题的地方。你想想看,一个理疗仪,它既要保证治疗波形准确输出,又要响应用户按键,还得监控温度、处理通信。这么多事情挤在一起,谁先谁后?谁让着谁?这就是调度要解决的问题。

4.1 任务优先级分配:别让“小事”耽误了“大事”

我个人习惯,在FreeRTOS里做优先级分配时,先画一张“任务紧急程度表”。理疗仪里,最紧急的永远是安全相关的任务。比如过温保护、过流检测,这些必须最高优先级。其次是治疗波形生成,它直接影响疗效,延迟个几毫秒,波形就失真了。再往下是用户界面响应、数据记录、通信等。

我遇到过的一个真实案例:有个同事把按键扫描任务的优先级设得比波形生成还高。结果呢?用户一按按键,波形就出现毛刺。病人感觉一阵刺痛。这可不是闹着玩的。所以我的原则是:安全 > 治疗 > 交互 > 记录

优先级分配黄金法则:

  • 最高优先级(5-7):安全监控(过温、过流、漏电检测)
  • 高优先级(3-4):治疗波形生成(PWM控制、DAC输出)
  • 中优先级(2):用户界面响应(按键、触摸、显示刷新)
  • 低优先级(1):数据记录、通信、日志
  • 空闲优先级(0):后台自检、统计

这里有个坑:优先级反转。低优先级任务拿了互斥锁,高优先级任务等着用,中间优先级任务插进来抢CPU。结果高优先级任务反而被堵死了。我建议用FreeRTOS的configUSE_PRIORITY_INHERITANCE,开启优先级继承,能缓解这个问题。

4.2 时间片轮转:让每个任务都有机会喘口气

FreeRTOS默认是抢占式调度,高优先级任务来了,低优先级就得让路。但同优先级的任务呢?它们之间怎么分时间?这就用到时间片轮转了。

说白了,就是给每个同优先级任务分配一个固定的时间片,比如5ms。时间到了,就切换到下一个。我一般把时间片设成1个系统Tick,也就是1ms。为什么?因为理疗仪里很多任务都是周期性的,1ms的粒度刚好够用。

// FreeRTOSConfig.h 中配置时间片
#define configUSE_PREEMPTION          1   // 开启抢占
#define configUSE_TIME_SLICING        1   // 开启时间片轮转
#define configTICK_RATE_HZ          1000  // 1ms一个Tick

但要注意,时间片轮转不是万能的。如果一个任务执行时间超过了时间片,它不会被打断,会继续执行完再切换。所以,每个任务里的循环、延时、阻塞操作,都要控制好时长。我习惯在每个任务里加一个“看门狗喂狗”操作,防止某个任务卡死。

我的小技巧: 在任务循环开头和结尾各放一个vTaskDelay(1),强制让出CPU。这样即使任务内部有长循环,也不会霸占CPU太久。说白了,就是“主动让位”,别等着系统来抢。

4.3 看门狗策略:最后一道防线

看门狗,我把它叫做“系统最后的守护者”。理疗仪是医疗设备,万一软件跑飞了,看门狗必须能拉回来。但怎么喂狗?什么时候喂?这里面有讲究。

我曾经犯过一个错误:在最高优先级的任务里喂狗。结果呢?安全监控任务一直跑,看门狗一直不被触发,但其他任务早就卡死了。系统看起来活着,其实已经半死不活了。这叫“假活”。

正确的做法是:在空闲任务里喂狗。为什么?因为空闲任务只有在所有其他任务都正常运行时才会被执行。如果某个任务卡死了,空闲任务就得不到CPU,看门狗就会超时复位。

// 空闲任务钩子函数中喂狗
void vApplicationIdleHook(void)
{
    // 只有所有任务都正常运行时,才会进入这里
    HAL_IWDG_Refresh(&hiwdg);  // 喂狗
}

但这样还不够。万一空闲任务本身卡死了呢?所以我建议再加一个多级看门狗策略

级别 超时时间 触发条件 动作
一级(软件看门狗) 100ms 高优先级任务未按时喂狗 记录错误,尝试恢复
二级(硬件看门狗) 500ms 空闲任务未喂狗 系统复位
三级(外部看门狗) 2s MCU完全死机 硬件断电重启

警告: 不要在中断服务函数里喂狗!我曾经见过有人这么干,结果中断一直触发,看门狗永远不超时,系统死锁了都发现不了。记住:喂狗只能在任务上下文中进行。

4.4 低功耗模式切换:省电但不省安全

理疗仪很多时候是电池供电的,低功耗设计很重要。但医疗设备有个矛盾:省电和安全怎么平衡?你总不能为了省电,把安全监控也关了吧?

我的做法是:分阶段降功耗。理疗仪有几种状态:工作态、待机态、休眠态。每个状态对功耗和实时性的要求不同。

  • 工作态: 全速运行,所有任务都激活。功耗最高,但实时性最好。
  • 待机态: 用户停止操作后30秒进入。关闭显示、降低通信频率,但安全监控和波形生成仍然运行。CPU进入睡眠模式,用RTC定时唤醒。
  • 休眠态: 待机超过5分钟进入。关闭所有治疗功能,只保留安全监控和唤醒按键。CPU进入深度睡眠,功耗降到uA级别。
// 进入待机态的低功耗模式
void EnterStandbyMode(void)
{
    // 关闭不必要的外设
    HAL_UART_DeInit(&huart1);  // 关闭通信
    HAL_LCD_DeInit();          // 关闭显示
    
    // 设置RTC定时唤醒(每100ms唤醒一次检查安全状态)
    HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN);
    
    // 进入睡眠模式
    HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
    
    // 唤醒后重新初始化
    HAL_UART_Init(&huart1);
    HAL_LCD_Init();
}

这里有个关键点:从低功耗模式唤醒后,要检查所有安全状态。我习惯在唤醒后的第一件事就是读取温度传感器、电流检测,确认一切正常再恢复治疗。如果唤醒后发现温度超标,直接进入安全保护状态,而不是继续治疗。

经验之谈: 低功耗切换时,别忘了看门狗。有些MCU在深度睡眠时,看门狗会停止计数。醒来后如果没及时重新配置,看门狗可能立即超时。我一般会在进入休眠前暂停看门狗,醒来后重新初始化。

嗯,这一章的内容差不多就这些。任务调度和实时性设计,说白了就是“谁先谁后、谁让谁、谁兜底、谁省电”。把这四个问题想清楚了,理疗仪的软件架构就稳了。下一章咱们聊聊通信协议设计,那又是另一番天地了。