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 本章知识体系
说了这么多,咱们用一张图来总结一下本章的核心逻辑。这张图也是整个课程的知识骨架。
这张图你看懂了吗?从上往下看:
- 问题层: 我们面临的是嵌入式软件的“大泥球”危机。
- 解决方案层: 模块化设计是解药。
- 三大原则: 单一职责、接口隔离、依赖倒置,是模块化设计的理论基石。
- 落地手段: 单元测试、静态分析、代码审查,是把原则变成现实的具体工具。
- 最终目标: 写出高质量、可维护、可复用的嵌入式软件。
嗯,这就是咱们这门课要干的事。后面的章节,我会带着你,一个工具一个工具地过,一个原则一个原则地练。别急,咱们慢慢来。