【QF状态机·下篇】6 万字 PDF + 全套源码 + 22 题作业答案
【摘要】拆解一个真实 QP 项目:1 个 AO 装下 13 个状态 + 8 个定时器,2000 行代码搞定双通道传感器采集 + 网络上报。文末有完整 6 万字 PDF + 配套源码包领取。
写完前两篇,我自己也复盘了一下:
中篇发完那个周末,我重新打开公司那个 QP 项目,居然第一次完全看懂了。之前看一头雾水的状态层次、定时器编排、跨 AO 通信,全部说得通。
但我也清楚地意识到一件事:学完 QP 又回去写 if-else 的人,我身边见过太多。
不是 QP 不好,而是状态机思维需要刻意练习,跟你学了一周 Vim 回去还是用方向键一回事。
真正的飞跃发生在亲手解剖一个真实工程的时候:
1 个活动对象 NetMgr 13 个状态 / 3 层嵌套 8 个定时器 / 18 个静态事件 2000 行 C 代码搞定双通道传感器采集 + 网络上报 + 错误恢复
这种规模用 FreeRTOS + switch case 写大概要 6000 行,且没人维护得了。
这是「QF 状态机」下篇,拆解一个真实 QP 项目,提炼单 AO 大 HSM的设计方法论,带你从「看懂」走到「会写」。文末有完整 6 万字 PDF + 配套源码包领取方式,内容比公众号 3 篇详细得多。
01 · 工程定位:这是个什么项目
demo_qfnet_v1.0 是一个双通道传感器数据采集 demo:
启动后做的事:
上电 → 初始化 PHY、加载 Flash 配置、获取 IP↓待机(Standby)↓收到上位机 UDP 命令 → 切到对应通道处理状态(Ch1Process / Ch2Process)↓周期采集传感器 → 打包成 UDP 帧上报↓出错 → 进入错误处理状态尝试自动恢复↓心跳 LED 持续闪烁、看门狗持续喂
🎯简单的业务,但用 QP 写出来的代码我服气。
02 · 一个直击灵魂的问题:为什么只用 1 个 AO?
第一次看 QP 工程的人最常问:
「为什么不拆成 4~5 个 AO?网络一个、采集一个、协议一个,职责清晰啊。」
答案是架构选择。单 AO + 大 HSMvs多 AO的取舍:
🔧这个工程的业务(采集+协议+通信)逻辑紧密耦合,切到 Ch1Process 后通信、采集、定时器全部联动。拆开 AO 反而要做大量同步。所以选了单 AO。
⚠️这是一个真正「看人下菜」的设计决策,不是 QP 教条说「必须 N 个 AO」,而是业务该用几个就用几个。
顺便聊一句:QP vs FreeRTOS 是不是直接竞争?
很多人会问「QP 和 FreeRTOS 到底谁强」,其实它们不在一个赛道上:
FreeRTOS 是主流,MIT 协议完全免费,生态成熟,中文资料海量,国内嵌入式岗位几乎默认要求 QP 是小众但深用,双协议(GPL+商业),专攻「状态多 + 嵌套深 + 并发杂」的复杂场景,主要在医疗、工业、航天
简单说:FreeRTOS 占领「任务调度」市场,QP 占领「复杂状态机+事件驱动」市场。
🎯 选哪个,决定了你这个项目 5 年后是好维护、还是被骂屎山。
03 · HSM 层次速览

