一句话结论:把状态机写成「迁移表 + 一个 dispatch 循环」之后——流程变成只读数据(可以整体放进 Flash)、父状态能替子状态收事件(子状态不用重复写取消/复位)、超时是引擎能力而不是每个状态各写一遍 if、而且可以静态检查出不可达状态和死状态。本文实测:4 个状态 14 条迁移的咖啡机,静态检查在正常表上 0 问题,而把唯一的入口迁移改成废迁移后,立刻报出 3 个不可达状态;把故障态的两条出口删掉,立刻报出 1 个死状态(编译器对这两类错误完全不吭声)。
一、完整工程下载
压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。
下载 fsm-engine.zip (19.2 KB,共 8 个文件)
.gitignore
Makefile
README.md
include/
fsm.h
platformio.ini
src/
fsm.c
main.c
test/
test_fsm.c一、switch 堆出来的状态机有什么问题
先用最朴素的办法写一个投币咖啡机:
switch (state) {
case IDLE:
if (ev == COIN) { credit += 25; state = CREDIT; }
break;
case CREDIT:
if (ev == COIN) { credit += 25; }
else if (ev == CANCEL) { refund(credit); credit = 0; state = IDLE; }
else if (ev == SELECT && credit >= price) { state = BREWING; }
else if (ev == TIMEOUT) { refund(credit); credit = 0; state = IDLE; }
break;
case BREWING:
if (ev == BREW_DONE) { brew(); state = IDLE; }
else if (ev == TEMP_HIGH) { state = FAULT; }
/* 忘了写 CANCEL! */
break;
...
}它会正常工作,直到有一天你要加一个状态。那时候你会发现:
- 取消逻辑(CANCEL)在 3 个状态里各写了一遍,改一次要改三处,漏一处就是 bug
- 超时要每个状态各写一套计时器,很容易漏
- 你没法回答「有没有哪个状态进不去」这种问题,只能靠人肉读代码
- 状态流转的轨迹只能靠打断点,没法打成日志
根本原因:流程逻辑被写成了「代码」,而不是「数据」。 代码只能执行,不能检查;数据可以打印、可以遍历、可以校验。
二、表驱动:把流程变成数据
核心结构就两个:
/* 一条迁移:在 state 状态下收到 event,如果守卫通过,就跳到 next 并执行 action */
typedef struct {
uint8_t state;
uint8_t event;
uint8_t guard; /* 0 = 没有守卫条件 */
uint8_t next;
uint8_t action;
} fsm_trans_t;
/* 一个状态:名字、父状态、超时、进出动作 */
typedef struct {
const char *name;
uint8_t parent; /* 0xFF = 没有父状态 */
uint16_t timeout_ms; /* 0 = 不超时 */
uint8_t timeout_event; /* 超时时往引擎里投的事件 */
uint8_t on_entry;
uint8_t on_exit;
} fsm_state_t;关键设计:表里只放数字,不放函数指针。 守卫和动作用下标引用两个独立数组:
static int guard_credit_enough(fsm_t *f);
static void act_add_credit(fsm_t *f);
static fsm_guard_fn guards[] = { NULL, guard_credit_enough };
static fsm_action_fn actions[] = { NULL, act_add_credit, act_refund };这么做的理由很实在:纯数字的 const 表可以整体放进 Flash,不占 RAM, 而且可以直接用工具(甚至脚本)解析、生成、Diff。 函数指针表在 Cortex-M 上要占 RAM(因为要重定位),也不方便外挂工具链检查。
引擎的 dispatch 就是一个循环:
1. 从当前状态开始,沿着父状态链往上找 (state, event) 匹配的迁移
2. 找到第一条「守卫通过」的迁移(所以表的顺序就是优先级)
3. 执行当前状态的 exit 动作
4. 执行迁移的 action
5. 切换状态,执行新状态的 entry 动作
6. 重置超时计时器三、层次状态:父状态替子状态收事件
上面那张 BREWING 忘了写 CANCEL 的 bug,用父子关系可以直接消灭:
S_IDLE
└── S_CREDIT (父状态)
└── S_BREWING (子状态)
S_FAULTCANCEL 和 START 这类「在哪都应该管用」的事件,只在 S_CREDIT 里写一条, S_BREWING 不用重复。dispatch 往父链上找的时候自然就命中了。
这就是层次状态机(HSM)最实用的部分——你不用实现完整 UML 那套理论, 只需要「找不到就往父状态找」这一条规则,就能消掉大部分重复迁移。
实测(本文第 4 项):在 S_BREWING 状态下发 CANCEL, 引擎沿父链找到 S_CREDIT: CANCEL -> S_IDLE,退币并退回空闲—— 而 S_BREWING 自己的迁移表里根本没有 CANCEL。
四、超时:引擎的能力,不是每个状态的负担
超时是嵌入式状态机最需要、也最容易写漏的东西:投币后不选要退币、 加热超时要报故障、握手超时要重连。
把它做成状态的属性,而不是每个状态自己的判断:
static const fsm_state_t states[S_COUNT] = {
/* name parent timeout timeout_event entry exit */
{ "S_IDLE", FSM_NO_PARENT, 0, EV_NONE, 0, 0 },
{ "S_CREDIT", S_IDLE, 3000, EV_TIMEOUT, 0, 0 },
{ "S_BREWING", S_CREDIT, 90000, EV_TIMEOUT, ACT_START_BREW, 0 },
{ "S_FAULT", FSM_NO_PARENT, 0, EV_NONE, ACT_LOG_FAULT, 0 }
};这里有个必须注意的坑:timeout_ms 得是 uint32_t,不能图省事写 uint16_t。 S_BREWING 的加热超时是 90000 ms,uint16 最大只能到 65535, 90000 会被静默截断成 24464(约 24 秒)——加热还没到 60 秒就报故障了。 好消息是 gcc 的 -Woverflow 会警告这件事(本文编译时加了 -Wextra 才看到), 坏消息是大多数嵌入式工程默认不开这个警告,而且这个截断完全不报错。 一旦你写的是 uint16_t,编译期只给一行警告,运行期就是现场那个「加热时间莫名其妙变短了」的怪 bug。
引擎在 fsm_tick(ms) 里累加时间,一旦超过当前状态的 timeout_ms, 就自动往事件队列里投一个 EV_TIMEOUT。于是:
- 超时逻辑只需要写一条迁移,不用写计时器判断
- 进入任何状态时计时器自动重置(在
entry步骤里统一做) - 想给某个状态加超时,只改表里一个数字
实测(第 3 项):投币 25 分后不再操作,跑到第 3000 个毫秒引擎自动投出 EV_TIMEOUT → 退币回 S_IDLE。 注意是恰好 3000:第 2900 ms 时还停在 S_CREDIT,说明计时器是「先进入状态、后开始计时」, 不会在刚进状态的那一刻误触发。
五、静态检查:这才是表驱动最大的回报
流程变成数据之后,可以在启动时(甚至编译期用脚本)做检查。实测里实现了四项:
| 检查项 | 怎么查 | 为什么重要 |
|---|---|---|
| 不可达状态 | 从初始状态做可达性 BFS,看哪些状态没被访问到 | 死代码,而且往往意味着某条迁移写错了 |
| 死状态 | 没有任何出口迁移的状态 | 进去就出不来,现场表现为「卡死」 |
| 迁移越界 | 检查 state/event/next 是否在表的范围内 | 改表时手滑写错一个数字,编译器不管 |
| 重复迁移 | 同一个 (state, event) 出现两次且都没守卫 | 语义歧义:只有第一条会生效 |
实测结果(第 6 项),正常表:状态 4 个、迁移 14 条 →
不可达状态 0 个,死状态 0 个,越界迁移 0 条,歧义迁移 0 条然后我把表故意改坏两次,看检查能不能抓住:
- 变体 A:把唯一的入口迁移
IDLE:COIN -> S_CREDIT的源状态改成0xFF(模拟改表时手滑),
结果报出 不可达状态 3 个(S_CREDIT、S_BREWING、S_FAULT)+ 越界迁移 1 条。 注意这两个数字是同一条错误的两种表现:入口断了,后面三个状态全进不去; 而那条废迁移自己又越界了。编译器对这两件事都不吭声—— 它只看到你写了一个 0xFF 的整数,S_CREDIT 还是合法枚举,语法上毫无问题。
- 变体 B:把
S_FAULT的两条出口全删掉 → 报出 死状态 1 个(S_FAULT)、不可达 0 个。
这个更阴险:S_BREWING --TEMP_HIGH--> S_FAULT 还在,故障态照进不误, 进了就再也出不来(RESET 迁移没了)——现场表现是「机器报故障后必须断电重启」, 而且测试用例里如果只测正常流程,永远发现不了。
这两条正好说明了静态检查的价值:它检查的是「表的整体结构」,而不是「某一条执行路径」。
六、主机实测(本文数据来源)
咖啡机:4 个状态(S_IDLE / S_CREDIT / S_BREWING / S_FAULT)、14 条迁移、 25 分一杯(两杯 50)、投币超时 3000 ms、加热超时 90000 ms、事件队列容量 8。
| # | 实验 | 实测结果 |
|---|---|---|
| 1 | 正常流程:投两次币 → 选咖啡 | 轨迹 IDLE -COIN-> CREDIT -COIN-> CREDIT -SELECT-> BREWING -BREW_DONE-> IDLE;扣 50,余额 0,出货 1 杯 |
| 2 | 投 50 后按取消 | 退币 50,累计退款 50,回 S_IDLE |
| 3 | 投币后不操作,看超时 | 第 2900 ms 仍在 S_CREDIT(未超时);第 3000 ms 超时计数 0→1,退币 25,回 S_IDLE |
| 4 | 在 S_BREWING 里按取消 | S_BREWING 自己的 CANCEL 迁移数 = 0;引擎沿父链找到下标 4(源状态是 S_CREDIT)→ 退币 50,回 S_IDLE |
| 5 | 加热故障 → 故障态 → 复位 | S_BREWING --TEMP_HIGH--> S_FAULT;FAULT 里投币有迁移但只是退币(余额归 0),SELECT 被拒绝(拒绝计数 +1);RESET → S_IDLE |
| 6 | 静态检查 | 正常表 0 问题;变体 A → 3 不可达 + 1 越界;变体 B → 1 死状态(S_FAULT)、0 不可达 |
| 7 | 事件队列打满 | 容量 8,一次投 20 个 → 入队成功 7 个、丢弃 13 个(队列里已占 1 个),余额仍累加到 200,引擎没崩、没死循环 |
| 8 | 只投 25 就选咖啡 | 同一个 (S_CREDIT, SELECT) 有两条迁移:带守卫的写在前面先匹配,守卫不过才轮到后面那条 → 留在 S_CREDIT,余额 25 |
| 9 | 加热超时边界 | 89999 ms 仍在 S_BREWING;90000 ms 进 S_FAULT。注意 90000 装不进 uint16(会截成 24464),所以 timeout_ms 必须是 uint32 |
| 10 | 三条「无害」迁移 | IDLE:RESET、IDLE:CANCEL 后仍停在 S_IDLE;BREWING:RESET 从冲泡中途强制回 S_IDLE |
| 11 | 迁移覆盖率 | 覆盖 14 / 14 条,每条迁移都有命中计数器(命中次数最高的两条是 CREDIT:COIN 13 次、IDLE:COIN 9 次) |
第 11 项值得单独说:「覆盖了多少条迁移」是状态机最有用的测试指标, 比行覆盖率有意义得多——行覆盖率 100% 只说明代码被执行过, 而迁移覆盖率 100% 才说明每一条状态流转路径都被真跑过一次。 表驱动的写法让这个统计变成几行代码(每条迁移加一个命中计数器就行), 而 switch 写法根本没法统计:因为「迁移」在 switch 里根本不是一个可以枚举的对象。 这也是把流程变成数据的直接回报。
七、写这个引擎时踩的两个坑
静态检查听起来很简单——「遍历一遍,看看谁没出口」。 但检查器自己写错的时候,它不会报错,只会给你一个看起来很合理的错误结论。 这两个坑都是我在写这篇的实测代码时真踩到的。
坑 1:可达性 BFS 里顺手统计出口,会把不可达状态误报成死状态
第一版 fsm_validate() 是这么写的:
/* 错误写法:在可达性 BFS 的过程中顺便数出口 */
while (queue 非空) {
s = 出队;
int exits = 0;
for (i = 0; i < ntrans; i++) {
if (trans[i].state == s) {
exits++;
if (!visited[trans[i].next]) { visited[...] = 1; 入队; }
}
}
if (exits == 0) rep->dead_end[rep->n_dead_end++] = s; /* ← 错在这 */
}用变体 A(把唯一入口 IDLE:COIN 改成废迁移)测的时候, 我期望的是「3 个不可达状态」,结果它报的是「3 个不可达 + 3 个死状态」—— S_CREDIT、S_BREWING、S_FAULT 全被算成了死状态。但事实是: 这三个状态都有自己的出口迁移,只是因为入口断了所以进不去而已。
原因就一句话:这个循环根本没执行到它们,所以 exits 永远是 0。 「没被检查到」被当成了「检查结果是没有出口」。
修法是把两件事拆成两遍独立的遍历:
/* 正确写法:出口统计要在**全表**上做,不能只在可达部分做 */
for (s = 0; s < n; s++) {
int exits = 0;
for (i = 0; i < ntrans; i++) {
if (trans[i].state == s) exits++;
}
if (exits == 0) rep->dead_end[rep->n_dead_end++] = s;
}
/* 可达性 BFS 单独做,只负责填 visited[] */这个坑的可怕之处在于:你完全看不出第一版有错。 逻辑读起来很顺,输出格式也很正常,甚至在你只测正常表的时候 (正常表里所有状态都可达)它给出的结论是对的—— 错误只在「表被改坏」的那一刻才暴露,而那时候你正在找的是表的 bug,不是检查器的 bug。
坑 2:迁移表里塞了一个「以后要用」的状态
我一开始在表里留了个 S_MAINT(维护模式)状态,想着「以后加清洗功能要用」, 但没写任何进出它的迁移。结果静态检查报出「1 个不可达状态:S_MAINT」。
这不是检查器的 bug,这是检查器在正常工作:它抓住了一个真实的死代码。 更重要的是它揭示了一件事——一个「预留」的状态会一直在检查器的告警里出现, 于是每个看告警的人都要重新判断一次「这个是不是有用」,或者干脆学会忽略告警。 一旦团队开始忽略告警,这套检查就废了。
所以最后的做法是:把 S_MAINT 从表里删掉,需要时再加。 表驱动的另一个好处在这里体现出来——加一个状态就是加一行数据, 成本低到没必要提前预留。
八、什么时候不该用状态机
- 纯线性流程(初始化 A→B→C,没有回退和分支):顺序代码更清楚,别硬套
- 状态少且没有共享事件(2 个状态、3 个事件):switch 就够了,别为了架构而架构
- 并发任务:状态机是单线程模型。多任务共享状态机的状态时,
要么加锁,要么把状态机整个放进一个任务里,别在多个任务里直接改 state
一旦出现下面任一信号,就说明该换成表驱动了:
- 同一个事件在三个以上状态里被处理
- 有了超时需求
- 需要打印状态轨迹做现场排查
- 有人问「这个状态怎么进去的」,你答不上来
完整代码
Makefile
CC ?= gcc
CFLAGS ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS ?=
SRC = src/fsm.c
TEST = test/test_fsm.c
ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)
all: run
$(BIN): $(SRC) $(TEST)
@mkdir -p build
$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)
run: $(BIN)
@$(BIN)
clean:
rm -rf build
.PHONY: all run cleaninclude/fsm.h
/**
* fsm.h - 表驱动状态机引擎(支持超时、层次状态、事件队列、静态检查)
*
* 设计要点:
* 1) 迁移表和状态表里只放**数字下标**,不放函数指针。
* 好处:const 表可以整体放进 Flash,不占 RAM,也方便外部工具解析。
* 2) dispatch 找不到迁移时会沿着**父状态链**往上找,
* 于是「取消」「复位」这类通用事件只需要在父状态写一次。
* 3) 超时是**状态的属性**,由引擎统一计时,不用每个状态各写一遍。
*/
#ifndef FSM_H
#define FSM_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct fsm_s;
typedef int (*fsm_guard_fn)(struct fsm_s *f);
typedef void (*fsm_action_fn)(struct fsm_s *f);
#define FSM_NO_PARENT 0xFFu
#define FSM_NO_TIMEOUT 0u
#define FSM_EQ_SIZE 8 /* 事件队列容量 */
#define FSM_STATE_MAX 24
#define FSM_TRANS_MAX 64
/** 一条迁移:state 下收到 event,守卫通过则跳到 next 并执行 action */
typedef struct {
uint8_t state;
uint8_t event;
uint8_t guard; /* 0 = 无守卫 */
uint8_t next;
uint8_t action; /* 0 = 无动作 */
} fsm_trans_t;
/** 一个状态 */
typedef struct {
const char *name;
uint8_t parent; /* FSM_NO_PARENT = 顶层 */
/* 注意是 uint32_t:90 秒 = 90000 ms,用 uint16 会静默截断成 24464。
* 这个字段被写成 uint16 是本工程早期版本真实踩过的坑。 */
uint32_t timeout_ms; /* 0 = 不超时 */
uint8_t timeout_event; /* 超时时投递的事件 */
uint8_t on_entry; /* 进入动作下标 */
uint8_t on_exit; /* 离开动作下标 */
} fsm_state_t;
/** 状态机定义(只读) */
typedef struct {
const fsm_state_t *states;
int nstates;
const fsm_trans_t *trans;
int ntrans;
const fsm_guard_fn *guards;
int nguards;
const fsm_action_fn *actions;
int nactions;
/** 每次成功迁移都会被调用,用于打日志 / 记录轨迹 */
void (*trace)(void *ctx, int from, uint8_t ev, int to);
void *trace_ctx;
int initial_state;
} fsm_def_t;
/** 运行实例 */
typedef struct fsm_s {
const fsm_def_t *def;
int state;
int prev_state;
uint32_t tick_ms;
uint32_t state_entered_ms;
uint32_t transitions;
uint32_t rejected; /* 该状态下没有对应迁移的次数 */
uint32_t timeouts;
uint32_t eq_dropped; /* 队列满导致丢弃的事件数 */
uint8_t eq[FSM_EQ_SIZE];
uint8_t eq_head;
uint8_t eq_tail;
/** 每条迁移的命中次数,用来算覆盖率 */
uint32_t hit[FSM_TRANS_MAX];
} fsm_t;
/** 静态检查报告 */
typedef struct {
int n_unreachable;
int unreachable[FSM_STATE_MAX];
int n_dead_end;
int dead_end[FSM_STATE_MAX];
int n_bad_transition;
int bad_transition[FSM_TRANS_MAX];
int n_duplicate;
int duplicate[FSM_TRANS_MAX];
} fsm_report_t;
void fsm_init(fsm_t *f, const fsm_def_t *def);
/** 投递一个事件(进队列)。返回 1 = 入队成功,0 = 队列满被丢弃 */
int fsm_post(fsm_t *f, uint8_t ev);
/** 把队列里的事件全部处理掉。返回处理了几个 */
int fsm_run(fsm_t *f);
/** 直接处理一个事件(跳过队列),返回 1 = 发生了迁移 */
int fsm_dispatch(fsm_t *f, uint8_t ev);
/** 推进时间,必要时自动投递超时事件 */
void fsm_tick(fsm_t *f, uint32_t ms);
/** 向上找迁移的下标;没找到返回 -1 */
int fsm_find_transition(const fsm_t *f, int state, uint8_t ev);
/** 状态名(越界时返回 "?") */
const char *fsm_state_name(const fsm_t *f, int state);
/** 静态检查:可达性、死状态、越界、歧义 */
void fsm_validate(const fsm_def_t *def, fsm_report_t *rep);
/** 迁移覆盖率:有命中记录的迁移数 / 总迁移数 */
void fsm_coverage(const fsm_t *f, int *covered, int *total);
#ifdef __cplusplus
}
#endif
#endif /* FSM_H */platformio.ini
[platformio]
default_envs = bluepill
[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
-Wall
-Wextra
-Isrc
lib_ldf_mode = deep+src/fsm.c
#include "fsm.h"
#include <string.h>
/* ---------------- 事件队列 ---------------- */
static int eq_push(fsm_t *f, uint8_t ev)
{
uint8_t next = (uint8_t)((f->eq_head + 1u) % FSM_EQ_SIZE);
if (next == f->eq_tail) {
/* 队列满。这里选择丢新事件而不是覆盖旧事件:
* 旧事件往往代表流程里更早、更关键的一步。 */
f->eq_dropped++;
return 0;
}
f->eq[f->eq_head] = ev;
f->eq_head = next;
return 1;
}
static int eq_pop(fsm_t *f, uint8_t *ev)
{
if (f->eq_tail == f->eq_head) {
return 0;
}
*ev = f->eq[f->eq_tail];
f->eq_tail = (uint8_t)((f->eq_tail + 1u) % FSM_EQ_SIZE);
return 1;
}
/* ---------------- 动作分发 ---------------- */
static void run_action(const fsm_t *f, uint8_t id)
{
if (id != 0u && (int)id < f->def->nactions) {
fsm_action_fn fn = f->def->actions[id];
if (fn != NULL) {
fn((fsm_t *)f);
}
}
}
static int run_guard(const fsm_t *f, uint8_t id)
{
if (id == 0u) {
return 1; /* 没有守卫 = 总是通过 */
}
if ((int)id >= f->def->nguards) {
return 0;
}
{
fsm_guard_fn fn = f->def->guards[id];
return (fn == NULL) ? 1 : fn((fsm_t *)f);
}
}
/* ---------------- 初始化 ---------------- */
void fsm_init(fsm_t *f, const fsm_def_t *def)
{
memset(f, 0, sizeof(*f));
f->def = def;
f->state = def->initial_state;
f->prev_state = def->initial_state;
f->state_entered_ms = 0;
}
int fsm_post(fsm_t *f, uint8_t ev)
{
return eq_push(f, ev);
}
/* ---------------- 迁移查找 ---------------- */
int fsm_find_transition(const fsm_t *f, int state, uint8_t ev)
{
const fsm_def_t *d = f->def;
int s = state;
int hop = 0;
/*
* 沿父状态链往上找。这就是层次状态机的全部实现——
* 没有复杂的历史状态、进入/退出链,只有「自己找不到就问父状态」。
* 但它已经足够消掉「取消」「复位」这类通用事件的重复迁移。
*/
while (s >= 0 && hop <= d->nstates) {
int i;
for (i = 0; i < d->ntrans; i++) {
if ((int)d->trans[i].state == s && d->trans[i].event == ev) {
if (run_guard(f, d->trans[i].guard)) {
return i; /* 表的顺序就是优先级:先写的先生效 */
}
}
}
if (s >= d->nstates || d->states[s].parent == FSM_NO_PARENT) {
break;
}
s = (int)d->states[s].parent;
hop++;
}
return -1;
}
/* ---------------- 事件处理 ---------------- */
int fsm_dispatch(fsm_t *f, uint8_t ev)
{
const fsm_def_t *d = f->def;
int idx = fsm_find_transition(f, f->state, ev);
int from = f->state;
const fsm_trans_t *tr;
int next;
if (idx < 0) {
f->rejected++;
return 0;
}
tr = &d->trans[idx];
next = (int)tr->next;
/* 1) 离开当前状态 */
if (from < d->nstates) {
run_action(f, d->states[from].on_exit);
}
/* 2) 执行迁移动作 */
run_action(f, tr->action);
/* 3) 进入新状态 */
f->prev_state = from;
f->state = next;
if (next < d->nstates) {
run_action(f, d->states[next].on_entry);
}
/* 4) 统一重置超时计时器。不用每个状态各写一遍 */
f->state_entered_ms = f->tick_ms;
f->transitions++;
f->hit[idx]++;
if (d->trace != NULL) {
d->trace(d->trace_ctx, from, ev, next);
}
return 1;
}
int fsm_run(fsm_t *f)
{
uint8_t ev;
int n = 0;
while (eq_pop(f, &ev)) {
fsm_dispatch(f, ev);
n++;
}
return n;
}
void fsm_tick(fsm_t *f, uint32_t ms)
{
const fsm_def_t *d = f->def;
uint32_t to = 0;
f->tick_ms += ms;
if (f->state >= 0 && f->state < d->nstates) {
to = d->states[f->state].timeout_ms;
}
if (to == FSM_NO_TIMEOUT) {
return;
}
if ((f->tick_ms - f->state_entered_ms) >= to) {
f->timeouts++;
/* 超时事件也走队列,保证和外部事件的处理顺序一致 */
if (!eq_push(f, d->states[f->state].timeout_event)) {
/* 队列满时兜底:直接派发,绝不能把超时丢掉 */
fsm_dispatch(f, d->states[f->state].timeout_event);
}
}
}
const char *fsm_state_name(const fsm_t *f, int state)
{
if (state < 0 || state >= f->def->nstates ||
f->def->states[state].name == NULL) {
return "?";
}
return f->def->states[state].name;
}
/* ---------------- 静态检查 ---------------- */
void fsm_validate(const fsm_def_t *def, fsm_report_t *rep)
{
uint8_t reachable[FSM_STATE_MAX];
uint8_t has_exit[FSM_STATE_MAX];
int queue[FSM_STATE_MAX];
int qh = 0;
int qt = 0;
int i;
memset(rep, 0, sizeof(*rep));
memset(reachable, 0, sizeof(reachable));
memset(has_exit, 0, sizeof(has_exit));
/* 1) 越界检查:改表时手滑写错一个数字,编译器是帮不了你的 */
for (i = 0; i < def->ntrans; i++) {
const fsm_trans_t *t = &def->trans[i];
if ((int)t->state >= def->nstates || (int)t->next >= def->nstates) {
if (rep->n_bad_transition < FSM_TRANS_MAX) {
rep->bad_transition[rep->n_bad_transition] = i;
rep->n_bad_transition++;
}
}
}
/* 2) 出口统计要在**全表**上做,不能只在可达部分做——
* 否则一个状态只要不可达,就会被误报成「死状态」。 */
for (i = 0; i < def->ntrans; i++) {
if ((int)def->trans[i].state < def->nstates) {
has_exit[def->trans[i].state] = 1;
}
}
/* 3) 可达性:从初始状态 BFS */
if (def->initial_state >= 0 && def->initial_state < def->nstates) {
reachable[def->initial_state] = 1;
queue[qt++] = def->initial_state;
while (qh < qt) {
int s = queue[qh++];
for (i = 0; i < def->ntrans; i++) {
if ((int)def->trans[i].state != s) {
continue;
}
{
int nx = (int)def->trans[i].next;
if (nx >= 0 && nx < def->nstates && !reachable[nx]) {
reachable[nx] = 1;
queue[qt++] = nx;
}
}
}
}
}
for (i = 0; i < def->nstates; i++) {
if (!reachable[i] && rep->n_unreachable < FSM_STATE_MAX) {
rep->unreachable[rep->n_unreachable] = i;
rep->n_unreachable++;
}
}
/* 4) 死状态:一条出口迁移都没有。进去就出不来了 */
for (i = 0; i < def->nstates; i++) {
if (!has_exit[i] && def->nstates > 1 &&
rep->n_dead_end < FSM_STATE_MAX) {
rep->dead_end[rep->n_dead_end] = i;
rep->n_dead_end++;
}
}
/* 5) 歧义:同一个 (state, event) 出现两次且都没有守卫 */
for (i = 0; i < def->ntrans; i++) {
int j;
if (def->trans[i].guard != 0u) {
continue;
}
for (j = i + 1; j < def->ntrans; j++) {
if (def->trans[j].state == def->trans[i].state &&
def->trans[j].event == def->trans[i].event &&
def->trans[j].guard == 0u) {
if (rep->n_duplicate < FSM_TRANS_MAX) {
rep->duplicate[rep->n_duplicate] = i;
rep->n_duplicate++;
}
}
}
}
}
void fsm_coverage(const fsm_t *f, int *covered, int *total)
{
int i;
int c = 0;
for (i = 0; i < f->def->ntrans; i++) {
if (f->hit[i] > 0u) {
c++;
}
}
*covered = c;
*total = f->def->ntrans;
}src/main.c
/**
* STM32F103 上把状态机挂到主循环里
*
* 引擎本身不认识任何 GPIO / 外设,业务动作里才碰硬件。
* 这样同一套流程可以在 PC 上跑单元测试,在板子上跑真机——见 test_fsm.c。
*/
#include "stm32f1xx_hal.h"
#include "fsm.h"
enum { EV_NONE = 0, EV_COIN, EV_CANCEL, EV_SELECT, EV_BREW_DONE,
EV_TEMP_HIGH, EV_RESET, EV_TIMEOUT };
enum { S_IDLE = 0, S_CREDIT, S_BREWING, S_FAULT, S_COUNT };
static fsm_t g_fsm;
static void act_start_heater(fsm_t *f) { (void)f; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); }
static void act_stop_heater(fsm_t *f) { (void)f; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); }
static void act_open_valve(fsm_t *f) { (void)f; HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET); }
static const fsm_action_fn actions[] = {
NULL, act_start_heater, act_stop_heater, act_open_valve
};
static const fsm_state_t states[S_COUNT] = {
{ "S_IDLE", FSM_NO_PARENT, 0, EV_NONE, 0, 0 },
{ "S_CREDIT", S_IDLE, 3000, EV_TIMEOUT, 0, 0 },
{ "S_BREWING", S_CREDIT, 90000, EV_TIMEOUT, 1, 2 },
{ "S_FAULT", FSM_NO_PARENT, 0, EV_NONE, 2, 2 }
};
static const fsm_trans_t trans[] = {
{ S_IDLE, EV_COIN, 0, S_CREDIT, 0 },
{ S_CREDIT, EV_SELECT, 0, S_BREWING, 0 },
{ S_CREDIT, EV_CANCEL, 0, S_IDLE, 2 },
{ S_CREDIT, EV_TIMEOUT, 0, S_IDLE, 2 },
{ S_BREWING, EV_BREW_DONE, 0, S_IDLE, 3 },
{ S_BREWING, EV_TEMP_HIGH, 0, S_FAULT, 2 },
{ S_FAULT, EV_RESET, 0, S_IDLE, 0 }
};
static void fsm_log_trace(void *ctx, int from, uint8_t ev, int to)
{
(void)ctx;
printf("[fsm] %s -ev%u-> %s\r\n", fsm_state_name(&g_fsm, from),
(unsigned)ev, fsm_state_name(&g_fsm, to));
}
static const fsm_def_t def = {
states, S_COUNT, trans, (int)(sizeof(trans) / sizeof(trans[0])),
NULL, 0, actions, (int)(sizeof(actions) / sizeof(actions[0])),
fsm_log_trace, NULL, S_IDLE
};
int main(void)
{
uint32_t last = 0;
HAL_Init();
SystemClock_Config();
/* 上电先做一次静态检查。表改错了自己会报出来,不用等到现场 */
{
fsm_report_t rep;
fsm_validate(&def, &rep);
if (rep.n_unreachable || rep.n_dead_end || rep.n_bad_transition) {
printf("[fsm] 状态机定义有问题:不可达 %d,死状态 %d,越界 %d\r\n",
rep.n_unreachable, rep.n_dead_end, rep.n_bad_transition);
}
}
fsm_init(&g_fsm, &def);
for (;;) {
/* 主循环里做两件事:投递外部事件 + 跑引擎 */
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
fsm_post(&g_fsm, EV_COIN);
}
/* 每 10 ms 推进一次时间,超时迁移由引擎自己负责 */
if (HAL_GetTick() - last >= 10u) {
last = HAL_GetTick();
fsm_tick(&g_fsm, 10u);
}
fsm_run(&g_fsm);
}
}test/test_fsm.c
/**
* 主机端测试:表驱动状态机(投币咖啡机)
*
* gcc -std=c99 -Wall -Wextra -Iinclude src/fsm.c test/test_fsm.c -o build/test
*/
#include <stdio.h>
#include <string.h>
#include "fsm.h"
/* ---------------- 事件 ---------------- */
enum {
EV_NONE = 0,
EV_COIN,
EV_CANCEL,
EV_SELECT,
EV_BREW_DONE,
EV_TEMP_HIGH,
EV_RESET,
EV_TIMEOUT
};
/* ---------------- 状态 ---------------- */
enum { S_IDLE = 0, S_CREDIT, S_BREWING, S_FAULT, S_COUNT };
/* ---------------- 动作 / 守卫 下标 ---------------- */
enum {
ACT_NONE = 0,
ACT_ADD_CREDIT,
ACT_REFUND,
ACT_START_BREW,
ACT_DONE,
ACT_LOG_FAULT
};
enum { GD_NONE = 0, GD_ENOUGH };
/* ---------------- 机器状态(业务数据) ---------------- */
typedef struct {
fsm_t fsm;
int credit; /* 已投币金额 */
int price; /* 一杯的价格 */
int cups; /* 已出货杯数 */
int refunded; /* 累计退款 */
int fault; /* 故障标志 */
int trace_len;
struct { int from; int ev; int to; } trace[64];
} machine_t;
static machine_t g_m;
/* ---------------- 动作实现 ---------------- */
static void act_refund_real(machine_t *m)
{
if (m->credit > 0) {
m->refunded += m->credit;
printf(" [退币 %d,累计退款 %d]\n", m->credit, m->refunded);
m->credit = 0;
}
}
static void act_refund(fsm_t *f) { act_refund_real((machine_t *)f); }
static void act_start_brew(fsm_t *f) { (void)f; }
static void act_done(fsm_t *f)
{
machine_t *m = (machine_t *)f;
int change = m->credit - m->price;
m->cups++;
printf(" [出货第 %d 杯,扣款 %d,找零 %d]\n", m->cups, m->price, change);
m->credit = 0;
}
static void act_log_fault(fsm_t *f)
{
machine_t *m = (machine_t *)f;
m->fault = 1;
printf(" [进入故障态:加热异常]\n");
}
static void act_add_credit_impl(fsm_t *f)
{
machine_t *m = (machine_t *)f;
m->credit += 25;
printf(" [投币 25,余额 %d]\n", m->credit);
}
static int guard_enough(fsm_t *f)
{
machine_t *m = (machine_t *)f;
return (m->credit >= m->price) ? 1 : 0;
}
/* ---------------- 表 ---------------- */
static const fsm_guard_fn guards[] = {
NULL, guard_enough
};
static const fsm_action_fn actions[] = {
NULL, act_add_credit_impl, act_refund, act_start_brew, act_done, act_log_fault
};
static const fsm_state_t states[S_COUNT] = {
/* name parent timeout to_ev entry exit */
{ "S_IDLE", FSM_NO_PARENT, 0, EV_NONE, 0, 0 },
{ "S_CREDIT", S_IDLE, 3000, EV_TIMEOUT, 0, 0 },
{ "S_BREWING", S_CREDIT, 90000, EV_TIMEOUT, ACT_START_BREW, 0 },
{ "S_FAULT", FSM_NO_PARENT, 0, EV_NONE, ACT_LOG_FAULT, 0 }
};
static fsm_trans_t trans_main[] = {
/* state event guard next action */
{ S_IDLE, EV_COIN, GD_NONE, S_CREDIT, ACT_ADD_CREDIT },
{ S_CREDIT, EV_COIN, GD_NONE, S_CREDIT, ACT_ADD_CREDIT },
{ S_CREDIT, EV_SELECT, GD_ENOUGH, S_BREWING, ACT_NONE },
{ S_CREDIT, EV_SELECT, GD_NONE, S_CREDIT, ACT_NONE },
{ S_CREDIT, EV_CANCEL, GD_NONE, S_IDLE, ACT_REFUND },
{ S_CREDIT, EV_TIMEOUT, GD_NONE, S_IDLE, ACT_REFUND },
{ S_BREWING, EV_BREW_DONE, GD_NONE, S_IDLE, ACT_DONE },
{ S_BREWING, EV_TEMP_HIGH, GD_NONE, S_FAULT, ACT_NONE },
{ S_BREWING, EV_TIMEOUT, GD_NONE, S_FAULT, ACT_NONE },
{ S_FAULT, EV_RESET, GD_NONE, S_IDLE, ACT_NONE },
{ S_FAULT, EV_COIN, GD_NONE, S_FAULT, ACT_REFUND },
{ S_IDLE, EV_RESET, GD_NONE, S_IDLE, ACT_NONE },
{ S_IDLE, EV_CANCEL, GD_NONE, S_IDLE, ACT_NONE },
{ S_BREWING, EV_RESET, GD_NONE, S_IDLE, ACT_NONE }
};
static void trace_cb(void *ctx, int from, uint8_t ev, int to)
{
machine_t *m = (machine_t *)ctx;
if (m->trace_len < 64) {
m->trace[m->trace_len].from = from;
m->trace[m->trace_len].ev = (int)ev;
m->trace[m->trace_len].to = to;
m->trace_len++;
}
}
static const char *ev_name(int ev)
{
static const char *n[] = {"NONE", "COIN", "CANCEL", "SELECT",
"BREW_DONE", "TEMP_HIGH", "RESET", "TIMEOUT"};
return (ev >= 0 && ev <= 7) ? n[ev] : "?";
}
static fsm_def_t make_def(void)
{
fsm_def_t d;
memset(&d, 0, sizeof(d));
d.states = states;
d.nstates = S_COUNT;
d.trans = trans_main;
d.ntrans = (int)(sizeof(trans_main) / sizeof(trans_main[0]));
d.guards = guards;
d.nguards = (int)(sizeof(guards) / sizeof(guards[0]));
d.actions = actions;
d.nactions = (int)(sizeof(actions) / sizeof(actions[0]));
d.trace = trace_cb;
d.trace_ctx = &g_m;
d.initial_state = S_IDLE;
return d;
}
static int failed = 0;
static void check(int cond, const char *what)
{
if (!cond) {
printf(" [FAIL] %s\n", what);
failed++;
}
}
static void dump_trace(const machine_t *m)
{
int i;
printf(" 轨迹:");
for (i = 0; i < m->trace_len; i++) {
printf("%s -%s-> ", fsm_state_name(&g_m.fsm, m->trace[i].from),
ev_name(m->trace[i].ev));
}
printf("%s\n", fsm_state_name(&g_m.fsm, g_m.fsm.state));
}
int main(void)
{
fsm_def_t def = make_def();
printf("===== 表驱动状态机实测(投币咖啡机,25 分一杯 x2 = 50)=====\n");
printf("状态 %d 个,迁移 %d 条,事件队列容量 %d\n\n",
def.nstates, def.ntrans, FSM_EQ_SIZE);
/* def 存在 main 的栈上,生命周期覆盖全程,指针一直有效 */
memset(&g_m, 0, sizeof(g_m));
g_m.price = 50;
fsm_init(&g_m.fsm, &def);
/* ---------- [1] 正常流程 ---------- */
printf("[1] 正常流程:投两次币 -> 选咖啡\n");
{
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_SELECT);
fsm_post(&g_m.fsm, EV_BREW_DONE);
fsm_run(&g_m.fsm);
dump_trace(&g_m);
printf(" 余额 %d,出货 %d 杯,退款 %d\n",
g_m.credit, g_m.cups, g_m.refunded);
check(g_m.cups == 1, "没有出一杯咖啡");
check(g_m.credit == 0, "余额没有清零");
check(g_m.fsm.state == S_IDLE, "没有回到 IDLE");
}
/* ---------- [2] 取消退款 ---------- */
printf("\n[2] 投 50 之后按取消\n");
{
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_CANCEL);
fsm_run(&g_m.fsm);
printf(" 余额 %d,累计退款 %d,状态 %s\n",
g_m.credit, g_m.refunded,
fsm_state_name(&g_m.fsm, g_m.fsm.state));
check(g_m.credit == 0, "取消后余额没退");
check(g_m.refunded == 50, "退款金额不对");
check(g_m.fsm.state == S_IDLE, "取消后没回 IDLE");
}
/* ---------- [3] 超时自动退款 ---------- */
printf("\n[3] 投 25 之后不操作,看超时\n");
{
int i;
int t0 = g_m.fsm.timeouts;
fsm_post(&g_m.fsm, EV_COIN);
fsm_run(&g_m.fsm);
printf(" 投币后状态 = %s,余额 %d\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state), g_m.credit);
check(g_m.fsm.state == S_CREDIT, "没有进入 S_CREDIT");
check(g_m.credit == 25, "余额不对");
/* 状态 S_CREDIT 的超时是 3000 ms,每 tick 推进 100 ms */
for (i = 0; i < 29; i++) {
fsm_tick(&g_m.fsm, 100);
fsm_run(&g_m.fsm);
}
printf(" 第 %d ms,状态 = %s(还没超时)\n",
g_m.fsm.tick_ms, fsm_state_name(&g_m.fsm, g_m.fsm.state));
check(g_m.fsm.state == S_CREDIT, "2900 ms 就超时了,太早");
fsm_tick(&g_m.fsm, 100); /* 累计 3000 ms */
fsm_run(&g_m.fsm);
printf(" 第 %d ms,超时计数 %d -> %d,状态 = %s,余额 %d\n",
g_m.fsm.tick_ms, t0, g_m.fsm.timeouts,
fsm_state_name(&g_m.fsm, g_m.fsm.state), g_m.credit);
check(g_m.fsm.state == S_IDLE, "超时后没回 IDLE");
check(g_m.credit == 0, "超时没退币");
check(g_m.fsm.timeouts == (uint32_t)(t0 + 1), "超时计数没加");
}
/* ---------- [4] 父状态接管:BREWING 里的 CANCEL ---------- */
printf("\n[4] 在 S_BREWING 里按取消(BREWING 自己没有 CANCEL 迁移)\n");
{
int refund_before = g_m.refunded;
int idx;
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_SELECT);
fsm_run(&g_m.fsm);
printf(" 现在状态 = %s\n", fsm_state_name(&g_m.fsm, g_m.fsm.state));
check(g_m.fsm.state == S_BREWING, "没有进入 BREWING");
/* 验证 S_BREWING 自己的迁移表里确实没有 CANCEL */
{
const fsm_trans_t *t;
int n, i;
int found = 0;
t = &trans_main[0];
n = (int)(sizeof(trans_main) / sizeof(trans_main[0]));
for (i = 0; i < n; i++) {
if (t[i].state == S_BREWING && t[i].event == EV_CANCEL) {
found = 1;
}
}
printf(" S_BREWING 自己的 CANCEL 迁移数量 = %d\n", found);
check(found == 0, "BREWING 里不该有 CANCEL 迁移");
}
idx = fsm_find_transition(&g_m.fsm, S_BREWING, EV_CANCEL);
printf(" 引擎沿父链找到的迁移下标 = %d(源状态 = %s)\n", idx,
(idx >= 0) ? fsm_state_name(&g_m.fsm, trans_main[idx].state) : "无");
check(idx >= 0, "沿父链没找到 CANCEL");
check(idx >= 0 && trans_main[idx].state == S_CREDIT,
"找到的迁移不是父状态 S_CREDIT 的");
fsm_post(&g_m.fsm, EV_CANCEL);
fsm_run(&g_m.fsm);
printf(" 结果:状态 %s,退款 %d -> %d\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state),
refund_before, g_m.refunded);
check(g_m.fsm.state == S_IDLE, "父状态接管后没回 IDLE");
check(g_m.refunded == refund_before + 50, "父状态没执行退款");
}
/* ---------- [5] 故障与恢复 ---------- */
printf("\n[5] 加热故障 -> 故障态,故障态里投币无效,复位恢复\n");
{
int rejected_before = g_m.fsm.rejected;
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_SELECT);
fsm_run(&g_m.fsm);
check(g_m.fsm.state == S_BREWING, "没有进入 BREWING");
fsm_post(&g_m.fsm, EV_TEMP_HIGH);
fsm_run(&g_m.fsm);
printf(" 状态 = %s,故障标志 = %d\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state), g_m.fault);
check(g_m.fsm.state == S_FAULT, "没有进入 FAULT");
/* FAULT 里连投币都有对应迁移(退币并留在 FAULT),验证一下 */
fsm_post(&g_m.fsm, EV_COIN);
fsm_run(&g_m.fsm);
printf(" FAULT 里投币后:状态 = %s,余额 = %d\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state), g_m.credit);
check(g_m.fsm.state == S_FAULT, "故障态被投币带跑了");
/* SELECT 在 FAULT 里没有迁移(父链上也没有)-> 被拒绝 */
fsm_post(&g_m.fsm, EV_SELECT);
fsm_run(&g_m.fsm);
printf(" FAULT 里 SELECT 被拒绝次数 %d -> %d\n",
rejected_before, g_m.fsm.rejected);
check(g_m.fsm.rejected > (uint32_t)rejected_before, "无效事件没有被拒绝");
fsm_post(&g_m.fsm, EV_RESET);
fsm_run(&g_m.fsm);
printf(" 复位后状态 = %s\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state));
check(g_m.fsm.state == S_IDLE, "复位后没回 IDLE");
}
/* ---------- [6] 静态检查 ---------- */
printf("\n[6] 静态检查\n");
{
fsm_report_t rep;
fsm_trans_t broken[sizeof(trans_main) / sizeof(trans_main[0])];
fsm_def_t bd = def;
int i;
fsm_validate(&def, &rep);
printf(" 正常表:状态 %d 个,迁移 %d 条\n", def.nstates, def.ntrans);
printf(" 不可达状态 %d 个,死状态 %d 个,越界迁移 %d 条,"
"歧义迁移 %d 条\n",
rep.n_unreachable, rep.n_dead_end, rep.n_bad_transition,
rep.n_duplicate);
check(rep.n_unreachable == 0, "正常表里竟然有不可达状态");
check(rep.n_dead_end == 0, "正常表里竟然有死状态");
check(rep.n_bad_transition == 0, "正常表里有越界迁移");
check(rep.n_duplicate == 0, "正常表里有歧义迁移");
/* --- 变体 A:把 IDLE:COIN 这条唯一入口变成废迁移 --- */
memcpy(broken, trans_main, sizeof(trans_main));
for (i = 0; i < def.ntrans; i++) {
if (broken[i].state == S_IDLE && broken[i].event == EV_COIN) {
broken[i].state = 0xFFu;
}
}
bd.trans = broken;
fsm_validate(&bd, &rep);
printf(" 变体 A(把唯一的入口迁移 IDLE:COIN 改成废迁移):\n");
printf(" 不可达状态 %d 个:", rep.n_unreachable);
for (i = 0; i < rep.n_unreachable; i++) {
printf("%s%s", states[rep.unreachable[i]].name,
(i + 1 < rep.n_unreachable) ? "、" : "");
}
printf("\n 越界迁移 %d 条\n", rep.n_bad_transition);
check(rep.n_unreachable == 3, "变体 A 应该报出 3 个不可达状态");
check(rep.n_bad_transition == 1, "废迁移的越界没被查出来");
/* --- 变体 B:把 FAULT 的两条出口全删掉 --- */
memcpy(broken, trans_main, sizeof(trans_main));
for (i = 0; i < def.ntrans; i++) {
if (broken[i].state == S_FAULT) {
broken[i].state = 0xFFu;
}
}
bd.trans = broken;
fsm_validate(&bd, &rep);
printf(" 变体 B(把故障态的两条出口全删掉):\n");
printf(" 不可达状态 %d 个,死状态 %d 个:", rep.n_unreachable,
rep.n_dead_end);
for (i = 0; i < rep.n_dead_end; i++) {
printf("%s%s", states[rep.dead_end[i]].name,
(i + 1 < rep.n_dead_end) ? "、" : "");
}
printf("\n");
check(rep.n_unreachable == 0, "变体 B 不该产生不可达状态");
check(rep.n_dead_end == 1 && rep.dead_end[0] == S_FAULT,
"变体 B 应该报出故障态是死状态");
printf(" -> 这两类错误编译器完全不管,只能靠静态检查\n");
}
/* ---------- [7] 事件队列打满 ---------- */
printf("\n[7] 事件队列打满(容量 %d,一次投 20 个)\n", FSM_EQ_SIZE);
{
int i;
int ok = 0;
/* 先把状态弄成 S_CREDIT,这样 COIN 有迁移,队列才会真的被消费 */
fsm_post(&g_m.fsm, EV_COIN);
fsm_run(&g_m.fsm);
for (i = 0; i < 20; i++) {
ok += fsm_post(&g_m.fsm, EV_COIN);
}
printf(" 入队成功 %d 个,丢弃 %d 个\n", ok, g_m.fsm.eq_dropped);
check(ok == FSM_EQ_SIZE - 1, "入队数量不符合预期");
check(g_m.fsm.eq_dropped == 20 - (FSM_EQ_SIZE - 1), "丢弃计数不对");
fsm_run(&g_m.fsm);
printf(" 处理完后状态 %s,余额 %d(没崩、没死循环)\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state), g_m.credit);
check(g_m.fsm.state == S_CREDIT || g_m.fsm.state == S_IDLE,
"队列打满后状态机跑飞了");
/* 清干净,别影响后面的覆盖率统计 */
fsm_post(&g_m.fsm, EV_CANCEL);
fsm_run(&g_m.fsm);
}
/* ---------- [8] 余额不足就选:守卫不通过,留在原状态 ---------- */
printf("\n[8] 只投 25 就选咖啡(守卫不通过)\n");
{
fsm_post(&g_m.fsm, EV_COIN);
fsm_run(&g_m.fsm);
fsm_post(&g_m.fsm, EV_SELECT);
fsm_run(&g_m.fsm);
printf(" 投 25 后选咖啡:状态 = %s,余额 = %d\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state), g_m.credit);
check(g_m.fsm.state == S_CREDIT, "余额不足竟然进入了 BREWING");
printf(" -> 同一个 (S_CREDIT, SELECT) 有两条迁移,\n");
printf(" 带守卫的写在前面先匹配,守卫不过才轮到后面那条\n");
fsm_post(&g_m.fsm, EV_CANCEL);
fsm_run(&g_m.fsm);
}
/* ---------- [9] 加热超时 ---------- */
printf("\n[9] 咖啡机卡在 BREWING:90 秒加热超时\n");
{
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_SELECT);
fsm_run(&g_m.fsm);
check(g_m.fsm.state == S_BREWING, "没有进入 BREWING");
fsm_tick(&g_m.fsm, 89999);
fsm_run(&g_m.fsm);
printf(" 89999 ms,状态 = %s(还没超时)\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state));
check(g_m.fsm.state == S_BREWING, "89999 ms 就超时了,太早");
fsm_tick(&g_m.fsm, 1);
fsm_run(&g_m.fsm);
printf(" 90000 ms,状态 = %s\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state));
check(g_m.fsm.state == S_FAULT, "加热超时没有进故障态");
printf(" -> 90000 ok 装不进 uint16(会截成 24464),\n");
printf(" 所以 timeout_ms 必须是 uint32 ——编译器会警告,别忽略\n");
fsm_post(&g_m.fsm, EV_RESET);
fsm_run(&g_m.fsm);
}
/* ---------- [10] 几个「无害」事件,为了把覆盖率跑满 ---------- */
printf("\n[10] 补跑几条无害迁移(IDLE:RESET / IDLE:CANCEL / BREWING:RESET)\n");
{
fsm_post(&g_m.fsm, EV_RESET);
fsm_post(&g_m.fsm, EV_CANCEL);
fsm_run(&g_m.fsm);
printf(" IDLE 下 RESET / CANCEL 后仍然是 %s\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state));
check(g_m.fsm.state == S_IDLE, "空闲态被无害事件带跑了");
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_COIN);
fsm_post(&g_m.fsm, EV_SELECT);
fsm_run(&g_m.fsm);
fsm_post(&g_m.fsm, EV_RESET);
fsm_run(&g_m.fsm);
printf(" BREWING 下 RESET 后回到 %s(冲泡中途强制复位)\n",
fsm_state_name(&g_m.fsm, g_m.fsm.state));
check(g_m.fsm.state == S_IDLE, "BREWING 下 RESET 没回 IDLE");
fsm_post(&g_m.fsm, EV_CANCEL);
fsm_run(&g_m.fsm);
}
/* ---------- [11] 迁移覆盖率 ---------- */
printf("\n[11] 迁移覆盖率(状态机最有用的测试指标)\n");
{
int cov = 0;
int tot = 0;
int i;
fsm_coverage(&g_m.fsm, &cov, &tot);
printf(" 覆盖 %d / %d 条迁移\n", cov, tot);
for (i = 0; i < tot; i++) {
printf(" [%2d] %-10s -%-9s-> %-10s 命中 %u 次\n", i,
states[trans_main[i].state].name,
ev_name(trans_main[i].event),
states[trans_main[i].next].name,
g_m.fsm.hit[i]);
}
check(cov == tot, "还有迁移没被覆盖到");
printf(" -> 每条迁移加一个命中计数器就得到了这个统计;\n");
printf(" switch 写法做不到这一点,这是表驱动的直接回报\n");
}
printf("\n===== %s =====\n",
failed == 0 ? "全部通过:以上数据由本机 gcc 实编译实运行"
: "有失败项!");
return failed == 0 ? 0 : 1;
}实测输出
下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):
===== 表驱动状态机实测(投币咖啡机,25 分一杯 x2 = 50)=====
状态 4 个,迁移 14 条,事件队列容量 8
[1] 正常流程:投两次币 -> 选咖啡
[投币 25,余额 25]
[投币 25,余额 50]
[出货第 1 杯,扣款 50,找零 0]
轨迹:S_IDLE -COIN-> S_CREDIT -COIN-> S_CREDIT -SELECT-> S_BREWING -BREW_DONE-> S_IDLE
余额 0,出货 1 杯,退款 0
[2] 投 50 之后按取消
[投币 25,余额 25]
[投币 25,余额 50]
[退币 50,累计退款 50]
余额 0,累计退款 50,状态 S_IDLE
[3] 投 25 之后不操作,看超时
[投币 25,余额 25]
投币后状态 = S_CREDIT,余额 25
第 2900 ms,状态 = S_CREDIT(还没超时)
[退币 25,累计退款 75]
第 3000 ms,超时计数 0 -> 1,状态 = S_IDLE,余额 0
[4] 在 S_BREWING 里按取消(BREWING 自己没有 CANCEL 迁移)
[投币 25,余额 25]
[投币 25,余额 50]
现在状态 = S_BREWING
S_BREWING 自己的 CANCEL 迁移数量 = 0
引擎沿父链找到的迁移下标 = 4(源状态 = S_CREDIT)
[退币 50,累计退款 125]
结果:状态 S_IDLE,退款 75 -> 125
[5] 加热故障 -> 故障态,故障态里投币无效,复位恢复
[投币 25,余额 25]
[投币 25,余额 50]
[进入故障态:加热异常]
状态 = S_FAULT,故障标志 = 1
[退币 50,累计退款 175]
[进入故障态:加热异常]
FAULT 里投币后:状态 = S_FAULT,余额 = 0
FAULT 里 SELECT 被拒绝次数 0 -> 1
复位后状态 = S_IDLE
[6] 静态检查
正常表:状态 4 个,迁移 14 条
不可达状态 0 个,死状态 0 个,越界迁移 0 条,歧义迁移 0 条
变体 A(把唯一的入口迁移 IDLE:COIN 改成废迁移):
不可达状态 3 个:S_CREDIT、S_BREWING、S_FAULT
越界迁移 1 条
变体 B(把故障态的两条出口全删掉):
不可达状态 0 个,死状态 1 个:S_FAULT
-> 这两类错误编译器完全不管,只能靠静态检查
[7] 事件队列打满(容量 8,一次投 20 个)
[投币 25,余额 25]
入队成功 7 个,丢弃 13 个
[投币 25,余额 50]
[投币 25,余额 75]
[投币 25,余额 100]
[投币 25,余额 125]
[投币 25,余额 150]
[投币 25,余额 175]
[投币 25,余额 200]
处理完后状态 S_CREDIT,余额 200(没崩、没死循环)
[退币 200,累计退款 375]
[8] 只投 25 就选咖啡(守卫不通过)
[投币 25,余额 25]
投 25 后选咖啡:状态 = S_CREDIT,余额 = 25
-> 同一个 (S_CREDIT, SELECT) 有两条迁移,
带守卫的写在前面先匹配,守卫不过才轮到后面那条
[退币 25,累计退款 400]
[9] 咖啡机卡在 BREWING:90 秒加热超时
[投币 25,余额 25]
[投币 25,余额 50]
89999 ms,状态 = S_BREWING(还没超时)
[进入故障态:加热异常]
90000 ms,状态 = S_FAULT
-> 90000 ok 装不进 uint16(会截成 24464),
所以 timeout_ms 必须是 uint32 ——编译器会警告,别忽略
[10] 补跑几条无害迁移(IDLE:RESET / IDLE:CANCEL / BREWING:RESET)
IDLE 下 RESET / CANCEL 后仍然是 S_IDLE
[投币 25,余额 75]
[投币 25,余额 100]
BREWING 下 RESET 后回到 S_IDLE(冲泡中途强制复位)
[11] 迁移覆盖率(状态机最有用的测试指标)
覆盖 14 / 14 条迁移
[ 0] S_IDLE -COIN -> S_CREDIT 命中 9 次
[ 1] S_CREDIT -COIN -> S_CREDIT 命中 13 次
[ 2] S_CREDIT -SELECT -> S_BREWING 命中 5 次
[ 3] S_CREDIT -SELECT -> S_CREDIT 命中 1 次
[ 4] S_CREDIT -CANCEL -> S_IDLE 命中 4 次
[ 5] S_CREDIT -TIMEOUT -> S_IDLE 命中 1 次
[ 6] S_BREWING -BREW_DONE-> S_IDLE 命中 1 次
[ 7] S_BREWING -TEMP_HIGH-> S_FAULT 命中 1 次
[ 8] S_BREWING -TIMEOUT -> S_FAULT 命中 1 次
[ 9] S_FAULT -RESET -> S_IDLE 命中 2 次
[10] S_FAULT -COIN -> S_FAULT 命中 1 次
[11] S_IDLE -RESET -> S_IDLE 命中 1 次
[12] S_IDLE -CANCEL -> S_IDLE 命中 2 次
[13] S_BREWING -RESET -> S_IDLE 命中 1 次
-> 每条迁移加一个命中计数器就得到了这个统计;
switch 写法做不到这一点,这是表驱动的直接回报
===== 全部通过:以上数据由本机 gcc 实编译实运行 =====