夜雨聆风学习资料网

ARTICLE · 1059891

QP 状态机快速上手:工程模板 + 最简可运行示例,抄完就能跑

QP 状态机快速上手:工程模板 + 最简可运行示例,抄完就能跑

上回聊完 QP 状态机的优势,不少朋友后台留言说,道理都懂,但真要上手还是一脸懵:工程该怎么搭?代码从哪写起?有没有能直接抄的模板?

安排。今天纯干货,不扯复杂理论,直接给一套可直接套用的 QP 工程目录模板,再加一个最简可运行的状态机示例。我们只用最核心的 QEP 状态机模块,不用操作系统、不用复杂内核,裸机就能跑,从状态定义到事件投递全给齐,看完就能在自己板子上跑通第一个 QP 状态机。

一、先搭架子:通用 QP 工程目录模板

这是经过多个项目验证的分层结构,从 8 位单片机到 ARM Cortex-M 都适用,逻辑清晰、移植方便、职责分明,团队开发也不会乱。

QP_Project/├── BSP/                // 板级驱动层:纯硬件操作,和状态机完全解耦│   ├── bsp_led.c       // LED驱动│   ├── bsp_key.c       // 按键扫描驱动│   ├── bsp_timer.c     // 定时器时基驱动│   └── bsp.h           // 驱动统一接口头文件├── QP_port/            // QP移植层:官方源码+芯片适配配置│   ├── qep_port.h      // 端口配置(数据类型、宏定义、编译器适配)│   ├── qep.c           // QP核心源码(官方最小裁剪版,仅状态机)│   └── qep.h           // QP核心头文件├── App/                // 应用层:所有业务状态机都放这│   ├── led_fsm.c       // LED状态机具体实现│   └── led_fsm.h       // LED状态机对外接口└── main.c              // 主入口:初始化+事件分发

分层核心原则:

  • BSP 层只碰硬件,不写任何业务逻辑;
  • QP_port 层只做适配,不同芯片只改这一层,应用层完全不动;
  • App 层只认事件,不直接操作硬件,彻底解耦。

后续换芯片、改硬件,只动底层两层,业务状态机代码一行不用改。

二、最简实战:按键切换 LED 状态机

我们用最经典的场景入门:一个按键、一个 LED,三个状态 ——熄灭、慢闪、快闪,按一次按键切换一个状态。没有花里胡哨的功能,就是帮你吃透 QP 的核心流程。

第一步:定义状态机骨架(头文件)

先把事件、状态机对象定义好,这是状态机的骨架。

App/led_fsm.h

#ifndef LED_FSM_H#define LED_FSM_H#include"qep.h"// 1. 事件定义(用户事件从Q_USER_SIG开始,前面是系统保留)typedef enum {    EVT_KEY_PRESS = Q_USER_SIG,  // 按键按下事件    EVT_TIMER_100MS,             // 100ms时基事件} LedEvent;// 2. 状态机对象(必须把QHsm放在第一个成员,相当于继承)typedef struct {    QHsm super;         // QP官方状态机基类    uint8_t tick_cnt;   // 自己的业务变量:闪烁计时} LedFsm;// 3. 对外接口:初始化状态机voidLedFsm_Init(LedFsm *me);#endif

第二步:实现状态处理函数(核心)

每个状态对应一个处理函数,只处理自己关心的事件,不关心的事件抛给父状态。

App/led_fsm.c

#include"led_fsm.h"#include"bsp.h"// 先声明所有状态函数(每个状态对应一个函数)static QState LedFsm_Off(LedFsm *me, QEvent const *e);static QState LedFsm_Slow(LedFsm *me, QEvent const *e);static QState LedFsm_Fast(LedFsm *me, QEvent const *e);// 状态机初始化:设置初始状态为熄灭voidLedFsm_Init(LedFsm *me){    QHsm_ctor(&me->super, (QStateHandler)&LedFsm_Off);    me->tick_cnt = 0;}// 状态1:LED熄灭static QState LedFsm_Off(LedFsm *me, QEvent const *e){    switch (e->sig) {        case Q_ENTRY_SIG:          // 进入状态的动作            BSP_Led_Off();            return Q_HANDLED();        case EVT_KEY_PRESS:        // 按键按下 -> 切到慢闪            Q_TRAN(&LedFsm_Slow);            return Q_HANDLED();        default:            break;    }    return Q_SUPER(&QHsm_top);  // 父状态是系统顶层}// 状态2:LED慢闪(1秒周期,500ms翻转)static QState LedFsm_Slow(LedFsm *me, QEvent const *e){    switch (e->sig) {        case Q_ENTRY_SIG:          // 进入状态:清零计数器            me->tick_cnt = 0;            return Q_HANDLED();        case EVT_TIMER_100MS:      // 100ms时基触发            me->tick_cnt++;            if (me->tick_cnt >= 5) {                me->tick_cnt = 0;                BSP_Led_Toggle(); // 翻转LED            }            return Q_HANDLED();        case EVT_KEY_PRESS:        // 按键按下 -> 切到快闪            Q_TRAN(&LedFsm_Fast);            return Q_HANDLED();        default:            break;    }    return Q_SUPER(&QHsm_top);}// 状态3:LED快闪(200ms周期,100ms翻转)static QState LedFsm_Fast(LedFsm *me, QEvent const *e){    switch (e->sig) {        case Q_ENTRY_SIG:            me->tick_cnt = 0;            return Q_HANDLED();        case EVT_TIMER_100MS:            me->tick_cnt++;            if (me->tick_cnt >= 1) {                me->tick_cnt = 0;                BSP_Led_Toggle();            }            return Q_HANDLED();        case EVT_KEY_PRESS:        // 按键按下 -> 切回熄灭            Q_TRAN(&LedFsm_Off);            return Q_HANDLED();        default:            break;    }    return Q_SUPER(&QHsm_top);}

