1. 课程导入

各位同学,欢迎来到这门课。我是你们的老朋友,一个在嵌入式圈子里摸爬滚打了十几年的工程师。

今天咱们不急着敲代码。先聊聊一个扎心的问题:为什么你的嵌入式项目,越做越痛苦?

1.1 嵌入式软件的“泥潭”

我刚开始工作的头两年,接手过一个智能家居网关项目。代码量大概五万行,全部写在一个 main.c 文件里。对,你没看错,五万行,一个文件。

全局变量满天飞,函数之间互相调用,绕来绕去像一团乱麻。那时候我管这叫“意大利面条代码”。后来我才知道,这玩意儿有个更专业的名字——大泥球架构。

为什么会这样?

说白了,嵌入式开发早期,大家追求的是“跑起来就行”。硬件资源有限,ROM、RAM 都抠着用,谁还顾得上代码结构?再加上项目周期紧,需求一天三变,今天加个传感器,明天换个通信协议。改代码?直接往 main.c 里怼新函数。

结果呢?

  • 改一个 bug,引出三个新 bug。 因为全局变量被到处修改,你根本不知道谁动了你的数据。
  • 新人接手,直接崩溃。 我见过一个同事,看了一周代码,最后在工位上哭了。
  • 测试?不存在的。 整个模块耦合在一起,你没法单独测某个功能。每次都是烧录整机,跑全功能测试,一测就是半天。

核心痛点: 嵌入式软件正面临一场“危机”。不是技术做不到,而是代码的复杂度已经超出了人脑能管理的极限。

1.2 从“面条代码”到“乐高积木”

那怎么破局?

我个人习惯用一个比喻:把写代码变成搭乐高积木。

你想想看,乐高积木为什么好玩?因为每一块积木都是独立的、标准的、可替换的。你想搭一个城堡,拿几块红色积木拼个墙,拿几块蓝色积木搭个塔。哪天看腻了,拆掉重来,积木本身不会坏。

模块化设计,就是这个道理。

我们把一个复杂的嵌入式系统,拆成若干个高内聚、低耦合的模块。每个模块只干一件事,干好一件事。模块之间通过定义好的接口通信,互不干扰。

这样做的好处,我列个表给你看:

对比维度 面条代码(大泥球) 模块化设计(乐高积木)
可读性 全局变量 + 长函数,读起来像破案 模块职责清晰,接口明确,一目了然
可维护性 改一处,牵全身,不敢动 改一个模块,不影响其他模块
可测试性 必须整机测试,效率极低 每个模块可独立单元测试
可复用性 代码和业务强绑定,换个项目重写 模块可以跨项目复用,积累技术资产
团队协作 只能串行开发,一个人写,其他人等 可以并行开发,每人负责一个模块

一个小技巧: 判断你的代码是不是“面条”,有个简单方法——看一个函数超过 50 行,或者一个文件超过 500 行,你就该警惕了。

1.3 模块化设计的三个核心原则

光说概念没用,咱们得落地。模块化设计不是玄学,它有几个硬性原则。我结合自己的项目经验,给你拆解一下。

1.3.1 单一职责原则

一个模块,只负责一个功能。比如,你写一个 uart_driver.c,它只负责 UART 的收发。别把 LED 闪烁、按键扫描的逻辑也塞进去。

我曾经在一个项目里,看到有人把 PID 控制算法和 WiFi 配网逻辑写在同一个文件里。原因?他说“这两个函数都要用同一个定时器”。嗯,这理由我至今无法理解。

1.3.2 接口隔离原则

模块之间通过接口通信,不要直接访问对方的内部变量。接口要尽量简单、稳定。

举个例子:

// 不好的做法:直接访问模块内部变量
extern int temperature_raw_value;
if (temperature_raw_value > 2500) {
    // 开风扇
}

// 好的做法:通过接口函数访问
int get_temperature(void) {
    return temperature_raw_value;
}
if (get_temperature() > 2500) {
    // 开风扇
}

你看,后者把内部实现隐藏起来了。以后你换温度传感器,或者改采样算法,只要 get_temperature() 的接口不变,调用方完全不用改。

1.3.3 依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖抽象接口。这个在嵌入式里尤其重要,因为硬件经常变。

我举个例子,你写一个数据采集模块,它需要从某个传感器读数据。如果你直接调用 i2c_read(),那这个模块就和 I2C 总线绑死了。哪天传感器换成 SPI 接口了,你整个模块都得重写。

正确的做法是定义一个抽象接口:

// 抽象接口
typedef struct {
    int (*init)(void);
    int (*read)(uint8_t *buf, uint16_t len);
} sensor_if_t;

// 数据采集模块只依赖这个接口,不关心底层是 I2C 还是 SPI
void data_collector_init(sensor_if_t *sensor) {
    sensor->init();
}

避坑指南: 我曾经在一个项目中,为了“省事”,让一个模块直接调用了另一个模块的内部结构体。结果后来需求变更,那个结构体改了三个字段,我花了整整两天去排查所有调用点。从那以后,我再也不敢跨模块访问内部数据了。

1.4 本章知识体系

说了这么多,咱们用一张图来总结一下本章的核心逻辑。这张图也是整个课程的知识骨架。

嵌入式软件模块化设计知识体系 嵌入式软件模块化设计知识体系 嵌入式软件危机:大泥球架构 解决方案:模块化设计(从面条到乐高) 单一职责原则 接口隔离原则 依赖倒置原则 单元测试 静态分析 代码审查 高质量、可维护、可复用的嵌入式软件 本课程将围绕“原则 → 手段 → 目标”这条主线,带你一步步掌握模块化设计的实战技能

这张图你看懂了吗?从上往下看:

  1. 问题层: 我们面临的是嵌入式软件的“大泥球”危机。
  2. 解决方案层: 模块化设计是解药。
  3. 三大原则: 单一职责、接口隔离、依赖倒置,是模块化设计的理论基石。
  4. 落地手段: 单元测试、静态分析、代码审查,是把原则变成现实的具体工具。
  5. 最终目标: 写出高质量、可维护、可复用的嵌入式软件。

嗯,这就是咱们这门课要干的事。后面的章节,我会带着你,一个工具一个工具地过,一个原则一个原则地练。别急,咱们慢慢来。