工业控制固件与 HMI 的工程架构复盘:从 STM32 到 Flutter 的全链路实践
> 一篇完整的项目复盘 — 架构演进、状态机设计、分层解耦、串口协议、双端协同
---
## 目录
1. [项目背景](#1-项目背景)
2. [系统架构概览](#2-系统架构概览)
3. [固件三层架构详解](#3-固件三层架构详解)
4. [状态机循环流程设计](#4-状态机循环流程设计)
5. [RTOS 任务模型与调度](#5-rtos-任务模型与调度)
6. [执行器统一映射模式](#6-执行器统一映射模式)
7. [通信协议体系](#7-通信协议体系)
8. [HMI 上位机架构](#8-hmi-上位机架构)
9. [工程难点与解决方案](#9-工程难点与解决方案)
10. [可复用经验与模板](#10-可复用经验与模板)
11. [后续优化方向](#11-后续优化方向)
12. [总结](#12-总结)
13. [顶层状态机路由](#13-顶层状态机路由)
14. [7 任务协作模型](#14-7-任务协作模型)
---
## 1. 项目背景
这是一个**工业自动化控制设备**的完整嵌入式系统,包含下位机固件和上位机 HMI。
### 下位机(执行节点)
基于 ARM Cortex-M3 MCU(72MHz/256KB Flash/48KB RAM),运行 FreeRTOS,控制 4 轴步进电机、2 路直流电机、多路继电器和传感器,通过 RS485 与上位主控通信。
**角色**:执行节点 — 不承担订单编排或业务协议,只负责接收命令、执行动作、返回状态。
### 上位机 HMI
跨平台桌面应用(Flutter),通过两路物理串口同时连接下位机和主控系统,承担调试、参数调节、日志监控的职能。
---
## 2. 系统架构概览
整个系统分为两个独立的软件项目,通过三路 UART 互联:
┌────────────────────────────────────────────────────────────────┐
│ 上位主控系统(外部) │
│ USART3 / RS485 / 20B 协议 │
└────────────────────────┬───────────────────────────────────────┘
│
┌────────────────────────▼───────────────────────────────────────┐
│ HMI 上位机(Flutter 跨平台应用) │
│ ┌──── 端口 A (USART3) ──── 20B 协议帧 ──── CRC16-Modbus ──┐ │
│ └──── 端口 B (USART1) ──── DGUS 日志/调参 ─── 5A A5 ─────┘ │
└────────────────────────┬───────────────────────────────────────┘
│
┌────────────────────────▼───────────────────────────────────────┐
│ 固件(STM32 + FreeRTOS) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ APP 层 ─ 协议分发 + 状态机 + 监控 │ │
│ │ Service 层 ─ 执行器编排 + 设备适配 + 运行时配置 │ │
│ │ BSP 层 ─ 步进/UART/GPIO/ADC/PWM 硬件驱动 │ │
│ └────────────────────────────────────────────────────────┘ │
│ USART1 ─ DGUS 串口屏与日志 │
│ USART2 ─ 打印机接口 │
│ USART3 ─ 主通信 (RS485/9600) │
│ TIM4 ─ 4 轴步进脉冲输出 │
│ TIM1/TIM8 ─ 直流电机 PWM │
└────────────────────────────────────────────────────────────────┘
### 硬件资源分配
| 外设 | 功能 | 速率 |
|---|---|---|
| USART3 / RS485 | 主通信协议 | 9600 8N1 |
| USART1 / RS232 | 串口屏 + 日志输出 | 9600 8N1 |
| USART2 / RS232 | 打印机接口 | 115200 8N1 |
| TIM4 | 四轴步进脉冲 | 1MHz 基准 |
| TIM1 / TIM8 | 直流电机 PWM | IR2104 驱动 |
| ADC1_IN4 | 功率采样 | PA4 |
| 软件 I2C | EEPROM (AT24C02) | PA11/PA12 |
---
## 3. 固件三层架构详解
整个固件的核心设计是 APP / Service / BSP 三层分层,经过 V2→V5 四轮重构逐步落地。
┌──────────────────────────────────────────────────────────────┐
│ APP 层(3个 .c 文件) │
│ 职责:业务流程决策,不做任何硬件操作 │
│ │
│ app_packer_proto.c 20B 协议帧分发、校验、回复 │
│ app_packer_sm.c 状态机初始化、周期调度、API 封装 │
│ app_monitor.c 栈水位诊断、通信看门狗、报警上报 │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ Service 层(25 个 .c 文件) │
│ 职责:设备能力抽象、动作编排、运行时支持 │
│ │
│ 回调注册表核心 │
│ packer_state_handler.c 状态机调度引擎 + 处理器注册表 │
│ │
│ 子流程状态机 │
│ sm_bag_flow.c / sm_seal_flow.c / sm_self_check.c │
│ sm_press_util.c / sm_printer_service.c │
│ │
│ 执行器与设备适配 │
│ packer_actuator.c / packer_adc.c / packer_heater.c │
│ packer_input.c / packer_dbus.c / packer_printer.c │
│ │
│ 运行时支持 │
│ packer_runtime_config.c / packer_fault_latch.c │
│ packer_status_snapshot.c / packer_command_gateway.c │
│ packer_runtime_flags.c / packer_power_limit.c │
│ packer_serial_frame.c / packer_screen_engine.c │
│ │
│ 阻塞 I/O 异步化 │
│ comm_task.c │
└──────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────┐
│ BSP 层(10 个 .c 文件) │
│ 职责:纯硬件驱动,不承载任何业务语义 │
│ │
│ bsp_stepper.c / bsp_dc_motor.c / bsp_actuator.c │
│ bsp_uart.c / bsp_adc.c / bsp_gpio.c │
│ bsp_log.c / bsp_eeprom.c / bsp_at24cxx.c / bsp_watchdog.c │
└──────────────────────────────────────────────────────────────┘
### 三层核心约束(物理级,非道德级)
用构建系统做隔离。CMakeLists.txt 为每层设置独立的 include 路径:
APP 层编译时,BSP 的 include 路径不可见
target_link_libraries(app INTERFACE service bsp_headers)
target_link_libraries(service INTERFACE bsp_headers)
# BSP 不链接任何上层
效果:
- APP 层 无法 #include "bsp_uart.h" — 编译直接报错
- Service 层 无法 #include "app_packer_sm.h" — 编译直接报错
- BSP 层 无法 include 任何上层头文件
### 为什么是三层不是两层
很多项目只有两层:HAL + 业务。对于简单项目够了。但在这个项目中:
- 4 个步进轴有不同的速度/加减速/停止模式
- 2 个直流电机有方向/PWM/功率限制
- 打印机有握手协议/超时/重试
- 多传感器输入需要消抖/同步/状态管理
如果 APP 直接调 BSP,APP 层会被设备适配细节膨胀到不可控。
如果 Service 不存在,那这些适配逻辑要么在 APP 层变成大杂烩,要么在 BSP 层被业务语义污染。
Service 层存在的理由是:把"硬件能力"翻译成"设备能力"。
---
## 4. 状态机循环流程设计
### 完整流程
POWER_ON (上电稳定)
│ 等待电源稳定 + 100ms
▼
SELF_CHECK (自检)
│ 打印机探测 + 输入重采样 + 轴位确认
▼
IDLE (空闲待命) ←───────────────────┐
│ │
├── TRIGGER_BAG (0x42) ────────┤
│ BAG_CALIB_CHECK │ 出袋流程
│ BAG_CALIB_CLEAR │
│ BAG_CALIB_SEEK │
│ BAG_CALIB_BACKOFF │
│ BAG_OUT │
│ PREHEAT │
└───────────────────────────────┘
│
├── TRIGGER_SEAL (0x43) ──────┐
│ SEAL_FORWARD │ 封口流程
│ CONVEYOR_RUN │ 投料传送带
│ SEAL_BACKWARD │ 热封后退
│ PRESS_RETRACT │ 轴2下压
│ PRESS_CLOSE │ 轴3/4闭合
│ SEAL_HOLD │ 封口保持
│ TEAR_OFF │ 轴1撕断
│ PRESS_OPEN │ 轴2回位+轴3/4打开
│ RESET │
└───────────────────────────────┘
│
├── TRIGGER_DELIVER (0x44) ───┐
│ CONVEYOR_RUN │ 投料流程
│ RESET │
└───────────────────────────────┘
│
▼
ERROR (故障态)
│ 立即停轴 + 释放使能 + 切断电源
│ 故障清除后 → POWER_ON → SELF_CHECK → IDLE
### 流程设计原则
1. 每个流程有明确的 active_flow 标记
状态机追踪当前在执行哪个动作流(BAG / SEAL / DELIVER / NONE)。CONTROL stop 只允许中断 active_flow 标记的动作,不影响自检、ERROR 恢复等非动作流链路。
2. 错误态走完整恢复路径
ERROR → POWER_ON(100ms) → SELF_CHECK → IDLE
不能直接从 ERROR 跳 IDLE。100ms 上电稳定确保所有执行器已进入安全状态,自检重新确认机械位置。
3. CONTROL stop 插队语义
CONTROL stop → 清空队列中所有待执行动作 → 插队到队首 → 执行
不是简单的 FIFO。停止命令有最高优先级。
### 子阶段设计
对于带有"多段固定脉冲"的单一状态(如 PRESS_CLOSE 包含"轴3/4同时闭合"和"轴2下压"两个子动作),拆分为显式子阶段:
typedef enum {
APP_PRESS_CLOSE_STAGE_IDLE = 0,
APP_PRESS_CLOSE_STAGE_AXIS2_RETRACT,
APP_PRESS_CLOSE_STAGE_AXIS34_CLOSE,
APP_PRESS_CLOSE_STAGE_DONE
} app_press_close_stage_t;
状态机只推进阶段编号,不直接调执行器启动下一段动作。执行器通过统一入口收口。
---
## 5. RTOS 任务模型与调度
### 7 个 FreeRTOS 任务
优先级(从高到低)
ProtoTask AboveNormal USART ISR 通知 + 50ms 兜底
│ 消费 UART DMA 字节流 → 解码20B帧 → 校验 CRC → 投递命令队列
│
StateMachineTask Normal7 10ms 固定周期
│ 推进状态机 → 子流程 on_run 回调
│
MotorTask AboveNormal1 5ms 固定周期 + SM 通知
│ packer_actuator_task_once() → 动作表映射
│
AdcTask Normal 100ms 固定周期
│ ADC 采样 + 功率记账
│
CommTask Normal 50ms 固定周期 + 命令触发
│ 异步执行阻塞 I/O(打印机对话、屏幕更新)
│
MonitorTask Normal1 500ms 固定周期
│ 栈水位诊断 + 通信看门狗 + 报警推送
│
HeaterTask Normal 100ms 固定周期
│ 加热占空比输出 → 继电器翻转
### 任务间协作模式
1. 任务通知链
StateMachineTask 状态变化后,通过 xTaskNotifyGive(MotorTaskHandle) 通知 MotorTask。MotorTask 收到通知立即重新查动作表,否则按 5ms 固定周期轮训。减少了 80% 以上的无效查表。
2. 句柄注册(禁止 extern)
// ❌ 反模式
extern osThreadId_t MotorTaskHandle;
// ✅ 显式注入
void app_sm_register_motor_task(void *handle);
3. CommTask 异步 I/O
打印机阻塞操作通过三函数接口异步化:
comm_task_enqueue(COMM_CMD_PRINTER_SELF_CHECK); // 投递,不阻塞
comm_task_get_result(COMM_CMD_PRINTER_SELF_CHECK); // BUSY / OK / FAIL
comm_task_register_handler(type, handler_fn); // 注册执行器
CommTask 优先级低于 StateMachineTask,确保阻塞 I/O 不会抢占流程推进。
### 临界区规则
- BSP 层:
__disable_irq()/__set_PRIMASK()(保存恢复) - APP 层:
taskENTER_CRITICAL()/taskEXIT_CRITICAL() volatile不能替代原子性和临界区保护
---
## 6. 执行器统一映射模式
### 从过程驱动到数据驱动
传统写法:
void execute_actions(state_t s) {
switch (s) {
case STATE_A: start_motor(M1, 1000); break;
case STATE_B: start_motor(M1, 0); break;
}
}
统一动作表写法:
typedef struct {
packer_state_t state;
pact_stepper_mode_t stepper_mode; // 单轴/双轴/运行时换算
bsp_dc_motor_id_t dc_motor;
bsp_dc_motor_dir_t dc_motor_dir; // STOP 表示不启动
uint8_t hopper_conveyor : 1;
uint8_t power_on : 1;
uint8_t heater_toggle : 1;
} state_action_t;
static const state_action_t s_actions[] = {
{STATE_BAG_OUT, STEP_BAG_RUNTIME, DC_NONE, STOP, 0, 1, 0},
{STATE_SEAL_FORWARD, STEP_SINGLE, DC_NONE, STOP, 0, 1, 0},
{STATE_CONVEYOR_RUN, STEP_NONE, DC_1, FORWARD, 1, 1, 0},
{STATE_PRESS_CLOSE, STEP_DUAL, DC_NONE, STOP, 0, 1, 0},
{STATE_SEAL_HOLD, STEP_NONE, DC_NONE, STOP, 0, 1, 1},
// ...
};
MotorTask 周期遍历整个表:
void packer_actuator_task_once(packer_state_t state) {
const state_action_t *action = find_action(state);
if (!action) return;
// 步进:单轴 / 双轴 / 运行时换算
handle_stepper(action->stepper_mode, state);
// 直流电机 + 功率限制统一入口
if (action->dc_motor != DC_MOTOR_ID_COUNT) {
packer_power_limit_request(action->dc_motor, action->dc_motor_dir);
}
// 继电器 + 加热
bsp_actuator_write(RELAY_HOPPER, action->hopper_conveyor);
if (action->heater_toggle) packer_heater_toggle();
}
### 优势
- 新增状态 = 加一行表,不改流程代码
- 所有执行器状态一目了然,可审计
- 功率限制、加热逻辑等横向关注点只需要在循环中加一行
### 步进电机控制模型
轴1(出袋/撕断) 轴2(压杆) 轴3/4(封口)
─────────────── ──────────── ──────────────
方向 正/反转 正/反转 正/反转
脉冲 有限/连续 有限/连续 有限/连续
停止模式 目标/溢出/连续 目标/溢出/连续 目标/溢出+IN1
加减速 抛物线起步 抛物线起步 抛物线起步
线性收尾 线性收尾 线性收尾
二次抛物线起步频率规划:
斜坡脉冲数 = (target_hz² - start_hz²) / (2 × accel_hz_per_s)
三角波条件: target_pulses ≤ 加速斜坡 + 减速斜坡
梯形条件: target_pulses > 加速斜坡 + 减速斜坡
使用 uint64_t 整数平方根(逐位逼近法)计算,避免浮点库引入。
---
## 7. 通信协议体系
### 物理层
| 端口 | 电平 | 速率 | 用途 |
|---|---|---|---|
| USART3 | RS485 | 9600 8N1 | 主通信(20B 固定帧) |
| USART1 | RS232 | 9600 8N1 | DGUS 串口屏 + 日志 |
| USART2 | RS232 | 115200 8N1 | 打印机 |
### USART3 20B 固定帧协议
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│HDR │ADDR│FUNC│Z1 │ DATA[0..13] (14 bytes payload) │CRC_L│CRC_H│ │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
1B 1B 1B 1B 14B data field 2B CRC (Modbus, low first)
功能码:
| 码值 | 命令 | 说明 |
|---|---|---|
| 0x40 | CONTROL | start / stop |
| 0x42 | TRIGGER_BAG | 触发出袋 |
| 0x43 | TRIGGER_SEAL | 触发封口 |
| 0x44 | TRIGGER_DELIVER | 触发投料 |
| 0x45 | JOG | 点动(步进/直流) |
| 0x46 | ALARM_QUERY | 查询报警 |
| 0x47 | PRINTER_FORWARD | 打印机透传 |
| 0x48 | HEARTBEAT | 心跳 |
| 0x49 | RESET_FAULT | 故障复位 |
回复时机约定(Z1 字段控制):
| Z1 | 语义 | 回复时机 |
|---|---|---|
| 0x00 | 停机/查询 | 立即回复 |
| 0x01 | 启动运行 | 立即回复 |
| 0x02 | 完成查询 | 立即回复 |
### DGUS (USART1) 协议
帧头: 5A A5
帧格式: 5A A5 + Length + Command + Data[n]
命令: 0x81 读变量 / 0x82 写变量 / 0x83 上报
用途: 参数读写 + 系统日志帧推送
### 打印机协议
基于最小 5A A5 框架,支持 0x81/0x82/0x83 命令,USART2 115200 8N1。
### 双端口隔离原则
USART3 和 USART1 是两个物理端口,两套协议,完全隔离:
- USART3 只解析 20B 协议,不认识 DGUS 帧
- USART1 只解析 DGUS 5A A5,不认识 20B 协议
- 不自动回退到另一个端口
- 每个端口独立配置波特率、CRC 算法、超时策略
---
## 8. HMI 上位机架构
### 技术选型
Flutter 3.41.7 / Dart 3.11.5,跨平台目标:Windows/macOS/Linux/Android/iOS/Web。桌面端(Windows/Linux/macOS)为优先调试平台。
### 模块结构
HMI/lib/
├── core/
│ ├── protocol/
│ │ ├── crc16_modbus.dart CRC16-Modbus 算法
│ │ ├── crc_algorithm.dart CRC 抽象接口
│ │ └── hmi_frame.dart 帧编解码
│ ├── serial/
│ │ ├── serial_transport.dart 串口抽象接口
│ │ └── serial_transport_impl.dart 平台实现
├── features/
│ └── hmi/
│ ├── hmi_controller.dart 主控制器(帧消费 + 命令执行)
│ ├── hmi_dashboard_page.dart 仪表盘页面
│ ├── hmi_param_config.dart 参数配置
│ ├── hmi_port_config.dart 串口配置
│ └── hmi_protocol.dart 协议层
└── util/
├── log_exporter.dart 日志导出抽象
├── log_exporter_io.dart IO 实现
└── log_exporter_web.dart Web 实现
### 双串口设计
端口 A (USART3)
主控协议 / 20B 固定帧 / CRC16-Modbus / 9600 8N1
功能码: 0x01~0x10 (上位机→主控), 0x40~0x53 (上位机→打包机)
仅处理 20B 主协议,不解析 DGUS
端口 B (USART1)
日志监控 + DGUS 调参 / 9600 8N1 / 帧头 5A A5
被动: 接收日志输出/调试信息
主动: 发送 DGUS 变参调节帧
仅处理 DGUS,不解析 20B 主协议
### 日志系统
日志包含完整时间标签 (yyyy-MM-dd HH:mm:ss.SSS),每条日志分多行展示(时间/方向行 + 内容行分离),便于检索 RX/TX。记录 HMI 控制器的每次命令执行(命令名、参数、结果、尝试次数、耗时)。
### HMI 控制器核心接口
// 帧消费
Future<CommandExecutionResult> _consumeFrames(
HmiFrame request, String label,
{Duration? totalTimeout, int maxAttempts});
// DGUS 参数读写
Uint8List _dgusEncode(int command, Uint8List data);
Future<_DgusFrame?> _dgusTransaction(_DgusFrame request);
// 端口扫描与连接
Future<PortScanResult> scanPorts();
Future<ConnectionResult> connect(PortConfig config);
---
## 9. 工程难点与解决方案
### 9.1 三层分层落地 — 建筑隔离 vs 文档规范
问题:团队知道要分层,但实际代码中 APP 层到处都是 HAL_GPIO_WritePin。
根因:文档规范缺乏强制力。CMakeLists 允许所有文件互相 include。
方案:用构建系统做物理隔离。三层各自定义 target_include_directories,APP 编译时 BSP 的 include 路径不可见。
结果:违反分层变成编译错误,不再是 code review 问题。
### 9.2 状态机与阻塞 I/O 冲突
问题:StateMachineTask 在 10ms 周期内调用打印机 UART 收发,卡住几百毫秒。
根因:状态机任务混合了"流程决策"和"阻塞 I/O"两种不相容的职责。
方案:引入 CommTask(专用 I/O 任务),三函数接口(enqueue / get_result / register_handler)解耦。CommTask 优先级低于 StateMachineTask。
### 9.3 步进电机脉冲溢出 — 超时保护不可靠
问题:给开环步进轴加 10 秒超时保护,ISR 正常时超时没有意义(done 先到),ISR 异常时超时也不准。
根因:超时信号和脉冲由同一个 ISR 产生,两者不独立。
方案:脉冲溢出保护 目标脉冲 + 10000 → 硬停止 + 报警。有传感器反馈的轴才保留超时作为辅助。
### 9.4 残留 done 标志误判
问题:步进电机完成有限脉冲后 done 保持置位。同一状态被复用于不同轴配置时,旧标志误导后续判断。
方案:motion_seen 模式——每个有限脉冲动作配一个"是否观察到运动开始"的标志。确认运动开始后才允许 done 收尾。
static uint8_t s_motion_seen = 0;
void on_enter(void) {
packer_actuator_start(...);
s_motion_seen = 0;
}
void on_run(void) {
if (!s_motion_seen && is_running(axis)) s_motion_seen = 1;
if (s_motion_seen && is_done(axis)) transition_to(NEXT);
}
### 9.5 编译期宏到运行时参数的迁移
问题:项目初期所有参数是 #define BAG_SPEED_HZ 10504,每次改参数要重新编译烧录。
方案:增量迁移——先在 runtime_config 里加字段(默认值等于旧宏),代码同时支持两套;grep 确认旧宏零引用后删除;同步更新 Doxygen 组。
分类策略:
- 运行时调参(速度/加减速/延时)→ runtime_config + EEPROM
- 固件期定死(硬件引脚/协议地址/任务栈)→ 保留宏
- 换算常量 → 局部 .c 静态常量
### 9.6 开环超时误区(纯开环轴)
问题:为无感测器步进轴尝试进行超时保护
根因:开环轴 ISR 正常则 done 先于超时,ISR 异常则超时也异常
方案:唯一硬保护采用脉冲溢出 目标脉冲 + APP_CFG_STEPPER_PULSE_OVERFLOW_MARGIN(10000),独立 ISR 计步
### 9.7 协议回复时机(Z1 语义优化)
问题:之前所有命令都在"完成后回复",导致上位机长时间等待后超时
方案:Z1 字段控制语义——0x01 启动立即回复(不等待动作完成),0x02 完成查询确认。启动和查询分离,上位机不用阻塞
### 9.8 HMI 双端口帧冲突
问题:两个协议(20B + DGUS)混用一个串口,字节流粘包时误判
根因:5A A5 恰好可能是 20B 帧的 data 段内容
方案:两个物理端口完全隔离,每个端口只解析一种协议,不自动回退
### 9.9 任务栈水位诊断
问题:FreeRTOS 任务栈溢出难以复现,随机崩溃
方案:MonitorTask 每 500ms 采集所有任务的最低栈水位,计算最小值,日志上报。提前预警而非事后崩溃
### 9.10 中断错误快照
问题:UART/DMA 硬件错误难以现场定位,重启后丢失
方案:在 .noinit 段保留错误快照结构体,下次启动时日志上报上次崩溃原因(UART 错误码、DMA 错误码、实例索引)
---
## 10. 可复用经验与模板
### 10.1 最佳实践清单
状态机设计
- 使用回调注册表替代 switch-case
- 每个状态独立文件,on_enter / on_run / on_exit 三回调
- 显式子阶段替代隐式脉冲斩杀
- 错误态走完整恢复路径(ERROR→POWER_ON→SELF_CHECK→IDLE)
执行器设计
- 统一动作表(数据驱动)替代散落 switch
- 开环轴唯一保护:脉冲溢出
- 有传感器轴:传感器 + 脉冲溢出双层保护
- motion_seen 模式防残留标志误判
RTOS 设计
- 阻塞 I/O 专用任务(CommTask 模式)
- 任务通知链减少轮询
- 优先级:状态机 > 执行器 > I/O > 监控
- 句柄注入注册替代 extern
分层设计
- 构建隔离(物理级)而非文档规范(道德级)
- BSP:纯硬件,无业务语义
- Service:设备能力,不直接调 BSP
- APP:业务流程,不直接操作硬件
协议设计
- 双端口隔离(不要混用协议)
- 启动回复与完成查询分离
- CRC 校验 + 帧头保护
### 10.2 推荐模板
CMake 三层模板
BSP 层
add_library(bsp STATIC)
target_include_directories(bsp PUBLIC BSP/Inc)
target_sources(bsp PRIVATE BSP/Src/bsp_stepper.c BSP/Src/bsp_uart.c ...)
# Service 层
add_library(service STATIC)
target_include_directories(service PUBLIC Service/Inc)
target_link_libraries(service PUBLIC bsp)
target_sources(service PRIVATE Service/Src/packer_actuator.c ...)
# APP 层 - 不直接看到 BSP 路径
add_executable(app)
target_include_directories(app PRIVATE APP/Inc)
target_link_libraries(app service)
target_sources(app PRIVATE APP/Src/app_packer_sm.c ...)
回调注册表状态机模板
static const packer_state_handler_t s_handler = {
.state = PACKER_STATE_MY_STATE,
.on_enter = my_state_on_enter,
.on_run = my_state_on_run,
.on_exit = my_state_on_exit,
.name = "MY_STATE"
};
void my_module_init(void) {
psh_register(&s_handler);
}
CommTask 异步 I/O 模板
// 头文件
void comm_task_init(void);
void comm_task_run(void);
void comm_task_register_handler(cmd_type_t, handler_t);
uint8_t comm_task_enqueue(cmd_type_t);
result_t comm_task_get_result(cmd_type_t);
// 使用(状态机侧)
void state_on_run(void) {
comm_task_enqueue(CMD_MY_OPERATION);
// 下一周期:
result_t r = comm_task_get_result(CMD_MY_OPERATION);
if (r == BUSY) return;
// r == OK / FAIL
}
串口帧 + CRC 模板
#define FRAME_SIZE 20
#define DATA_SIZE 14
typedef struct {
uint8_t header;
uint8_t addr;
uint8_t func;
uint8_t z1;
uint8_t data[DATA_SIZE];
uint16_t crc;
} __attribute__((packed)) serial_frame_t;
uint8_t frame_decode(const uint8_t *raw, serial_frame_t *out);
uint16_t crc16_modbus(const uint8_t *data, uint32_t len);
### 10.3 项目文档体系(.agents/ 范式)
.agents/
├── index.md # 模块导航
├── SKILL.md # 架构约束 + 历史故障 TOP 10 + 修改清单
├── common/ # 项目基线、协议口径、构建规则
├── app/ # APP 层模块 agent 文档
├── service/ # Service 层模块 agent 文档
└── bsp/ # BSP 层模块 agent 文档
doc/
├── 文档索引.md # 阅读路线
├── 打包机上位主控系统说明书.md # 对外协议
├── DGUS串口屏协议.md
├── 步进电机二次抛物线S曲线设计与调参.md
├── 构建与协作环境.md
├── 硬件使用情况和配置.md
├── 硬件原理图与网表索引.md
└── CubeMX_ioc外设规划表.md
---
## 11. 后续优化方向
### 短期(可快速落地)
- 功率限制正式启用 —
packer_power_limit框架已预埋,当前阈值设为超大值仅做记账。现场参数核对后可开启实际限流。 - 二次抛物线起步验证 — 步进抛物线起步已代码落地,但 FW 路径未验证。需要在实物上测试三角波/梯形波边界情况。
- HMI 打印机功能扩展 — CommTask 模式已支持打印机透传,可以通过 HMI 增加打印机测试面板。
### 中期(需架构调整)
- 日志系统统一化 — 当前 BSP log 和 DGUS log 两套并存。可以统一为可配置的日志后端(UART / DGUS / RingBuffer)。
- HMI 参数同步 — 运行时参数通过 DGUS 写入后,可以考虑自动同步到 EEPROM,减少手动保存操作。
- 更细粒度的功率限制 — 当前为 DC+NMOS 合并采样。可以拆分为独立采样通道。
- CI/CD 集成 — 可以接入 GitHub Actions 做固件自动构建 + 单元测试(在 PC 模拟层/硬件在环)。
### 长期(思考方向)
- 多模块总控 — 当前固件是执行节点,不承担整机编排。需要多个执行节点时,上位主控的调度策略需要重构。
- 通信协议版本协商 — 20B 固定帧当前硬编码版本号。可以做协议版本协商实现向后兼容。
- 单元测试框架 — 将 BSP 层抽离 HAL 依赖后,可以在 PC 上跑 Service 层单元测试。
- 配置管理平台 — EEPROM 参数可通过上位机导出/导入,多个执行节点统一配置分发。
---
## 12. 总结
这个项目从 V2 到 V5 的四轮重构,本质上完成了三件事:
第一,把"怎么执行"从"决定做什么"里分离。 APP 层的文件从 10+ 个缩减到 3 个,每个不到 200 行。不是因为功能变少了——是因为细节下沉到了正确的位置。
第二,把"阻塞"从"周期"里分离。 CommTask 的出现让状态机保持了 10ms 的确定性脉搏,不再被打印机 UART 劫持。
第三,用编译器替代了 code review 来做架构合规。 物理层的 include 隔离比一万字文档都有效。允许编译通过就一定会被人利用——所以不让它编译通过。
311 次提交,5 个本地分支,12 个远程分支。278 个源文件,约 50K LOC C + 3K LOC Dart。这不是一个"写好架构图再写代码"的项目——它是一个"每改一次 bug 架构就干净一分"的项目。
后记:这套架构的价值不在代码本身,而在分层思想 + 状态机模式 + 接口规范 + 异常处理 + 可复用模板。下次做类似工业嵌入式项目,几乎可以直接套用。
---
## 13. 顶层状态机路由
### 19 状态主循环
整个系统围绕一个 19-state 主状态机运转,每个状态由独立的子状态机(Sub-SM)或直接回调处理。状态序列如下:
POWER_ON → SELF_CHECK → IDLE → BAG_CALIB_CHECK → BAG_INIT → BAG_EXTRUDE → BAG_PULL → BAG_CUT → BAG_UNLOAD → SEAL_INIT → SEAL_PRESS → SEAL_HOLD → SEAL_COOL → SEAL_UNLOAD → DELIVER_INIT → DELIVER_RUN → DELIVER_DONE → WAIT_NEXT → ERROR
每个状态的角色与对应的子状态机处理器:
| 状态 | 角色 | 处理器 / Sub-SM |
|---|---|---|
| POWER_ON | 上电初始化,时钟、GPIO、外设复位 | sm_self_check (初始化子集) |
| SELF_CHECK | 自检:步进归零、传感器校验、通信环路 | sm_self_check |
| IDLE | 空闲等待指令,心跳维持,参数查询响应 | app_packer_sm (轮询) |
| BAG_CALIB_CHECK | 装袋校准:袋子位置 / 长度验证 | sm_bag_flow |
| BAG_INIT | 取袋准备:压杆抬起、封口就位 | sm_bag_flow |
| BAG_EXTRUDE | 挤出出袋:步进电机旋转挤出 | sm_bag_flow |
| BAG_PULL | 拉袋牵引:牵引辊动作至目标位置 | sm_bag_flow |
| BAG_CUT | 切袋:切断机构动作 | sm_bag_flow |
| BAG_UNLOAD | 卸袋过渡:等待封口工位就绪 | sm_bag_flow |
| SEAL_INIT | 封口预热:加热管使能、温度稳定 | sm_seal_flow |
| SEAL_PRESS | 封口压制:加热压合动作 | sm_seal_flow |
| SEAL_HOLD | 保压固化:保持压力直到密封固化 | sm_seal_flow |
| SEAL_COOL | 冷却脱模:自然冷却 / 主动散热 | sm_seal_flow |
| SEAL_UNLOAD | 封口卸料:松开夹具,就绪出料 | sm_seal_flow |
| DELIVER_INIT | 投料准备:输送带就位 | sm_deliver_flow |
| DELIVER_RUN | 投料运行:输送电机动作 | sm_deliver_flow |
| DELIVER_DONE | 投料完成:确认成品输出 | sm_deliver_flow |
| WAIT_NEXT | 周期等待:延时 / 等待触发后再入 IDLE | app_packer_sm (定时器) |
| ERROR | 故障处理:记录快照、报警、等待复位 | packer_fault_latch |
### 流路由(Flow Routing)
主状态机根据指令类型将流程路由到三个独立流:
┌─────────────────────────┐
│ app_packer_sm │
│ (顶层路由调度) │
└────────┬────────────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ BAG FLOW │ │ SEAL FLOW │ │ DELIVER FLOW │
│ │ │ │ │ │
│ BAG_INIT │ │ SEAL_INIT │ │ DELIVER_INIT │
│ BAG_EXTRUDE │ │ SEAL_PRESS │ │ DELIVER_RUN │
│ BAG_PULL │ │ SEAL_HOLD │ │ DELIVER_DONE │
│ BAG_CUT │ │ SEAL_COOL │ │ │
│ BAG_UNLOAD │ │ SEAL_UNLOAD │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
│
▼
┌────────────────┐
│ WAIT_NEXT │
│ → IDLE │
└────────────────┘
路由规则:
- 收到 TRIGGER_BAG (0x42) → 路由到 BAG_FLOW,完成后自动进入 SEAL_FLOW
- 收到 TRIGGER_SEAL (0x43) → 直接路由到 SEAL_FLOW
- 收到 TRIGGER_DELIVER (0x44) → 路由到 DELIVER_FLOW
- 任意流遇到故障 → 共路到 ERROR 状态
- ERROR → 复位后走 POWER_ON → SELF_CHECK → IDLE 完整恢复路径
### 24 阶段码映射
每个状态映射为一个 1 字节的阶段码(Phase Code),用于上位机状态指示和日志:
| 阶段码 | 状态 | 阶段码 | 状态 |
|---|---|---|---|
| 0x01 | POWER_ON | 0x0D | SEAL_PRESS |
| 0x02 | SELF_CHECK | 0x0E | SEAL_HOLD |
| 0x03 | IDLE | 0x0F | SEAL_COOL |
| 0x04 | BAG_CALIB_CHECK | 0x10 | SEAL_UNLOAD |
| 0x05 | BAG_INIT | 0x11 | DELIVER_INIT |
| 0x06 | BAG_EXTRUDE | 0x12 | DELIVER_RUN |
| 0x07 | BAG_PULL | 0x13 | DELIVER_DONE |
| 0x08 | BAG_CUT | 0x14 | WAIT_NEXT |
| 0x09 | BAG_UNLOAD | 0x15 | ERROR |
| 0x0A | BAG_COMPLETE | 0x16 | FAULT_LATCH |
| 0x0B | SEAL_INIT | 0x17 | RECOVERY |
| 0x0C | SEAL_PREHEAT | 0x18 | STOPPED |
上位机通过读取阶段码即可精确定位当前工艺步骤,而不需要理解整个状态机拓扑。
### app_packer_sm:薄外观模式
app_packer_sm.c 是整个状态机系统的外观入口,其职责被刻意限制在最小范围:
// app_packer_sm.c — 薄外观模式(~180 行)
void app_packer_sm_init(void) {
psh_init(); // 初始化回调注册表引擎
sm_self_check_init(); // 子 SM 自注册
sm_bag_flow_init();
sm_seal_flow_init();
sm_deliver_flow_init();
psh_transition(PACKER_STATE_POWER_ON); // 首次转态
}
void app_packer_sm_run(void) {
// StateMachineTask 10ms 周期调用
if (psh_is_busy()) return; // 正在跨状态切换中
psh_run(); // 委托给当前状态 on_run
}
void app_packer_sm_handle_cmd(uint8_t func, const uint8_t *data) {
// 协议分发 → 流路由
switch (func) {
case 0x42: flow_route = FLOW_BAG; break;
case 0x43: flow_route = FLOW_SEAL; break;
case 0x44: flow_route = FLOW_DELIVER; break;
case 0x49: flow_route = FLOW_RESET; break;
}
}
关键约束:
- 不做任何硬件操作 — 所有执行器操作由 Service 层的 actuator 完成
- 不做协议解析 — 由 app_packer_proto.c 完成校验和分发
- 不做超时管理 — 各子 SM 内部自行管理状态驻留超时
- 不感知具体执行器 — 只通过 packer_actuator.h 的抽象接口通信
这种刻意削薄的设计保证了即使状态机数量增长到 30+ 状态,顶层入口依然保持可读和可测试。每次添加新状态只需新增一个 sm_*.c 文件并注册回调,app_packer_sm.c 本身几乎不需要修改。
---
## 14. 7 任务协作模型
### 任务定义表
系统运行 7 个 FreeRTOS 任务,参数如下:
| 任务名 | 周期 | 栈大小 | 优先级 | 唤醒机制 | 职责 |
|---|---|---|---|---|---|
| ProtoTask | 5ms | 2 KB | AboveNormal (4) | 通知 / 超时 | 串口协议帧接收、校验、分发 |
| StateMachineTask | 5ms | 2 KB | Normal (7) | 超时 | 主状态机调度 + 流路由 |
| MotorTask | 10ms | 2 KB | AboveNormal (4) | 通知 / 超时 | 步进电机 / 直流电机动作执行 |
| AdcTask | 10ms | 512 B | Normal (7) | 超时 | ADC 采样、功率计算、过流检测 |
| CommTask | 10ms | 2560 B | Normal (7) | 通知 / 超时 | 阻塞 I/O 异步化(打印机 / 串口屏) |
| MonitorTask | 20ms | 2 KB | Normal (7) | 超时 | 栈水位诊断、通信看门狗、报警上报 |
| HeaterTask | 10ms | 1536 B | Normal (7) | 超时 | 加热管 PID 闭环 + 温度超限保护 |
优先级说明(FreeRTOS 数值越小优先级越大):ProtoTask 和 MotorTask 使用 AboveNormal(比 Normal 高一级),确保协议帧不丢失、电机响应实时。StateMachineTask 虽然是 Normal 中的最高级(7),但不抢占 ProtoTask,保证接收稳定性。
### 任务间通信机制
四个核心通信原语:
1. 任务通知(Task Notifications)
用于轻量级信号传递,替代全局标志位:
// ProtoTask → StateMachineTask:新帧就绪
void proto_task_notify_sm(void) {
xTaskNotifyGive(s_state_machine_task_handle);
}
// StateMachineTask 等待通知或超时
void state_machine_task_run(void) {
uint32_t notified;
xTaskNotifyWait(0, ULONG_MAX, ¬ified, pdMS_TO_TICKS(5));
if (notified) {
frame_t f = proto_task_consume_frame();
app_packer_sm_handle_cmd(f.func, f.data);
}
app_packer_sm_run();
}
2. 命令队列(Command Queue)
StateMachineTask → CommTask 的阻塞 I/O 委托:
// 状态机侧:非阻塞入队
comm_task_enqueue(CMD_PRINT_LABEL);
// CommTask 侧:阻塞出队 + 执行
void comm_task_run(void) {
cmd_t cmd;
if (xQueueReceive(s_cmd_queue, &cmd, pdMS_TO_TICKS(10)) == pdTRUE) {
execute_blocking_io(cmd); // 串口收发可能阻塞数十 ms
xTaskNotifyGive(s_sm_task_handle); // 通知完成
}
}
3. 临界区(Critical Sections)
保护跨任务访问的共享内存(运行时配置、故障锁存):
taskENTER_CRITICAL();
s_runtime_config.speed_hz = new_speed;
s_runtime_config.dirty = 1;
taskEXIT_CRITICAL();
4. 句柄注入(Handle Injection)
用注册替代全局 extern,每个模块在初始化阶段接收依赖的句柄:
void comm_task_register_sm_handle(TaskHandle_t sm_handle) {
s_sm_task_handle = sm_handle;
}
void proto_task_register_sm_handle(TaskHandle_t sm_handle) {
s_state_machine_task_handle = sm_handle;
}
### 唤醒模式
两种唤醒策略共存:
- 超时唤醒 — StateMachineTask / MotorTask / AdcTask / MonitorTask / HeaterTask 以固定周期运行,即使无事可做也执行检查(超时检测、水位诊断)。
- 通知唤醒 — ProtoTask 收到完整帧后才通知 StateMachineTask;CommTask 在有命令入队时才被唤醒,否则阻塞等待。
混合模式:ProtoTask 和 MotorTask 既接受通知(快速响应),也使用超时(保底检查 DMA 溢出或停止状态)。
### 任务交互 ASCII 图
┌──────────────────────┐
│ USART3 中断 (ISR) │
│ DMA 接收完成 → 通知 │
└──────────┬───────────┘
│ xTaskNotifyGive
▼
┌─────────────────────────┐
│ ProtoTask (5ms) │
│ 帧校验 → 分发 → 通知 SM │
└────┬────────────────┬───┘
│ │
xTaskNotify │ │ xQueueSend
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ StateMachineTask │ │ CommTask │
│ (5ms/Normal7) │ │ (10ms/Normal) │
│ │ │ │
│ app_packer_sm │ │ 阻塞 I/O 执行 │
│ 路由判断 → │ │ 打印机 / 串口屏 │
│ 子 SM 调度 │ │ 完成后通知 SM │
└───┬────┬────┬───┘ └──────────────────┘
│ │ │
cmd_q │ │ │ xTaskNotify
▼ │ ▼
┌────────────┐ │ ┌────────────┐
│ MotorTask │ │ │ HeaterTask │
│ (10ms) │ │ │ (10ms) │
│ 步进/直流 │ │ │ PID 闭环 │
│ 电机动作 │ │ │ 温度保护 │
└────────────┘ │ └────────────┘
│
▼
┌──────────────────┐
│ AdcTask │
│ (10ms/512B) │
│ 功率采样 + 检测 │
└──────────────────┘
│
▼
┌──────────────────┐
│ MonitorTask │
│ (20ms/2KB) │
│ 栈水位 / 看门狗 │
└──────────────────┘
### 设计要点
- ProtoTask 是唯一从 ISR 接收通知的任务,确保帧接收路径最短,不经过队列或信号量中转。
- StateMachineTask 占 Normal 最高优先级(7),ProtoTask 和 MotorTask 用 AboveNormal(4)更高一级,保证 I/O 和动作优先于决策。
- CommTask 栈最大(2560B),因为其内部调用可能嵌套多层协议栈(DGUS 命令 + 打印机透传)。
- AdcTask 栈最小(512B),没有函数调用栈深,纯粹寄存器轮询 + 计算。
- 所有任务共用一个错误通道 — 在 MonitorTask 中统一采集和上报,避免每个任务独立处理故障。
Comments NOTHING