记录matlab状态机demo

从零搭建一个状态机:MATLAB → Stateflow → 生成 C 代码 → 跑起来

关键词:有限状态机(FSM)、MATLAB、Stateflow、Simulink Coder、代码生成

环境:MATLAB R2021b + Simulink + Stateflow + Simulink Coder

示例模型:turnstile(地铁闸机 / 十字转门)

本教程用一个大家都熟悉的例子------地铁闸机------把"状态机"从一段 MATLAB 脚本,一路做到"生成 C 代码并编译运行"。你会看到:

  1. 状态机在代码里长什么样
  2. 怎么用 Stateflow 图形化建模并自动保存成 .slx
  3. .slx 一键生成 C 代码
  4. 生成的 C 代码和状态机如何一一对应
  5. 写一个 main.c 把生成代码真正跑起来
  6. 几个容易踩的坑(变步长、输入事件 vs 输入数据、首步不解锁、乱码、最小文件集)

0. 为什么用"闸机"做例子

闸机只有两种状态,规则也极简,却包含了状态机的全部精髓:

  • 状态Locked(锁定)/ Unlocked(放行)
  • 事件coin(投币)/ push(推门)
  • 转移规则
当前状态 事件 结果
Locked coin → Unlocked(投币开门)
Locked push 保持 Locked(没投币推不动)
Unlocked push → Locked(通过后重新锁)
Unlocked coin 保持 Unlocked(重复投币无效)

一句话:只认"先投币、再推门",一张票过一个人。


1. 先用纯 MATLAB 手写状态机(最直观)

不用任何工具箱,纯 .m 就能表达状态机。核心是三样东西:一组状态、一个"当前状态"变量、一组转移规则

1.1 事件驱动版(switch 实现)

matlab 复制代码
% turnstile_statemachine.m
function turnstile_statemachine()
    state = 'LOCKED';
    fprintf('初始: %s\n', state);

    events = {'coin', 'push', 'coin', 'coin', 'push', 'push'};
    for i = 1:numel(events)
        e = events{i};
        switch state
            case 'LOCKED'
                if strcmp(e, 'coin')
                    state = 'UNLOCKED';
                    fprintf('事件 %-6s -> 投币成功,闸机打开\n', e);
                else
                    fprintf('事件 %-6s -> 门是锁的,推不动!\n', e);
                end
            case 'UNLOCKED'
                if strcmp(e, 'push')
                    state = 'LOCKED';
                    fprintf('事件 %-6s -> 通过,闸机重新锁定\n', e);
                else
                    fprintf('事件 %-6s -> 已打开,重复投币无效\n', e);
                end
        end
    end
end

运行:turnstile_statemachine

1.2 能生成 C 代码版(MATLAB Coder 友好)

上面的写法用了字符串状态、cell 数组、strcmp不能直接生成 C 代码 。要生成 C,得用整数表示状态/事件,且不能有动态字段名、不能有 pause

matlab 复制代码
% turnstile_step.m
%#codegen
function nextState = turnstile_step(currentState, event)
    LOCKED = 0; UNLOCKED = 1; COIN = 0; PUSH = 1;
    nextState = currentState;          % 默认保持当前状态
    switch currentState
        case LOCKED
            if event == COIN,  nextState = UNLOCKED; end
        case UNLOCKED
            if event == PUSH,  nextState = LOCKED;   end
    end
end

生成 C 代码(需 MATLAB Coder):

matlab 复制代码
codegen turnstile_step -args {0, 0} -report

这一版是"手写状态机直接出 C"的路线。但我们更想看的是 Stateflow 图形化建模 → 生成 C ,所以下面走 .slx 路线。


2. 用 Stateflow API 自动生成 .slx 模型

不想手点鼠标画状态图?可以用 Stateflow 的 API 写个脚本,程序化建图并保存成 .slx

