2. 模块化编程思想:高内聚低耦合原则

模块化编程,说白了就是把一个复杂的嵌入式软件拆成若干个独立的小模块。每个模块各司其职,互不干扰。我刚开始做STM32项目时,也喜欢把所有代码塞进一个main.c里,结果项目一大了,找个bug就像大海捞针。后来才明白,模块化不是花架子,是真正能救命的工程方法。

核心思想:模块化编程 = 高内聚 + 低耦合。高内聚让模块内部拧成一股绳,低耦合让模块之间尽量少说话。

2.1 高内聚:模块内部要「专一」

高内聚的意思是:一个模块只做一件事,并且把这件事做到极致。比如LED控制模块,它只负责初始化GPIO、点亮、熄灭、闪烁。你千万别把按键扫描的逻辑也塞进去。

我在项目中遇到过这样的坑:一个同事把UART收发、定时器配置、LED闪烁全写在一个文件里。后来要改波特率,结果LED闪烁频率也跟着变了。你说这找谁说理去?

内聚类型 说明 推荐度
功能内聚 模块内所有元素共同完成一个功能 ⭐⭐⭐⭐⭐
顺序内聚 模块内元素按顺序执行,输出作为输入 ⭐⭐⭐⭐
通信内聚 模块内元素操作同一数据 ⭐⭐⭐
逻辑内聚 模块内元素通过参数选择执行 ⭐⭐
偶然内聚 模块内元素毫无关联 ❌ 避免

你想想看,一个LED模块如果既管显示又管按键,那它就不是「高内聚」。正确的做法是:LED模块只管LED,按键模块只管按键,各玩各的。

2.2 低耦合:模块之间要「客气」

低耦合就是模块之间尽量少依赖。A模块改了,B模块不应该受影响。我见过最离谱的代码:一个全局变量在8个文件里被修改,最后谁都不敢动它。

耦合度从高到低大致分这么几类:

  • 内容耦合:一个模块直接修改另一个模块的内部数据。这是最糟糕的,千万别干。
  • 公共耦合:多个模块共享同一个全局变量。能避免就避免。
  • 控制耦合:一个模块通过参数控制另一个模块的流程。偶尔用用还行。
  • 数据耦合:模块之间只传递简单数据。这是最理想的。

我曾经踩过的坑:一个项目里,我在timer.c里定义了一个全局变量 uint32_t sys_tick,然后在led.c、uart.c、key.c里都用extern引用。后来要加一个RTC模块,结果发现sys_tick被改了,整个系统时间都乱了。从那以后,我立了个规矩:全局变量必须封装在模块内部,对外只提供接口函数。

2.3 头文件(.h)与源文件(.c)的职责划分

很多新手搞不清.h和.c到底该放什么。我个人的习惯是:.c放实现,.h放接口。就这么简单。

文件类型 放什么 不放什么
.h 头文件 函数声明、宏定义、类型定义、extern变量声明 函数实现、变量定义
.c 源文件 函数实现、模块内部变量定义、static函数 对外暴露的实现细节

举个例子,一个LED模块:

// led.h
#ifndef __LED_H
#define __LED_H

#include "stm32f1xx_hal.h"

/* 对外接口 */
void LED_Init(void);
void LED_On(void);
void LED_Off(void);
void LED_Toggle(void);

#endif
// led.c
#include "led.h"

/* 模块内部变量,对外不可见 */
static GPIO_TypeDef* LED_PORT = GPIOB;
static uint16_t LED_PIN = GPIO_PIN_0;

void LED_Init(void)
{
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = LED_PIN;
    gpio.Mode = GPIO_MODE_OUTPUT_PP;
    gpio.Pull = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(LED_PORT, &gpio);
}

void LED_On(void)
{
    HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET);
}

void LED_Off(void)
{
    HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET);
}

void LED_Toggle(void)
{
    HAL_GPIO_TogglePin(LED_PORT, LED_PIN);
}

小技巧:头文件里只放别人需要知道的东西。LED_PORT和LED_PIN这种实现细节,用static藏在.c里就好。这样以后换GPIO引脚,只需要改.c文件,所有调用者都不用动。

2.4 全局变量与局部变量的使用规范

全局变量就像公共厕所,谁都能用,但谁都不爱惜。局部变量就像自家卫生间,自己用着放心。

我个人的使用规范是这样的:

  • 能用局部变量,绝不用全局变量。函数内部临时数据,全部用局部变量。
  • 模块内部共享数据,用static全局变量。只在当前.c文件内可见,对外隐藏。
  • 跨模块共享数据,用接口函数。通过get/set函数访问,不要直接extern。
  • 真正需要全局的,用extern声明在.h里。但这种情况极少,我一年也用不了几次。
变量类型 作用域 生命周期 使用场景
局部变量 函数内部 函数执行期间 临时计算、循环计数
static局部变量 函数内部 程序运行期间 需要保持状态的计数器
static全局变量 当前.c文件 程序运行期间 模块内部共享数据
extern全局变量 整个程序 程序运行期间 极少使用,尽量用接口替代

注意:在中断服务函数和主循环之间共享数据时,记得加volatile关键字。我曾经因为忘了加volatile,调试了一个下午才发现是编译器优化把变量值给优化掉了。

2.5 模块化编程知识体系

下面这张图总结了本章的核心知识点,你可以把它当作模块化编程的「地图」:

模块化编程知识体系 模块化编程 高内聚 功能内聚 顺序内聚 通信内聚 偶然内聚 ❌ 低耦合 数据耦合 ✅ 控制耦合 公共耦合 ❌ 内容耦合 ❌ .h vs .c 职责 .h: 接口声明 .c: 实现细节 别混着放 ❌ 变量使用规范 局部变量优先 static封装 少用extern ❌ 核心原则:.c藏实现,.h露接口,static封变量,接口传数据

嗯,这张图把本章的知识点串起来了。你写代码的时候,可以对照着看看:你的模块内聚够不够高?耦合够不够低?文件职责分清楚了没?变量用对了吗?

我的个人习惯:每写一个模块,先画一个简单的框图,标清楚这个模块对外的接口是什么,内部有哪些static变量。画清楚了再动手写代码。这样写出来的代码,基本不会出现耦合过高的问题。