课程导论:为什么需要架构重构?嵌入式软件面临的三大痛点

各位同学,大家好。我是这门课的主讲。

做嵌入式这行十几年了,我见过太多项目从“能跑就行”变成“改一行崩一片”。说实话,每次看到新手工程师在几千行的 main.c 里翻找 bug,我都觉得特别心疼。不是他们不努力,是代码的底子——也就是架构——出了问题。

这一章,我们不谈具体技术。先聊聊最根本的问题:为什么你的嵌入式软件需要架构重构?

1.1 嵌入式软件的“隐形债务”

先问大家一个问题:你手头的项目,是不是经常出现这种情况?

  • 换一个 MCU,代码要重写 80%?
  • 加一个新功能,要动三个模块的代码?
  • 写单元测试?别想了,根本没法测?

如果有,恭喜你,你欠了一笔“技术债”。而且利息很高。

核心观点:架构重构不是为了炫技,是为了还债。这笔债不还,项目越往后走,维护成本会指数级上升。

我个人习惯把嵌入式软件的架构问题归纳为三大痛点。这三大痛点,几乎贯穿了所有失败或难产的项目。你想想看,是不是这么回事?

1.2 痛点一:耦合——牵一发而动全身

耦合,说白了就是代码之间的“依赖关系”。

我在项目中遇到过最夸张的一个案例:一个温控器项目,逻辑代码和硬件驱动全部写在同一个文件里。结果呢?客户要换一个型号的温控探头,我们改了整整两周。不是因为探头驱动难写,而是因为驱动和业务逻辑完全搅在一起,改驱动的时候,生怕把温度控制的 PID 算法改坏了。

耦合的典型表现:

  • 一个全局变量,被 10 个函数读写。
  • 硬件寄存器地址直接写在业务逻辑里。
  • 中断服务函数里调用了延时函数。

避坑指南:我曾经接手过一个项目,为了修复一个 SPI 通信的时序问题,结果把 LCD 显示给修坏了。原因就是 SPI 的初始化代码和 LCD 的初始化代码共用了一个全局的 GPIO 配置结构体。这就是典型的“耦合灾难”。

耦合带来的直接后果是什么?修改成本高、风险大。你改了一行代码,不知道会影响到哪里。测试人员要回归整个系统,开发人员要提心吊胆。

1.3 痛点二:可移植性——换个平台就要“回炉重造”

嵌入式开发最头疼的事情之一,就是换平台。

从 STM32 换到 GD32,从 FreeRTOS 换到 RT-Thread,甚至只是换一个编译器版本,都可能引发一场“代码地震”。

为什么可移植性差?

  • 硬件抽象层缺失:代码里到处都是 GPIOA->ODR、*(volatile uint32_t *)0x40020000。
  • 操作系统 API 直接调用:业务逻辑里直接调用 vTaskDelay()、xSemaphoreTake()。
  • 编译器特性依赖:用了特定编译器的内置函数或扩展关键字。

我的建议:写代码的时候,多问自己一句:“如果明天老板让我换一个 MCU,我这部分代码能直接复制过去吗?”如果不能,说明你的架构有问题。

可移植性差,意味着你的代码资产无法复用。每次换平台,都是一次“从零开始”。这在商业项目里,是巨大的浪费。

1.4 痛点三:可测试性——代码写完了,但没法测

这一点,我感触特别深。

很多嵌入式工程师觉得“测试是测试工程师的事”。但你想过没有?如果你的代码没法做单元测试,你怎么保证它的质量?靠肉眼 review?靠硬件上电跑一遍?

不可测试的代码长什么样?

  • 函数内部直接操作硬件寄存器。
  • 函数依赖全局状态,无法独立运行。
  • 函数有“副作用”,调用一次和调用两次结果不一样。

举个例子:

// 不可测试的写法
void set_temperature(int temp) {
    // 直接操作硬件
    DAC->DHR12R1 = temp;
    // 还依赖全局变量
    if (system_state == STATE_HEATING) {
        // ...
    }
}

// 可测试的写法
void set_temperature(int temp, DAC_HandleTypeDef *hdac, int state) {
    // 通过接口操作硬件
    HAL_DAC_SetValue(hdac, temp);
    // 状态由外部传入
    if (state == STATE_HEATING) {
        // ...
    }
}

你看,第二种写法,我可以在 PC 上模拟一个 DAC_HandleTypeDef,然后传入不同的 state,轻松测试所有分支。这就是可测试性带来的好处。

1.5 三大痛点的关系与影响

这三个痛点不是孤立的。它们互相影响,形成一个恶性循环。

下面这张图,是我自己总结的“嵌入式软件架构恶性循环图”:

嵌入式软件架构恶性循环图 高耦合 可移植性差 不可测试 维护成本高 硬件依赖 全局状态 重复开发 质量不可控 时间压力 → 继续耦合 恶性循环:高耦合 → 可移植性差 + 不可测试 → 维护成本高 → 时间压力 → 继续耦合

你看,这个循环一旦形成,项目就会陷入“越改越乱,越乱越不敢改”的泥潭。而打破这个循环的唯一方法,就是架构重构。

1.6 架构重构能带来什么?

说了这么多痛点,那架构重构到底能解决什么问题?

痛点 重构目标 具体收益
高耦合 低耦合、高内聚 修改一个模块,不影响其他模块
可移植性差 硬件抽象层 + 接口隔离 换 MCU 只需改驱动层,业务代码不动
不可测试 依赖注入 + 接口化 可以在 PC 上做单元测试,提前发现 bug

一句话总结:架构重构不是重写代码,而是通过合理的接口设计和分层,把“混乱”变成“有序”。让代码变得可维护、可扩展、可测试。

这门课接下来的 29 章,我会带着大家一步步实践这些方法。从接口设计原则,到分层架构实战,再到测试框架搭建,全部基于真实项目案例。

嗯,准备好了吗?我们开始吧。