1. 驱动层设计哲学:为什么需要驱动层?

做嵌入式开发这些年,我见过太多人上来就对着寄存器一顿操作。代码能跑,但换个芯片、换个平台,全部重写。说白了,这就是没有驱动层的后果。

今天咱们聊聊驱动层设计哲学。不扯虚的,就讲清楚三个问题:为什么需要驱动层?分层架构到底在分什么?设计原则有哪些?

1.1 没有驱动层的代码长什么样?

先看一段我早期项目里的代码。那时候刚入行,觉得“封装”就是多此一举。

// 直接操作寄存器点亮LED
void led_on(void) {
    // 假设LED接在PA0
    GPIOA->BSRR = (1 << 0);   // 置位
}

void led_off(void) {
    GPIOA->BRR = (1 << 0);    // 复位
}

// 延时函数
void delay_ms(uint32_t ms) {
    for(uint32_t i = 0; i < ms * 1000; i++) {
        __NOP();
    }
}

这段代码有什么问题?

  • 硬件耦合太紧:GPIOA、BSRR、BRR 全是 STM32 特有的寄存器
  • 不可移植:换到 GD32、AT32,甚至换个引脚,全得改
  • 不可测试:没有硬件,这段代码根本跑不了
  • 业务逻辑和硬件混在一起:哪天想改成 PWM 控制亮度,得动整个函数
我曾经在一个量产项目里吃过这个亏。产品卖得好,客户说要换国产芯片降成本。结果一算,光驱动层重写就要两个月。老板脸都绿了。从那以后,我再也不敢把硬件操作和业务逻辑写在一起。

1.2 分层架构到底在分什么?

分层架构的核心,说白了就是把“做什么”和“怎么做”分开。

我习惯把嵌入式软件分成三层:

层次 职责 依赖关系 典型内容
应用层 业务逻辑、状态机、协议解析 只调用驱动层接口 按键处理、数据显示、通信协议
驱动层 硬件抽象、外设操作、资源管理 调用HAL/LL层,向上提供统一接口 GPIO、UART、SPI、I2C、定时器
硬件抽象层(HAL) 寄存器操作、芯片相关初始化 直接操作硬件 寄存器定义、中断向量、时钟配置

你想想看,应用层需要知道LED是接在PA0还是PB5吗?不需要。它只关心“调用led_on()就能亮”。这就是分层的目的——隔离变化。

1.3 驱动层设计原则

做了十几个项目后,我总结出几条铁律。嗯,每条都是用加班换来的。

原则一:接口稳定,实现可变

驱动层对外暴露的接口,一旦确定就不要轻易改。内部实现随便折腾,换芯片、换引脚、换外设库,都不影响上层代码。

// 好的驱动接口 —— 稳定
void led_init(void);
void led_on(void);
void led_off(void);
void led_toggle(void);

// 内部实现可以随便换
// 方案A:直接操作寄存器
// 方案B:使用HAL库
// 方案C:换成PWM控制亮度

原则二:单一职责

一个驱动文件只负责一种外设。GPIO驱动不要掺和定时器的事,UART驱动不要管SPI。

我个人习惯:每个外设一个.c/.h文件。比如led.c/led.h、uart.c/uart.h、spi.c/spi.h。文件多了不怕,结构清晰比什么都重要。

原则三:可配置性

驱动层应该支持通过配置来适配不同硬件。不要硬编码引脚号、波特率这些参数。

// led_cfg.h —— 配置头文件
#ifndef LED_CFG_H
#define LED_CFG_H

typedef struct {
    uint8_t port;      // GPIO端口号
    uint8_t pin;       // GPIO引脚号
    uint8_t active_level; // 有效电平:0-低电平有效,1-高电平有效
} led_cfg_t;

// 板级配置
#define LED_RED_CFG     {GPIOA, 0, 1}
#define LED_GREEN_CFG   {GPIOA, 1, 1}
#define LED_BLUE_CFG    {GPIOB, 5, 0}

#endif

原则四:错误处理不吞没

驱动层遇到错误,不能自己吃掉。要返回错误码,或者通过回调通知上层。

// 好的做法:返回错误码
int uart_send(uint8_t *data, uint16_t len);

// 坏的做法:死等或者直接忽略
void uart_send(uint8_t *data, uint16_t len) {
    while(!(USART1->SR & USART_SR_TXE)); // 死等
    // 如果硬件挂了,这里就永远卡死
}

1.4 驱动层架构图

下面这张图,是我每次做新项目都会贴在墙上的。它清楚地展示了各层之间的关系。

嵌入式软件分层架构 应用层 业务逻辑 · 状态机 · 协议解析 · 用户交互 驱动层(本章重点) GPIO驱动 · UART驱动 · SPI驱动 · I2C驱动 · 定时器驱动 统一接口 · 可配置 · 可移植 · 错误处理 硬件抽象层(HAL/LL) 寄存器操作 · 芯片初始化 · 中断向量 · 时钟配置 硬件(MCU · 外设 · 传感器) 依赖 方向 上层 依赖 下层 接口 稳定 实现 可变

1.5 什么时候该做驱动层封装?

有人问我:“做个流水灯也要封装驱动层吗?是不是小题大做?”

我的回答是:看项目规模。

  • 学习验证、一次性Demo:直接操作寄存器,怎么快怎么来
  • 产品级项目、可能迭代:必须做驱动层封装,这是投资
  • 团队协作项目:不做封装,后期维护成本指数级上升

我的判断标准:如果这个代码三个月后还要看,或者要给别人看,那就值得做驱动层封装。你想想看,三个月后的你,其实就是另一个人。别让未来的自己骂现在的你。

1.6 小结

驱动层设计哲学,说到底就一句话:把不变的接口留给上层,把变化的实现藏在下层。

这一章我们讲了:

  • 没有驱动层的代码有多坑
  • 三层架构的职责划分
  • 四条核心设计原则
  • 什么时候该做封装