4、驱动初始化与注册流程:platform_driver注册、设备树匹配、端口资源申请

好,咱们进入第四章。这一章讲的是驱动怎么“活”起来——说白了,就是UART驱动从代码到跑在板子上的完整初始化流程。我见过不少工程师,驱动能跑但不知道为啥能跑,出了问题就抓瞎。今天咱们把这块掰开揉碎了讲。

4.1 platform_driver注册——驱动的“身份证”

Linux内核里,UART驱动通常以platform_driver的形式存在。为什么?因为UART控制器大多挂在内核总线上,不是PCI那种可枚举的,而是需要设备树告诉内核“这里有颗UART”。

注册入口很简单,就一个宏:

module_platform_driver(uart_driver);

展开后其实就是:

static int __init uart_init(void) {
    return platform_driver_register(&uart_driver);
}
module_init(uart_init);

这里有个细节——probe函数什么时候被调用? 不是注册完立刻调,而是等设备和驱动匹配上之后。匹配机制有三种:

匹配方式 说明 优先级
设备树 compatible 最常用,匹配设备树节点的compatible属性 高
ACPI ID X86平台用,ARM上很少见 中
name表 传统方式,通过platform_device.name匹配 低

核心要点: 设备树匹配是现在的主流。你的驱动里一定要有of_match_table,否则probe可能永远不执行。

4.2 设备树匹配——驱动和设备“对上暗号”

设备树里一个UART节点大概长这样:

uart0: serial@10000000 {
    compatible = "vendor,uart-1.0";
    reg = <0x10000000 0x1000>;
    interrupts = <33>;
    clocks = <&uart_clk>;
    status = "okay";
};

驱动里对应的匹配表:

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

我刚开始做驱动时,犯过一个低级错误——compatible字符串写错了大小写。设备树里是"UART-1.0",驱动里写"uart-1.0",结果probe死活不执行。查了两天才发现,唉,这种坑踩过一次就记住了。

注意: compatible匹配是精确字符串匹配,多一个空格、大小写不对都不行。建议从设备树里直接复制字符串到驱动里。

4.3 端口资源申请——IO内存、中断号

probe函数被调用后,第一件事就是拿资源。UART驱动需要两类核心资源:寄存器地址空间和中断号。

4.3.1 IO内存申请

寄存器地址从设备树的reg属性来。获取方式:

struct resource *res;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
    dev_err(&pdev->dev, "failed to get memory resource\n");
    return -ENXIO;
}

uart_base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(uart_base)) {
    return PTR_ERR(uart_base);
}

这里我习惯用devm_ioremap_resource而不是ioremap。为什么?因为前者会自动检查资源冲突,而且内存释放由内核管理,不用手动操心。我在项目中见过有人用ioremap忘了释放,导致模块反复加载卸载后系统崩溃——这种问题很难排查。

4.3.2 中断号申请

中断号获取有两种方式:

// 方式一:从设备树直接拿
int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
    dev_err(&pdev->dev, "failed to get irq\n");
    return irq;
}

// 方式二:通过设备树解析(适用于复杂中断)
int irq = irq_of_parse_and_map(pdev->dev.of_node, 0);

拿到中断号后,注册中断处理函数:

ret = devm_request_irq(&pdev->dev, irq, uart_isr, 0,
                       dev_name(&pdev->dev), port);
if (ret) {
    dev_err(&pdev->dev, "failed to request irq %d\n", irq);
    return ret;
}

经验之谈: 中断标志位(第四个参数)别乱填。如果UART支持唤醒功能,记得加上IRQF_NO_SUSPEND,否则系统休眠后UART中断可能被屏蔽。

4.4 初始化顺序与依赖——谁先谁后有讲究

驱动初始化不是一股脑全干完,得有顺序。我总结了一个标准流程:

  1. 获取资源——先拿IO内存和中断号,拿不到后面全白搭
  2. 时钟使能——UART模块需要时钟才能工作,先开时钟再操作寄存器
  3. 复位释放——有些SoC的UART默认处于复位状态,需要先释放
  4. 寄存器初始化——配置波特率、数据位、停止位等
  5. 中断注册——等硬件准备好再注册中断,避免假中断
  6. tty设备注册——最后才注册到tty层,让用户空间能访问

这里有个容易忽略的点——时钟依赖。我曾经在一个项目里,UART驱动probe时直接操作寄存器,结果系统卡死。查了半天发现是时钟还没稳定,读寄存器直接挂起。后来加了clk_prepare_enable和延时,问题解决。

避坑指南: 如果UART依赖外部晶振或PLL,一定要等时钟稳定后再操作寄存器。建议在probe里加一个udelay(10)或轮询时钟状态寄存器。

4.5 整体流程SVG图

下面这张图把整个初始化流程串起来了,建议你保存下来对照着看:

UART驱动初始化与注册流程 UART驱动初始化与注册流程 module_platform_driver 设备树compatible匹配 probe函数执行 获取IO内存资源 获取中断号资源 时钟使能与复位释放 寄存器初始化配置 中断注册与tty设备注册 关键依赖顺序: 1. 先拿资源,再开时钟 2. 时钟稳定后,操作寄存器 3. 硬件就绪后,注册中断 4. 最后注册tty设备

4.6 初始化顺序检查清单

最后,我整理了一份检查清单。每次写UART驱动probe时,对着这个清单过一遍,能省不少调试时间:

步骤 操作 常见错误
1 platform_get_resource获取MEM reg属性写错或索引不对
2 devm_ioremap_resource映射 地址冲突或长度越界
3 platform_get_irq获取中断 interrupts属性缺失或类型不对
4 clk_prepare_enable开时钟 时钟未定义或频率不对
5 复位释放操作 复位寄存器地址搞错
6 寄存器初始化(波特率等) 分频系数计算错误
7 devm_request_irq注册中断 中断号无效或flags冲突
8 tty_port/tty_driver注册 ops函数指针未赋值

重要提醒: 如果probe函数中间某一步失败,记得把前面已经申请的资源释放掉。用devm_系列函数可以自动处理,但时钟和中断释放还是得手动写清楚。