QHsm_top└── NetMgr_Initial (一次性初始化伪状态)└── NetMgr_Running (★ 父状态:管理 8 个 QTimeEvt)├── NetMgr_Standby├── NetMgr_Ch1Process (父:通道 1 共享逻辑)│ ├── NetMgr_Ch1Idle│ ├── NetMgr_Ch1Busy│ ├── NetMgr_Ch1SeekZero│ └── NetMgr_Ch1BuildPac├── NetMgr_Ch2Process (父:通道 2 共享逻辑)│ ├── NetMgr_Ch2Idle│ ├── NetMgr_Ch2Busy│ ├── NetMgr_Ch2SeekZero│ └── NetMgr_Ch2BuildPac├── NetMgr_Ch1ErrHnd└── NetMgr_Ch2ErrHnd
13 个状态、3 层嵌套,这种规模用平坦 FSM 写是灾难。光是「错误处理任意时刻都能进入」这一条,就要在 12 个状态各写一次。HSM 用一个父状态 Running 一招摆平。
04 · 怎么读这个工程?三步法
第一次接手 QP 工程别想着「从 main 一行行往下读」,你会迷失在 lwIP 的 1000 行回调里。正确顺序:
第 1 步:看 HSM 层次图→ 理解「状态机长什么样」,不看任何代码第 2 步:看 AO 结构体 + ctor + 静态事件清单→ 理解「AO 有哪些字段、哪些信号、哪些定时器」第 3 步:从 Initial 伪状态开始,沿「启动路径」读→ Initial → Running.ENTRY → 默认子状态 → 等事件
📌这套读法对任何 QP 工程都适用,把它记下来,以后接手老项目少走 80% 的弯路。
05 · NetMgr 数据结构
typedef struct NetMgrTag {QActive super; // 继承 QActive// 8 个时间事件(全部静态嵌入!)QTimeEvt te_LWIP_SLOW_TICK; // 250ms:lwIP 协议栈维护QTimeEvt te_DAQ_CYCLE; // 周期上报数据QTimeEvt te_IDLE_STATE; // 空闲超时QTimeEvt te_HEART_BEAT; // 500ms 心跳QTimeEvt te_POLLING; // 50ms 轮询通道QTimeEvt te_ALARM_STATE; // 1000ms 告警上报QTimeEvt te_CH1_ERR_RESET; // 通道 1 错误恢复QTimeEvt te_CH2_ERR_RESET; // 通道 2 错误恢复// 网络资源struct netif* netif;struct udp_pcb* upcb_comm; // 配置命令收发struct udp_pcb* upcb_tx; // 数据发送struct udp_pcb* upcb_ptp; // PTP 时钟同步/* ... */} NetMgr;static NetMgr l_netMgr; // 单例QActive* const AO_NetMgr = (QActive*)&l_netMgr; // 不透明指针
关键设计:8 个 QTimeEvt 全部嵌入 AO 结构体,不是 Q_NEW 出来的,跟 AO 同生命周期。这是 QP 工程师标配写法。
06 · 静态事件清单:为什么有这么多 Tran2XXX
static QEvt const evt_Reset = { RESET_SIG, 0, 0 };static QEvt const evt_Tran2Standby = { TRAN2STANDBY_SIG, 0, 0 };static QEvt const evt_Tran2Ch1ErrHnd = { TRAN2CH1ERRHND_SIG, 0, 0 };static QEvt const evt_Tran2Ch1Process = { TRAN2CH1PROCESS_SIG, 0, 0 };/* ... 共 18 个静态事件 ... */
疑问:为什么不直接在事件分支里 return Q_TRAN(&Standby)?为什么要绕一圈「投递 TRAN2STANDBY_SIG → 在父状态 case 这个信号 → 才 Q_TRAN」?
原因:状态迁移的触发点不在状态函数里,而是在中断或lwIP 回调函数里。
回顾中篇的金科玉律:中断里只能 POST 事件,不能调任何状态机 API。所以中断里检测到「该切 Standby 了」时,不能直接 Q_TRAN(那不是状态函数上下文),只能:
// 在 UDP 接收回调里(中断/回调上下文)QACTIVE_POST(AO_NetMgr, &evt_Tran2Standby, sender);// → 框架在 RTC 时把这个事件交给状态函数 → case TRAN2STANDBY_SIG: return Q_TRAN(&Standby);
🎯Tran2XXX 事件的本质:「跨上下文传递的迁移请求」。所有这些都做成静态 const,因为它们不带参数,可反复 POST,零分配开销。
07 · Initial 伪状态:一次性的「开机仪式」
Initial 在 QHsm_init 时被框架调用一次,专门做只在上电时执行的初始化:
QState NetMgr_Initial(NetMgr* me, QEvt const* e) {/* === 硬件层初始化 === */Phy_Reset(); // 复位以太网 PHYLoadConfigFromFlash(); // 加载配置Led_Init();Timer0Init(); // 启动 1ms tick 定时器(QF_TICK 的源头)/* === 网络层初始化(lwIP)=== */me->netif = eth_driver_init((QActive*)me, macaddr);me->upcb_comm = udp_new();udp_bind(me->upcb_comm, &localIp, local_port);udp_recv(me->upcb_comm, &udp_rx_handler, me);/* === 进入运行态 === */return Q_TRAN(&NetMgr_Running);}
为什么把这些放 Initial 而不是 ctor?
NetMgr_ctor() | ||
NetMgr_Initial |
📌 ctor 在 QF_run 前跑,那时候中断、调度都没启动,硬件初始化必须等 Initial。
08 · Running 父状态:本工程最复杂的一个状态函数

下面把它按职责拆成 3 段,逐段读:
① ENTRY / EXIT:定时器集中管理
case Q_ENTRY_SIG:QTimeEvt_postEvery(&me->te_LWIP_SLOW_TICK, me, MS_TO_TICK(250));QTimeEvt_postEvery(&me->te_DAQ_CYCLE, me, MS_TO_TICK(1000)); // 数据上报周期QTimeEvt_postEvery(&me->te_HEART_BEAT, me, MS_TO_TICK(500));QTimeEvt_postEvery(&me->te_ALARM_STATE, me, MS_TO_TICK(1000));return Q_HANDLED();case Q_EXIT_SIG:QTimeEvt_disarm(&me->te_LWIP_SLOW_TICK);QTimeEvt_disarm(&me->te_DAQ_CYCLE);QTimeEvt_disarm(&me->te_HEART_BEAT);QTimeEvt_disarm(&me->te_ALARM_STATE);QTimeEvt_disarm(&me->te_POLLING);return Q_HANDLED();
② 跨状态全局命令:Tran2XXX 一字排开
case Q_INIT_SIG: return Q_TRAN(&NetMgr_Standby); // 默认子状态case RESET_SIG: return Q_TRAN(&NetMgr_Running); // ★ 自迁移=软重置case TRAN2STANDBY_SIG: return Q_TRAN(&NetMgr_Standby);case TRAN2CH1PROCESS_SIG: return Q_TRAN(&NetMgr_Ch1Process);case TRAN2CH2PROCESS_SIG: return Q_TRAN(&NetMgr_Ch2Process);case TRAN2CH1ERRHND_SIG: return Q_TRAN(&NetMgr_Ch1ErrHnd);case TRAN2CH2ERRHND_SIG: return Q_TRAN(&NetMgr_Ch2ErrHnd);
③ 周期任务:所有子状态共享
case LWIP_SLOW_TICK_SIG:/* lwIP 协议栈周期维护 */return Q_HANDLED();case HEART_BEAT_TMROUT_SIG:led_toggle(); // 心跳灯ewdt_feed(); // 喂外部硬件看门狗return Q_HANDLED();
末尾必须有一行 return Q_SUPER(&QHsm_top); 兜底,HSM 铁律。
4 个值得学的设计亮点
🎯亮点 1:自迁移 = 软重置
RESET_SIG → Q_TRAN(&Running) 触发 EXIT + ENTRY,所有定时器自动重启。一行代码搞定「重启所有周期任务」,不需要手写一堆 disarm + postEvery。
🎯亮点 2:Tran2XXX 都在父状态处理
无论当前在哪个子状态,收到 TRAN2STANDBY_SIG 时子状态不处理 → 上溯到 Running 处理 → 切到 Standby。这一招让「跨状态全局命令」零重复。
🎯亮点 3:心跳灯 / lwIP 维护放在父状态周期处理
子状态切换时定时器不停(因为父状态没退),所以心跳灯永远在闪、lwIP 协议栈永远在跑。子状态只关心自己的业务逻辑。
🎯亮点 4:定时器 EXIT 集中撤销
disarm 全集中在父状态 EXIT 一处,看代码时一目了然,不需要在每个子状态翻找有没有漏撤的定时器。
09 · 中断怎么跟状态机交互
netmgr.c 里大量使用 lwIP 的 UDP 接收回调,这些回调跑在网卡中断上下文:
static void udp_rx_handler(void* arg, struct udp_pcb* upcb,struct pbuf* p, ...) {NetMgr* me = (NetMgr*)arg;uint8_t cmd = p->payload[0];// ❌ 绝对不能这样写:// QHsm_dispatch(&me->super, &evt); // 中断里直调状态机会破坏 RTC// ✅ 正确:构造事件 POST 给 AO,等 dispatch 在 RTC 时取出处理switch (cmd) {case CMD_GO_STANDBY: QACTIVE_POST(&me->super, &evt_Tran2Standby, me); break;case CMD_START_CH1: QACTIVE_POST(&me->super, &evt_Tran2Ch1Process, me); break;/* ... */}pbuf_free(p);}
一个完整 UDP 命令到状态切换的链路
① 网卡接收以太网帧(DMA)↓② 网卡中断触发,lwIP 读取↓③ lwIP 解析到 UDP,调用 udp_rx_handler(仍在中断上下文)↓④ 回调里解析包头,识别命令↓⑤ QACTIVE_POST(AO_NetMgr, &evt_Tran2XXX, me)↓ ★ 边界点:从中断上下文回到主循环上下文⑥ 中断退出,主循环扫描发现 NetMgr 就绪↓⑦ 取出 evt_Tran2XXX → dispatch → Running 父状态处理 → Q_TRAN(&XXX)↓⑧ 完成状态切换
🎯金科玉律:中断/回调里只 POST 事件,业务逻辑全部在状态函数里执行。这是 QP 工程的纪律线,越是贵的项目越严格。
10 · 设计方法论:一个 HSM 怎么从 0 画出来
学完这 3 篇还不够,下次你要自己设计一个状态机时,可能还是不知道从哪下手。
🔧 这套方法是我自己摸出来的,7 步,顺着走基本不会错:
Step 1:列状态(不分层)
把系统**所有「工作模式」**列出来。判断标准:「这一刻在做的事,跟下一刻有本质不同吗?」
⚠️陷阱:把「瞬时事件」当状态。「按按钮」不是状态,它是事件。「按下按钮后等 1 秒」才是状态。
Step 2:列事件
外部输入:按键、UART 命令、传感器超阈值、网络包 内部信号:定时器到期、错误检测、子任务完成
Step 3:画初版图(平面,先不分层)
工具:纸笔 / Excalidraw / Visio / QM。矩形=状态,箭头=迁移,先把「状态 X 收到事件 Y 转到哪」全部画完。
Step 4:识别共性 → 抽父状态(HSM 关键转折)
Step 5:识别默认子状态(Q_INIT_SIG)
每个复合状态必须指定一个默认子状态。
Step 6:补全 ENTRY/EXIT 动作
每个状态问两个问题:进入要做什么?离开要清理什么?
Step 7:写代码,按层次「挖洞」
1. 写顶层 Initial(一行 Q_TRAN(&Top) 即可)2. 写所有父状态空骨架(只写 ENTRY/EXIT,事件分支留空)3. 编译跑通,确认状态层次正确4. 填子状态业务5. 调试
🔧生产经验:千万不要一开始就追求一次写完。编译 → 跑空骨架 → 看 LED/log 确认状态对了 → 再填业务,少走 80% 弯路。
11 · 10 个最常踩的坑
按出现频率排序:
delay_ms | QTimeEvt_postIn | |
QHsm_dispatch | QACTIVE_POST | |
Q_NEW | ||
QActive_start | ||
QF_poolInit | ||
Q_SUPER(&QHsm_top) | ||
me->xxx | ||
📌建议截图存起来,以后写 QP 代码每次自查一遍,bug 减半。
12 · 三篇总收获
QP 作者 Miro Samek 用了二十多年时间,持续输出关于「事件驱动状态机」的教材、讲座、开源代码,他的代表作《Practical UML Statecharts in C/C++》是嵌入式行业讲状态机最系统的一本书,几乎每个 QP 工程师入门都绕不过它。
他的核心观点贯穿整本书:状态机不是一种代码组织技巧,而是一种思维范式,学会之后,看待「系统怎么响应输入」的角度会被永久改变。
我学完 QP 三个月,深以为然。
跟着 3 篇下来,你应该已经具备:
✅状态机思想:FSM/HSM/LCA/Q_TRAN/Q_SUPER 几个核心概念都讲得清✅QF 框架机制:活动对象、RTC、内存池、发布订阅、时间事件全打通✅真实工程读图能力:看懂任何 QP 项目的 HSM 层次和事件流✅设计方法论:能独立画出新需求的 HSM 草图
但,真正的功夫在写代码。
🎯建议:拿一个你正在做的项目,重构其中一个最复杂的模块为 QP 状态机。第一次会很痛苦,第二次会很爽,第三次你会再也回不去原来的写法。
🎯最后送一句话:「10 年嵌入式经验 ≠ 10 年成长」,很多人是 1 年经验用了 10 次,而不是真的进步了 10 年。学一次 QP,你会被迫重新思考自己写过的所有 if-else 代码,这种「重新思考」才是真正的成长加速器。
13 · 完整 PDF + 源码包领取
3 篇文章是公众号版的精炼版,完整版还有这些内容:
📦完整版包括(共 6 万字 + 9 张机制图 + 完整源码):
✅ 每天独立成章 + 导览(跟着学绝不迷路) ✅ 所有作业 + 参考答案(22 道题,从思维到追踪到设计) ✅ QP/C 4.5.04 完整框架源码(开源 GPL 版本) ✅ 附录 A:QP/C API 速查 ✅ 附录 B:常见错误排查清单 ✅ 附录 C:综合设计大作业(双通道温控器骨架代码) ✅ 进阶内容:QK 抢占式内核 + RTOS 对比 + QM 建模工具
怎么领
关注公众号「胡说有点理」→ 后台回复QF(输入 qf / 状态机 / PDF 也一样触发,不用怕记错)
⏰ 自动回复一个网盘链接,资料无限期有效,不定期补充更新。
互动 & 关注
📌本期评论区话题:你身边有没有还在用 QP 写商业项目的公司?业务领域是什么?评论区聊聊,我会汇总一下做下一篇调研稿。
如果觉得这 3 篇有用,点【在看】+ 转发给一个还在写 if-else 的同事。这是对我最大的支持。
关于作者
我是 Winston,嵌入式开发 8 年。最近在重构公司一个 12 年前的 QP/C 老项目,踩了一堆坑,把过程整理成 3 篇笔记 + 6 万字 PDF 分享出来。
关注公众号「胡说有点理」,聊嵌入式架构、状态机、RTOS 选型,有问题后台留言。
声明:本教程基于 QP/C 4.5.04 开源版本(GPL v3)讲解,用于学习交流。商业项目使用 QP/C 需联系 Quantum Leaps Inc. 获取商业 license。涉及的应用代码已脱敏,不代表任何特定公司项目。
夜雨聆风