第1章:驱动框架基础:Linux设备驱动模型简介

各位同学,今天我们来聊聊驱动框架的基础。说实话,我刚入行那会儿,对Linux设备驱动模型也是一头雾水。那时候写驱动就是简单粗暴地注册一个字符设备,然后跟硬件直接打交道。后来项目越做越大,发现这种"野路子"根本撑不住——代码复用性差、调试困难、移植更是噩梦。

嗯,所以Linux内核团队搞出了这套设备驱动模型。说白了,就是把硬件和驱动的关系规范化、标准化。你想想看,一个SoC上可能有几十个外设,每个外设的驱动都自己搞一套注册方式,那不乱套了?

1.1 为什么需要设备驱动模型?

我遇到过不少刚入行的工程师,觉得设备驱动模型就是个"花架子",直接操作硬件寄存器多痛快。但实际项目里,这种想法会吃大亏。

设备驱动模型要解决的核心问题有三个:

  • 解耦:把硬件设备和驱动程序分开管理
  • 标准化:统一设备注册、发现、匹配的流程
  • 可移植性:同样的驱动代码,换颗芯片也能用

举个例子,我在做一款智能手表项目时,需要同时支持多个厂家的加速度传感器。如果没有设备驱动模型,每个传感器驱动都得自己写一套probe、remove、suspend/resume逻辑。有了模型之后,只需要实现核心的读写函数,剩下的框架都帮你搞定了。

1.2 三大核心:platform bus、device、driver

Linux设备驱动模型里,最核心的就是这三个概念。我习惯把它们比作"相亲"的过程——device是"征婚者",driver是"应征者",bus就是那个"媒人"。

1.2.1 platform bus

platform bus是Linux内核中一种虚拟的总线。为什么叫"虚拟"?因为它不像I2C、SPI那样有物理总线。它专门用来管理那些"挂在CPU内存空间上"的设备——说白了,就是那些直接通过地址访问寄存器的外设。

我在项目中遇到过一种情况:一个UART控制器,在芯片手册里说它挂在APB总线上,但在Linux驱动里,它其实是通过platform bus来管理的。为什么?因为APB总线是芯片内部的物理总线,Linux不直接管这个,它用platform bus来抽象。

关键点:platform bus负责维护两个链表——device链表和driver链表。当有新的device或driver注册时,bus会遍历另一个链表,看看有没有匹配的。

1.2.2 device

device代表一个硬件设备。在设备树(Device Tree)流行的今天,device通常由设备树节点描述。比如:

uart1: serial@10001000 {
    compatible = "vendor,uart-v1";
    reg = <0x10001000 0x1000>;
    interrupts = <33>;
    clocks = <&clk_uart>;
};

这段设备树描述了一个UART设备,它的寄存器基地址是0x10001000,中断号是33。内核启动时,会解析设备树,为每个节点创建一个platform_device结构体。

我个人习惯在写驱动前,先把设备树仔细看一遍。因为很多坑都藏在设备树的属性里。比如reg属性的大小对不对?中断号有没有冲突?这些在设备树里就能发现。

1.2.3 driver

driver就是驱动本身。它包含了一组操作函数,比如probe、remove、suspend、resume。当driver和device匹配成功后,内核会调用driver的probe函数。

static struct platform_driver my_uart_driver = {
    .probe  = my_uart_probe,
    .remove = my_uart_remove,
    .driver = {
        .name = "my_uart",
        .of_match_table = my_uart_of_match,
    },
};

module_platform_driver(my_uart_driver);

你看,这个驱动注册非常简单。module_platform_driver宏帮我们做了很多事情。但要注意,probe函数里才是真正的"重头戏"——申请资源、初始化硬件、注册字符设备等等。

我的经验:probe函数里一定要做错误处理。我曾经在一个项目中,probe函数里申请了三个资源,第二个申请失败了,结果第一个资源没释放,导致内存泄漏。后来我养成了习惯:每个资源申请后,如果后续失败,一定要goto到对应的错误处理标签。

1.3 驱动与硬件层的交互方式