matlab 复制代码
% generate_turnstile_slx.m
function generate_turnstile_slx()
    model = 'turnstile';
    if bdIsLoaded(model), close_system(model, 0); end
    if exist([model '.slx'], 'file'), delete([model '.slx']); end

    sfnew(model); open_system(model);                       % 新建带 Stateflow 图的模型
    rt = sfroot;
    chart = rt.find('-isa', 'Stateflow.Chart', '-and', 'Path', [model '/Chart']);

    % 输入数据 coin / push(boolean),作为图块输入端口
    dCoin = Stateflow.Data(chart); dCoin.Name = 'coin'; dCoin.Scope = 'Input'; dCoin.DataType = 'boolean';
    dPush = Stateflow.Data(chart); dPush.Name = 'push'; dPush.Scope = 'Input'; dPush.DataType = 'boolean';

    % 两个状态
    lock = Stateflow.State(chart); lock.Name = 'Locked';   lock.Position = [40 60 120 70];
    unl  = Stateflow.State(chart); unl.Name  = 'Unlocked'; unl.Position  = [240 60 120 70];

    % 默认转移:上电即 Locked
    defT = Stateflow.Transition(chart); defT.Destination = lock;

    % 状态间转移:用输入数据做守卫条件 [ ]
    t1 = Stateflow.Transition(chart); t1.Source = lock; t1.Destination = unl; t1.LabelString = '[coin]';
    t2 = Stateflow.Transition(chart); t2.Source = unl;  t2.Destination = lock; t2.LabelString = '[push]';

    % 根部接 Inport,让图能被驱动
    add_block('simulink/Sources/In1', [model '/coin_in'], 'Position', [20 100 50 120]);
    add_block('simulink/Sources/In1', [model '/push_in'], 'Position', [20 220 50 240]);
    add_line(model, 'coin_in/1', 'Chart/1');
    add_line(model, 'push_in/1', 'Chart/2');

    % 关键:固定步长求解器 + 仅生成代码(免 C 编译器)
    set_param(model, 'SolverType', 'Fixed-step');
    set_param(model, 'Solver', 'FixedStepDiscrete');
    set_param(model, 'FixedStep', '1');
    set_param(model, 'GenCodeOnly', 'on');

    save_system(model, [model '.slx']); close_system(model, 0);
end

运行 generate_turnstile_slx 即得到 turnstile.slx。双击 Chart 就能看到图形化的 Locked --[coin]--> UnlockedUnlocked --[push]--> Locked

⚠️ 坑 1:输入事件 vs 输入数据

一开始我把 coin/push 设成"输入事件(Input Event)",结果生成的 C 代码里图被优化成空函数------因为没有东西去触发这些事件。改成"输入数据 + [ ] 守卫"并从根部 Inport 喂数据后,状态机才真正进入生成代码。


3. 从 .slx 生成 C 代码

matlab 复制代码
load_system('turnstile')
slbuild('turnstile')      % 等价于在模型里按 Ctrl+B

⚠️ 坑 2:变步长求解器报错

sfnew 建的模型默认是变步长 ,而 grt/ert 代码生成要求固定步长 ,否则报:

The specified code generation target ... cannot be used with a variable-step solver.

解决:脚本里已设 SolverType='Fixed-step' + Solver='FixedStepDiscrete'

生成成功后,代码在 turnstile_grt_rtw/ 下。重点文件:

  • turnstile.c / turnstile.h ------ 模型步函数与接口
  • 状态机逻辑就内联在 turnstile_step()

4. 生成的 C 代码,和状态机怎么对应

turnstile.c 里最关键的几段。

4.1 状态名 → 整数常量

c 复制代码
#define turnstile_IN_Locked            ((uint8_T)1U)
#define turnstile_IN_NO_ACTIVE_CHILD   ((uint8_T)0U)
#define turnstile_IN_Unlocked          ((uint8_T)2U)

Stateflow 把图里的 Locked/Unlocked 字符串编成了整数枚举。

4.2 当前状态 → 一个变量

c 复制代码
typedef struct {
  uint8_T is_active_c3_turnstile;   /* 图是否已激活 */
  uint8_T is_c3_turnstile;          /* 当前状态(存上面的 IN_ 常量)*/
} DW_turnstile_T;

这就是 FSM 里的"当前状态"变量,跨 step 调用一直保持。

4.3 输入 → 一个结构体

c 复制代码
typedef struct {
  boolean_T coin_in;   /* 根 Inport: coin */
  boolean_T push_in;   /* 根 Inport: push */
} ExtU_turnstile_T;

4.4 转移逻辑 → turnstile_step()

