16、状态机工具:QPC、YAKINDU、State Machine Compiler (SMC) 介绍与使用
说实话,我刚开始做嵌入式那会儿,状态机都是手写的。一个 switch-case 打天下,状态多了就嵌套 if-else,代码越写越长,最后自己都看不懂。后来项目复杂度上来了,一个通信协议栈里十几个状态,几十个事件,手写真的扛不住。
这时候就需要工具了。今天聊三款我用过的状态机工具:QPC、YAKINDU 和 SMC。它们各有各的脾气,也各有各的绝活。
16.1 QPC:轻量级的状态机框架
QPC 的全称是 Quantum Platform C,作者是 Miro Samek。这老哥写了一本《Practical UML Statecharts in C/C++》,我当年啃了好几遍。QPC 是他实现的一个事件驱动框架,专门给嵌入式系统用的。
它的核心思想很简单:把状态机抽象成一个层次化的结构,用事件驱动的方式运行。你不需要自己维护状态表,框架帮你干了。
16.1.1 QPC 的核心概念
- 状态机(State Machine):一个状态机包含多个状态,每个状态可以处理不同的事件。
- 事件(Event):触发状态切换的信号,比如按键按下、定时器超时。
- 动作(Action):进入状态、退出状态、处理事件时执行的代码。
- 层次化状态(Hierarchical State):子状态可以继承父状态的行为,减少重复代码。
我最早用 QPC 是在一个工业控制项目里。那台设备有启动、运行、暂停、故障、停止五个状态,每个状态里还有子状态。如果用 switch-case 写,代码量至少翻三倍。QPC 的层次化状态帮我省了不少事。
16.1.2 QPC 的代码示例
下面是一个简单的 QPC 状态机定义。它有两个状态:空闲(IDLE)和激活(ACTIVE)。
#include "qpc.h"
// 定义状态机类
typedef struct {
QActive super; // 继承 QActive
QTimeEvt timer; // 定时器事件
} MySM;
// 状态处理函数
static QState MySM_initial(MySM * const me, QEvt const * const e);
static QState MySM_idle(MySM * const me, QEvt const * const e);
static QState MySM_active(MySM * const me, QEvt const * const e);
// 初始化状态机
QState MySM_initial(MySM * const me, QEvt const * const e) {
(void)e;
QActive_subscribe(&me->super, BUTTON_PRESSED);
return Q_TRAN(&MySM_idle);
}
// 空闲状态
QState MySM_idle(MySM * const me, QEvt const * const e) {
QState status;
switch (e->sig) {
case Q_ENTRY_SIG: {
// 进入空闲状态
status = Q_HANDLED();
break;
}
case BUTTON_PRESSED: {
// 按键触发,切换到激活状态
status = Q_TRAN(&MySM_active);
break;
}
default: {
status = Q_SUPER(&QHsm_top);
break;
}
}
return status;
}
// 激活状态
QState MySM_active(MySM * const me, QEvt const * const e) {
QState status;
switch (e->sig) {
case Q_ENTRY_SIG: {
// 启动定时器
QTimeEvt_armX(&me->timer, 1000, 0);
status = Q_HANDLED();
break;
}
case Q_EXIT_SIG: {
// 退出时取消定时器
QTimeEvt_disarm(&me->timer);
status = Q_HANDLED();
break;
}
case TIMEOUT_SIG: {
// 超时回到空闲
status = Q_TRAN(&MySM_idle);
break;
}
default: {
status = Q_SUPER(&QHsm_top);
break;
}
}
return status;
}
16.2 YAKINDU:图形化状态机建模工具
YAKINDU 是德国公司 itemis 的产品。它最大的特点是:你可以画图,然后它帮你生成代码。说白了,你拖拽几个状态框,连几条线,代码就出来了。
我记得有一次给客户做方案演示,现场用 YAKINDU 画了一个电梯控制器的状态机,然后一键生成 C 代码。客户看了直呼「这玩意儿靠谱」。嗯,确实靠谱。
16.2.1 YAKINDU 的核心功能
- 图形化编辑:状态、事件、转换都用图形表示,所见即所得。
- 代码生成:支持 C、C++、Java、Python 等多种语言。
- 仿真调试:可以在 IDE 里单步运行状态机,观察状态切换过程。
- 验证检查:自动检测死状态、不可达状态、冲突转换等问题。
YAKINDU 的代码生成质量很高。它生成的代码结构清晰,注释完整,几乎可以直接用到产品里。我有个同事用它生成过一个 CAN 总线协议的状态机,跑了两年没出过问题。
16.2.2 YAKINDU 生成的代码示例
下面是一个 YAKINDU 生成的 C 代码片段。它定义了一个简单的交通灯状态机。
/* 状态机结构体 */
typedef struct {
sc_integer state;
sc_integer timer;
} TrafficLight;
/* 状态枚举 */
typedef enum {
TrafficLight_main_region_Red,
TrafficLight_main_region_Green,
TrafficLight_main_region_Yellow
} TrafficLightStates;
/* 状态机初始化 */
void TrafficLight_init(TrafficLight *handle) {
handle->state = TrafficLight_main_region_Red;
handle->timer = 0;
}
/* 状态机运行函数 */
void TrafficLight_runCycle(TrafficLight *handle) {
switch (handle->state) {
case TrafficLight_main_region_Red: {
handle->timer++;
if (handle->timer >= 30) {
handle->timer = 0;
handle->state = TrafficLight_main_region_Green;
}
break;
}
case TrafficLight_main_region_Green: {
handle->timer++;
if (handle->timer >= 25) {
handle->timer = 0;
handle->state = TrafficLight_main_region_Yellow;
}
break;
}
case TrafficLight_main_region_Yellow: {
handle->timer++;
if (handle->timer >= 5) {
handle->timer = 0;
handle->state = TrafficLight_main_region_Red;
}
break;
}
}
}
16.3 State Machine Compiler (SMC):文本驱动的状态机生成器
SMC 是一个开源工具。它用文本文件描述状态机,然后生成代码。支持的语言非常多:C、C++、Java、C#、Python、Go、甚至 Lua。
我个人很喜欢 SMC 的简洁。你写一个 .sm 文件,定义状态、事件、转换规则,然后运行 smc 命令,代码就出来了。没有图形界面,没有 IDE 依赖,纯命令行操作,非常适合集成到构建系统里。
16.3.1 SMC 的语法示例
下面是一个 SMC 的状态机定义文件。它描述了一个简单的门禁系统:门可以打开、关闭、锁定。
// Door.sm
Initial: Closed
%%
Closed {
OpenDoor Open
LockDoor Locked
}
Open {
CloseDoor Closed
LockDoor Locked
}
Locked {
UnlockDoor Closed
}
%%
运行 smc Door.sm -c 后,会生成 Door.h 和 Door.c 两个文件。生成的代码长这样:
/* 状态枚举 */
typedef enum {
Door_Closed,
Door_Open,
Door_Locked
} DoorState;
/* 事件枚举 */
typedef enum {
Door_OpenDoor,
Door_CloseDoor,
Door_LockDoor,
Door_UnlockDoor
} DoorEvent;
/* 状态机结构体 */
typedef struct {
DoorState state;
} DoorContext;
/* 状态转换函数 */
void Door_OpenDoor(DoorContext *ctx) {
switch (ctx->state) {
case Door_Closed:
ctx->state = Door_Open;
break;
default:
// 无效转换,忽略
break;
}
}
void Door_LockDoor(DoorContext *ctx) {
switch (ctx->state) {
case Door_Closed:
case Door_Open:
ctx->state = Door_Locked;
break;
default:
break;
}
}
void Door_CloseDoor(DoorContext *ctx) {
if (ctx->state == Door_Open) {
ctx->state = Door_Closed;
}
}
void Door_UnlockDoor(DoorContext *ctx) {
if (ctx->state == Door_Locked) {
ctx->state = Door_Closed;
}
}
16.4 三款工具的对比
为了让你看得更清楚,我整理了一个对比表:
| 特性 | QPC | YAKINDU | SMC |
|---|---|---|---|
| 类型 | 框架 + 库 | 图形化建模工具 | 代码生成器 |
| 输入方式 | 手写 C 代码 | 图形拖拽 | 文本文件 (.sm) |
| 支持语言 | C/C++ | C/C++/Java/Python 等 | C/C++/Java/Python/Go 等 |
| 层次化状态 | 支持 | 支持 | 支持 |
| 运行时开销 | 低(几 KB ROM) | 中(依赖生成代码) | 极低(纯函数调用) |
| 学习曲线 | 中等 | 低(画图即可) | 低(语法简单) |
| 许可证 | 开源(GPL) | 商业(有社区版) | 开源(MPL) |
| 适用场景 | 资源受限的嵌入式系统 | 复杂状态机的快速原型 | 需要集成到构建系统的项目 |
16.5 我的选择建议
你可能会问:那我到底该用哪个?
我的建议是这样的:
- 如果你在做一个资源受限的嵌入式项目,比如 Cortex-M0 或者 8 位单片机,选 QPC。它轻量、高效,而且有成熟的事件驱动架构。
- 如果你需要快速原型验证,或者团队里有非程序员(比如系统工程师)参与状态机设计,选 YAKINDU。画图比写代码直观得多。
- 如果你希望状态机代码完全独立,不依赖任何框架,而且想把它集成到 CI/CD 流水线里,选 SMC。它生成的代码就是纯 C 函数,想怎么用就怎么用。
我曾经在一个项目里同时用了 QPC 和 SMC。底层驱动用 QPC 的事件驱动框架,上层业务逻辑用 SMC 生成的状态机。两者配合得很好,代码量比纯手写少了 40%,而且 bug 率明显下降。
嗯,以上就是我对这三款工具的使用心得。工具只是手段,关键还是你对状态机本身的理解。选一个顺手的,多用多练,慢慢就有感觉了。