1. 驱动层设计思想:为什么需要封装?

做单片机开发好些年头了,我见过太多人一上来就对着寄存器猛写。不是说不行,但项目一复杂,你就知道苦头在哪了。

先讲个我自己的经历。几年前做一个多传感器采集的项目,用了 STM32F4 系列。刚开始图省事,直接在 main 函数里操作寄存器。功能倒是跑通了,结果客户说要换芯片型号。好家伙,那代码改得我头皮发麻——每个外设的寄存器地址都不一样,中断号也不同,几乎等于重写。

从那以后,我养成了一个习惯:写驱动必须先想封装。说白了,就是给底层硬件穿件「外套」,让上层代码不直接跟寄存器打交道。

1.1 为什么需要封装?三个核心理由

你想想看,封装到底解决了什么问题?我总结了三件事:

  • 隔离变化——硬件换了你不用重写业务逻辑
  • 降低复杂度——上层调用者不需要知道寄存器怎么配
  • 提高复用——写好的驱动模块,下个项目直接拿来用

举个例子,你用 GPIO 控制一个 LED 灯亮灭。不封装的话,代码可能是这样:

// 直接操作寄存器
GPIOA->BSRR = (1 << 5);   // 点亮 PA5

封装之后呢?

// 调用驱动接口
led_on(LED_RED);

哪个好维护?一目了然。而且如果哪天你把 LED 换到了 PB3,只需要改驱动层,应用层代码一行不动。

核心观点:封装不是「多此一举」,而是给未来的自己留条活路。

1.2 分层架构:应用层、驱动层、HAL层

说到分层,很多人觉得是「高大上」的东西。其实没那么玄乎。我习惯把单片机软件分成三层,每层各司其职。

下面这张图能帮你快速理解三层的关系:

单片机软件分层架构 应用层(Application Layer) 业务逻辑、任务调度、用户交互 不关心硬件细节,只调用驱动接口 驱动层(Driver Layer) 外设功能封装、接口标准化、资源管理 调用HAL接口,向上提供统一API HAL层(Hardware Abstraction Layer) 寄存器操作、时钟配置、中断向量 直接与硬件打交道,屏蔽芯片差异 硬件(MCU + 外设) 调用方向:从上到下

嗯,这里要注意:箭头方向是调用关系。应用层调用驱动层,驱动层调用HAL层,HAL层操作寄存器。反过来不行——底层不能反过来调用上层。

1.3 各层职责划分

我见过不少新手把三层混在一起写。比如在应用层里直接配定时器预分频系数,或者在驱动层里写业务逻辑。这都是坏习惯。

下面这个表格,把每层该干什么、不该干什么说清楚:

层级 职责 典型内容 不该做的事
应用层 实现业务逻辑 状态机、协议解析、用户交互 直接操作寄存器、配置时钟
驱动层 封装外设功能 LED驱动、按键扫描、传感器读写 写业务逻辑、直接操作寄存器
HAL层 屏蔽硬件差异 GPIO配置、定时器初始化、中断注册 实现业务逻辑、调用应用层函数
我的习惯:写代码前先画个分层图,标清楚每层对外提供的接口。这样写起来思路清晰,后期维护也省心。

1.4 一个实际例子:LED驱动

光说理论太干,咱们看个具体的。假设你要控制一个LED灯,三层分别怎么写?

HAL层:封装GPIO的初始化操作

// hal_gpio.c
void hal_gpio_init(GPIO_TypeDef* port, uint16_t pin, uint32_t mode) {
    GPIO_InitTypeDef gpio_cfg = {0};
    gpio_cfg.Pin = pin;
    gpio_cfg.Mode = mode;
    gpio_cfg.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(port, &gpio_cfg);
}

驱动层:封装LED的开关行为

// drv_led.c
#include "hal_gpio.h"

#define LED_PORT  GPIOB
#define LED_PIN   GPIO_PIN_5

void led_init(void) {
    hal_gpio_init(LED_PORT, LED_PIN, GPIO_MODE_OUTPUT_PP);
}

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);
}

应用层:只管业务逻辑

// app_main.c
#include "drv_led.h"

void app_task(void) {
    led_init();
    while(1) {
        led_on();
        delay_ms(500);
        led_off();
        delay_ms(500);
    }
}

你看,应用层根本不知道LED接在哪个引脚,也不知道GPIO怎么配。这就是封装的价值。

避坑提醒:我曾经在一个项目里,把LED的引脚定义写在了应用层的头文件里。后来换板子,改引脚定义改了三个文件,还漏了一个。从那以后,所有硬件相关的宏定义全部放在驱动层,应用层只管调用接口。

1.5 什么时候可以「偷懒」?

当然,不是所有场景都需要严格分层。我个人的经验是:

  • 小项目(几百行代码)——可以适当简化,但至少把硬件操作封装成函数
  • 中等项目(几千行)——建议严格分层,尤其是多人协作时
  • 大型项目(上万行)——必须分层,否则后期维护成本会让你崩溃

说白了,分层架构不是「教条」,而是帮你管理复杂度的工具。用得好,事半功倍;用得僵,反而增加工作量。

嗯,这一章就聊到这儿。记住一句话:写驱动的时候,多想想「如果换芯片了,我这代码能直接复用吗?」——有这个意识,你就已经走在正确的路上了。