1. 架构思维筑基:为什么嵌入式需要架构设计?从单体到模块化的演进之路

1.1 一个让我翻车的项目

先讲个真事。

几年前我接手过一个智能家居网关项目。代码量大概 8 万行,全部写在一个 main.c 里。对,你没看错——一个文件。

当时团队的想法很简单:「单片机资源有限,搞什么架构?能跑就行。」

结果呢?

  • 加一个新传感器,要改 6 个函数,动 3 个全局变量
  • 修一个 Wi-Fi 重连的 bug,把 LCD 驱动搞崩了
  • 新人入职两周,连编译都过不了
  • 最要命的是——OTA 升级时死锁,产品已经量产了

那次之后我彻底想明白一件事:嵌入式不是不需要架构,而是太需要了。资源越受限,架构的容错空间就越小。

⚠ 我曾经踩过的坑: 别以为小项目就不用架构。我见过一个 2000 行的遥控器程序,因为全局变量满天飞,最后改一个按键逻辑花了三天。架构不是大公司的专利,是每个嵌入式工程师的保命符。

1.2 单体架构:为什么它曾是「默认选择」?

咱们先看看最原始的状态——单体架构。

说白了就是:所有代码揉在一起,函数随便调,全局变量随便用。

特征 表现 后果
函数调用 A 函数直接调 B 的内部变量 牵一发动全身
全局变量 30+ 个全局变量散落各处 改一个,崩一片
模块边界 没有,或者说没人遵守 代码像意大利面条
测试难度 几乎无法单元测试 全靠硬件联调
复用性 几乎为零 换平台就得重写

你想想看,为什么早期嵌入式项目都这么写?

原因其实很现实:

  • 硬件资源太紧:ROM/RAM 按 KB 算,函数调用开销都心疼
  • 功能简单:一个 8051 跑个 LED 流水灯,确实不需要架构
  • 工期短:老板说「下周出样机」,谁还管设计
  • 工具链落后:很多 IDE 连代码折叠都没有

但问题在于——项目会变大。今天加个传感器,明天加个通信协议,后天要支持 OTA。单体架构的复杂度是指数级增长的,不是线性。

💡 我的经验: 单体架构的「能跑」是假象。它只是在项目前 20% 的阶段显得快,后面 80% 的时间全在填坑。我见过太多项目死在「重构」的路上——因为根本没法重构。

1.3 模块化:从「一团乱麻」到「乐高积木」

那怎么办?

答案就是——模块化。

模块化不是啥高深理论。说白了就是:把系统拆成若干独立单元,每个单元只干一件事,单元之间通过明确的接口通信。

我习惯用「乐高积木」来比喻:

  • 每个模块是一块积木
  • 接口是积木的凸点和凹槽
  • 你可以自由组合,也可以随时替换某一块

来看一个具体例子。这是单体架构的典型代码:

// 单体架构:所有逻辑混在一起
void system_run(void) {
    // 读传感器
    adc_value = HAL_ADC_Read();
    temp = adc_value * 3.3 / 4096 * 100;
    
    // 处理数据
    if (temp > 50) {
        HAL_GPIO_WritePin(FAN_GPIO, 1);
    }
    
    // 显示
    sprintf(buf, "Temp: %.1f", temp);
    LCD_DisplayString(buf);
    
    // 通信
    UART_Send(buf, strlen(buf));
}

再看模块化之后的版本:

// 模块化:每个模块只关心自己的事
void system_run(void) {
    float temp = sensor_read_temperature();
    
    if (temp > TEMP_THRESHOLD) {
        actuator_control(FAN, ON);
    }
    
    display_show_temperature(temp);
    comm_send_temperature(temp);
}

看出区别了吗?

  • sensor_read_temperature() 只负责读传感器,不管显示和通信
  • actuator_control() 只负责控制执行器,不关心温度怎么来的
  • display_show_temperature() 只负责显示,不碰硬件
  • comm_send_temperature() 只负责通信,不碰传感器

