4. Linux 内核驱动框架:字符设备驱动基础、platform 驱动模型、设备树简介

在网关外设驱动开发中,理解 Linux 内核的驱动框架是挂载 SPI 与 I2C 设备的前提。本章聚焦三个核心层次:字符设备驱动作为用户空间与内核交互的基本接口,platform 驱动模型作为非枚举总线的设备-驱动匹配机制,以及设备树作为硬件描述语言在嵌入式平台中的标准化用法。

学习提示: 字符设备驱动是基础,platform 模型是桥梁,设备树是硬件描述。三者共同构成了现代 Linux 嵌入式驱动开发的“铁三角”。

4.1 字符设备驱动基础

字符设备(char device)是 Linux 中最简单的设备类型,以字节流方式读写,典型代表包括串口、GPIO、SPI 控制器等。字符设备驱动的核心是 struct file_operations 结构体,它定义了用户空间 open/read/write/ioctl 等系统调用在内核中的实现。

4.1.1 核心数据结构与注册流程

组件 作用 关键函数/宏
struct cdev 内核中表示字符设备的对象 cdev_init(), cdev_add()
dev_t 设备号(主设备号 + 次设备号) MKDEV(), register_chrdev_region()
struct file_operations 设备操作函数表 .open, .read, .write, .unlocked_ioctl
class_create() 在 sysfs 中创建设备类 device_create() 自动生成设备节点

标准注册流程如下:

  1. 分配 dev_t 设备号(静态或动态)。
  2. 初始化 struct cdev 并关联 file_operations。
  3. 调用 cdev_add() 将设备加入内核。
  4. 创建 class 和 device,自动生成 /dev/ 节点。
// 字符设备驱动注册示例(简化)
static int __init my_driver_init(void)
{
    dev_t devno;
    int ret;

    // 1. 分配设备号
    ret = alloc_chrdev_region(&devno, 0, 1, "my_device");
    if (ret < 0) return ret;

    // 2. 初始化 cdev
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    // 3. 添加 cdev 到内核
    ret = cdev_add(&my_cdev, devno, 1);
    if (ret) goto err_cdev;

    // 4. 创建类与设备节点
    my_class = class_create(THIS_MODULE, "my_class");
    device_create(my_class, NULL, devno, NULL, "my_device");

    return 0;

err_cdev:
    unregister_chrdev_region(devno, 1);
    return ret;
}
关键点: 字符设备驱动必须实现 file_operations 中的 .open 和 .release,否则用户空间无法正常打开设备文件。对于 SPI/I2C 设备,.unlocked_ioctl 常用于传递 SPI 模式、时钟频率等配置参数。
字符设备驱动注册流程 alloc_chrdev_region cdev_init cdev_add class_create device_create 最终在 /dev/ 下生成设备节点,用户空间可通过 open/read/write 访问硬件

4.2 platform 驱动模型

在 Linux 内核中,并非所有设备都挂载在 PCI、USB 等可枚举总线上。对于 SoC 内部集成的 SPI 控制器、I2C 适配器、GPIO 控制器等,Linux 引入了 platform 驱动模型(也称为“平台总线”)。它使用虚拟总线 platform_bus 将设备与驱动进行匹配。

4.2.1 platform 设备与驱动分离

platform 模型的核心思想是设备与驱动分离:设备描述“有什么硬件”,驱动描述“如何操作硬件”。匹配由内核自动完成,匹配依据是 name 字段或设备树中的 compatible 属性。

组件 结构体 关键字段
platform 设备 struct platform_device .name, .id, .num_resources, .resource
platform 驱动 struct platform_driver .driver.name, .probe, .remove, .id_table
匹配方式 设备树 / 传统板级代码 of_match_table 或 id_table
// platform 驱动模板
static const struct of_device_id my_of_match[] = {
    { .compatible = "vendor,my-spi-controller" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);

static int my_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;

    // 获取 I/O 内存资源
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 注册字符设备或 SPI 控制器
    dev_info(&pdev->dev, "device probed successfully\n");
    return 0;
}

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_spi_controller",
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_driver);
常见误区: platform 驱动并不直接操作 SPI/I2C 总线协议,而是作为控制器驱动注册到 SPI 或 I2C 核心层。真正的 SPI 设备驱动应使用 struct spi_driver,I2C 设备驱动应使用 struct i2c_driver。
platform 驱动模型:设备与驱动匹配 platform 设备 SPI 控制器 #0 I2C 适配器 #1 GPIO 控制器 UART 设备 ... platform_bus 匹配依据:name / compatible 自动调用 .probe() platform 驱动 spi_controller_drv i2c_adapter_drv gpio_drv uart_drv ... 设备与驱动通过 platform_bus 匹配后,内核调用驱动的 .probe() 完成初始化

