板卡上电,串口滚过一串驱动名。设备树源文件里躺着几十个节点。/sys 目录下又藏着同名的目录。三处名字指向同一套机制
本文基于 5.x/6.x 内核的设备模型。结构体与真实定义有出入,字段做了删减,只为说明对象之间的关系。字符设备接口、中断、DMA、电源管理策略不在讨论范围内。
驱动是什么
应用想点亮一颗 LED。它写下的代码只有一行:
write(fd, buf, 1);这行代码走到硬件,要经过这样一条路:
应用 write() → 系统调用 → 内核核心 → 驱动 → 寄存器 → 引脚电平翻转内核核心认识”字符设备""网络设备”这些类别,认识 open、read、write 这些动作。它唯独不认识”这颗 LED 接在 GPIO 17,高电平点亮,写 1 之前要先解复用引脚”。认识这些的是驱动:内核里操作具体硬件的那段代码。寄存器地址、时序、协议细节,全部封装在驱动里。
把硬件写死在内核里,每支持一种新芯片就要重编一次。内核源码里,drivers/ 目录的行数超过总量的一半,每块新板子还会带来新芯片。驱动模型给出的出路:硬件描述(设备树)和代码(驱动模块)各自独立提交,内核负责让它们相遇。
把驱动装进内核只是第一步。modprobe 成功不等于硬件动起来,你可能遇到过:模块加载了,设备没反应。模型在中间管三件事:
- 登记。内核里存在哪些设备、哪些驱动,各自挂在哪条总线。
- 撮合。哪段代码服务哪个设备,由总线按规则裁决。
- 表现。每个对象在 /sys 里有一个目录,用户态能看见、能查询、能干预。
设备、总线、驱动
模型的核心只有三个对象:
- 设备(device):系统里一份硬件实例。LED、UART 控制器、I2C 控制器,各占一个。
- 驱动(driver):一段代码,加一张它认识哪些设备的声明表。
- 总线(bus):登记处。设备和驱动都挂在某条总线上,总线拿着撮合规则。
三个结构体的骨架长这样,完整定义在 include/linux/device/ 下:
/* 教学简化:字段做了删减,只为说明关系 */struct device { const char *name; struct bus_type *bus; /* 我挂在哪条总线 */ struct device_driver *driver; /* 正在驱动我的代码 */ void *driver_data; /* 驱动放在我身上的私有数据 */ struct kobject kobj; /* 对应 /sys 里的一个目录 */};
struct device_driver { const char *name; /* 总线上的登记名 */ const struct of_device_id *of_match_table; /* 设备树匹配表 */ int (*probe)(struct device *dev); /* 匹配成功后被调用 */ void (*remove)(struct device *dev); struct bus_type *bus;};
struct bus_type { const char *name; /* "platform"、"i2c"、"usb" */ int (*match)(struct device *dev, struct device_driver *drv);2 collapsed lines
int (*probe)(struct device *dev); /* 总线级包装,最终调用驱动 probe */};设备知道自己的总线,驱动知道自己认哪些设备,总线认识所有人,并裁决谁配谁。
kobject:/sys 从哪来
struct device 里那个 kobject 是钥匙。每个 kobject 带三样东西:引用计数、父子指针、一个 sysfs 目录。内核对象注册时创建目录,注销时删除目录。/sys 是这些内核对象在用户态的投影。
在一条能进 shell 的板子上看一眼(QEMU 的 virt 板就有),platform 总线的登记处是这两个目录:
/sys/bus/platform/├── devices/ ← 挂在这条总线上的设备(软链接,指向 /sys/devices 下的真实层级)└── drivers/ ← 挂在这条总线上的驱动ls /sys/bus/platform/devices/# 9010000.pl031 a000000.virtio_mmio 42820000.demo-led ...# (示意输出,条目随板子不同)
ls /sys/bus/platform/drivers/# rtc-pl031 virtio_mmio demo-led ...设备名 42820000.demo-led 的格式是”地址.节点名”,来自设备树节点的单元地址。撮合成功后,你会在这两个目录里同时看到对方的出现。
总线怎么撮合
登记入口只有两个,分别对应设备侧和驱动侧:
device_register():设备登记。挂上总线的设备链表,然后请总线扫一遍现有驱动。driver_register():驱动登记。挂上驱动链表,然后扫一遍总线上还没配到驱动的设备。
两边注册都会触发一次向对方的扫描,所以先后顺序无关紧要。USB 盘插进已经跑着的系统,是设备后到;insmod 一个模块去匹配启动时就存在的设备,是驱动后到。热插拔和模块加载走的是同一段代码。
撮合规则写在 bus_type 的 match 函数指针里,每种总线自带一套:
| 总线 | match 比较什么 |
|---|---|
| USB | 厂商 ID 加产品 ID |
| I2C | of_match_table 或 i2c_device_id 表 |
| platform | of_match_table 的 compatible 字符串,失败再比 id_table 和名字 |
另外,platform 设备有一个调试后门 driver_override:设置之后只认这个名字的驱动。
匹配通过后,内核沿这条调用链走进你的代码:
driver_register() └─ bus_add_driver() drivers/base/bus.c └─ driver_attach() drivers/base/dd.c └─ __driver_attach() ├─ bus->match(dev, drv) 规则:compatible 相等? └─ driver_probe_device() └─ really_probe() └─ bus->probe() → drv->probe(dev) 你的代码probe 的返回值有明确含义。返回 0,接管成功,device.driver 指向这个驱动,sysfs 里出现双向链接;返回负数,这次撮合作废,设备回到无驱动状态,内核可以继续找下一个候选。
把这条链放进时间线跑一遍会看得更清楚。下面的实验准备了两台设备、一个驱动和三种 probe 返回值:
撮合时序实验
切换注册顺序与 probe 返回值,观察总线如何逐个询问设备,以及三种返回值把设备带向哪种结局。
- 已绑定(粗实线)
- match 命中
- 未命中
- 排队重试
LED 绑定完成。温度传感器没有匹配的驱动,继续留在总线上等待,这是正常状态。
- [ 1.204318] platform 42820000.demo-led: 登记设备,总线上暂无候选驱动
- [ 1.205690] platform 42820000.demo-temp: 登记设备,总线上暂无候选驱动
- [ 2.008104] demo-led: 驱动登记,开始扫描总线上未绑定的设备
- [ 2.008811] demo-led: match 42820000.demo-temp → compatible 不等,跳过
- [ 2.009150] demo-led: match 42820000.demo-led → compatible 相等,命中
- [ 3.140002] demo-led 42820000.demo-led: 设备 42820000.demo-led 找到了驱动
- [ 3.140880] platform: sysfs 出现双向链接,设备进入已绑定状态
总线上的两台设备:LED 有匹配的驱动,温度传感器没有。无论哪种顺序,登记都会触发一次向对方的扫描;probe 的返回值决定设备此后的处境。
实验里的两台设备值得再看一眼
platform 总线:没有物理总线的设备
I2C 设备挂在 I2C 总线上,USB 设备挂在 USB 总线上,总线硬件自带发现机制
platform 设备的主流来源是设备树。内核启动时展开设备树,把每个带 compatible 的节点变成一个 platform_device:
/ { demo_led: demo-led@42820000 { compatible = "mycompany,demo-led"; reg = <0x42820000 0x100>; };};驱动侧用 of_match_table 声明自己认识的 compatible:
static const struct of_device_id demo_led_of_match[] = { { .compatible = "mycompany,demo-led" }, { /* 哨兵,必须留空结尾 */ }};match 拿设备节点的 compatible 与表项逐一整串比较,相等即命中。compatible 没命中时还有两级回退:先比 id_table(若有),再比驱动登记名与节点名。名字回退是最后防线,规范写法仍是保持 compatible 一致。
整串比较意味着一个字符的差别就会被判出局。下面的实验把 platform_match() 的三级判定摆在一起,你可以亲手把 compatible 改错:
compatible 比较实验
改动驱动声明表里的 compatible,观察逐字符比较、名字回退,以及三级全部失败后的静默表现。
本驱动没有提供 platform_device_id 表。
节点名 "demo-led" 与登记名 "demo-led" 逐字符比较。
[ 3.140002] demo-led 42820000.demo-led: 设备 42820000.demo-led 找到了驱动drivers/demo-led/ 出现 42820000.demo-led 链接比较逐字符进行,区分大小写与空格。真实内核在这之前还会看 driver_override,这里略过 ACPI 与 id_table 的情形。名字回退命中只说明登记名与节点名碰巧相同,不要把正确性建立在它上面。
把登记名换成与节点名不同的 demo-led-drv,再选一个打错的 compatible:三级全部未命中,模块加载成功,dmesg 没有任何输出,驱动目录下没有设备链接。这就是下一节三种典型失败里最隐蔽的一种。
一个最小的 platform 驱动
把上面的对象拼成一份完整代码,大约三十行:
#include <linux/module.h>#include <linux/platform_device.h>#include <linux/mod_devicetable.h>
static int demo_led_probe(struct platform_device *pdev){ /* 通常在这里:取资源、映射寄存器、申请时钟与中断 */ dev_info(&pdev->dev, "设备 %s 找到了驱动\n", pdev->name); return 0;}
static void demo_led_remove(struct platform_device *pdev){ /* 6.x 起 remove 返回 void,旧内核返回 int */ dev_info(&pdev->dev, "驱动离开,设备交还内核\n");}
static const struct of_device_id demo_led_of_match[] = { { .compatible = "mycompany,demo-led" }, { }15 collapsed lines
};MODULE_DEVICE_TABLE(of, demo_led_of_match);
static struct platform_driver demo_led_driver = { .probe = demo_led_probe, .remove = demo_led_remove, .driver = { .name = "demo-led", .of_match_table = demo_led_of_match, },};module_platform_driver(demo_led_driver);
MODULE_LICENSE("GPL");MODULE_DESCRIPTION("A minimal platform driver");逐段对应到模型:
demo_led_of_match:驱动的声明表,match 的比较对象。.driver.name = "demo-led":总线上的登记名,也是 /sys 里驱动目录的名字。module_platform_driver():宏展开成 module_init 和 module_exit,装载时调用platform_driver_register(),走上一节的调用链。probe:撮合成功后内核调用它。真实驱动在这里取内存资源(platform_get_resource()、devm_platform_ioremap_resource())、拿中断号(platform_get_irq()),然后把自己注册到某个子系统。本文只打印一行,资源部分留给下一篇。- 返回 0:接管成功。装载后 dmesg 里会出现:
[ 12.345678] demo-led 42820000.demo-led: 设备 42820000.demo-led 找到了驱动probe 失败与重试
probe 返回负数,最常见的原因是依赖没就绪:时钟、复位线、电源域的驱动还没加载。内核给这种情况留了标准返回值 -EPROBE_DEFER:
clk = devm_clk_get(&pdev->dev, NULL);if (IS_ERR(clk)) return dev_err_probe(&pdev->dev, PTR_ERR(clk), "拿不到时钟");返回这个值后,设备进入 deferred 队列,内核在其他驱动注册、初始化收尾时重试它们。dev_err_probe() 还会识别 EPROBE_DEFER 并降低日志级别,避免刷屏。这句返回值告诉内核:设备没坏,晚点再试。
在 /sys 里验证
选一块你能拿到 shell 的板子,QEMU 也行。装载驱动后:
sudo insmod demo-led.ko
ls /sys/bus/platform/drivers/demo-led/# 42820000.demo-led bind module uevent unbind42820000.demo-led 这个软链接出现,说明 match 通过、probe 已经跑过。设备目录和驱动目录互相挂着链接:设备知道自己被谁驱动,驱动知道自己管着谁。
总线还留了一对操作接口,让你手动拆开撮合:
echo 42820000.demo-led | sudo tee /sys/bus/platform/drivers/demo-led/unbinddmesg | tail -1# demo-led 42820000.demo-led: 驱动离开,设备交还内核
echo 42820000.demo-led | sudo tee /sys/bus/platform/drivers/demo-led/bindunbind 之后设备还在,只是回到”没人管”的状态;bind 让总线重新执行一遍 match 加 probe。谁驱动谁由模型维护,你可以干预。
三种典型失败
- compatible 打错。设备树写
mycompany,demo-led,驱动表里写成mycompany,demoled。compatible 这一级失败后,内核还会试 id_table 和名字回退;只要登记名与节点名不同,三条路都走不通,模块加载成功,probe 永远不被调用,dmesg 没有任何输出。检查办法:驱动目录下没有设备软链接。 - probe 返回 -ENODEV。match 通过,probe 里发现资源对不上,立即失败。设备回到无驱动状态,dmesg 里能看到你的报错。
- 依赖未就绪。probe 返回 -EPROBE_DEFER,内核静默重试,dmesg 看不到失败。排队名单在 debugfs:
cat /sys/kernel/debug/devices_deferred(需要挂载 debugfs)。
排查时先查 devices_deferred 再怀疑代码:第三种失败没有日志,一直在队列里等。
模型之外
本文回答的是:对象怎么登记,总线怎么撮合,probe 何时被调。驱动接管设备之后的事,另有一条线:字符设备接口把硬件能力交给用户态,中断处理响应硬件事件,DMA 负责搬数据。电源管理回调同样挂在 device_driver 上,内核沿设备树逐层挂起和恢复,策略细节本文没有展开。
设备树里 compatible、reg、interrupts 的语法,以及 pinctrl、regulator 这些资源子系统,属于设备描述层,可以单独学习。
下次装载模块没反应,先去 /sys/bus 查:驱动目录在不在?目录里有没有设备链接?没有链接,说明 match 没通过,问题范围缩小一半;有链接但设备不工作,才轮到查 probe 里的代码。