工业控制固件与 HMI 的工程架构复盘:从 STM32 到 Flutter 的全链路实践

Babel36acl 工程复盘 无~ 17 次阅读 预计阅读时间: 40 分钟 发布于 1 天前 最后更新于 1 小时前 8739 字


工业控制固件与 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, &notified, 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 中统一采集和上报,避免每个任务独立处理故障。
此作者没有提供个人介绍。
最后更新于 2026-05-30