4.3 设备树(Device Tree)简介

设备树(Device Tree, DT)是一种描述硬件配置的数据结构,以树状节点形式表示 CPU、内存、外设、中断、GPIO、时钟等硬件资源。它替代了传统的板级代码(board-*.c),实现了内核代码与硬件描述的解耦。

4.3.1 设备树文件体系

文件类型 扩展名 说明
设备树源文件 .dts 可读的文本格式,描述硬件
设备树头文件 .dtsi 公共 SoC 定义,被 .dts 包含
设备树二进制 .dtb 编译后的二进制,由 bootloader 加载
设备树覆盖 .dtbo 运行时叠加的硬件片段

4.3.2 设备树节点与属性

每个设备树节点包含 compatible(驱动匹配)、reg(地址/大小)、interrupts(中断号)等属性。以下是一个 SPI 控制器的设备树节点示例:

// 设备树节点示例:SPI 控制器
spi0: spi@ff010000 {
    compatible = "vendor,spi-controller";
    reg = <0xff010000 0x1000>;          // 基地址 + 大小
    interrupts = <0 32 4>;              // 中断类型 编号 触发方式
    clocks = <&clkc 15>;                // 时钟引用
    #address-cells = <1>;
    #size-cells = <0>;

    // 挂载在 SPI 总线上的从设备
    spi_device0: sensor@0 {
        compatible = "vendor,temperature-sensor";
        reg = <0>;                       // 片选编号 0
        spi-max-frequency = <1000000>;   // 最大 SPI 时钟 1MHz
    };
};
匹配机制: 驱动中的 of_match_table 与设备树节点的 compatible 属性进行字符串匹配。匹配成功后,内核将设备树中的 reg、interrupts 等资源自动传递给驱动的 .probe() 函数。
设备树结构示意 / (根节点) cpus { } memory@0 { } soc { } spi@ff010000 i2c@... sensor@0 adc@1 设备树节点通过 compatible 属性与驱动匹配,reg 属性提供地址资源

4.3.3 设备树在驱动开发中的典型用法

在驱动代码中,通过 of_* 系列 API 读取设备树属性:

// 驱动中读取设备树属性
static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct device_node *np = dev->of_node;
    u32 max_freq;
    int irq;

    // 读取 spi-max-frequency 属性
    if (of_property_read_u32(np, "spi-max-frequency", &max_freq)) {
        dev_err(dev, "missing spi-max-frequency\n");
        return -EINVAL;
    }

    // 获取中断号
    irq = irq_of_parse_and_map(np, 0);
    if (!irq) {
        dev_err(dev, "failed to get irq\n");
        return -EINVAL;
    }

    dev_info(dev, "max freq: %u Hz, irq: %d\n", max_freq, irq);
    return 0;
}
实践要点:
  • 设备树中的 reg 属性对应 platform_get_resource(pdev, IORESOURCE_MEM, n)。
  • interrupts 属性对应 platform_get_irq(pdev, n) 或 irq_of_parse_and_map()。
  • 自定义属性(如 spi-max-frequency)使用 of_property_read_u32/index 读取。
  • 设备树覆盖(.dtbo)允许在不重新编译内核的情况下添加新硬件,适合网关产品的模块化扩展。

4.4 三者的协同关系

在网关外设驱动开发中,字符设备、platform 模型、设备树三者协同工作:

  • 设备树描述硬件拓扑和资源(地址、中断、时钟)。
  • platform 驱动通过 of_match_table 匹配设备树节点,在 .probe() 中获取资源并初始化硬件。
  • 字符设备作为用户空间的访问接口,通过 file_operations 提供 open/read/write/ioctl 操作。

对于 SPI 和 I2C 设备,platform 驱动通常用于实现控制器驱动(如 SPI 控制器、I2C 适配器),而具体的外设驱动(如温度传感器、ADC)则使用 spi_driver 或 i2c_driver 框架,这些内容将在后续章节中详细展开。

字符设备 + platform + 设备树协同架构 用户空间:/dev/spi_device0 /dev/i2c_device1 内核驱动:file_operations → platform_driver.probe() 硬件描述:设备树 .dts / .dtb(compatible / reg / interrupts) 设备树提供硬件资源 → platform 驱动匹配并初始化 → 字符设备暴露给用户空间