驱动写好了,怎么跟硬件打交道?这里有几个常用的方式:

1.3.1 内存映射I/O(MMIO)

这是最常用的方式。通过ioremap把物理地址映射到内核虚拟地址空间,然后直接读写寄存器。

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

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    // 读写寄存器
    writel(0x01, base + UART_CTRL_REG);
    val = readl(base + UART_STATUS_REG);

    return 0;
}

这里有个细节:devm_ioremap_resource是"managed"版本,它会在驱动卸载时自动释放映射。我强烈建议用这种带devm_前缀的函数,能省去很多资源管理的麻烦。

1.3.2 中断处理

硬件事件来了,总不能一直轮询吧?中断是必须的。

static irqreturn_t my_uart_irq_handler(int irq, void *dev_id)
{
    struct my_uart_dev *uart = dev_id;
    u32 status;

    status = readl(uart->base + UART_STATUS_REG);
    if (status & RX_READY) {
        // 处理接收数据
    }

    return IRQ_HANDLED;
}

static int my_uart_probe(struct platform_device *pdev)
{
    int irq, ret;

    irq = platform_get_irq(pdev, 0);
    ret = devm_request_irq(&pdev->dev, irq, my_uart_irq_handler,
                           IRQF_TRIGGER_HIGH, "my_uart", uart);
    // ...
}

避坑指南:我曾经在中断处理函数里调用了printk,结果导致系统死锁。记住,中断上下文里不能调用可能引起睡眠的函数。比如kmalloc(GFP_KERNEL)就不行,要用GFP_ATOMIC。

1.3.3 DMA传输

对于大数据量的传输,比如音频、视频,用CPU一个一个字节搬数据太浪费了。这时候要用DMA。

DMA的交互稍微复杂一些,需要配置DMA控制器、申请DMA缓冲区、设置传输描述符等。但好处是CPU可以去做别的事情,传输完成后DMA会触发中断通知你。

1.4 匹配机制:设备树与驱动如何"牵手"

前面说了,device和driver通过bus来匹配。那具体怎么匹配的呢?

核心就是设备树里的compatible属性和驱动里的of_match_table。比如:

// 设备树
compatible = "vendor,uart-v1";

// 驱动
static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "vendor,uart-v1", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);

当内核启动时,platform bus会遍历所有device,对每个device,遍历driver链表,比较compatible字符串。匹配上了,就调用probe。

这里有个小技巧:compatible字符串的命名规范是"厂商,设备名-版本"。比如"ti,am3359-uart"。这样能避免命名冲突。

1.5 知识体系总览

为了让大家更直观地理解,我画了一张图:

Linux设备驱动模型核心结构 Platform Bus(虚拟总线) 匹配(Match) Device 设备树节点描述 compatible, reg, irq Driver probe, remove suspend, resume 驱动与硬件交互方式 MMIO(内存映射I/O) 中断处理 DMA传输

这张图把整个流程串起来了。从上到下看:Platform Bus负责匹配Device和Driver,匹配成功后,Driver通过MMIO、中断、DMA等方式与硬件交互。

1.6 小结

这一章我们讲了Linux设备驱动模型的三个核心概念:platform bus、device、driver。说白了,就是一套标准化的"相亲"流程。device描述硬件有什么资源,driver描述怎么操作这些资源,bus负责牵线搭桥。

驱动与硬件的交互方式主要有三种:MMIO直接读写寄存器、中断处理异步事件、DMA批量传输数据。这三种方式在实际项目中经常组合使用。

我个人觉得,理解这套模型的关键在于"抽象"二字。不要把它想得太复杂,它就是一套帮你管理硬件资源的框架。你只要按照它的规则来写驱动,剩下的框架帮你搞定。

给新手的建议:刚开始学驱动开发,不要急着看复杂的驱动代码。先找一个简单的GPIO驱动或者UART驱动,把probe函数里的每一行代码都搞清楚。我当时就是这么过来的,一个简单的LED驱动,反复看了三天,才真正理解了platform_driver的工作流程。


公众号:蓝海资料掘金营,微信deep3321