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 号都要重新编译内核,那得多痛苦?设备树就是来解决这个问题的。

动态注册的流程大致是这样的:

  1. Bootloader 加载设备树二进制文件(DTB)到内存
  2. 内核启动时,调用 unflatten_device_tree() 解析 DTB
  3. 生成设备树节点的树形结构
  4. 遍历所有节点,对 compatible 属性匹配的节点调用 of_platform_device_create()
  5. 自动创建 platform_device 并注册

注意:并不是所有设备树节点都会生成 platform_device。只有满足以下条件的节点才会被处理:

  • 节点包含 compatible 属性
  • 节点不是简单的总线节点(如 i2c、spi 等有自己驱动模型的)
  • 节点的父节点是 platform 总线或根节点

4.3 设备树节点与 platform_device 的关联

这是很多人搞不清楚的地方。设备树里的一个节点,到底是怎么变成内核里的一个 platform_device 的?

我画了一张图,帮你理解这个映射关系:

设备树节点 → platform_device 映射关系 设备树(DTB) led { compatible = "my-led"; reg = <0x56000050 0x8>; interrupts = <5>; status = "okay"; }; 内核 platform_device struct platform_device { .name = "my-led" .resource[0] = { IORESOURCE_MEM, 0x56000050 .resource[1] = { IORESOURCE_IRQ, 5 of_platform_device_create()

说白了,设备树里的 compatible 属性就是“身份证号”,内核用它来匹配驱动。而 reginterrupts 这些属性,会被转换成 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