2. Linux内核基础

说实话,我刚入行那会儿,写驱动就是一把梭。拿到芯片手册,直接对着寄存器开干。一个文件上千行,probe 函数里塞满了硬件初始化、中断注册、文件操作接口。当时觉得挺爽,效率高啊。

直到后来接手一个项目——换了一颗主控芯片,驱动几乎要重写。那时候我才意识到,没有分层的代码,就是给自己挖坑。

为什么非得分这个层?

你想想看,驱动开发最头疼的是什么?是业务逻辑和硬件细节搅在一起。今天要适配一个新板子,明天要换一颗 sensor,后天要改一下数据上报策略。如果所有代码都揉在一块,改一个地方就得动全局。

我总结下来,分层能解决三个核心问题:

  • 复用性:核心逻辑写一次,换硬件不用重来
  • 可移植性:换平台时只改底层,上层不动
  • 可测试性:每一层都能单独测,不用等硬件就绪

核心观点:分层不是炫技,是降低维护成本的手段。说白了,就是让你少加班。

驱动分层架构:三层各自管什么?

我习惯把驱动分成三层:平台层(Platform Layer)、核心层(Core Layer)、HAL 层(Hardware Abstraction Layer)。每一层都有自己的职责,不能越界。

层级 职责 依赖关系 典型内容
平台层 硬件寄存器操作、中断处理、DMA 配置 直接操作硬件 readl/writel、ioremap、中断号
核心层 业务逻辑、数据流管理、状态机 调用 HAL 接口 数据队列、策略调度、错误处理
HAL 层 抽象硬件差异,提供统一接口 平台层实现 + 核心层调用 struct xxx_ops、回调函数表

我的习惯:HAL 层只定义接口,不实现具体逻辑。实现交给平台层,调用交给核心层。这样换硬件时,核心层代码一行都不用改。

设计原则:别让层与层之间「谈恋爱」

分层最怕什么?怕层与层之间耦合太紧。我见过有人把 HAL 层的结构体直接传到核心层用,结果换硬件时核心层也得跟着改。这就失去了分层的意义。

几个原则,我每次做架构都会贴在工位上:

  • 单向依赖:核心层只能调用 HAL 接口,不能反向依赖平台层
  • 接口稳定:HAL 层的 API 一旦定下来,尽量不改
  • 数据隔离:每一层都有自己的数据结构,不共用全局变量
  • 错误传递:底层错误通过返回值向上传递,上层统一处理

避坑指南:我曾经在一个项目里,核心层直接访问了平台层的寄存器地址。后来换了芯片,寄存器偏移全变了,核心层代码改得面目全非。从那以后,我强制要求所有硬件访问必须经过 HAL 接口。

职责划分:谁该干什么,清清楚楚

我画了一张图,把三层的职责和交互关系展示出来。你看一眼就明白了。

驱动分层架构职责划分图 驱动分层架构职责划分 平台层(Platform Layer) 寄存器读写 | 中断处理 | DMA 配置 | GPIO 控制 直接操作硬件,提供最底层能力 HAL 层(Hardware Abstraction Layer) struct xxx_ops | 回调函数表 | 设备抽象接口 定义统一接口,屏蔽硬件差异 核心层(Core Layer) 业务逻辑 | 数据流管理 | 状态机 | 策略调度 不关心硬件细节,只调用 HAL 接口 调用 HAL 接口 调用平台层实现 说明:箭头方向表示「调用/依赖」关系。核心层只依赖 HAL 接口,不直接接触硬件。 换硬件时,只需重写平台层,HAL 接口和核心层代码保持不变。

实际案例:一个 SPI 传感器的分层设计

拿一个 SPI 温湿度传感器举例。我做过类似的项目,当时就是这么拆的:

/* ========== HAL 层:定义接口 ========== */
struct sensor_ops {
    int (*init)(void);
    int (*read_temp)(float *temp);
    int (*read_humidity)(float *hum);
    int (*deinit)(void);
};

/* ========== 平台层:实现具体硬件操作 ========== */
static int spi_sensor_init(void) {
    /* 配置 SPI 时钟、GPIO CS 引脚 */
    writel(0x01, SPI_CR1);  // 使能 SPI
    gpio_set_value(CS_PIN, 1);
    return 0;
}

static int spi_sensor_read_temp(float *temp) {
    u8 buf[4];
    /* 发送读命令,接收数据 */
    spi_write_then_read(spi_dev, cmd, 1, buf, 4);
    *temp = (buf[0] << 8 | buf[1]) * 0.01;
    return 0;
}

static struct sensor_ops spi_ops = {
    .init = spi_sensor_init,
    .read_temp = spi_sensor_read_temp,
    .read_humidity = spi_sensor_read_humidity,
    .deinit = spi_sensor_deinit,
};

/* ========== 核心层:业务逻辑 ========== */
static void sensor_work_handler(struct work_struct *work) {
    float temp, hum;
    
    /* 只调用 HAL 接口,不关心底层是 SPI 还是 I2C */
    ops->read_temp(&temp);
    ops->read_humidity(&hum);
    
    /* 数据上报、阈值判断等业务逻辑 */
    if (temp > THRESHOLD) {
        send_alert(temp);
    }
}

关键点:核心层里没有出现任何 SPI 相关的代码。如果下次换成 I2C 传感器,只需要重新实现 platform 层的函数,核心层代码直接复用。

分层带来的实际收益

我统计过,采用分层架构后,驱动代码的复用率能提升 60% 以上。具体来说:

  • 换芯片:只改平台层,平均 2 天搞定
  • 加功能:在核心层加逻辑,不影响已有硬件
  • 修 Bug:定位问题范围缩小,不用通读整个驱动
  • 团队协作:硬件工程师写平台层,软件工程师写核心层,互不干扰

我的建议:刚开始做分层时,别追求完美。先拆出 HAL 接口,把硬件操作隔离出去。等架构稳定了,再逐步细化核心层的业务逻辑。一步到位反而容易过度设计。

嗯,分层设计说白了就是「把变化隔离起来」。硬件会变,平台会变,但业务逻辑相对稳定。让稳定的部分不依赖变化的部分,这就是分层的本质。