摘要:这份发布版报告把一组嵌入式项目修复记录收拢成问题分布图,重点展示哪些故障最常见、哪些类别最贵、哪些风险具有重复性。
如果把 bug 只看成零散事件,就很难发现它们背后的结构规律。这份报告把某 STM32 运动控制项目的修复历史按类型、严重度和时间分布重新整理,适合做技术债盘点、团队复盘和架构改造前的底稿。
一台 STM32 设备是怎么在 83 次修复里暴露出自己的问题分布的
单个 bug 看起来往往只是局部失误,几十个 bug 放到一起看,才会显出工程真正的受伤方式。
这份报告做的事情很直接:不再按时间顺序复读提交记录,而是把修复历史重新按问题类型、严重度和出现阶段归并,看看一套嵌入式控制工程到底最容易在哪些地方反复出错。
发布版已去掉仓库身份和提交哈希,保留统计口径与问题模式,方便把阅读重点放在共性结论上。
项目: 某 STM32 运动控制项目(STM32F103 + FreeRTOS)
方法: Git 历史语义挖掘(MSR)
范围: 329 个提交,其中 83 个修复类(25%)
时段: 2025-02 初始化 → 2026-05-25
一、问题分类统计
1.1 总览
| 类别 | 数量 | 占比 | 关键短语 |
|---|---|---|---|
| 业务逻辑错误 | 18 | 22% | 加热时序、状态机条件、流程标志 |
| 硬件抽象错误 | 10 | 12% | 引脚模式、定时器频率、默认值 |
| 并发/调度问题 | 8 | 10% | DMA 缓冲、栈溢出、队列竞争 |
| 配置/参数错误 | 8 | 10% | 宏门控、默认值错误、语义不一致 |
| 文档/注释脱节 | 9 | 11% | 文档未同步、注释过时、Doxygen 丢失 |
| 协议语义未收敛 | 4 | 5% | STOP 粒度、应答模型、条件门控 |
| 其他修正 | 26 | 31% | 超时调整、方向修正、斩杀线、括号残留 |
1.2 按严重等级细分
| 等级 | 数量 | 判断标准 | 典型 |
|---|---|---|---|
| 致命(产生不可用状态) | 5 | 流程卡死 / 报警停机 / 端口永久失效 | [提交哈希已隐去] alarm=11, [提交哈希已隐去] USART1 永久停, [提交哈希已隐去] 校准恒跳过 |
| 严重(功能异常但可恢复) | 18 | 动作执行错误 / 参数不对 / 状态残留 | [提交哈希已隐去] 风机不关, [提交哈希已隐去] 加热时序, [提交哈希已隐去] run_state |
| 中等(边界条件遗漏) | 31 | 超时不准 / 方向反 / 门控遗漏 | [提交哈希已隐去] 三角波公式, [提交哈希已隐去] motion_seen 守卫 |
| 轻微(文档/注释/风格) | 29 | 注释不一致 / 格式化 | [提交哈希已隐去] 注释恢复, [提交哈希已隐去] 代码格式 |
1.3 时间分布(按周)
第 1 周(初始化):0 修复
第 2 周(功能填充):2 修复
第 3 周(状态机建设):4 修复
第 4 周(协议搭建):6 修复
第 5 周(重构 v4 / 解耦):18 修复 ← 峰值
第 6 周(运行时配置迁移):12 修复
第 7 周(加热/风机/协议修正):15 修复 ← 次峰
第 8 周(加热 GPIO + 自检修正):10 修复
第 9 周(DMA + HMI Session + doc 清理):16 修复
修复频率在第 5~9 周激增——恰好对应加热模块 3 次方案变更 + USART1 协议切换 + v1.1 板卡适配的前期阶段。
二、高频根因
根因 1:职责错位——一个物理单元分散在多个模块,或一个模块承载多个物理单元
证据链(按时间顺序):
加热历程:
加热在 bsp_dc_motor.c(初始)→ 软件 PWM → GPIO → 周期修正 → STOP 逻辑 → 风机解耦
6 周 7 版方案,加热始终没有自己的 BSP 文件
dc_motor.c 承载的实际物理单元:
- 加热丝 ← 功率继电器
- 风机 ← 功率继电器
- NMOS1~4 ← 功率输出
- IR2104 组1 ← DC 电机
- IR2104 组2 ← DC 电机
共 5 种物理语义,只有最后两种真正是 DC 电机
影响扩散模式:
加热丝 BSP 出问题
→ packer_heater.c/h 跟着改
→ freertos.c 任务周期跟着改
→ app_define.h 宏跟着改
→ doc/debug命令.md 跟着改
→ .agents/*.md 跟着改
→ AGENTS.md 路由表跟着改
→ 最终文档修正还需要另一次提交
同类型问题: app_packer_sm.c = 状态机 + 命令分发 + 报警处理,
packer_actuator.c = 所有执行器的动作编排 + 脉冲计算 + 功率限制。
量化: dc_motor 文件有 14+ 次加热相关变更,只有 4 次是真正的 DC 电机变更。
加热的寄生增加了 dc_motor 的变更频率 350%。
根因 2:共享资源的同步约束未显式建模
证据链:
| 提交 | 共享资源 | 约束 | 发现方式 |
|---|---|---|---|
| [提交哈希已隐去] | TIM4(四轴步进共用) | 同频启动 | 运行时报 BSP_STEPPER_RESULT_BUSY |
| [提交哈希已隐去] | USART TX DMA 队列 | 单次单任务写入 | 队列损坏 |
| [提交哈希已隐去] | USART1 RX DMA | 错误后不自动恢复 | 端口永久失效 |
| [提交哈希已隐去] | configMAX_TASK_NAME_LEN | 长度不足 → configASSERT | 上电死锁 |
| [提交哈希已隐去] | UART RX DMA 缓冲 | 259B 帧需 ≥ 256B | 打印机帧截断 |
模式: 所有约束都是运行时发现的——没有编译期检查,没有设计文档,没有显式断言。开发者看到外设可用就直接用,不知道其他模块对它的使用约束。
最典型的例子——TIM4:
- bsp_stepper 要求同频率才能 start_group()
- 但 pact_fill_runtime_press_dual_action() 和 pact_try_fill_press_axis2_return_action() 各自独立计算频率
- 两个频率不一致时,start_group() 返回 BUSY
- 轴 2 单独运行 → 超时 → alarm=11
- 修复后,在 v1.1 板卡中 ST1/ST2 被拆到独立定时器(但代码尚未适配)
问题的递归性: 因为 TIM4 共享 → 限制了频率自由度 → 被迫拆定时器 → 拆了定时器要重写步进驱动 → 重写成本太大 → v1.1 文档先写但代码一直没动。
根因 3:配置数据的多版本真相
证据链:
| 提交 | 参数 | 涉及的存储位置数 | 不一致类型 |
|---|---|---|---|
| [提交哈希已隐去] | heater_pwm_duty_per_mille | 6+ | 命名语义 vs 实际行为 |
| [提交哈希已隐去] | 自检 3/4 轴角度 | 5+ | 固定值 vs 运行时计算 |
| [提交哈希已隐去] | APP_CFG_BAG_CALIB_SENSOR_ENABLE | 3+ | 条件编译失效(include 顺序) |
| [提交哈希已隐去] | printer_bag_trigger_len_mm_x1000 | 2 | 默认值 30U vs 30000U |
| [提交哈希已隐去] | CONTRO stop 门控条件 | 3+ | 条件遗漏 boot_sequence_active |
参数从定义到使用的路径:
app_define.h(宏定义)
→ app_runtime_config_defaults.h(默认值)
→ packer_runtime_config_t(结构体字段 + 注释)
→ s_param_desc[](运行时描述表)
→ EEPROM(持久化)
→ HMI 屏幕显示
→ doc/ 硬件文档
→ .agents/ 项目文档
每个节点都可能独立修改,
没有机制保证它们的同步。
最严重的一次: heater_pwm_duty_per_mille 字段名叫千分比,代码解释为百分比存、运行时 *10。字段名、代码、EEPROM 三层语义不一致。修正波及了参数管道的每一个节点。
根因 4:协议/状态机的语义设计只有主路径
证据链(同一个问题的 3 次迭代):
CONTROL stop:
版本 1: 收到 stop 就全部停止
→ 打断正在进行的 BAG/SEAL/DELIVER(不好)
版本 2: 被 boot_sequence_active 门控阻止
→ 上电默认停机不生效(更糟)
版本 3: 停新不停车中(当前)
→ 禁止启动下一轮,已进行的自然收尾
0x47 打印机透传:
版本 1: 同步应答
→ 打印机慢于串口,响应超时
版本 2: 异步双阶段
→ Z1=0x03"已受理" + 打印机应答
加热 STOP 行为:
版本 1: stop 不涉及加热
版本 2: stop 立即关断加热
版本 3: stop 关加热,但 42/43/44 各自行为不同
模式: 每个命令只设计了"正常情况下的主路径",没有在最初覆盖:
| 遗漏维度 | 后果 | 例子 |
|---|---|---|
| 中断粒度 | 反复修 stop 口径 | CONTROL stop x3 |
| 状态残留 | A 流程设置的状态 B 流程继承 | 风机 x3 |
| 异步应答模型 | 重做 0x47 响应机制 | 0x47 x2 |
| 交叉影响 | 加热/风机关断时机错 | 加热时序 x4 |
| 幂等性 | 重复命令行为未定义 | 0x42 多次触发 |
根因 5:补丁式修复累积——越过架构层直接在上层加适配
识别的补丁层(按添加时间排序):
| 补丁层 | 解决的问题 | 架构问题 | 后续成本 |
|---|---|---|---|
功率限制适配层 packer_power_limit |
DC 电机加功率门控 | 旧入口未删除 | AGENTS.md 写规则"必须走新入口" |
HMI 桥接 bridge_dispatch |
USART1 发控制命令 | USART1 绕回 USART3 | 第三协议口需再建 bridge |
| 2000 脉冲斩杀线 | 步进收尾不准 | 应用层 magic number | AGENTS.md 禁用此模式 |
| 日志队列 + 节流 | 多任务抢 UART 队列 | Logger 非重入未修 | 3 层限制包着 |
| 收包预算(192B/4帧) | CommTask 被洪峰占满 | 任务设计没估算容量 | 运行时保护替代架构约束 |
| DGUS 键值写零 | 按键防重入 | 屏幕配置能解决但没配 | 后处理 hook 替代事前配置 |
补丁层的生命周期:
第 1 层:原始代码(有设计缺陷)
第 2 层:补丁层(绕过缺陷但不拆除缺陷)
第 3 层:规则约束(AGENTS.md "必须通过 XXX")
第 4 层:补丁的补丁(hack on top of hack)
量化代价: 6 条补丁层 ≈ 12 个额外 commit + 4 条 AGENTS.md 约束规则 + 约 500 行间接代码。
三、模块脆弱性分析
3.1 模块级 Heatmap
按修复数 × 波及面 × 复发次数加权:
模块 修复数 波及面 复发 脆弱指数
─────────────────────────────────────────────────────────────
bsp_dc_motor.c 14+ 高 5次 ██████████
app_packer_sm.c 18+ 极高 4次 █████████
packer_actuator.c 12+ 高 3次 ████████
packer_state_handler.c 10+ 中高 3次 ███████
packer_heater.c/h 8+ 高 4次 ███████
bsp_stepper.c 8+ 中高 3次 ██████
app_packer_proto.c 7+ 高 3次 ██████
bsp_uart.c 6+ 中 2次 ████
packer_hmi_proto.c 4+ 中高 2次 ████
comm_task.c 4+ 中 2次 ███
3.2 第一脆弱模块包:加热子系统(dc_motor + heater + 相关 doc)
演化链:
v0 bsp_dc_motor_heater_pwm_start() ← TIM5 CH1 硬件 PWM
v1 软件 PWM(百分比存 *10) ← 第一次解耦(但仍寄生在 dc_motor)
v2 真千分比 ← 语义修正
v3 GPIO 继电器(移除 TIM PWM) ← 硬件事实修正
v4 周期 100ms→10ms ← 分辨率不够
v5 STOP/ERROR 关断逻辑 ← 语义修正
v6 风机从加热拆分 ← 没拆彻底,还在 dc_motor 里
结论:
加热子系统 6 周 7 版,平均每周换一个方案。
自始至终加热没有自己的 BSP 文件。
dc_motor 文件 14+ 次变更中只有 4 次是真正 DC 电机相关。
加热寄生在 dc_motor 下使 dc_motor 的变更频率增加了 350%。
3.3 第二脆弱模块:状态机(app_packer_sm)
演化链:
状态数量:12 个初始状态 → 20+ 个状态(含子阶段)
主要问题模式:
状态退出时残留标志 → [提交哈希已隐去] 修复
状态间交叉影响(加热/风机/stop) → [提交哈希已隐去] + [提交哈希已隐去]
状态转换条件遗漏(motion_seen 守卫) → [提交哈希已隐去]
状态超时边界条件错误 → [提交哈希已隐去] + [提交哈希已隐去]
状态 done 标志被跳过(残留旧标志) → [提交哈希已隐去] + [提交哈希已隐去]
核心问题: 状态机没有统一的进入/退出/清理契约。每个状态自己决定什么时候清理标志、什么时候释放资源。这种不统一是大多数"状态残留"类 bug 的源头。
3.4 第三脆弱模块:协议分发(app_packer_proto + bridge_dispatch)
演化链:
v1 USART3 独占 20B 协议
v2 USART1 调试模式备选主口 → 增加协议口
v3 USART1 DGUS 协议 → 协议切换
v4 USART1 HMI Session Protocol → 再次协议切换
v5 HMI Session 桥接 0x30~0x37 控制命令 → bridge_dispatch 回环
v6 双通信口共存 → 文档补充
问题:
每次扩展协议口都加一层适配。
bridge_dispatch 引入了一条回环路径:
HMI → USART1 → 拆 20B → USART3 分发 → 截获 → 拆包 → USART1
路径上的每一步都可能出问题。
四、典型 Bug 演化链(图文)
演化链 1:加热——职责错位导致的连锁修复
[根本原因]
加热放在了 bsp_dc_motor.c
↓
[第一层修复] PWM → GPIO
[提交哈希已隐去] + [提交哈希已隐去]:删除 TIM5 PWM 路径,改为 GPIO
波及:6 个文件,文档遗留 2 处
↓
[第二层修复] 周期不对
[提交哈希已隐去]:100ms → 10ms
因为软件 PWM 精度不够
↓
[第三层修复] 单位不对
[提交哈希已隐去]:伪千分比 → 真千分比
EEPROM 已有数据与新语义不兼容
↓
[第四层修复] STOP/ERROR 行为不对
[提交哈希已隐去]:流程不同阶段加热行为不同
↓
[第五层修复] 风机解耦
[提交哈希已隐去]:风机从加热拆分 → 但加热还在 dc_motor 里
↓
[第六层(未发生)] 加热拆出 dc_motor
“涉及面太大,暂时不动”
总代价: 5 轮修复 + 2 轮文档补 + 1 次未完成拆分 = 至少 8 个 commit。
演化链 2:TIM4 同频约束——共享资源未建模
[设计] TIM4 四轴共用,同频运行
↓
[矛盾] 不同子阶段可能请求不同频率
pact_fill_runtime_press_dual_action() → speed_seal_return_hz
pact_try_fill_press_axis2_return_action() → scaled auto_hz
↓
[撞车] bsp_stepper_start_group() 返回 BUSY
[提交哈希已隐去]:轴 2 单独运行 → IN1 超时 → alarm=11
↓
[第一次修复] 强制算同一频率
在 OPEN_DUAL_RETURN profile 中统一频率
↓
[第二次修复] 2000 脉冲斩杀线
[提交哈希已隐去]:收尾不准,加 magic number patch
↓
[第三次修复] AGENTS.md 禁用斩杀线
规则:必须走目标脉冲停轴,不使用斩杀线
↓
[第四次(未完成)] 拆分独立定时器
v1.1 板卡:ST1→TIM2, ST2→TIM5, 仅 ST3/ST4 共用 TIM4
文档已更新,代码未适配
总代价: 4 轮修复 + 2 条 AGENTS.md 约束 + 架构迁移至今未完成。
演化链 3:CONTROL stop——协议语义设计不完整
[初始设计] stop = 全部停止
→ 打断 BAG/SEAL/DELIVER
↓
[第一次修正] stop 被 boot_sequence_active 门控
[提交哈希已隐去]:上电默认停机状态,但 stop 无效
→ 条件遗漏
↓
[第二次修正] 停新不停车中
[提交哈希已隐去]:禁止启动下一轮,已进行的收尾
→ 同时关加热([提交哈希已隐去])
→ 但不关风机([提交哈希已隐去])
↓
[第三次修正] 文档回写
[提交哈希已隐去]:协议文档 + AGENTS.md + .agents 全部更新
↓
[第四次连锁] USART1 Session run_state 未同步
[提交哈希已隐去]:停机状态已更新,但 HMI Session 的缓存没跟上
总代价: 4 次语义修正 + 3 次文档修正 + 1 次连锁修复。
五、架构问题总结
5.1 六个架构缺陷
| # | 缺陷 | 严重度 | 证据 |
|---|---|---|---|
| 1 | BSP 按外设类型而非物理单元组织 | 高 | dc_motor 承载 5 种物理语义 |
| 2 | 共享资源约束隐式存在,无显式建模 | 高 | TIM4 同频、DMA 队列单写 |
| 3 | 配置参数不存在单一事实来源 | 高 | 一个参数在 6+ 处冗余定义 |
| 4 | 协议接口层扩展采用「加层」而非重构 | 中 | bridge_dispatch、power_limit |
| 5 | 跨任务状态一致性无契约 | 高 | run_state 不同步、标志残留 |
| 6 | 状态机无统一进入/退出/清理契约 | 中 | done 残留,motion_seen 守卫漏 |
5.2 债务累积结构
加一层补丁(power_limit)
→ 不删旧入口
→ AGENTS.md 加一条规则
→ 修复提交再波及这里
→ 再补一层(更不敢删了)
当前 AGENTS.md 中有 6 条规则是为补丁层兜底的:
- "DC 电机启动必须走 packer_power_limit 入口"
- "轴2 PRESS_RETRACT 禁止使用 2000 脉冲斩杀线"
- "禁止在 StateMachineTask 中同步等待 UART"
- "阻塞 I/O 必须通过 comm_task_enqueue 异步化"
- "新增步进轴保护先判断是否有传感器反馈"
- "压杆下压与回位必须保持不同语义"
6 条规则 = 6 个本不该存在的架构假设需要人工记忆。
六、推荐重构方案
P0 级(立即,影响可靠性)
| 项 | 理由 | 风险 | 操作 |
|---|---|---|---|
| 加热拆出 dc_motor | dc_motor 50%+ 的变更来自加热 | 低 | 新建 bsp_heater.c/h,移过去 |
| 验证 USART1 DMA 恢复 | [提交哈希已隐去] 已合入,需测 HMI 长时间运行 | 低 | HIL 跑 24h |
| 步进驱动适配 v1.1 TIM2/TIM5 | 架构依赖:当前代码假设四轴同频 | 高 | 重写 bsp_stepper 分配层 |
P1 级(短期,降修复成本)
| 项 | 理由 | 风险 | 操作 |
|---|---|---|---|
| 协议分发器多端口化 | 消除 bridge 回环,第三口不重写 | 中 | proto_port_cfg_t 注册表 |
| packer_pin_map.h 单一事实 | 引脚变更只需改一处 | 低 | 新头文件 + 静态断言 |
| 状态机进入/退出/清理契约 | 消除标志残留 | 中 | sm_*_flow 统一 enter/exit/cleanup |
P2 级(中期,消补丁债)
| 项 | 理由 | 风险 | 操作 |
|---|---|---|---|
| 消除 power_limit 适配层 | 合并到 bsp_dc_motor 入口 | 中 | 删旧入口,强制不走旧路径 |
| 消除 HMI bridge 回环 | proto 原生支持多端口 | 高 | 重写 dispatch |
| Logger 重入实现 | 移除队列+节流三层限制 | 中 | 锁或专用任务 |
七、工程改进建议
7.1 提交规范
当前 329 个提交中,26% 没有 Conventional Commit 前缀(如 fix:、feat:、refactor:)。建议:
fix(加热): GPIO 控制(移除错误的 TIM5 PWM 路径)
fix(步进): 双轴频率不匹配导致 alarm=11
fix(协议): CONTROL stop 口径——停新不停车中
refactor(dc_motor): 加热拆出到独立 bsp_heater
AI 分析时,前缀就是分类标签。
7.2 修复流程改进
每个修复类提交建议包含:
fix(模块): 一句话问题描述
Root cause: [根因一句话]
Fix: [做了啥]
File list: [波及文件]
例:
fix(步进): 双轴频率不匹配导致 alarm=11
Root cause: pact_fill_runtime_press_dual_action() 和
pact_try_fill_press_axis2_return_action() 独立计算频率,
但 TIM4 要求所有轴同频启动。
Fix: 在 OPEN_DUAL_RETURN profile 中显式计算同一 auto_hz,
确保三轴同时启动。
File list: packer_actuator.c, sm_press_util.c
7.3 周期性审计
每月一次:
git log --since="1 month ago" --grep="fix\|修正\|修复"
统计月度修复热力图。如果某模块连续两个月出现在热力图中——说明该模块有架构问题,不是"修复度不够"的问题。
7.4 文档同步检查
commit 钩子检查:如果 commit 改了 .c 或 .h 文件,检查是否有对应的 .md(doc 或 agents)也需要更新。不强制阻止提交,但打印提醒。
结论: 项目 25% 的提交是修复。这不是个案,而是架构层面没有为变化留出位置的系统性代价。最有效的单一改进是 加热拆出 dc_motor——它用最低的代价(新建一个文件)切断了一整类修复的根因链路。
Comments NOTHING