ARTICLE · 1059891
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);}
几个核心概念讲透:
- Q_ENTRY_SIG / Q_EXIT_SIG
:进入 / 退出状态的固定事件,初始化、清理逻辑必须放这,别乱放; - Q_TRAN()
:状态跳转宏,事件处理完后才会真正跳转,是异步的; - Q_HANDLED()
:表示这个事件我处理完了; - 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 的层次状态机,只需要加一个父状态。
改造步骤:
新增事件 EVT_POWER_OFF;加一个父状态 LedFsm_On,统一处理关机事件;慢闪、快闪的父状态改成 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 个最容易踩的坑
返回值千万别写错处理了的事件返回
Q_HANDLED(),要上交父状态返回Q_SUPER()。写错会出现状态不跳转、事件丢失,是新手第一大坑。Q_TRAN 是异步的调用跳转宏之后,状态不会立刻切换,要等当前事件处理完才会执行退出、进入动作。别在跳转后面马上写新状态的逻辑。
状态初始化放 ENTRY 里计数器、标志位的初始化一定要放在
Q_ENTRY_SIG里,别乱放,不然状态切回来变量值不对,排查半天找不到原因。别在中断里直接分发事件中断里只做标记或者放队列,主循环再调用 dispatch。中断里处理状态机,实时性和稳定性直接崩盘。
别为了分层而分层简单状态就用平面的,没必要硬加父状态。绝大多数项目 2-3 层足够,层数多了反而增加跳转复杂度。
最后说两句
其实 QP 入门真的没那么难,不用一上来就啃全量官方文档、研究内核调度。先跑通这个最简单的 LED 例子,理解事件驱动和状态跳转的基本逻辑,再慢慢加层次、加定时器、加子状态机,循序渐进很快就能上手。
上面的模板和代码直接抄到自己工程里,改改 BSP 驱动就能跑,算是给大家的入门敲门砖。
‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧
关注我的微信公众号,回复“星球”加入知识星球,有问必答。
点击“阅读原文”查看知识星球详情,欢迎点分享、收藏、点赞、在看。