4、Platform设备注册:静态注册与动态注册,设备树节点与platform_device的关联
设备注册这个话题,说白了就是告诉内核:“嘿,这里有个硬件设备,你管一下!”
但怎么告诉内核,方式可不一样。我早年做项目时,就因为在注册方式上踩了坑,导致设备死活枚举不上。今天咱们就把这块彻底捋清楚。
4.1 静态注册:最原始的方式
静态注册,就是直接在代码里定义一个 platform_device 结构体,然后调用 platform_device_register() 把它注册进内核。
这种方式在早期内核(比如 2.6 时代)非常普遍。那时候还没有设备树,大家全靠 C 代码硬编码。
核心特点:
- 设备信息在编译时就确定了
- 修改设备信息需要重新编译内核
- 适合硬件固定、不需要变动的场景
来看一个典型的静态注册示例:
/* 定义资源 */
static struct resource led_resource[] = {
[0] = {
.start = 0x56000050,
.end = 0x56000050 + 8 - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_EINT0,
.end = IRQ_EINT0,
.flags = IORESOURCE_IRQ,
},
};
/* 定义 platform_device */
static struct platform_device led_device = {
.name = "my_led",
.id = -1,
.num_resources = ARRAY_SIZE(led_resource),
.resource = led_resource,
};
/* 在初始化函数中注册 */
static int __init led_device_init(void)
{
return platform_device_register(&led_device);
}
module_init(led_device_init);
嗯,这里要注意:id = -1 表示只有一个设备实例。如果有多个同类型设备,id 要依次递增。
我的经验:静态注册虽然“笨”,但在调试阶段特别好用。我曾经在开发板上验证一个新外设,设备树还没配好,直接用静态注册先跑起来,确认驱动逻辑没问题再回头搞设备树。这叫“先让车跑起来,再换轮胎”。
4.2 动态注册:设备树时代的标配
动态注册,就是内核在启动时解析设备树(DTB),自动为每个匹配的节点创建 platform_device。
你想想看,如果每次改个 GPIO 号都要重新编译内核,那得多痛苦?设备树就是来解决这个问题的。
动态注册的流程大致是这样的:
- Bootloader 加载设备树二进制文件(DTB)到内存
- 内核启动时,调用
unflatten_device_tree()解析 DTB - 生成设备树节点的树形结构
- 遍历所有节点,对 compatible 属性匹配的节点调用
of_platform_device_create() - 自动创建
platform_device并注册
注意:并不是所有设备树节点都会生成 platform_device。只有满足以下条件的节点才会被处理:
- 节点包含
compatible属性 - 节点不是简单的总线节点(如 i2c、spi 等有自己驱动模型的)
- 节点的父节点是 platform 总线或根节点
4.3 设备树节点与 platform_device 的关联
这是很多人搞不清楚的地方。设备树里的一个节点,到底是怎么变成内核里的一个 platform_device 的?
我画了一张图,帮你理解这个映射关系:
说白了,设备树里的 compatible 属性就是“身份证号”,内核用它来匹配驱动。而 reg、interrupts 这些属性,会被转换成 resource 结构体。
4.4 两种注册方式的对比
| 对比项 | 静态注册 | 动态注册(设备树) |
|---|---|---|
| 灵活性 | 低,改参数需重新编译 | 高,改 dts 文件即可 |
| 代码量 | 需要写 C 代码定义资源 | 只需写 dts 文件 |
| 调试难度 | 容易,代码逻辑清晰 | 需要懂设备树语法 |
| 适用场景 | 原型验证、简单设备 | 产品级、多平台适配 |
| 内核版本 | 所有版本都支持 | 3.x 以后主流方式 |
避坑指南:我曾经在一个项目里,设备树配好了,但设备就是没注册成功。查了半天,发现是 status = "disabled" 忘了改成 "okay"。内核默认会跳过 disabled 的节点,这个细节很容易忽略。
4.5 驱动中如何获取设备树信息
驱动编写时,我们通常用 of_match_table 来匹配设备树节点。来看一个典型的驱动框架:
static const struct of_device_id led_of_match[] = {
{ .compatible = "my-led" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, led_of_match);
static int led_probe(struct platform_device *pdev)
{
struct resource *res;
int irq;
/* 获取内存资源 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res)
return -ENODEV;
/* 获取中断号 */
irq = platform_get_irq(pdev, 0);
if (irq < 0)
return irq;
/* 从设备树获取自定义属性 */
struct device_node *np = pdev->dev.of_node;
u32 delay;
of_property_read_u32(np, "delay-ms", &delay);
return 0;
}
static struct platform_driver led_driver = {
.probe = led_probe,
.driver = {
.name = "my_led",
.of_match_table = led_of_match,
},
};
module_platform_driver(led_driver);
这里有个关键点:pdev->dev.of_node 就是指向设备树节点的指针。通过它,你可以读取任何自定义属性。
核心总结:
- 静态注册:代码里硬编码,适合调试和简单场景
- 动态注册:设备树自动解析,产品级开发的首选
- 设备树的 compatible 属性是驱动匹配的“钥匙”
- 资源(地址、中断)通过
platform_get_resource系列函数获取
嗯,关于设备注册这块,其实核心就这些。你只要记住:设备树是“数据”,驱动是“逻辑”,platform 总线是“桥梁”。三者配合好了,设备就能正常工作。
我个人建议,新项目直接上设备树方式。别再用静态注册了,那玩意儿维护起来太痛苦。除非你只是在做实验板上的快速验证。
公众号:蓝海资料掘金营,微信deep3321