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在深度睡眠时,看门狗会停止计数。醒来后如果没及时重新配置,看门狗可能立即超时。我一般会在进入休眠前暂停看门狗,醒来后重新初始化。
嗯,这一章的内容差不多就这些。任务调度和实时性设计,说白了就是“谁先谁后、谁让谁、谁兜底、谁省电”。把这四个问题想清楚了,理疗仪的软件架构就稳了。下一章咱们聊聊通信协议设计,那又是另一番天地了。