从零搭建一个状态机:MATLAB → Stateflow → 生成 C 代码 → 跑起来
关键词:有限状态机(FSM)、MATLAB、Stateflow、Simulink Coder、代码生成
环境:MATLAB R2021b + Simulink + Stateflow + Simulink Coder
示例模型:
turnstile(地铁闸机 / 十字转门)
本教程用一个大家都熟悉的例子------地铁闸机------把"状态机"从一段 MATLAB 脚本,一路做到"生成 C 代码并编译运行"。你会看到:
- 状态机在代码里长什么样
- 怎么用 Stateflow 图形化建模并自动保存成
.slx - 从
.slx一键生成 C 代码 - 生成的 C 代码和状态机如何一一对应
- 写一个
main.c把生成代码真正跑起来 - 几个容易踩的坑(变步长、输入事件 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]--> Unlocked 和 Unlocked --[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 1 :
is_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 只 #include 了 turnstile.h 和 turnstile_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.m、turnstile_step.m、generate_turnstile_slx.m、
main.c、build_main.bat、turnstile.slx、turnstile_grt_rtw/。