c 复制代码
void turnstile_step(void)
{
  if (turnstile_DW.is_active_c3_turnstile == 0U) {
    turnstile_DW.is_active_c3_turnstile = 1U;
    turnstile_DW.is_c3_turnstile = turnstile_IN_Locked;   // 默认转移 -> Locked
  } else if (turnstile_DW.is_c3_turnstile == turnstile_IN_Locked) {
    if (turnstile_U.coin_in) {
      turnstile_DW.is_c3_turnstile = turnstile_IN_Unlocked; // t1: [coin]
    }
  } else if (turnstile_U.push_in) {
    turnstile_DW.is_c3_turnstile = turnstile_IN_Locked;     // t2: [push]
  }
}
生成代码 状态机含义
第 1 个 if(is_active==0) 默认转移 defT → Locked
else if (is_c3==Locked) { if(coin_in) ... } t1: Locked --[coin]--> Unlocked
else if (push_in)(走到这必是 Unlocked) t2: Unlocked --[push]--> Locked

代码生成器把"每个状态一个 if 块"压成了 if/else 链,语义和手写 switch 完全等价。


5. 写 main.c 把生成代码跑起来

生成代码不会自己运行,需要一个宿主程序去喂输入、推步、读状态:

c 复制代码
/* main.c */
#include <stdio.h>
#include "turnstile.h"

/* 与 turnstile.c 中的 #define 保持一致(Stateflow 把状态编成整数)*/
#ifndef turnstile_IN_Locked
#define turnstile_IN_Locked   ((uint8_T)1U)
#define turnstile_IN_Unlocked ((uint8_T)2U)
#endif

static const char* state_name(uint8_T s) {
    if (s == turnstile_IN_Locked)   return "Locked";
    if (s == turnstile_IN_Unlocked) return "Unlocked";
    return "NO_ACTIVE_CHILD";
}

int main(void) {
    const int coin_seq[6] = {1, 0, 1, 1, 0, 0};
    const int push_seq[6] = {0, 1, 0, 0, 1, 1};
    int i;

    turnstile_initialize();
    printf("initial state: %s\n", state_name(turnstile_DW.is_c3_turnstile));

    for (i = 0; i < 6; i++) {
        turnstile_U.coin_in = (boolean_T)coin_seq[i];  // 喂输入
        turnstile_U.push_in = (boolean_T)push_seq[i];
        turnstile_step();                               // 推进一步
        printf("step %d: coin=%d push=%d -> state: %s\n",
               i + 1, coin_seq[i], push_seq[i],
               state_name(turnstile_DW.is_c3_turnstile));
    }
    turnstile_terminate();
    return 0;
}

5.1 用 MATLAB 自带的 LCC 编译

编译命令(关键标志已标出):

bat 复制代码
rem build_main.bat (自动探测 MATLAB 根目录)
set LCC=%MATLAB_ROOT%\sys\lcc64\lcc64\bin\lcc64.exe
set LNK=%MATLAB_ROOT%\sys\lcc64\lcc64\bin\lcclnk64.exe
set LCCINC=%MATLAB_ROOT%\sys\lcc64\lcc64\include64
set INC=.\turnstile_grt_rtw
set MATLABINC=%MATLAB_ROOT%\extern\include
set SIMINC=%MATLAB_ROOT%\simulink\include
set RTWSRC=%MATLAB_ROOT%\rtw\c\src
set LIB=%MATLAB_ROOT%\sys\lcc64\lcc64\lib64

rem 编译(注意 -noregistrylookup 关掉交互式询问)
"%LCC%" -c -w -noregistrylookup -nodeclspec -I"%LCCINC%" -I"%INC%" -I"%MATLABINC%" -I"%SIMINC%" -I"%RTWSRC%" main.c -Fomain.obj
"%LCC%" -c -w -noregistrylookup -nodeclspec -I"%LCCINC%" -I"%INC%" -I"%MATLABINC%" -I"%SIMINC%" -I"%RTWSRC%" %INC%\turnstile.c -Foturnstile.obj

rem 链接(用 -L 指定库路径,不是 -libpath)
"%LNK%" -s -L"%LIB%" -o turnstile_demo.exe main.obj turnstile.obj

⚠️ 坑 3:LCC 的两个编译细节

  • 必须加 -noregistrylookup,否则 LCC 会弹窗交互式询问头文件路径;
  • 链接用 -L<lib64> 指定库搜索路径,不是 -libpath

运行 turnstile_demo.exe

复制代码
initial state: NO_ACTIVE_CHILD
step 1: coin=1 push=0 -> state: Locked
step 2: coin=0 push=1 -> state: Locked
step 3: coin=1 push=0 -> state: Unlocked
step 4: coin=1 push=0 -> state: Unlocked
step 5: coin=0 push=1 -> state: Locked
step 6: coin=0 push=1 -> state: Locked

