嵌入式软件架构基础:分层、模块化与接口
做嵌入式开发久了,你会发现一个规律:代码的腐烂,往往是从架构的混乱开始的。我见过太多项目,前期跑得飞快,后期改一个功能要动半套代码。说白了,就是没打好地基。
这一章,我们来聊聊嵌入式软件架构最核心的三个东西——分层架构、模块化设计、接口定义。这三个东西绑在一起,就是你代码的骨架。
为什么需要分层?
先问个问题:你写单片机程序时,是不是经常把 GPIO 操作、算法逻辑、通信协议全塞在一个文件里?
嗯,我刚开始也这么干。结果呢?有一次客户要换 MCU,我对着 8000 行的 main.c 发呆了三天。从那以后,我彻底倒向了分层架构。
分层架构的核心思想很简单:把不同职责的代码放到不同层,层与层之间只通过接口通信。就像公司的组织架构——销售部不需要知道研发部怎么写代码,他们只需要一个 CRM 接口。
嵌入式软件典型分层(从上到下):
- 应用层:业务逻辑、状态机、用户交互
- 服务层:协议栈、文件系统、任务调度
- 抽象层:硬件抽象层(HAL)、驱动接口
- 驱动层:寄存器操作、中断处理、DMA
- 硬件层:MCU、外设、传感器
每一层只依赖它的下一层。应用层不直接操作寄存器,驱动层不关心业务逻辑。这样换硬件时,你只需要改驱动层和抽象层,应用层基本不动。
模块化设计:高内聚、低耦合
分层解决了纵向的依赖问题,那横向呢?同一个层里,不同功能之间怎么组织?
答案就是模块化。每个模块只做一件事,并且做好。我个人的习惯是:一个 .c 文件不超过 300 行,一个函数不超过 50 行。超过这个数,我就开始考虑拆模块了。
模块化设计原则:
- 单一职责:一个模块只负责一个功能领域
- 接口清晰:对外暴露的函数尽量少,内部实现对外不可见
- 无循环依赖:A 模块不能既调用 B,又被 B 调用
- 可替换性:换一个模块实现,不影响其他模块
举个例子。我之前做一个温控项目,把温度采集、PID 计算、加热控制全写在一个模块里。后来要换温度传感器,结果 PID 参数也要跟着调——因为耦合太深了。重构之后,我把它们拆成三个独立模块:temp_sensor、pid_controller、heater_driver。每个模块只暴露 3-5 个函数接口。换传感器?只改 temp_sensor 模块,其他两个完全不动。
接口定义:模块之间的契约
分层和模块化都离不开一个东西——接口。接口就是模块之间的契约。你提供什么服务,我传入什么参数,返回什么结果,都得说清楚。
我见过最坑的代码是什么?一个函数叫 process_data(),参数是 void *,返回值是 int。你根本不知道它要处理什么数据,也不知道返回的 int 是状态码还是数据长度。这种接口,说白了就是给自己挖坑。
接口定义避坑指南(我踩过的):
- 不要用 void * 传复杂数据——类型安全全没了,调试时想死
- 返回值要统一风格:要么全部返回错误码,要么全部返回指针,别混着来
- 参数顺序要固定:我习惯「输出参数放前面,输入参数放后面,长度参数紧跟缓冲区」
- 接口文档要写:哪怕只是注释,也要说明「什么条件下调用」「调用后有什么副作用」
来看一个我常用的接口风格:
/* 温度传感器模块接口 */
/* 返回值:0=成功,-1=参数错误,-2=硬件错误 */
int temp_sensor_init(void);
int temp_sensor_read(float *temperature);
int temp_sensor_set_range(float min, float max);
void temp_sensor_deinit(void);
你看,每个函数做什么,参数是什么,返回值代表什么,一目了然。调用者不需要知道内部用了 I2C 还是 SPI,也不需要知道寄存器地址。这就是接口封装的价值。
接口与分层的关系
分层架构中,接口的作用更加关键。层与层之间的接口,我建议用结构体函数指针表或者抽象接口头文件来定义。这样上层代码只依赖接口,不依赖具体实现。
| 接口类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 函数直接调用 | 同层模块之间 | 简单直接 | 耦合度高 |
| 函数指针结构体 | 层间接口、驱动抽象 | 可替换、可测试 | 代码量稍大 |
| 回调函数 | 事件驱动、异步处理 | 解耦彻底 | 调试困难 |
| 消息队列 | 多任务通信 | 松耦合、可扩展 | 需要 RTOS 支持 |
我个人最常用的是函数指针结构体。比如一个 UART 驱动接口:
typedef struct {
int (*init)(uint32_t baudrate);
int (*send)(const uint8_t *data, uint32_t len);
int (*receive)(uint8_t *buffer, uint32_t timeout_ms);
void (*irq_handler)(void);
} uart_driver_t;
/* 使用示例 */
const uart_driver_t *uart = get_uart_driver(UART_1);
uart->init(115200);
uart->send("Hello", 5);
这样上层代码根本不知道底层用的是 STM32 的 USART 还是 NXP 的 LPUART。换平台?换一个 get_uart_driver() 的实现就行。
本章核心要点:
- 分层架构解决纵向依赖,模块化解决横向组织,接口定义解决协作契约
- 每一层只依赖下一层,同层模块之间通过接口通信
- 接口要清晰、稳定、文档化——这是团队协作的基础
- 函数指针结构体是实现层间解耦的利器
好了,这一章就到这里。记住一句话:架构不是写出来的,是设计出来的。花时间把分层、模块、接口想清楚,后面写代码就是水到渠成的事。