【中微半导体】嵌入式C语言,函数指针表驱动状态机( 代码风格抽象,在 C 里模拟了“对象“、“多态“的效果)
1️⃣ 先看数据结构
typedef struct
{union _State_Context_Flag flag; // 状态标志位(bit)unsigned short int State_Identifier; // 当前状态编号void (*FuncPtr[6])(); // 各个状态对应的处理函数数组void (*ContextPtr)(); // 状态机调度函数
}_State_Context;
所以一个 _State_Context
变量(比如 State_Context
)里面装了:
flag
→ 各种状态标志(bit 位存储)State_Identifier
→ 当前状态编号(0=初始化,1=等待,2=准备,3=运行,4=故障,5=停止)FuncPtr[6]
→ 一个函数指针数组,存放 6 个函数(分别处理 6 种状态)ContextPtr
→ 一个额外的“总调度函数指针”
2️⃣ 默认初始化(STATE_CONTEXT_DEFAULTS)
宏定义:
#define STATE_CONTEXT_DEFAULTS { 0, // flag = 0 (清零)CM_INITIAL_STATE, // 当前状态 = 0(初始态)(void (*)(unsigned int))Initial_Deal_Func, // FuncPtr[0] = 初始态函数(void (*)(unsigned int))Waiting_Deal_Func, // FuncPtr[1] = 等待态函数(void (*)(unsigned int))Ready_Deal_Func, // FuncPtr[2] = 准备态函数(void (*)(unsigned int))Run_Deal_Func, // FuncPtr[3] = 运行态函数(void (*)(unsigned int))Fault_Deal_Func, // FuncPtr[4] = 故障态函数(void (*)(unsigned int))Stop_Deal_Func, // FuncPtr[5] = 停止态函数(void (*)(unsigned int))State_Context_Func // ContextPtr = 状态机调度函数
}
于是:
_State_Context State_Context = STATE_CONTEXT_DEFAULTS;
等价于:
State_Context.flag.all = 0; // 标志位清零
State_Context.State_Identifier = 0; // 当前状态 = 初始态
State_Context.FuncPtr[0] = Initial_Deal_Func;
State_Context.FuncPtr[1] = Waiting_Deal_Func;
State_Context.FuncPtr[2] = Ready_Deal_Func;
State_Context.FuncPtr[3] = Run_Deal_Func;
State_Context.FuncPtr[4] = Fault_Deal_Func;
State_Context.FuncPtr[5] = Stop_Deal_Func;
State_Context.ContextPtr = State_Context_Func;
3️⃣ 数据演练:状态机如何运行
第一步:初始化
State_Context.flag.all = 0; // 标志位全清
State_Context.State_Identifier = 0; // 当前状态 = CM_INITIAL_STATE
第二步:运行调度函数
假设主循环里调用:
State_Context.ContextPtr(&State_Context);
这等价于:
State_Context_Func(&State_Context);
第三步:在 State_Context_Func()
里会调度
通常 State_Context_Func()
逻辑是这样的:
void State_Context_Func(State_Context_Handle h) {unsigned short s = h->State_Identifier; // 取当前状态h->FuncPtr[s](h); // 执行对应的状态处理函数
}
假设 State_Identifier = 0
,那么:
h->FuncPtr[0](h);
会调用:
Initial_Deal_Func(h);
第四步:状态切换
在 Initial_Deal_Func()
里,可能会这样:
void Initial_Deal_Func(State_Context_Handle h) {printf("运行初始化流程...\n");h->flag.bit.Initial_Ok = 1; // 设置标志位:初始化完成h->State_Identifier = 1; // 切换到等待态
}
于是:
- 标志位
Initial_Ok
被置位 - 状态编号
State_Identifier
变成1
第五步:下一轮调度
下次再执行:
State_Context.ContextPtr(&State_Context);
就会运行:
h->FuncPtr[1](h);
即:
Waiting_Deal_Func(h);
等待态逻辑执行完,可能把 State_Identifier
改成 2
(进入就绪态)……
依此类推,就完成了 状态机自动跳转。
✅ 总结
这种写法的关键点是:
- 用 结构体保存当前状态编号和所有状态处理函数指针。
State_Context_Func()
根据State_Identifier
调用不同的函数。- 各状态处理函数可以修改
State_Identifier
,实现状态跳转。
所以它就像一个 “跳转表” + “调度器”,而不是一大堆 if/else
。
好问题 👍
你现在已经理解了代码的执行过程,下一步就是理解 这种写法为什么在嵌入式里常见,以及它的价值。
🏷 这种方式叫什么?
这种写法一般叫做:
- 函数指针状态机 (Function Pointer State Machine)
- 或者 表驱动状态机 (Table-Driven State Machine)
因为它的核心是:
用一个 函数指针表 (FuncPtr[]) 存储各个状态的处理函数,
再用 状态编号 (State_Identifier) 决定调用哪个函数。
✅ 意义(为什么嵌入式喜欢这样写?)
在嵌入式 C 里,状态机随处可见,比如:
- 电机控制流程:初始化 → 预充电 → 就绪 → 运行 → 故障
- 通信协议处理:IDLE → 接收头 → 接收数据 → 校验 → 结束
- UI 菜单导航:主界面 → 设置 → 参数修改 → 返回
传统写法就是一堆 if
或 switch
:
switch(state) {case INIT: Initial_Deal_Func(); break;case WAIT: Waiting_Deal_Func(); break;...
}
但是 表驱动 + 函数指针 写法,把这些逻辑抽象了:
State_Context.FuncPtr[state](...);
这就有几个优势。
⚖️ 优点
-
结构清晰、易扩展
- 新增状态,只需要写一个新的处理函数 + 把函数指针放进表里,不用改一大堆
switch
。 - 状态数量多时,代码不会爆炸。
- 新增状态,只需要写一个新的处理函数 + 把函数指针放进表里,不用改一大堆
-
高可维护性
- 每个状态逻辑独立在一个函数里,不会写成几百行大
switch-case
。 - 模块化更强,方便调试。
- 每个状态逻辑独立在一个函数里,不会写成几百行大
-
执行效率高
switch
可能会被编译器优化成跳转表,但函数指针数组直接就是跳转表 → 执行快。- 在实时系统里,这种确定性很重要。
-
更接近面向对象的思想
- 每个状态函数就像“对象的方法”,状态切换靠
State_Identifier
,逻辑清晰。 - 在 C 里模拟了“多态”的效果。
- 每个状态函数就像“对象的方法”,状态切换靠
⚖️ 缺点
-
不易理解(学习成本高)
- 初学者一眼看不懂,尤其是
函数指针数组 + 调度函数
,会觉得绕。
- 初学者一眼看不懂,尤其是
-
调试麻烦
- 如果指针赋错,可能直接跳到野函数地址 → MCU 硬件跑飞。
- 比
switch-case
难跟踪。
-
灵活性过高,容易滥用
- 写状态机时应该保持清晰,如果把所有逻辑都“丢到函数指针里”,有时反而失去整体把握。
-
对小项目可能过度设计
- 如果状态就两个,比如 “开 / 关”,用
if
可能更简单。
- 如果状态就两个,比如 “开 / 关”,用
🎯 总结一句话
这种写法是 函数指针表驱动状态机,
意义在于 让状态机更模块化、易扩展、执行高效,
但代价是 代码风格抽象,调试复杂,学习门槛高。