每个模块内部可以随便改,只要接口不变,外面完全不受影响。

🔧 实用技巧: 模块化的第一步不是写代码,而是画框图。我每次做新项目,都会先在白板上画出模块划分和接口定义。画清楚了再动手,写代码就是填空。

1.4 模块化的三个核心原则

光说「拆模块」还不够。怎么拆?拆到什么粒度?

我总结了三个原则:

原则一:单一职责

一个模块只做一件事。比如:

  • ✅ 温度传感器驱动模块:只负责读取原始数据
  • ❌ 温度传感器驱动模块:既读数据,又做滤波,还负责显示

原则二:接口稳定

模块之间的接口要像「法律条文」一样稳定。一旦定下来,尽量不改。我习惯用 .h 文件来定义接口,.c 文件里随便折腾。

原则三:依赖单向

模块之间的依赖关系应该是单向的,不能有循环依赖。比如:

  • ✅ 应用层 → 服务层 → 驱动层
  • ❌ 应用层 → 驱动层 → 应用层(循环依赖)
⚠ 避坑指南: 我曾经在一个项目中把模块拆得太细——一个 I2C 驱动拆成了 5 个文件。结果接口数量爆炸,每次改接口都要改 5 个地方。模块不是越细越好,找到合适的粒度才是关键。

1.5 从单体到模块化的演进路径

很多团队不是不想模块化,而是不知道怎么从一团乱麻里走出来。

我建议分四步走:

  1. 先画现状图:把当前代码的调用关系、全局变量依赖画出来。这一步很痛苦,但必须做。
  2. 识别「坏味道」:找那些被到处调用的函数、被多个模块修改的全局变量、超过 500 行的函数。
  3. 从最痛的地方下手:别想着一次重构完。先挑一个最影响开发效率的模块拆出来。
  4. 逐步替换:新功能用模块化写,旧功能保持不动。等旧功能需要改的时候,再顺便重构。

这个过程可能持续几个月。别急,架构演进是马拉松,不是百米冲刺。

1.6 本章知识体系

下面这张图总结了从单体到模块化的核心逻辑:

从单体到模块化的演进路径 从单体到模块化的演进路径 单体架构 全局变量满天飞 · 函数耦合严重 改一处崩一片 · 无法单元测试 复杂度爆炸 · 维护成本飙升 模块化架构 单一职责 · 接口稳定 · 依赖单向 可测试 · 可复用 · 可替换 四步演进法 画现状 → 识坏味 → 拆痛点 → 逐步替 核心思想:不是推翻重来,而是逐步演进

1.7 小结:架构思维是「投资」不是「成本」

很多人觉得做架构设计浪费时间。我刚开始也这么想。

但后来我算了一笔账:

  • 一个没有架构的项目,前 3 个月开发速度很快,但第 6 个月开始断崖式下跌
  • 一个有架构的项目,前 3 个月慢一些,但第 6 个月之后速度反而越来越快

为什么?因为架构设计是在「还技术债」之前先把债还了。

你想想看,一个模块化的系统:

  • 新人上手快——只看自己负责的模块就行
  • 测试容易——每个模块可以单独测
  • 复用方便——这个项目的驱动,下个项目直接拿来用
  • 改需求不怕——只要接口不变,内部随便改

所以我的建议是:从第一个项目就开始用模块化思维。哪怕只是一个 LED 灯的程序,也把驱动、逻辑、应用分开。养成习惯之后,你会发现——架构设计不是负担,而是让你少加班的利器。

🎯 本章核心要点:
  • 单体架构在项目初期「快」,后期「慢」
  • 模块化的本质是「分而治之」——拆成独立单元,通过接口通信
  • 三个原则:单一职责、接口稳定、依赖单向
  • 演进路径:画现状 → 识坏味 → 拆痛点 → 逐步替
  • 架构思维是长期投资,不是短期成本