大家好,欢迎来到立芯嵌入式,今天继续聊聊嵌入式系统里的有限状态机(FSM)。上一篇文章我带大家用 C 语言实现了一个按钮开关灯的简单例子。这次,咱们要更进一步,把状态机的“引擎”和具体实现分开,搞出一个更通用、更实用的 C 语言状态机框架。
为什么要升级?
如果你的任务只是点亮一盏灯,上一期的代码可能已经够用了。但嵌入式开发哪有这么简单?实际项目里,经常要处理多个事件、多个状态,甚至还要加上定时器、状态切换时的额外操作。靠之前那种硬写的 switch-case
,代码很快就会变得乱七八糟,维护起来跟噩梦一样。所以,咱们得整一个更靠谱的工具,既能保持代码清晰,又能应付复杂场景。
这次,我还是拿那个“按钮开关灯”例子开刀。虽然对于这么简单的功能,代码可能显得有点“过度设计”,但别急,我故意保持例子简单,就是想让大家把注意力集中在状态机本身的实现上,而不是被复杂的业务逻辑带偏。
从上一期说起
上一期里,我们用函数指针实现了一个简单的状态机,代码大概是这样的:
typedef enum {
BUTTON_PRESSED
} Event;
typedefstruct LightSwitcher LightSwitcher;
typedefvoid* (*State)(LightSwitcher*, Event);
struct LightSwitcher {
State current_state;
};
void* state_off(LightSwitcher* self, Event e) {
if (e == BUTTON_PRESSED) {
gpio_write(1); // 灯亮
return state_on;
}
return state_off;
}
void* state_on(LightSwitcher* self, Event e) {
if (e == BUTTON_PRESSED) {
gpio_write(0); // 灯灭
return state_off;
}
return state_on;
}
void feed(LightSwitcher* self, Event e) {
self->current_state = self->current_state(self, e);
}
void init_light_switcher(LightSwitcher* self) {
self->current_state = state_off;
}
这种方式的好处是,用函数指针直接跳转状态,效率高,还省掉了繁琐的 switch
语句。但问题来了——状态类型用 void*
有点偷懒,类型安全性不够,而且没法方便地调试打印状态名(比如用枚举配合一些库)。这次,咱们要改进它。
分离引擎和实现
为了让状态机更通用,咱们要把“状态机引擎”和“具体实现”分开。引擎负责状态跳转的核心逻辑,而具体实现(我叫它“宿主”)负责定义状态和处理事件。先来看看怎么改:
typedef struct StateMachine StateMachine;
typedefvoid* (*StateFn)(void*, void*); // 状态函数类型:接收宿主和事件,返回新状态
struct StateMachine {
void* host; // 指向宿主结构
StateFn current_state; // 当前状态函数
};
void sm_init(StateMachine* sm, void* host, StateFn initial_state) {
sm->host = host;
sm->current_state = initial_state;
}
void sm_feed(StateMachine* sm, void* event) {
sm->current_state = sm->current_state(sm->host, event); // 更新状态
}
struct LightSwitcher {
StateMachine fsm;
};
void* state_off(void* host, void* event) {
Event* e = (Event*)event;
if (*e == BUTTON_PRESSED) {
gpio_write(1); // 灯亮
return state_on;
}
return state_off;
}
void* state_on(void* host, void* event) {
Event* e = (Event*)event;
if (*e == BUTTON_PRESSED) {
gpio_write(0); // 灯灭
return state_off;
}
return state_on;
}
void init_light_switcher(LightSwitcher* self) {
sm_init(&self->fsm, self, state_off);
gpio_write(0); // 初始化硬件状态
}
void feed_light_switcher(LightSwitcher* self, Event e) {
sm_feed(&self->fsm, &e);
}
这里,StateMachine
是一个通用的状态机引擎,负责状态跳转的核心逻辑。LightSwitcher
作为宿主,定义具体的状态和行为。这样,不管你的状态机有多复杂,引擎部分都能拿来就用。
类型安全一点:去掉 void*
用 void*
虽然简单,但不够严谨,咱们可以用具体的函数指针类型来提高安全性。改一下状态机的定义:
typedef struct LightSwitcher LightSwitcher;
typedefvoid* (*LightStateFn)(LightSwitcher*, Event*);
struct StateMachine {
LightSwitcher* host; // 宿主类型明确
LightStateFn current_state;
};
void sm_init(StateMachine* sm, LightSwitcher* host, LightStateFn initial_state) {
sm->host = host;
sm->current_state = initial_state;
}
void sm_feed(StateMachine* sm, Event* event) {
sm->current_state = sm->current_state(sm->host, event);
}
struct LightSwitcher {
StateMachine fsm;
};
void* state_off(LightSwitcher* self, Event* e) {
if (*e == BUTTON_PRESSED) {
gpio_write(1);
return state_on;
}
return state_off;
}
void* state_on(LightSwitcher* self, Event* e) {
if (*e == BUTTON_PRESSED) {
gpio_write(0);
return state_off;
}
return state_on;
}
这样,LightStateFn
明确了状态函数的签名,编译器能帮我们检查类型错误,代码更健壮了。
加点复杂功能:定时器
假设我们要实现一个带定时器的开关:按一下按钮灯亮,过段时间自动熄灭,再按一下重启定时器。得在状态机里加点数据,比如定时器状态。直接在 LightSwitcher
里加数据是可行的,但有个小问题——所有状态都能访问这些数据,隔离性不够。来看代码:
typedef enum {
BUTTON_PRESSED,
TIMER_EXPIRED
} Event;
struct LightSwitcher {
StateMachine fsm;
int timer; // 简单模拟定时器
};
void* state_off(LightSwitcher* self, Event* e) {
if (*e == BUTTON_PRESSED) {
gpio_write(1);
self->timer = 100; // 设置定时器
return state_on;
}
return state_off;
}
void* state_on(LightSwitcher* self, Event* e) {
if (*e == BUTTON_PRESSED) {
self->timer = 100; // 重启定时器
return state_on;
} elseif (*e == TIMER_EXPIRED) {
gpio_write(0);
return state_off;
}
return state_on;
}
虽然能跑,但我不喜欢这种设计。更好的办法是为每个状态定义独立的结构体来隔离数据,但这会让代码变复杂,尤其在嵌入式里还得小心动态内存。所以,先凑合着用,靠自觉保证数据只在对应状态里操作。
进阶:加上 onEntry 和 onExit
为了让状态机更强大,咱们再加点料:状态进入(onEntry
)和退出(onExit
)的处理逻辑。C 语言没有 std::variant
,咱们可以用一个简单的枚举和联合来实现:
typedef enum {
EVENT_TYPE_ON_ENTRY,
EVENT_TYPE_ON_EXIT,
EVENT_TYPE_APP
} EventType;
typedefstruct {
EventType type;
union {
Event app_event;
} data;
} StateArg;
struct StateMachine {
LightSwitcher* host;
LightStateFn current_state;
};
void sm_init(StateMachine* sm, LightSwitcher* host, LightStateFn initial_state) {
sm->host = host;
sm->current_state = initial_state;
StateArg arg = {EVENT_TYPE_ON_ENTRY};
sm->current_state(sm->host, &arg); // 进入初始状态
}
void sm_feed(StateMachine* sm, Event* event) {
StateArg arg = {EVENT_TYPE_APP, {.app_event = *event}};
LightStateFn next_state = sm->current_state(sm->host, &arg);
if (next_state != NULL && next_state != sm->current_state) {
StateArg exit_arg = {EVENT_TYPE_ON_EXIT};
sm->current_state(sm->host, &exit_arg); // 退出当前状态
sm->current_state = next_state;
StateArg entry_arg = {EVENT_TYPE_ON_ENTRY};
sm->current_state(sm->host, &entry_arg); // 进入新状态
}
}
struct LightSwitcher {
StateMachine fsm;
};
void* state_off(LightSwitcher* self, StateArg* arg) {
switch (arg->type) {
case EVENT_TYPE_ON_ENTRY:
gpio_write(0);
returnNULL; // NULL 表示保持当前状态
case EVENT_TYPE_ON_EXIT:
returnNULL;
case EVENT_TYPE_APP:
if (arg->data.app_event == BUTTON_PRESSED) {
return state_on;
}
returnNULL;
}
returnNULL;
}
void* state_on(LightSwitcher* self, StateArg* arg) {
switch (arg->type) {
case EVENT_TYPE_ON_ENTRY:
gpio_write(1);
returnNULL;
case EVENT_TYPE_ON_EXIT:
returnNULL;
case EVENT_TYPE_APP:
if (arg->data.app_event == BUTTON_PRESSED) {
return state_off;
}
returnNULL;
}
returnNULL;
}
void init_light_switcher(LightSwitcher* self) {
sm_init(&self->fsm, self, state_off);
}
void feed_light_switcher(LightSwitcher* self, Event e) {
sm_feed(&self->fsm, &e);
}
这里用 StateArg
模拟了 C++ 的 std::variant
,通过 EventType
区分进入、退出和应用事件。状态函数返回 NULL
表示不切换状态,引擎会忽略这种返回值。
总结与展望
到这儿,咱们用 C 语言搞出了一个还算实用的状态机引擎。它能处理状态跳转、事件响应,还支持进入和退出动作。
下一期,我会试着用一些宏技巧模拟 DSL,优化这套框架,让代码更简洁易读。敬请期待!
欢迎留言讨论! 有啥想法或者建议,随时在公众号后台私信或者评论区留言。