几个核心概念讲透:

  1. Q_ENTRY_SIG / Q_EXIT_SIG
    :进入 / 退出状态的固定事件,初始化、清理逻辑必须放这,别乱放;
  2. Q_TRAN()
    :状态跳转宏,事件处理完后才会真正跳转,是异步的;
  3. Q_HANDLED()
    :表示这个事件我处理完了;
  4. Q_SUPER()
    :表示这个事件我不处理,交给父状态处理。

这里三个状态都是平级的,父状态都是系统顶层,属于最简单的平面状态机,效果和 switch-case 一致,但已经是标准事件驱动结构了。

第三步:主函数跑起来

在 main.c 里初始化硬件、启动状态机,然后把硬件产生的事件投递给状态机即可。

main.c

#include"bsp.h"#include"led_fsm.h"static LedFsm led_fsm;  // 定义状态机实例intmain(void){    // 1. 硬件初始化    BSP_Init();    // 2. 状态机初始化+启动(会自动执行初始状态的进入动作)    LedFsm_Init(&led_fsm);    QHsm_init((QHsm *)&led_fsm);    while (1) {        // ① 扫描按键,按下就投递按键事件        if (BSP_Key_Scan() == KEY_PRESSED) {            QEvent evt;            evt.sig = EVT_KEY_PRESS;            QHsm_dispatch((QHsm *)&led_fsm, &evt);        }        // ② 检查100ms时基,到点就投递定时器事件        if (BSP_Get100msFlag()) {            BSP_Clear100msFlag();            QEvent evt;            evt.sig = EVT_TIMER_100MS;            QHsm_dispatch((QHsm *)&led_fsm, &evt);        }    }}

核心就一个函数QHsm_dispatch():把事件扔给状态机,它自动找对应状态处理。状态机完全不知道按键、定时器是怎么实现的,只认事件,硬件和业务彻底解耦。

到这里,一个完整可运行的 QP 状态机就写完了。编译下载到板子,按按键就能切换 LED 闪烁模式。

三、进阶一步:感受层次状态机的威力

上面是平面状态机,还体现不出 QP 的核心优势。现在加一个需求:新增一个关机按键,任何闪烁状态下按关机键,都回到熄灭状态

如果是 switch 写法,慢闪、快闪两个状态里都要抄一遍关机判断逻辑;状态越多,重复代码越多。用 QP 的层次状态机,只需要加一个父状态。

改造步骤:

  1. 新增事件EVT_POWER_OFF
  2. 加一个父状态LedFsm_On,统一处理关机事件;
  3. 慢闪、快闪的父状态改成LedFsm_On,自动继承关机逻辑。

改造后的核心代码:

// 新增父状态:运行态(所有闪烁状态的父状态)static QState LedFsm_On(LedFsm *me, QEvent const *e) {    switch (e->sig) {        case EVT_POWER_OFF:      // 统一处理关机,只写一次            Q_TRAN(&LedFsm_Off);            return Q_HANDLED();        default:            break;    }    return Q_SUPER(&QHsm_top);}// 慢闪:只改最后一行返回值,父状态改成Onstatic QState LedFsm_Slow(LedFsm *me, QEvent const *e) {    // ... 原有逻辑完全不动    return Q_SUPER(&LedFsm_On);  // 自动继承关机逻辑}// 快闪:同理,只改返回值static QState LedFsm_Fast(LedFsm *me, QEvent const *e) {    // ... 原有逻辑完全不动    return Q_SUPER(&LedFsm_On);}


就加了一个父状态、改了两行返回值,两个子状态就都拥有了关机处理能力。以后再加呼吸灯、频闪等模式,只要继承这个父状态,自动支持关机,不用重复写一行代码。

这就是层次状态机的核心价值:公共逻辑上浮,独有逻辑下沉,彻底告别重复代码。

四、新手必看:5 个最容易踩的坑

  1. 返回值千万别写错处理了的事件返回Q_HANDLED(),要上交父状态返回Q_SUPER()。写错会出现状态不跳转、事件丢失,是新手第一大坑。

  2. Q_TRAN 是异步的调用跳转宏之后,状态不会立刻切换,要等当前事件处理完才会执行退出、进入动作。别在跳转后面马上写新状态的逻辑。

  3. 状态初始化放 ENTRY 里计数器、标志位的初始化一定要放在Q_ENTRY_SIG里,别乱放,不然状态切回来变量值不对,排查半天找不到原因。

  4. 别在中断里直接分发事件中断里只做标记或者放队列,主循环再调用 dispatch。中断里处理状态机,实时性和稳定性直接崩盘。

  5. 别为了分层而分层简单状态就用平面的,没必要硬加父状态。绝大多数项目 2-3 层足够,层数多了反而增加跳转复杂度。

最后说两句

    其实 QP 入门真的没那么难,不用一上来就啃全量官方文档、研究内核调度。先跑通这个最简单的 LED 例子,理解事件驱动和状态跳转的基本逻辑,再慢慢加层次、加定时器、加子状态机,循序渐进很快就能上手。

    上面的模板和代码直接抄到自己工程里,改改 BSP 驱动就能跑,算是给大家的入门敲门砖。

‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

关注我的微信公众号,回复“星球”加入知识星球,有问必答。


点击“阅读原文”查看知识星球详情,欢迎点分享、收藏、点赞、在看。

相关学习资料