行为完全正确:Locked 下 coin 才解锁、push 无效;Unlocked 下 push 才回锁、coin 无效。

⚠️ 坑 4:中文乱码

若把输出写成中文,源文件是 UTF-8,而 cmd 默认用 GBK,会乱码。最稳的做法是输出字符串用英文(ASCII);或在 cmd 里先 chcp 65001 再运行。


6. 为什么 step 1 投了币却还是 Locked?

这是状态机的标准语义,不是 bug:

turnstile_step()if / else if 链,一步只走一个分支

  • step 1is_active == 0,进入"激活"分支,执行默认转移进入 Locked。因为进了 if,后面的 else if(含 coin 判断)这一步不执行coin=1 被"吞掉"。
  • step 3 :图表早已激活且处于 Locked,才走 coin 分支 → Unlocked

即:第一步是"闸机通电、默认锁上",事件从下一步才开始被处理。想要 step 1 就解锁,只要在最前面加一个"预热步"(coin=0, push=0)即可。


7. 生成代码里哪些文件才是必需的?

不用全用。对这个简单模型,编译时只需要 turnstile.c + turnstile.h (外加它们 #include 的一堆纯头文件,不需要再编额外 .c)。

文件 需要?
turnstile.c ✅ 必须编译
turnstile.h 及它包含的 *.h(类型定义) ✅ 留着(纯头文件,不编译)
rt_nonfinite.c / rtGetInf.c / rtGetNaN.c ❌ 本模型没用到(浮点 Inf/NaN 支持被省去)
*.mat / *.dmr / *.mk / *.rsp / rtw_proj.tmw ❌ 生成元数据,不参与编译

实测 turnstile.c#includeturnstile.hturnstile_private.h,且完全没有调用任何 rt_* 函数 ------所以只编这一个 .c 就能出可执行文件。

注意:因为 turnstile.h 间接包含了 MATLAB 的几个头文件(tmwtypes.h 等),若放到没装 MATLAB 的机器上编译,需要把这些头文件一并复制过去(都是纯头文件,无害)。


8. 总结与延伸

一路下来,我们打通了:

复制代码
手写 .m 状态机  →  Stateflow 建 .slx 图  →  slbuild 生成 C  →  main.c 驱动  →  编译运行

三条路线对比:

路线 工具 适合
手写 .m MATLAB 逻辑简单、想直接读代码
.m + MATLAB Coder MATLAB Coder 从脚本直接出 C,轻量
.slx + Simulink Coder Stateflow / Simulink Coder 图形化、复杂状态、要产品级代码

可以继续玩的

  • state 输出加回 .slx,让 main.c 直接打印 0/1
  • 用 Embedded Coder(ert.tlc)生成产品级代码,对比 GRT 的差异
  • 扩展状态机:加"投币后超时自动回锁"、加子状态/并行状态
  • 把最小文件包整理出来,放到没有 MATLAB 的嵌入式目标上编译

示例代码均在 turnstile_proj/ 目录下,包含:

turnstile_statemachine.mturnstile_step.mgenerate_turnstile_slx.m

main.cbuild_main.batturnstile.slxturnstile_grt_rtw/

相关推荐
牡丹雅忻12 小时前
AES 加密模式演进:从 ECB、CBC 到 GCM 的 C# 深度实践
java·开发语言·c#
paopaokaka_luck2 小时前
基于springboot3+vue3的智能文库平台(AI智能搜索、AI智能汇总、实时在线状态展示、多格式文档预览与富文本编辑、Echarts图形化分析)
前端·网络·spring boot·网络协议·echarts
学者猫头鹰2 小时前
Java 8 核心新特性
java
冰心孤城2 小时前
Excel: xls与xlsx格式转换排坑指南
java·前端·excel
自强的小白2 小时前
maven高级(聚合和私服)以及私服的上传和下载
java·maven
唐青枫2 小时前
Java ReentrantLock 实战详解:比 synchronized 更灵活的可重入锁
java
Yang96112 小时前
告别“慢工出细活”:如何平衡光纤熔接的速度与精度?
网络
吃好睡好便好3 小时前
MATLAB中图像的线性变换
开发语言·图像处理·学习·计算机视觉·matlab
ShiXZ2133 小时前
Java 8 Stream API 实用技巧详解:从入门到精通
java·开发语言