3. 软件架构:状态机设计(空闲、运行、保护、故障)、任务调度与优先级
好,咱们接着聊软件架构。这部分我把它看作是整个按摩仪安全保护的「大脑」。
说白了,你硬件做得再扎实,传感器选得再好,如果软件逻辑一团浆糊,那设备就是个定时炸弹。我个人习惯,在设计这类安全关键系统时,第一步不是写代码,而是画状态机。
3.1 为什么必须用状态机?
你想想看,按摩仪在工作时,会经历各种不同的「阶段」。比如刚开机、正在按摩、用户按了暂停、或者突然检测到异常。
如果不用状态机,代码很容易写成一大坨 if-else 嵌套。我曾经在一个项目里接手过这样的代码,维护起来简直想哭。一个按键按下,要判断十几个条件,稍不留神就漏掉了某个边界情况,导致设备死机。
状态机的好处就是:它把系统的行为「形式化」了。每个时刻,系统只能处于一个确定的状态。事件来了,根据当前状态和事件,决定下一步去哪。逻辑清晰,不容易出幺蛾子。
3.2 四个核心状态详解
我们这个按摩仪,我设计了四个核心状态。不多不少,刚好覆盖所有场景。
| 状态名称 | 状态标识 | 核心行为 | 进入条件 |
|---|---|---|---|
| 空闲 (IDLE) | 0x00 | 待机、监听按键、低功耗 | 上电复位、保护解除、故障恢复后 |
| 运行 (RUNNING) | 0x01 | 电机驱动、力度调节、定时 | 空闲状态下按下启动键 |
| 保护 (PROTECT) | 0x02 | 立即停止电机、保持传感器监测 | 检测到过流、过温、堵转等异常 |
| 故障 (FAULT) | 0x03 | 完全停机、记录错误码、等待复位 | 保护动作失败或检测到硬件致命错误 |
空闲状态: 这是系统的「家」。上电后第一件事就是进空闲。在这个状态里,我只做两件事:扫描按键,以及监测一个非常基础的「心跳」信号。功耗要压到最低,毕竟用户可能一整天都不用。
运行状态: 用户按下启动,就从空闲跳到运行。这里要注意,进入运行的第一件事,不是立刻转电机,而是做一次「安全检查」。比如,再次确认温度传感器读数正常,电流检测回路没问题。我在项目中就遇到过,传感器在空闲时是好的,但一上电瞬间被干扰,导致误判。所以,进入运行前的「自检」必不可少。
保护状态: 这是安全机制的核心。一旦检测到任何异常(比如电流超过1.5A,或者温度超过60度),系统必须无条件从任何状态(包括运行)跳转到保护状态。保护状态下的动作是「硬」的:直接切断电机PWM输出,拉高刹车引脚。同时,蜂鸣器发出急促的报警声。
故障状态: 这是最后的「安全网」。一旦进入故障,系统就锁死了。所有输出全部关闭,只有断电再上电才能恢复。同时,我会在EEPROM里记录一个故障码,方便售后分析。比如0x03代表「过温保护超时」,0x04代表「电流传感器失效」。
3.3 任务调度与优先级设计
好,状态机搭好了,接下来就是怎么跑起来。在嵌入式系统里,我们通常用RTOS(实时操作系统)或者一个简单的前后台系统。对于按摩仪这种成本敏感的设备,我倾向于用前后台系统 + 定时器中断。
为什么不用RTOS?说白了,成本。一个RTOS内核要占几KB的RAM和ROM,对于一颗几毛钱的MCU来说,太奢侈了。而且,按摩仪的任务数量很少,前后台完全够用。
我设计的任务调度,核心就是一句话:中断里做紧急的事,主循环里做耗时的事。
任务优先级划分
| 优先级 | 任务名称 | 执行位置 | 最大执行时间 |
|---|---|---|---|
| 最高 (0) | 安全监测任务 | 定时器中断 (1ms) | 0.1ms |
| 高 (1) | 状态机调度任务 | 定时器中断 (5ms) | 0.5ms |
| 中 (2) | 电机控制任务 | 主循环 | 2ms |
| 低 (3) | UI显示与按键扫描 | 主循环 | 10ms |
| 最低 (4) | 通信与日志任务 | 主循环 | 50ms |
安全监测任务: 这是整个系统里最不能被打断的任务。我用一个1ms的硬件定时器中断来执行它。在这个中断里,我只做三件事:读电流、读温度、读堵转检测引脚。如果发现异常,直接置位一个「保护请求」标志位。注意,不要在中断里调用任何延时函数或者复杂运算。我曾经见过有人在中断里用printf调试,结果系统直接卡死。中断里要快进快出。
状态机调度任务: 5ms执行一次。它读取「保护请求」标志位,以及来自主循环的「按键事件」和「定时事件」,然后根据当前状态,执行状态转移。这个任务也放在中断里,因为状态转移必须及时。比如,从运行到保护的转移,如果延迟了10ms,电机可能已经烧了。
电机控制任务: 这个放在主循环里。它根据当前状态(是运行还是保护),来计算PWM的占空比。在运行状态下,它读取用户设定的力度值,然后通过PID算法输出PWM。在保护状态下,它直接输出0。
UI显示与按键扫描: 这个任务优先级最低。因为用户按个键,延迟几十毫秒根本感觉不出来。但要注意,按键扫描要做「消抖」处理。我一般用20ms的延时消抖,或者用状态机来做按键检测,防止误触发。
3.4 代码骨架示例
说了这么多,咱们看一段核心代码。这是状态机调度任务的简化版。
// 状态枚举
typedef enum {
STATE_IDLE = 0,
STATE_RUNNING,
STATE_PROTECT,
STATE_FAULT
} SystemState_t;
// 全局状态变量
SystemState_t currentState = STATE_IDLE;
// 5ms定时器中断服务函数
void TIM5_IRQHandler(void) {
// 1. 清除中断标志
// 2. 读取安全监测结果
uint8_t protectRequest = getProtectRequestFlag();
// 3. 状态机调度
switch(currentState) {
case STATE_IDLE:
if(key_press_event) {
// 进入运行前,再做一次安全检查
if(safety_check_pass()) {
currentState = STATE_RUNNING;
motor_enable();
} else {
// 安全检查失败,进故障
currentState = STATE_FAULT;
error_code = 0x10;
}
}
break;
case STATE_RUNNING:
if(protectRequest) {
// 立即进入保护
currentState = STATE_PROTECT;
motor_disable();
buzzer_on();
protect_timer = 0;
}
// 处理定时结束事件
if(timer_expired) {
currentState = STATE_IDLE;
motor_disable();
}
break;
case STATE_PROTECT:
protect_timer++;
if(protect_timer > PROTECT_TIMEOUT) {
// 保护超时,进故障
currentState = STATE_FAULT;
error_code = 0x03;
}
// 如果异常消失,回到空闲
if(!protectRequest) {
currentState = STATE_IDLE;
buzzer_off();
}
break;
case STATE_FAULT:
// 故障状态,啥也不干,等看门狗复位
break;
}
// 4. 喂看门狗
watchdog_feed();
}
你看,整个逻辑非常清晰。每个case里做的事情都很单一。这就是状态机的魅力。
3.5 避坑指南
最后,分享几个我踩过的坑。
- 状态转移条件要「互斥」:我曾经设计过一个状态机,从运行状态既可以进保护,也可以进空闲。结果有一次两个条件同时满足了,代码执行了第一个case后,状态变了,第二个case就没执行到。后来我改成用if-else if结构,保证每次只执行一个转移。
- 不要在中断里做浮点运算:MCU的FPU(浮点运算单元)不是标配。如果你在中断里做浮点乘除,轻则增加中断延迟,重则导致堆栈溢出。我一般把所有浮点运算都放在主循环里,中断里只用整数运算。
- 优先级反转:虽然我们没用RTOS,但在前后台系统里,如果主循环里的一个低优先级任务占用了CPU太久,会导致中断里的高优先级任务无法及时处理。所以,我要求每个主循环任务的执行时间不能超过1ms。如果某个任务确实耗时,就把它拆分成多个小步骤。
嗯,关于软件架构的状态机和任务调度,我就讲这么多。这部分是软件安全的核心骨架,一定要理解透彻。下一章,咱们聊聊具体的硬件驱动和传感器数据处理。