驱动是如何向内核**注册(Register)**自己的?是 register_chrdev 还是 platform_driver_register?二者有什么区别?为什么有了register_chrdev还要有platform_driver_register?它们到底谁才是真正的“注册”?
答案是:它们都是注册,但注册的“对象”和“目的”完全不同。
简单来说:
platform_driver_register是为了找硬件(匹配设备树)。register_chrdev是为了给用户提供接口(生成/dev/xxx)。
在现代 Linux 驱动开发中,通常是两者配合使用:先用 Platform 注册找到硬件,然后在找到硬件的回调函数(Probe)里,再进行字符设备注册。
学习笔记:Linux 驱动的两层注册机制
1. 核心区别概览
| 特性 | register_chrdev (字符设备注册) | platform_driver_register (平台驱动注册) |
|---|---|---|
| 所属层级 | 接口层(Interface Layer) | 设备模型层(Device Model Layer) |
| 核心目的 | 告诉内核:“我是一个字符设备,我的操作函数是f_op” | 告诉内核:“我就是这硬件的驱动,把符合名字的设备交给我” |
| 面向谁? | 面向用户空间(APP) | 面向内核/硬件(Device Tree) |
| 产出物 | 主设备号、/dev/xxx节点(配合 class_create) | 触发probe函数的执行 |
| 必须性 | 如果你想让 APP 操作,必须有 (或其变体cdev_add) | 不是必须的 (简单的驱动可以没有),但标准驱动都有 |
2. 深度解析:为什么要分两层?
第一层:platform_driver_register(我是谁?我去哪里上班?)
这是 Linux“总线-设备-驱动” (Bus-Device-Driver)模型的一部分。
背景:
在设备树(Device Tree)引入之前,驱动代码里写死了硬件地址(比如 0x020E0068)。
现在,硬件信息都写在设备树 (.dts) 里,驱动代码里不再写死地址。
作用:
platform_driver_register 的作用就是**“相亲”**。
- 设备树说:“我有一个硬件,名字叫
my-led。” - 驱动代码说:“我是一个驱动,我能支持名字叫
my-led的硬件。” - 内核:把两者撮合在一起。一旦名字匹配成功,内核就会调用驱动里的
probe(探测)函数。
- 设备树说:“我有一个硬件,名字叫
第二层:register_chrdev(我能干什么?)
这是我们之前讲过的字符设备功能注册。
作用:
不管你是怎么找到硬件的,最终你都得让 APP 能控制它。
register_chrdev 就是去申请一个主设备号,并挂载 file_operations,让 APP 可以通过 open/read/write 来操作。
3. 它们是如何配合工作的? (标准模板)
在现代驱动中,这套流程是固定的“套娃”模式:
- 模块入口 (
module_init):只做一件事,调用platform_driver_register。 - 匹配成功:内核发现设备树有对应节点,自动调用驱动的
probe函数。 - Probe 函数:这里才是真正干活的地方!
- 获取硬件资源(读设备树里的寄存器地址、中断号)。
- 调用
register_chrdev(或者是现代的alloc_chrdev_region+cdev_add)。 - 创建设备节点 (
class_create,device_create)。
代码骨架(可以背下来这个结构!)
/* 1. 定义字符设备的操作函数 (给 APP 用的) */ static struct file_operations my_fops = { .owner = THIS_MODULE, .write = my_write, .open = my_open, }; /* 2. probe 函数:当驱动和设备树匹配成功时,内核自动调用 */ static int my_probe(struct platform_device *pdev) { printk("硬件匹配成功!开始初始化...\n"); // A. 获取硬件资源 (从设备树拿) // ... // B. 注册字符设备 (核心关联点!!!) // 只有到了这一步,APP 才能看到 /dev/xxx major = register_chrdev(0, "my_device", &my_fops); // C. 创建设备节点 // class_create(...); // device_create(...); return 0; } /* 3. remove 函数:卸载驱动或设备移除时调用 */ static int my_remove(struct platform_device *pdev) { // 注销字符设备 unregister_chrdev(major, "my_device"); return 0; } /* 4. 定义 Platform 驱动结构体 */ static const struct of_device_id my_match_table[] = { { .compatible = "100ask,led" }, // 这里的名字必须和设备树里的一样! { }, }; static struct platform_driver my_driver = { .probe = my_probe, // 匹配成功调这个 .remove = my_remove, // 移除时调这个 .driver = { .name = "my_led_driver", .of_match_table = my_match_table, // 指定匹配表 }, }; /* 5. 模块入口:只负责注册 Platform 驱动 */ static int __init my_driver_init(void) { // 向内核注册在这个 Platform 驱动 return platform_driver_register(&my_driver); } static void __exit my_driver_exit(void) { platform_driver_unregister(&my_driver); } module_init(my_driver_init); module_exit(my_driver_exit); module_license("GPL");4. 图解流程
- insmod→ \rightarrow→执行
module_init。 - platform_driver_register→ \rightarrow→把驱动名字放入“相亲角”。
- Kernel Check→ \rightarrow→发现设备树里也有个叫
"100ask,led"的硬件。 - Match!→ \rightarrow→触发
my_probe函数。 - Inside Probe→ \rightarrow→执行
register_chrdev。 - Result→ \rightarrow→生成
/dev/led,APP 可以使用了。
5. 常见疑惑解答
Q1: 我可以不用platform_driver_register,只写register_chrdev吗?
- 可以。这就是所谓的“老式驱动”或“非设备树驱动”。
- 后果:你必须在代码里硬编码寄存器物理地址(比如
0x20200000)。这导致你的驱动只能在这个特定的板子上跑,换个板子(地址变了)就得改代码重新编译。 - 现状:为了代码通用性,现在几乎都用 Platform 模式。
Q2: 我可以不用register_chrdev,只写platform_driver_register吗?
- 不可以(除非你是做 LED 子系统、输入子系统这种高级开发)。
- 如果你不注册字符设备,你就没有主设备号,没有
/dev/xxx节点,APP 根本没办法通过文件操作来访问你的驱动。 platform_driver只是帮你管理硬件资源,它不负责给 APP 提供接口。
6. 总结
platform_driver_register:是管理层。负责让驱动程序和设备树里的硬件描述**“对上号”**。它让驱动更灵活、更通用。register_chrdev:是业务层。负责实打实地生成用户接口。- 关系:通常在
platform_driver的probe函数中,去调用register_chrdev。前者是壳,后者是核。