4、驱动分层架构:芯片级、板级、应用级,接口隔离实战

驱动代码写成一坨,是嵌入式项目最常见的噩梦。

我见过太多项目,一个 gpio_set() 函数里直接操作寄存器,上层业务逻辑也掺和进来。换颗芯片?重写。换块板子?重写。加个功能?到处打补丁。

说白了,问题出在「没有分层」。今天我们就聊聊怎么把驱动拆成三层:芯片级、板级、应用级。每层各司其职,接口隔离,互不干扰。

4.1 为什么需要三层?

先看一个反面教材。我曾经接手过一个智能家居项目,LED 控制代码长这样:

// 反面教材:所有逻辑揉在一起
void led_on(void) {
    // 直接操作寄存器
    *(volatile uint32_t *)0x40020C14 |= (1 << 13);
    // 还带延时
    for (int i = 0; i < 100000; i++);
}

这个函数有什么问题?

  • 芯片绑定:寄存器地址写死,换 STM32 到 GD32 就得改。
  • 板级绑定:LED 接在哪个引脚?写死在代码里。
  • 逻辑耦合:延时混在驱动里,上层想改闪烁频率?没门。

分层之后,同样的功能,代码结构会清晰很多。我习惯把驱动拆成三层,每层只做一件事:

层级 职责 依赖 典型文件
芯片级 操作寄存器,提供硬件原子操作 芯片手册 stm32_gpio.h
板级 引脚映射、外设配置、板级资源管理 芯片级接口 board_led.h
应用级 业务逻辑、状态机、策略 板级接口 app_blink.h

核心原则:上层只能调用相邻下层接口。应用级不能直接调芯片级,板级不能跳过芯片级去操作寄存器。

4.2 芯片级驱动:跟寄存器打交道

芯片级驱动是最底层。它的任务就是封装寄存器操作,提供「原子化」的硬件接口。

举个例子,GPIO 的芯片级驱动:

// chip_gpio.h - 芯片级 GPIO 接口
#ifndef CHIP_GPIO_H
#define CHIP_GPIO_H

#include <stdint.h>

// 初始化 GPIO 端口
void chip_gpio_init(uint32_t port, uint32_t pin, uint32_t mode);

// 设置引脚输出电平
void chip_gpio_set(uint32_t port, uint32_t pin, uint8_t level);

// 读取引脚输入电平
uint8_t chip_gpio_get(uint32_t port, uint32_t pin);

#endif

你看,这个接口里只有「端口号」、「引脚号」、「模式」、「电平」这些硬件概念。没有「LED」、「按键」、「蜂鸣器」这些业务词汇。

我在项目中遇到过一种情况:有人把芯片级接口设计成 chip_led_on()。这就不对了。芯片级不知道什么是 LED,它只知道 GPIO 的 0 和 1。

我的习惯:芯片级接口用 chip_ 前缀,参数用纯硬件术语。这样换芯片时,只需要重写这一层,上层完全不动。

4.3 板级驱动:把引脚映射成功能

板级驱动是承上启下的关键层。它把芯片级的「端口+引脚」映射成具体的「板级功能」。

比如,某块板子上 LED 接在 PA5:

// board_led.h - 板级 LED 驱动
#ifndef BOARD_LED_H
#define BOARD_LED_H

#include "chip_gpio.h"

// 板级 LED 定义
#define BOARD_LED_PORT    GPIOA
#define BOARD_LED_PIN     5

// 板级接口:开灯、关灯、翻转
void board_led_on(void);
void board_led_off(void);
void board_led_toggle(void);

#endif

实现的时候,板级驱动调用芯片级接口:

// board_led.c
#include "board_led.h"

void board_led_on(void) {
    chip_gpio_set(BOARD_LED_PORT, BOARD_LED_PIN, 1);
}

void board_led_off(void) {
    chip_gpio_set(BOARD_LED_PORT, BOARD_LED_PIN, 0);
}

void board_led_toggle(void) {
    uint8_t level = chip_gpio_get(BOARD_LED_PORT, BOARD_LED_PIN);
    chip_gpio_set(BOARD_LED_PORT, BOARD_LED_PIN, !level);
}

