嵌入式工程中最容易忽略的 10 个问题

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


工程复盘 经验教训

嵌入式工程中最容易忽略的 10 个问题

来自一个 STM32 + FreeRTOS 工业控制项目的真实复盘。每一个都踩过、修过、沉淀过。


1. 中断错误快照:下次开机谁还记得上次死在哪

UART 或 DMA 发生硬件错误时,MCU 通常不一定会死——但功能受损。重启后错误现场丢失,故障难以定位。

问题:UART DMA 静默死亡——噪声导致 IDLE 标志异常,接收口永久失联,不报错。
// 在 .noinit 段保留错误快照,下次启动时自动上报
static volatile app_irq_snapshot_t s_snapshot
    __attribute__((section(".noinit"), used, aligned(4)));

void Error_Handler(void) {
    s_snapshot.uart_error = get_uart_error(UART3);
    s_snapshot.dma_error  = get_dma_error(DMA1);
    s_snapshot.magic      = SNAPSHOT_MAGIC;
}

void boot_report(void) {
    if (s_snapshot.magic == SNAPSHOT_MAGIC) {
        log("上次崩溃: UART=%d DMA=%d",
            s_snapshot.uart_error, s_snapshot.dma_error);
        s_snapshot.magic = 0;
    }
}

2. 跳变响应 vs 完成响应:协议回复时机该怎么选

问题:启动命令"执行完再回复"——上位机长时间等待后超时,但其实设备正在执行。
// 旧方案:执行完才回复
上位机: TRIGGER_BAG → 等待 3 秒 → 超时 → 重试

// 新方案:启动立即回复,完成确认分离
上位机: TRIGGER_BAG → 立即回复 OK → 轮询完成查询
上位机: COMPLETE_QUERY → 回复 已完成/进行中

Z1 字段统一控制回复时机:0x01 启动立即回复,0x02 完成查询。启动和查询分离,上位机不再阻塞。

3. 协议回复地址校验不足

问题:同一总线上多节点时,收到不是发给自己的响应帧,误当作有效数据。
// 响应帧必须校验目标节点地址
if (frame.addr != MY_ADDR && frame.addr != BROADCAST) {
    return; // 不是给我的
}

4. extern 跨文件任务句柄

问题:多个 .c 文件通过 extern 引用 osThreadId_t,重构时根本不知道谁依赖了谁。
// ❌ 坏味道
extern osThreadId_t MotorTaskHandle;

// ✅ 显式注册函数
void app_sm_register_motor_task(void *handle);
// 使用: app_sm_register_motor_task(MotorTaskHandle);

5. 残留 Done 标志

问题:步进电机完成有限脉冲后,done 保持置位。同一状态复用于不同轴时,旧标志误导后续判断。
// motion_seen 模式
static uint8_t s_motion_seen = 0;
void on_enter(void) {
    start_motor(axis, pulses);
    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(NEXT);
}

6. 隐式脉冲斩杀(Magic Number 补丁)

问题:到目标前 2000 脉冲直接停轴——2000 是哪来的?为什么不是 1500?
// ❌ 反模式
if (remaining_pulses <= 2000) {
    stop_axis(); // 谁也不敢删这个 2000
}

// ✅ 显式停止模式
typedef enum {
    STOP_AT_TARGET,     // 到目标脉冲停
    STOP_AT_OVERFLOW,   // 脉冲溢出停
    STOP_CONTINUOUS     // 连续模式
} stop_mode_t;

7. 超时保护的错觉

问题:给开环步进轴加 10 秒超时保护——但 ISR 正常时 done 先到,ISR 异常时超时也不准。
// 开环轴唯一保护:脉冲溢出
if (emitted_pulses > target + APP_CFG_PULSE_OVERFLOW_MARGIN) {
    stop_axis();
    trigger_alarm(ALARM_PULSE_OVERFLOW);
}

有传感器反馈的轴才保留超时作为辅助保护。

8. 状态机同步等待 UART

问题:StateMachineTask 在 10ms 周期内调 printer.send() 等应答——卡住几百毫秒。
// ✅ CommTask 异步模式
void state_on_run(void) {
    if (first_entry) {
        comm_task_enqueue(CMD_PRINT);
        first_entry = 0;
    }
    result = comm_task_get_result(CMD_PRINT);
    if (result == BUSY) return;
    // OK / FAIL
}

9. 宏清理不干净,Doxygen 留空组

问题:删了一个宏但忘了清理 Doxygen 注释,@name {} 块空在那里,生成文档时出现空白段。
// 删宏时同时检查:
// 1. grep 确认零引用
// 2. 同步清理 Doxygen 组
// 3. 更新 runtime_config 默认值

典型的"改代码时顺手"场景——改完记得 grep 全局。

10. 架构规范写了但没人遵守

问题:"APP 层不要直接调 HAL_GPIO_WritePin"——文档写了,但编译能过,下次改 bug 的人一定有"就这一次"的冲动。
// 解决方案:用 CMake 做物理隔离
// APP 层编译时,BSP 的 include 目录不可见
target_include_directories(app PRIVATE
    ${CMAKE_SOURCE_DIR}/Service/Inc
    # 不包含 BSP/Inc
)

物理隔离比道德约束有效。让违反分层变成编译错误,不再是 code review 问题。


这 10 个问题来自同一个项目、311 次提交、四轮重构。每一个都有对应的提交记录和修复方案。最值得分享的不是某个具体的 fix,而是这些模式背后共同的思维方式:让错误显式化、让协议确定化、让架构编译化

此作者没有提供个人介绍。
最后更新于 2026-05-30