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 初始化顺序与依赖——谁先谁后有讲究
驱动初始化不是一股脑全干完,得有顺序。我总结了一个标准流程:
- 获取资源——先拿IO内存和中断号,拿不到后面全白搭
- 时钟使能——UART模块需要时钟才能工作,先开时钟再操作寄存器
- 复位释放——有些SoC的UART默认处于复位状态,需要先释放
- 寄存器初始化——配置波特率、数据位、停止位等
- 中断注册——等硬件准备好再注册中断,避免假中断
- tty设备注册——最后才注册到tty层,让用户空间能访问
这里有个容易忽略的点——时钟依赖。我曾经在一个项目里,UART驱动probe时直接操作寄存器,结果系统卡死。查了半天发现是时钟还没稳定,读寄存器直接挂起。后来加了clk_prepare_enable和延时,问题解决。
避坑指南: 如果UART依赖外部晶振或PLL,一定要等时钟稳定后再操作寄存器。建议在probe里加一个udelay(10)或轮询时钟状态寄存器。
4.5 整体流程SVG图
下面这张图把整个初始化流程串起来了,建议你保存下来对照着看:
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_系列函数可以自动处理,但时钟和中断释放还是得手动写清楚。