1. 模块化设计思想:为什么需要模块化?核心原则

各位同学,咱们今天聊点实在的。你写嵌入式代码多久了?有没有遇到过这种情况:一个文件几千行,全局变量满天飞,改一个 bug 引出三个新 bug。我当年刚入行时就这样,被折腾得够呛。

后来我明白了,问题出在代码的组织方式上。说白了,就是缺了模块化这根弦。

1.1 为什么需要模块化?

先问个问题:你拆过闹钟吗?里面的齿轮、发条、表盘,每个零件各司其职。坏了一个齿轮,换掉就行,不用把整个闹钟扔了。模块化就是这个道理。

嵌入式软件里,模块化就是把功能拆成独立的“零件”。每个零件干自己的事,零件之间通过明确的接口通信。好处很明显:

  • 好维护:改一个模块,不影响其他模块。我有个项目,驱动层换了三次芯片,应用层一行代码没动。
  • 好复用:写好的模块,下个项目直接拿来用。你想想看,每次重写一遍 I2C 驱动,多浪费时间。
  • 好测试:单独测一个模块,比测整个系统容易得多。我习惯先写模块测试用例,再集成。
  • 好分工:团队里你写通信,我写传感器,他写控制。接口定好了,各干各的,互不干扰。

核心观点:模块化不是花架子,是解决复杂系统问题的基本手段。没有模块化,代码就是一团乱麻。

1.2 模块化的核心原则:高内聚、低耦合

这两个词你肯定听过。但到底什么意思?我用自己的话给你解释。

高内聚

一个模块里的东西,应该高度相关。比如一个 UART 驱动模块,里面只放 UART 初始化、发送、接收、中断处理。别把温度传感器的代码塞进去。

内聚分几种,我列个表:

内聚类型 说明 评价
功能内聚 模块内所有元素完成一个单一功能 最好
顺序内聚 元素按顺序执行,前一个输出是后一个输入 较好
通信内聚 元素操作同一数据 一般
过程内聚 元素按某个流程组合 较差
时间内聚 元素因执行时间相近而组合 差
逻辑内聚 元素因逻辑相似而组合 很差
偶然内聚 元素毫无关联,只是凑在一起 最差

我建议你追求功能内聚。一个模块只做一件事,并且把它做好。

低耦合

模块之间的依赖关系要尽量少。耦合越低,一个模块变化时对其他模块的影响越小。

耦合也分等级:

耦合类型 说明 评价
数据耦合 模块间只通过参数传递数据 最好
标记耦合 传递复杂数据结构,但只用部分字段 较好
控制耦合 传递控制标志,影响对方逻辑 一般
外部耦合 模块依赖外部环境(如文件系统) 较差
公共耦合 多个模块共享全局变量 差
内容耦合 一个模块直接访问另一个模块的内部数据 最差

避坑指南:我曾经在一个项目里用了大量全局变量做模块间通信。结果后期加功能时,根本不知道哪个模块改了哪个变量。调试了三天,最后全部重构成接口传递参数。从那以后,我再也不敢用公共耦合了。

1.3 模块化设计在嵌入式中的特殊挑战

嵌入式系统资源有限,模块化不能做得太“重”。我见过有人把模块化搞成 Java 那样,一层套一层,结果 ROM 和 RAM 都不够用。

嵌入式模块化要注意几点:

  • 资源开销:函数调用有栈开销,接口抽象可能有性能损失。要在模块化和效率之间找平衡。
  • 硬件相关性:底层驱动必然依赖硬件。我的做法是:驱动层内部可以耦合硬件,但对外暴露的接口要抽象。
  • 实时性要求:中断服务程序里别搞复杂的模块调用。我习惯把中断处理拆成两部分:ISR 只做最必要的事,具体处理交给任务。

1.4 模块化设计框架图

下面这张图展示了模块化设计的核心逻辑。你看,从问题出发,经过模块拆分、接口定义、高内聚低耦合设计,最终得到可维护、可复用的系统。

模块化设计核心逻辑 复杂系统问题 模块拆分 接口定义 高内聚 + 低耦合 可维护 · 可复用 · 可测试 关键要点 • 每个模块职责单一 • 接口清晰稳定 • 避免全局变量 • 隐藏内部实现 • 考虑资源开销

1.5 一个简单的例子

说了这么多理论,来个实际例子。假设你要写一个 LED 控制模块。

不好的做法:

// 直接操作寄存器
#define LED_PORT PORTB
#define LED_PIN 5

void led_on(void) {
    LED_PORT |= (1 << LED_PIN);
}

void led_off(void) {
    LED_PORT &= ~(1 << LED_PIN);
}

这个模块耦合了硬件寄存器。换个平台,全得重写。

好的做法:

// led.h - 接口定义
typedef struct {
    void (*on)(void);
    void (*off)(void);
    void (*toggle)(void);
} led_t;

// led.c - 实现
static void gpio_led_on(void) {
    // 平台相关实现
    HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
}

static void gpio_led_off(void) {
    HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
}

static void gpio_led_toggle(void) {
    HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}

const led_t led = {
    .on = gpio_led_on,
    .off = gpio_led_off,
    .toggle = gpio_led_toggle
};

应用层只用 led.on()、led.off(),根本不知道底层是什么。换平台时,只改 led.c,接口不变。

我的习惯:写模块时,先定义接口头文件,再写实现。接口就是合同,定好了就别轻易改。这样团队协作时,大家各干各的,最后拼起来就能跑。

1.6 小结

模块化设计不是高深的理论,是实战中总结出来的经验。记住两句话:

  • 高内聚:一个模块只做一件事。
  • 低耦合:模块之间少依赖,通过接口通信。

做到这两点,你的代码会越来越清爽。后面几章,我会带你一步步实践这些原则,从接口抽象到架构设计,咱们慢慢来。