2. 嵌入式系统基础

做嵌入式开发久了,你会发现一个规律:代码的腐烂,往往是从架构的混乱开始的。我见过太多项目,前期跑得飞快,后期改一个功能要动半套代码。说白了,就是没打好地基。

这一章,我们来聊聊嵌入式软件架构最核心的三个东西——分层架构、模块化设计、接口定义。这三个东西绑在一起,就是你代码的骨架。

为什么需要分层?

先问个问题:你写单片机程序时,是不是经常把 GPIO 操作、算法逻辑、通信协议全塞在一个文件里?

嗯,我刚开始也这么干。结果呢?有一次客户要换 MCU,我对着 8000 行的 main.c 发呆了三天。从那以后,我彻底倒向了分层架构。

分层架构的核心思想很简单:把不同职责的代码放到不同层,层与层之间只通过接口通信。就像公司的组织架构——销售部不需要知道研发部怎么写代码,他们只需要一个 CRM 接口。

嵌入式软件典型分层(从上到下):

  • 应用层:业务逻辑、状态机、用户交互
  • 服务层:协议栈、文件系统、任务调度
  • 抽象层:硬件抽象层(HAL)、驱动接口
  • 驱动层:寄存器操作、中断处理、DMA
  • 硬件层:MCU、外设、传感器

每一层只依赖它的下一层。应用层不直接操作寄存器,驱动层不关心业务逻辑。这样换硬件时,你只需要改驱动层和抽象层,应用层基本不动。

嵌入式软件分层架构 应用层(业务逻辑、状态机) 服务层(协议栈、文件系统、调度) 硬件抽象层(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() 的实现就行。

本章核心要点:

  • 分层架构解决纵向依赖,模块化解决横向组织,接口定义解决协作契约
  • 每一层只依赖下一层,同层模块之间通过接口通信
  • 接口要清晰、稳定、文档化——这是团队协作的基础
  • 函数指针结构体是实现层间解耦的利器

好了,这一章就到这里。记住一句话:架构不是写出来的,是设计出来的。花时间把分层、模块、接口想清楚,后面写代码就是水到渠成的事。