找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3534

积分

0

好友

476

主题
发表于 3 小时前 | 查看: 5| 回复: 0

很多开发者都能熟练驱动各种单片机外设,但一写完整的项目代码就容易迷失,毫无逻辑与框架感。这说明仅停留在操作外设的层面还不够,掌握一套优秀的编程思想才是进阶的关键——比如模块化编程、状态机 编程、分层架构等。  

本文就来深入聊聊状态机编程,并通过一个具体的按键控制 LED 灯的例子,展示如何从状态转换图到 C 代码落地实现。

什么是状态机?

状态机(State Machine)包含五个核心要素:

  • 状态(State):系统在某一时刻所处的稳定工作模式,整个生命周期中可能包含多个状态。例如,一台电动机具有正转、反转、停转三种状态。
  • 迁移(Transition):系统从一个状态转移到另一个状态的过程,迁移需要外部事件驱动,不会自动发生。停转的电机必须上电才能转动。
  • 事件(Event):在某一时刻发生的、对系统有意义的事情,是触发状态迁移的原因。对电机而言,加正电压、加负电压、断电就是事件。
  • 动作(Action):状态机在迁移过程中执行的其它行为,是对事件的响应。比如给停转的电机加正电压时,电机不仅迁移到正转状态,还会启动转动——这个启动过程就是动作。
  • 条件(Guard):即使事件发生,状态机也可能需要满足一定条件才执行迁移。例如,电机虽然已合闸,但若供电线路故障,仍然不能转起来。

一个状态机必须从初始状态开始运行。  

实例:按键控制 LED 灯

电路与功能需求

电路如下图所示,包含一个单片机 MCU、一个按键 K0 和两个 LED 灯 L1、L2。  

MCU按键控制LED电路原理图  

功能需求如下:  

  • L1 和 L2 的状态按顺序转换:OFF/OFF → ON/OFF → ON/ON → OFF/ON → OFF/OFF  
  • 每次状态转换需要连续按键 5 次  
  • 初始状态为 OFF/OFF  

状态转换图

在状态机编程中,先设计状态转换图,再编写代码才是正确的顺序。下面是一张使用类似 UML 语法绘制的按键控制流水灯状态转换图:  

按键控制流水灯状态转换图  

图中:  

  • 圆角矩形表示状态,内部标注状态名称(如 LS_OFFOFFLS_ONOFF 等)。  
  • 带箭头的直线或弧线表示状态迁移,从初态指向次态。  
  • 迁移上的标注格式为:事件[条件] / 动作列表(条件和动作列表可选)。例如 KEY[g_sIFSM_u8KeyCnt > 3] / g_sIFSM_u8KeyCnt--; le_d_off(LED1); 表示发生按键事件,且条件满足时执行相应动作并迁移状态。  
  • 黑色实心圆点表示运行前的不可知状态,启动时强制迁移到初始状态,该迁移不需要事件但可以有动作。  
  • 包含实心圆点的圆圈表示状态机生命周期的结束,本例中的状态机循环运行,因此没有状态指向该圆圈。  

代码实现

根据状态转换图,写出如下 C 代码:  

void main(void)
{
  sys_init();
  led_off(LED1);
  led_off(LED2);
  g_stFSM.u8LedStat = LS_OFFOFF;
  g_stFSM.u8KeyCnt = 0;

  while(1)
  {
    if(test_key() == TRUE)
    {
      fsm_active();
    }
    else
    {
      ; /*idle code*/
    }
  }
}

void fsm_active(void)
{
  if(g_stFSM.u8KeyCnt > 3) /*击键是否满 5 次*/
  {
    switch(g_stFSM.u8LedStat)
    {
      case LS_OFFOFF:
        led_on(LED1);      /*输出动作*/
        g_stFSM.u8KeyCnt = 0;
        g_stFSM.u8LedStat = LS_ONOFF; /*状态迁移*/
        break;

      case LS_ONOFF:
        led_on(LED2);      /*输出动作*/
        g_stFSM.u8KeyCnt = 0;
        g_stFSM.u8LedStat = LS_ONON;  /*状态迁移*/
        break;

      case LS_ONON:
        led_off(LED1);     /*输出动作*/
        g_stFSM.u8KeyCnt = 0;
        g_stFSM.u8LedStat = LS_OFFON; /*状态迁移*/
        break;

      case LS_OFFON:
        led_off(LED2);     /*输出动作*/
        g_stFSM.u8KeyCnt = 0;
        g_stFSM.u8LedStat = LS_OFFOFF; /*状态迁移*/
        break;

      default: /*非法状态*/
        led_off(LED1);
        led_off(LED2);
        g_stFSM.u8KeyCnt = 0;
        g_stFSM.u8LedStat = LS_OFFOFF; /*恢复初始状态*/
        break;
    }
  }
  else
  {
    g_stFSM.u8KeyCnt++;  /*状态不迁移,仅记录击键次数*/
  }
}

代码解析与扩展性分析

fsm_active() 函数中,g_stFSM.u8KeyCnt = 0;switch-case 里出现了多次,其实完全可以合并到 switch 之前。这里保持重复写法,目的是清晰展示每个状态迁移时的完整动作,与状态转换图的意图完全对齐。  

状态机变量 g_stFSM 包含两个成员:u8LedStat(LED 状态,质变因子)和 u8KeyCnt(按键计数,量变因子)。量变因子逐步积累,达到阈值后才触发质变因子变化,从而实现状态迁移。这种结构称为扩展状态机(Extended State Machine)。  

假设需求从“连按 5 次”改为“连按 100 次”,只需将条件 g_stFSM.u8KeyCnt > 3 改成 g_stFSM.u8KeyCnt > 98 即可。而如果仅用一个状态变量,每个状态需要拆分成 5 个小状态,连按 100 次就需要 400 个状态,switch-case 会膨胀到无法维护。扩展状态机通过分离质变与量变,让代码的扩展性和可读性都得到了质的提升。  


通过这个简单的例子,状态机编程的威力可见一斑。当你面对复杂度更高的嵌入式系统时,这种思想能帮助你构建出逻辑清晰、便于维护的程序框架。




上一篇:OpenClaw崩盘:频繁更新、Anthropic封杀、竞品反超,下载量腰斩
下一篇:美国军方联合七大AI巨头,军事人工智能化进入“终极竞赛”阶段
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-2 21:07 , Processed in 0.643859 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表