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 模块化编程知识体系
下面这张图总结了本章的核心知识点,你可以把它当作模块化编程的「地图」:
嗯,这张图把本章的知识点串起来了。你写代码的时候,可以对照着看看:你的模块内聚够不够高?耦合够不够低?文件职责分清楚了没?变量用对了吗?
我的个人习惯:每写一个模块,先画一个简单的框图,标清楚这个模块对外的接口是什么,内部有哪些static变量。画清楚了再动手写代码。这样写出来的代码,基本不会出现耦合过高的问题。