课程导论:为什么需要架构重构?嵌入式软件面临的三大痛点
各位同学,大家好。我是这门课的主讲。
做嵌入式这行十几年了,我见过太多项目从“能跑就行”变成“改一行崩一片”。说实话,每次看到新手工程师在几千行的 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 章,我会带着大家一步步实践这些方法。从接口设计原则,到分层架构实战,再到测试框架搭建,全部基于真实项目案例。
嗯,准备好了吗?我们开始吧。