嵌入式老司机带你解锁 C 语言状态机的正确姿势

立芯嵌入式 2025-09-27 21:41

大家好,欢迎来到立芯嵌入式,今天继续聊聊嵌入式系统里的有限状态机(FSM)。上一篇文章我带大家用 C 语言实现了一个按钮开关灯的简单例子。这次,咱们要更进一步,把状态机的“引擎”和具体实现分开,搞出一个更通用、更实用的 C 语言状态机框架。

为什么要升级?

如果你的任务只是点亮一盏灯,上一期的代码可能已经够用了。但嵌入式开发哪有这么简单?实际项目里,经常要处理多个事件、多个状态,甚至还要加上定时器、状态切换时的额外操作。靠之前那种硬写的 switch-case,代码很快就会变得乱七八糟,维护起来跟噩梦一样。所以,咱们得整一个更靠谱的工具,既能保持代码清晰,又能应付复杂场景。

这次,我还是拿那个“按钮开关灯”例子开刀。虽然对于这么简单的功能,代码可能显得有点“过度设计”,但别急,我故意保持例子简单,就是想让大家把注意力集中在状态机本身的实现上,而不是被复杂的业务逻辑带偏。

从上一期说起

上一期里,我们用函数指针实现了一个简单的状态机,代码大概是这样的:

typedef enum {
    BUTTON_PRESSED
} Event;

typedefstruct LightSwitcher LightSwitcher;

typedefvoid* (*State)(LightSwitcher*, Event);

struct LightSwitcher {
    State current_state;
};

voidstate_off(LightSwitcher* self, Event e) {
    if (e == BUTTON_PRESSED) {
        gpio_write(1); // 灯亮
        return state_on;
    }
    return state_off;
}

voidstate_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;
};

voidstate_off(void* host, void* event) {
    Event* e = (Event*)event;
    if (*e == BUTTON_PRESSED) {
        gpio_write(1); // 灯亮
        return state_on;
    }
    return state_off;
}

voidstate_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;
};

voidstate_off(LightSwitcher* self, Event* e) {
    if (*e == BUTTON_PRESSED) {
        gpio_write(1);
        return state_on;
    }
    return state_off;
}

voidstate_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; // 简单模拟定时器
};

voidstate_off(LightSwitcher* self, Event* e) {
    if (*e == BUTTON_PRESSED) {
        gpio_write(1);
        self->timer = 100// 设置定时器
        return state_on;
    }
    return state_off;
}

voidstate_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;
};

voidstate_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;
}

voidstate_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,优化这套框架,让代码更简洁易读。敬请期待!


欢迎留言讨论! 有啥想法或者建议,随时在公众号后台私信或者评论区留言。

嵌入式老司机带你解锁 C 语言状态机的正确姿势图1

声明:内容取材于网络,仅代表作者观点,如有内容违规问题,请联系处理。 
嵌入式
more
电子与嵌入式技术年度大展(深圳)观众登记倒计时
STM32嵌入式开发项目实战|RTOS+MQTT+阿里云平台全流程教学+1V1辅导
RTOS与Linux融合,是嵌入式行业的大趋势
瑞萨嵌入式开发 | 瑞萨MCU开发常用工具
PCIM2025论文摘要 | 基于英飞凌S-cell产品的嵌入式PCB方案在主驱逆变器应用的优势分析与研究
嵌入式代码,一个函数写多少行才合适?
嵌入式代码参数管理框架(C语言)
嵌入式开发少不了的网络故障排除
几个受欢迎的嵌入式开源项目
【北京 西安】TI嵌入式实验室免费培训:AI加速ADAS、AI DSP、多协议无线、精密电机控制等 | 即将开讲,预报从速!)
Copyright © 2025 成都区角科技有限公司
蜀ICP备2025143415号-1
  
川公网安备51015602001305号