1. 课程导入

说实话,我见过太多嵌入式项目从「能跑就行」变成「谁都不敢动」。

刚入行那几年,我接手过一个智能家居网关项目。代码跑了三年,功能倒是全的,但每次加一个新传感器驱动,都要花两周去梳理中断、定时器和状态机之间的纠缠关系。项目经理催得紧,我只能硬着头皮往上堆代码。结果呢?修一个 bug 引出三个新 bug。

这就是典型的「技术债」——你欠下的每一行烂代码,最后都要连本带利还回去。

1.1 嵌入式系统的「三座大山」

我总结了一下,嵌入式系统走到需要重构的地步,通常逃不过下面三个问题。

问题类型 典型表现 后果
意大利面条式代码 全局变量满天飞,函数动辄上千行,goto 语句到处跳 改一处崩一片,新人根本不敢接手
接口耦合 驱动层直接操作业务逻辑,硬件变更导致上层代码全改 换一个传感器型号,要改五个模块
维护噩梦 没有单元测试,文档和代码对不上,编译警告当没看见 版本迭代基本靠猜,线上问题定位以天为单位
⚠️ 注意: 这三个问题往往是共生的。接口耦合会催生意大利面条式代码,而混乱的代码结构又让维护变得不可能。别指望「先忍一忍,后面再重构」——后面只会更糟。

1.2 意大利面条式代码:从「能跑」到「不敢跑」

什么叫意大利面条式代码?我举个例子你就明白了。

// 典型的嵌入式「面条」代码
void main_task(void *param) {
    uint32_t flags = 0;
    int sensor_value = 0;
    
    while(1) {
        if(HAL_GPIO_ReadPin(BTN_GPIO, BTN_PIN) == GPIO_PIN_SET) {
            flags |= 0x01;
            if(flags & 0x02) {
                sensor_value = read_adc();
                if(sensor_value > 1000) {
                    // 这里居然直接操作了显示和通信
                    lcd_show_number(sensor_value);
                    uart_send_byte(sensor_value & 0xFF);
                    // 还顺带改了一个全局变量
                    g_system_state = STATE_ALERT;
                }
            }
        }
        // 500 行后还有类似的逻辑
        // 而且全局变量 g_system_state 在 8 个地方被修改
    }
}

你看,一个函数里塞了按键检测、ADC 读取、LCD 显示、UART 通信、状态管理。这还算好的——我见过更夸张的,一个中断服务程序里做了 300 行的业务逻辑处理。

为什么会这样?

说白了,就是「赶工期」和「缺设计」的产物。项目经理说「这周必须出功能」,你想着「先跑起来再说」。结果这一跑,就是三年。

💡 我的经验: 判断一个项目是不是面条代码,有个简单方法——打开一个 .c 文件,如果前 50 行全是 #include 和全局变量定义,然后一个函数超过 200 行,那基本就是了。我曾经在一个项目里看到过 800 行的中断处理函数,当时真想给作者寄一箱意大利面。

1.3 接口耦合:牵一发而动全身

接口耦合是嵌入式系统最隐蔽的杀手。它不像面条代码那么明显,但破坏力更大。

我参与过一个车载 OBD 诊断仪的项目。硬件团队说「先按这个传感器型号做,后面可能会换」。我当时图省事,直接在业务代码里调用了传感器的 I2C 读写函数。

// 耦合版本:业务逻辑直接依赖硬件
void read_engine_temp(void) {
    uint8_t buf[2];
    // 直接调用 I2C 驱动
    i2c_master_read(I2C1, 0x76, buf, 2);
    int temp = (buf[0] << 8) | buf[1];
    if(temp > 120) {
        // 业务逻辑
        set_alarm(ALARM_OVERHEAT);
    }
}

三个月后,传感器换成了 SPI 接口的。我傻眼了——所有调用 i2c_master_read 的地方都要改。全局搜索下来,23 个文件、47 处调用。改了两天,还改出了三个 bug。

这就是接口耦合的代价。硬件层、驱动层、业务层揉在一起,任何一层的变更都会引发连锁反应。

🔑 核心观点: 好的架构应该让「变化」被隔离。换一个传感器,只改驱动层;换一个通信协议,只改传输层。业务逻辑永远不应该知道它用的是 I2C 还是 SPI。

1.4 维护噩梦:当代码变成「黑盒」

维护噩梦是什么感觉?

我记得有一次,凌晨两点被电话叫醒——产线上的设备批量死机。我远程连上去一看,日志里只有一行「Error: 0x03」。没有堆栈信息,没有上下文,连哪个模块报的错都不知道。

这就是典型的「维护噩梦」症状:

  • 没有单元测试——改完代码只能靠人工点按,测不测得到全看运气
  • 文档和代码脱节——注释写的是「v1.0 版本逻辑」,实际代码已经迭代了五个版本
  • 编译警告成堆——我见过一个项目编译时有 200 多个 warning,团队已经「习惯」了
  • 全局变量滥用——一个全局变量在 10 个以上地方被读写,根本没法追踪

你想想看,这样的项目,新人接手要多久才能上手?我见过最夸张的,一个工作了半年的工程师,还在问「这个全局变量到底是谁改的」。

⚠️ 避坑指南: 我曾经在一个项目里发现,一个名为 g_timer_count 的全局变量,在 6 个中断服务程序和 12 个普通函数里被修改。最后定位一个定时器不准的 bug,花了整整三天。从那以后,我定了一条铁律:全局变量必须封装成模块,只通过接口访问。

1.5 重构的价值:不只是「改代码」

说了这么多问题,那重构到底能带来什么?

我用一张图来总结一下重构的核心价值:

架构重构的核心价值 架构重构 可维护性 改一处,不影响全局 可扩展性 加功能,不改旧代码 可测试性 模块独立,单元测试 可移植性 换硬件,不改业务 重构不是重写 保持外部行为不变,逐步改善内部结构 每次只改一小步,确保每一步都能编译、运行、测试

重构不是重写。这是很多团队容易搞混的地方。

重写是「推倒重来」,风险极高——业务逻辑可能丢失,新 bug 层出不穷,项目周期不可控。重构是「逐步改善」,每次只改一小块,改完就编译、测试、验证,确保系统始终处于可用状态。

📌 记住这个原则: 重构的每一步,都要让代码比之前更好。如果改完反而更乱了,那就不叫重构,叫「折腾」。

1.6 什么时候该重构?

不是所有项目都需要重构。我总结了三个「重构信号」:

  1. 修改成本 > 重写成本——加一个小功能要改五个模块,修一个 bug 要花三天定位
  2. 新人上手周期 > 3 个月——新来的工程师三个月了还不敢独立改代码
  3. 线上问题定位时间 > 1 天——出问题了要翻代码、加日志、复现,一天过去了还没找到根因

如果你现在的项目符合上面任意一条,那就要认真考虑重构了。

💡 我的建议: 别想着「一次重构到位」。嵌入式系统不像互联网后端,可以随时灰度发布。我们的代码跑在硬件上,出问题就是死机、重启、甚至烧板子。所以,小步快跑,持续重构才是正道。