3、状态图设计:绘制状态转换图、定义状态ID、定义事件ID、定义动作函数
好,咱们进入状态机设计的核心环节——状态图设计。这一步说白了,就是把脑子里的逻辑变成一张看得见、摸得着的图。我个人习惯,画图之前先想清楚三件事:系统有哪些状态?什么事件能触发状态跳转?跳转的时候要执行什么动作?
你想想看,按摩仪的模式切换其实并不复杂。无非就是用户按一下按键,机器从当前模式切到下一个模式。但这里有个坑——如果用户长按、短按、或者同时按了两个键,系统该怎么反应?嗯,这就是状态图要解决的问题。
3.1 绘制状态转换图
先看一张我手绘的状态转换图(别笑,我画图水平就那样,但逻辑是清楚的):
+-----------+
| 待机模式 |
+-----+-----+
|
短按电源键(事件)
|
v
+-----------+
+---------+ 放松模式 +---------+
| +-----+-----+ |
| | |
短按模式键 短按模式键 短按模式键
| | |
v v v
+-----------+ +-----------+ +-----------+
| 舒缓模式 | | 强劲模式 | | 睡眠模式 |
+-----------+ +-----------+ +-----------+
| | |
+-------+-------+-------+-------+
| |
长按电源键 长按电源键
| |
v v
+-----------+
| 待机模式 |
+-----------+
这张图里,每个方框代表一个状态,箭头代表状态转换的方向,箭头上的文字就是触发转换的事件。我在项目中遇到过,有些同事喜欢把状态图画得特别复杂,恨不得把所有细节都塞进去。其实没必要,状态图的核心是「谁在什么条件下能到谁那里去」,其他的细节交给代码实现就好。
3.2 定义状态ID
状态图画好了,接下来要给每个状态起个名字、编个号。我习惯用枚举类型来定义状态ID,这样代码可读性高,编译器还能帮你检查拼写错误。
/* 状态ID定义 */
typedef enum {
STATE_IDLE = 0x00, /* 待机模式 */
STATE_RELAX = 0x01, /* 放松模式 */
STATE_SOFT = 0x02, /* 舒缓模式 */
STATE_STRONG = 0x03, /* 强劲模式 */
STATE_SLEEP = 0x04, /* 睡眠模式 */
STATE_MAX = 0x05 /* 状态总数,用于边界检查 */
} state_id_t;
这里有个细节:我把STATE_MAX放在最后,值为5。这样如果以后要增加新状态,只需要在它前面插入,然后更新STATE_MAX的值就行。我曾经在一个项目里忘了更新这个值,结果数组越界,程序跑飞了。嗯,从那以后我每次加状态都会检查这个宏。
| 状态ID | 枚举名称 | 说明 |
|---|---|---|
| 0x00 | STATE_IDLE | 待机模式,电机不转,等待用户操作 |
| 0x01 | STATE_RELAX | 放松模式,低速轻柔按摩 |
| 0x02 | STATE_SOFT | 舒缓模式,中速适中按摩 |
| 0x03 | STATE_STRONG | 强劲模式,高速深度按摩 |
| 0x04 | STATE_SLEEP | 睡眠模式,间歇性轻柔按摩 |
3.3 定义事件ID
事件就是「什么东西触发了状态转换」。在按摩仪这个场景里,事件主要来自按键操作。我建议把事件也定义成枚举,和状态ID保持一致的风格。
/* 事件ID定义 */
typedef enum {
EVT_NONE = 0x00, /* 无事件 */
EVT_POWER_SHORT = 0x01, /* 电源键短按 */
EVT_POWER_LONG = 0x02, /* 电源键长按 */
EVT_MODE_SHORT = 0x03, /* 模式键短按 */
EVT_MODE_LONG = 0x04, /* 模式键长按 */
EVT_TIMEOUT = 0x05, /* 定时超时 */
EVT_MAX = 0x06 /* 事件总数 */
} event_id_t;
你可能会问:为什么要把短按和长按分开定义?因为它们在状态机里的行为完全不同。短按是「切到下一个模式」,长按是「回到待机」。如果混在一起,状态转换逻辑会变得很混乱。
3.4 定义动作函数
动作函数,就是状态转换时要执行的具体操作。比如进入放松模式时,要设置电机转速、开启LED指示灯、启动定时器等等。我习惯把每个状态的动作封装成一个函数,这样状态机的主循环看起来特别清爽。
/* 动作函数声明 */
void action_enter_idle(void); /* 进入待机模式 */
void action_enter_relax(void); /* 进入放松模式 */
void action_enter_soft(void); /* 进入舒缓模式 */
void action_enter_strong(void); /* 进入强劲模式 */
void action_enter_sleep(void); /* 进入睡眠模式 */
void action_exit_idle(void); /* 退出待机模式 */
void action_exit_relax(void); /* 退出放松模式 */
/* ... 以此类推 */
每个动作函数内部做什么?举个例子:
void action_enter_relax(void)
{
/* 设置电机转速为低速 */
motor_set_speed(MOTOR_SPEED_LOW);
/* 开启放松模式指示灯(蓝色) */
led_set_color(LED_BLUE);
led_on();
/* 启动30分钟定时器 */
timer_start(30 * 60 * 1000); /* 30分钟,单位毫秒 */
/* 记录当前模式,用于下次切换 */
g_current_mode = MODE_RELAX;
/* 输出调试信息 */
printf("[状态机] 进入放松模式\n");
}
这里有个设计原则:动作函数只做「进入状态时该做的事」,不做「状态转换的判断」。判断逻辑全部交给状态机的主循环去处理。这样做的最大好处是——每个函数只干一件事,出了问题好排查。
最后,我建议把这三样东西放在同一个头文件里,比如state_machine_def.h。这样不管是自己看还是给别人看,都能一目了然。我在团队里推行这个做法后,代码review的效率提高了不少——因为大家不用翻来翻去找定义在哪了。