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;
}
我的经验:QPC 的代码风格偏 OOP,用 C 语言模拟了类的继承。刚开始可能不习惯,但用顺手了会发现它的状态切换逻辑非常清晰。我建议在资源受限的 MCU(比如 Cortex-M0)上优先考虑 QPC,因为它只占几 KB 的 ROM。

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;
        }
    }
}
注意:YAKINDU 是商业软件,社区版有功能限制。我曾经在社区版里画了一个超过 20 个状态的状态机,结果导出代码时提示「超出免费限制」。所以,如果项目规模大,建议直接上专业版。

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;
    }
}
核心优势:SMC 生成的代码是纯 C 的,没有依赖任何框架。你可以直接把它嵌入到任何嵌入式项目中。我曾在 STM32 和 ESP32 上都用过 SMC,效果很好。

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 率明显下降。

一个小技巧:不管你用哪个工具,都建议先画状态图。哪怕你最后用手写代码,画图也能帮你理清逻辑。我见过太多人上来就写代码,写到一半发现状态漏了,或者转换条件错了。画图花 10 分钟,能省你后面 2 小时的调试时间。
状态机工具选型决策流程 开始选型 资源受限的嵌入式系统? QPC 需要图形化建模? YAKINDU 需要集成到构建系统? SMC 手写状态机 开始编码

嗯,以上就是我对这三款工具的使用心得。工具只是手段,关键还是你对状态机本身的理解。选一个顺手的,多用多练,慢慢就有感觉了。

公众号:蓝海资料掘金营,微信deep3321