注意,板级驱动里没有寄存器操作。它只做「映射」和「组合」。换板子?改 board_led.h 里的引脚定义就行,芯片级和应用级都不用动。

我曾经踩过的坑:板级驱动里写了延时、滤波、状态机。结果换板子时,这些逻辑也得跟着改。记住,板级只做「映射」,不做「策略」。

4.4 应用级驱动:只管业务逻辑

应用级驱动是离业务最近的一层。它只调用板级接口,不关心硬件细节。

比如,一个呼吸灯效果:

// app_blink.h - 应用级呼吸灯
#ifndef APP_BLINK_H
#define APP_BLINK_H

void app_blink_start(uint16_t period_ms);
void app_blink_stop(void);

#endif

// app_blink.c
#include "app_blink.h"
#include "board_led.h"
#include "os_timer.h"

static uint16_t g_period;

void app_blink_start(uint16_t period_ms) {
    g_period = period_ms;
    // 启动定时器,周期性调用 board_led_toggle
    os_timer_start(g_period, board_led_toggle);
}

void app_blink_stop(void) {
    os_timer_stop();
    board_led_off();
}

你看,应用级不知道 LED 接在哪个引脚,不知道芯片是 STM32 还是 GD32。它只知道「调用 board_led_toggle 就能翻转 LED」。

这就是接口隔离的价值。换芯片?重写芯片级。换板子?重写板级。业务逻辑?不动。

4.5 三层架构的依赖关系图

下面这张图,是我每次做驱动架构时都会画一遍的。它清晰地展示了三层之间的依赖方向:

驱动分层架构 & 接口隔离依赖图 应用级驱动 app_blink.c / app_sensor.c 板级驱动 board_led.c / board_key.c 芯片级驱动 chip_gpio.c / chip_uart.c 调用 调用 依赖方向:应用级 → 板级 → 芯片级。上层只能调用相邻下层,禁止跨层调用。

4.6 接口隔离的落地检查清单

每次写完驱动,我都会对照这个清单检查一遍。你可以直接拿去用:

检查项 通过标准 常见违规
芯片级接口 参数只有端口、引脚、寄存器值 出现 LED、按键等业务名词
板级接口 只做映射和组合,无延时/状态机 板级函数里写 HAL_Delay()
应用级接口 只调板级接口,不碰芯片级 应用代码里出现 GPIOA、0x40020C14
跨层调用 无应用级直接调芯片级 app_led.c 里 #include "chip_gpio.h"

一个小技巧:在芯片级头文件里加编译检查。比如 #ifdef APP_LAYER 就报错,防止应用层误引用。

4.7 实战案例:从混乱到分层

最后分享一个真实案例。我之前做一个数据采集器,原来的代码长这样:

// 混乱版:采集、处理、输出全在一个文件
void adc_collect(void) {
    // 芯片级:启动 ADC
    ADC1->CR2 |= ADC_CR2_SWSTART;
    while(!(ADC1->SR & ADC_SR_EOC));
    uint16_t val = ADC1->DR;
    
    // 应用级:滤波
    static uint16_t last_val;
    val = (val + last_val) / 2;
    last_val = val;
    
    // 板级:通过串口发送
    USART1->DR = val & 0xFF;
    while(!(USART1->SR & USART_SR_TC));
}

重构之后,分成三层:

// 芯片级:chip_adc.c
uint16_t chip_adc_read(void) {
    ADC1->CR2 |= ADC_CR2_SWSTART;
    while(!(ADC1->SR & ADC_SR_EOC));
    return ADC1->DR;
}

// 板级:board_sensor.c
uint16_t board_sensor_get_value(void) {
    return chip_adc_read();
}

// 应用级:app_monitor.c
void app_monitor_process(void) {
    uint16_t raw = board_sensor_get_value();
    // 滤波、阈值判断、报警逻辑...
}

重构之后,换 ADC 芯片?只改 chip_adc.c。换传感器?只改 board_sensor.c。业务逻辑调整?只改 app_monitor.c。

嗯,这就是接口隔离的魅力。代码写出来,自己看着舒服,别人接手也轻松。