Administrator
发布于 2026-09-25 / 0 阅读
0
0

别再用 switch 堆状态了:表驱动状态机 + 超时 + 层次状态 + 静态检查

一句话结论:把状态机写成「迁移表 + 一个 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_FAULT

CANCEL 和 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 clean

include/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 实编译实运行 =====

评论