嵌入式linux学习记录四,设备树

  1. 设备树写法:
    1. 设备树的核心结构与语法

      设备树的语法类似于 C 语言和 JSON 的结合体。一个标准的 .dts 文件主要由头文件引用根节点子节点组成。

      /dts-v1/;              // 1. 声明设备树的版本(必须写在第一行)
      #include <dt-bindings/gpio/gpio.h> // 2. 引用头文件(可使用标准宏定义)
      #include "imx6ull.dtsi" // 3. 包含芯片级的设备树(像C语言一样继承父模板)

      / {                    // 4. 根节点,用斜杠 "/" 表示
          model = "Alientek i.MX6ULL Alpha Board"; // 这块板子的名字
          compatible = "alientek,imx6ull-alpha", "fsl,imx6ull"; // 兼容性标志(匹配驱动的关键)

          /* 5. 自定义子节点:描述具体的硬件 */
          my_led {
              compatible = "gpio-leds";
              status = "okay";
              gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; // 硬件资源属性
          };
      };

    2. 节点中高频使用的四大标准属性

      编写设备树时,你绝大多数时间都在跟以下几个属性打交道:

      1. compatible(兼容性,最重要)
        作用:驱动和设备匹配的唯一密码
        写法"厂商,具体芯片/硬件型号"
        示例compatible = "alientek,mini-led", "generic-led";(内核会先尝试匹配第一个精确名字,如果没有对应的驱动,则尝试匹配第二个通用驱动)。

      2. status(设备状态)
        作用:决定这个硬件在系统启动时是否被启用。
        可选值

        1. "okay":硬件正常启用,内核会为它分配并匹配驱动。

        2. "disabled":硬件被禁用。即使节点写得再完美,内核也会完全忽略它。

      3. reg(寄存器地址与大小)
        作用:描述硬件寄存器的物理基地址长度
        写法reg = <address length>;
        示例reg = <0x020ac000 0x4>;(表示该硬件寄存器物理地址从 0x020ac000 开始,占 4 个字节)。

      4. 自定义属性(如 gpiosinterrupts
        作用:向驱动程序传递个性化的硬件参数。
        示例gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;(告诉驱动,我用的是 GPIO1 组的第 3 号引脚,低电平点亮)。

    3. 设备树的两种编写模式
      在实际工程中,芯片厂商(如 NXP、ST、Rockchip)已经把芯片内部的通用外设(如 I2C、SPI、UART、定时器)全部写好了,放在 .dtsi 纯净文件中。作为板级开发者,你有两种方式添加自己的硬件:

      1. 模式 A:直接在根节点 / 下追加新节点(适合独立的硬件,如 LED、蜂鸣器)直接在根节点的括号 { ... }; 内部塞入一个全新的独立节点即可。

      2. 模式 B:使用引脚引用 & 节点追加(适合挂在总线上的外设,如 I2C 传感器)

        如果你的 AP3216C 传感器挂在芯片的 I2C1 总线上,你千万不要在根节点下乱写,而是要使用 & 符号(Label 引用) 找到 I2C1 控制器,把传感器塞进它的肚子里:
         

        /* 找到芯片原生的 i2c1 节点,并向里面追加你的设备 */
        &i2c1 {
            clock-frequency = <100000>; // 设置 I2C 速率为 100KHz
            status = "okay";            // 记得开启 I2C1 控制器

            /* 你的具体传感器挂在 i2c1 总线上 */
            ap3216c@1e {
                compatible = "alientek,ap3216c";
                reg = <0x1e>;           // 传感器的 I2C 从机设备地址
            };
        };

    4. 实战:从零编写一个控制 LED 灯的设备树节点。假设我们要为板子上的一个 GPIO 灯编写设备树,它连接在 GPIO5 的第 3 号引脚 上,高电平点亮。

      1. 第一步:检查引脚是否被别人复用

        在修改设备树前,一定要确保这个引脚没有被其他外设(比如串口或网口)占用。在 .dts 里全局搜索 gpio5 3,如果有冲突,将其删除或把状态改为 "disabled"

      2. 第二步:编写 pinctrl 节点(配置引脚电气属性)

        现代 SoC 要求先把引脚配置成 GPIO 功能,并设置上下拉电阻等特性。通常在 iomuxc 节点下编写:

        &iomuxc {
            pinctrl_my_led: my_led_grp {
                fsl,pins = <
                    /* 将 GPIO5_IO03 复用为 GPIO 功能,后面的一串十六进制是电气属性参数 */
                    MX6ULL_PAD_SNVS_TAMPER3__GPIO5_IO03        0x10B0 
                >;
            };
        };

      3. 第三步:在根节点下编写设备节点,回到根节点下,把刚才配好的引脚吃进来,并打上 compatible 标签:

        / {
            my_gpio_led {
                compatible = "user,my-gpio-led";  // 匹配驱动的暗号
                status = "okay";
                
                pinctrl-names = "default";
                pinctrl-0 = <&pinctrl_my_led>;     // 绑定刚才写好的引脚配置
                
                led-gpio = <&gpio5 3 GPIO_ACTIVE_HIGH>; // 传入具体的引脚和有效电平
            };
        };
    5. 驱动程序(Driver)怎么读取你写的设备树?
      设备树写好并编译好之后,驱动通过前面提到的 【方法二:设备树匹配】 成功进入 probe 函数。此时,驱动可以调用内核提供的 of_ 系列 API 来提取你写在里面的参数:

      static int my_led_probe(struct platform_device *pdev)
      {
          struct device_node *node = pdev->dev.of_node; // 拿到设备树节点指针
          int gpio_num;

          // 1. 获取名为 "led-gpio" 的 GPIO 编号
          gpio_num = of_get_named_gpio(node, "led-gpio", 0);
          if (!gpio_is_valid(gpio_num)) {
              dev_err(&pdev->dev, "无法获取有效的 GPIO 引脚\n");
              return -EINVAL;
          }

          // 2. 申请并使用这个 GPIO
          devm_gpio_request_one(&pdev->dev, gpio_num, GPIOF_OUT_INIT_LOW, "led_pin");
          
          printk("驱动成功从设备树获取到引脚编号: %d,并初始化完成!\n", gpio_num);
          return 0;
      }

    6.  避坑黄金法则

      1. 少用新建,多用追加(&:芯片内部自带的外设(I2C, SPI, UART, EMMC),永远使用 &外设名 { ... }; 的方式去覆盖和追加属性,不要在根节点重新发明轮子。

      2. 遵守命名规范:节点名称通常采用 [设备类型]@[地址] 的格式,例如 ethernet@02188000ap3216c@1e。如果是纯虚拟或独立的 GPIO 设备,通常直接用功能命名如 my_led

  2. 设备树在linux文件系统中的位置:

    1. Linux 内核通过虚拟文件系统(procfssysfs),将内存中的设备树以目录和文件的形式暴露了出来。你可以在板子的终端中通过以下两个路径找到它们:

      1. 路径一:/proc/device-tree/(最直观,查看节点和属性)
        这是最常用的路径。内核在这个目录下,将设备树的各个节点还原成了文件夹,将节点内部的属性还原成了文件

      2. 路径二:/sys/firmware/devicetree/base/(完全等价)
        在较新的 Linux 内核中,/proc/device-tree 实际上只是一个软链接(快捷方式),它指向的真实 sysfs 路径是:

    2. 这两个目录下的内容是完全一模一样的,你访问任意一个都可以。

  3. 设备树中的节点,能够被转换成platform_device的要求:

    1. 第一大类:根节点 / 下的第一级子节点(重点)
      只要是紧跟在根节点 / 下面的第一级子节点,并且该节点带有 compatible 属性,它就一定会被转换成 platform_device
      示例:
      / {
          /* 这是一个根节点下的第一级子节点,带 compatible,成功转换! */
          my_led {
              compatible = "gpio-leds";
              gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
          };
          /* 这个节点没有 compatible 属性,忽略,不会转换 */
          chosen {
              bootargs = "earlycon console=ttymxc0,115200";
          };
      };

    2.  第二大类:特殊总线节点下的“子子节点”
      如果一个节点不是根节点的第一级子节点,而是藏在某个大外设节点的肚子里,通常情况下它不会被转换。但是,如果这个大外设节点(父节点)的 compatible 属性包含以下几个特殊的“总线标签”之一,那么它里面的所有子节点也都会被扫描并转换成 platform_device
      常见的特殊父节点 compatible 属性值有:

      1. "simple-bus"(最常见,代表简单总线,比如芯片内部的片上系统 SoC 总线)

      2. "simple-mfd"(简单多功能设备)

      3. "isa"

      4. "arm,amba-bus"
        示例:

        / {
            /* 父节点:在根线下,有 compatible,转换成 platform_device */
            soc {
                compatible = "simple-bus"; # 🌟 关键:因为有这个属性,内核会进到它肚子里面去扫描
                #address-cells = <1>;
                #size-cells = <1>;

                /* 子子节点:虽然不是第一级子节点,但因为父节点是 "simple-bus",成功转换! */
                my_timer@020bc000 {
                    compatible = "fsl,imx6ul-gpt";
                    reg = <0x020bc000 0x4000>;
                };
            };
        };

    3. 第三大类:某些特定子系统软件触发的节点
      有些节点本身在系统启动的默认扫描中被忽略了,但当对应的核心驱动加载后,驱动会手动调用内核 API,强行把该节点或其子节点注册为 platform_device
      例如 I2C / SPI 总线下的设备
      正常情况下,挂在 &i2c1&spi2 节点下的子节点(如传感器、OLED 屏幕),在默认启动时绝对不会被转换成 platform_device,因为它们应该被转换成 i2c_clientspi_device。但是,如果有些设备非常特殊,需要利用 platform_device 来管理(例如某些片选复杂的特殊片外总线),驱动中调用了 of_platform_populate()of_platform_device_create(),这些节点就会被强制转换。

    4. 哪些节点绝对【不会】被转换成 platform_device?
      为了彻底理清边界,以下节点在启动时会被内核自动过滤,不会生成 platform_device

      1. 没有 compatible 属性的节点。例如:chosenaliasesmemory 等。

      2. 状态为禁用的节点。只要节点里写了 status = "disabled";,内核直接视而不见。

      3. 已经归属于其他标准总线的子节点

        • 挂在 i2c 控制器下的子节点 $\rightarrow$ 转换成 struct i2c_client

        • 挂在 spi 控制器下的子节点 $\rightarrow$ 转换成 struct spi_device

        • 挂在 usb 控制器下的子节点 $\rightarrow$ 转换成 struct usb_device

      4. 纯软件配置节点。例如 pinctrl(引脚复用配置节点)、clocks(时钟树节点),它们只负责提供配置参数,由专门的子系统去解析,不属于平台设备。

    5. 一句话总结: 检查你的节点,如果它 在根节点下一级 或者 其父节点的 compatible 里写着 "simple-bus",只要它自己带 compatible 且没有被 disabled,它就会变成一个 platform_device

  4. 第一级子节点的子节点,不一定会被转换成 platform_device。
    核心规则是:深度 > 1 的节点,默认不会自动转换,但有两个例外条件可以让它们被转换。

    1. 例外一:父节点不是总线控制器(non-bus parent)

      如果父节点没有对应的 struct bus_type(即内核不认为它是一条总线),那么它的子节点会被递归地注册为 platform_device。

      /
      └── soc                    ← 有 compatible,是 platform_device
          └── uart@ff000000      ← 父节点 soc 没有驱动/总线 → ✅ 也会变成 platform_device

      本质:内核在创建 soc 这个 platform_device 时,发现它没有具体总线来"认领"子节点,就会继续向下递归创建。

    2. 例外二:父节点的 compatiblesimple-bus / simple-mfd

      如果父节点的 compatible 包含以下值,内核会主动遍历其子节点并转换:

      compatible 值含义
      simple-bus简单总线,子节点全部转成 platform_device
      simple-mfd简单多功能设备,同上
      isaISA 总线
      arm,amba-busAMBA 总线
       

      soc {
          compatible = "simple-bus";   // ← 关键
          #address-cells = <1>;
          #size-cells = <1>;

          uart0: uart@ff000000 {
              compatible = "ns16550a"; // ← ✅ 会被转成 platform_device
          };
      };

    3. 反例:父节点是真实总线控制器

      如果父节点是 i2c/spi/pci 控制器,子节点就不会变成 platform_device,而是由对应总线驱动来管理:

      /
      └── i2c@ff160000           ← platform_device(i2c 控制器)
          └── sensor@48          ← ❌ 不是 platform_device,而是 i2c_client

    4. 总结一句话:
      第二级及更深的节点,只有在父节点是 simple-bus 或内核无法识别其总线类型时,才会被递归转换成 platform_device;否则由对应的总线子系统(i2c/spi/pci等)负责处理.
  5. I2C/SPI 子节点变成 platform_device方法:

    1. 为什么 I2C/SPI 子节点默认不会变成 platform_device?
      在 Linux 内核看来,外设总线是有明确家族划分的:

      1. 平台总线(Platform Bus)管理 platform_device

      2. I2C 总线(I2C Bus)管理 i2c_client

      3. SPI 总线(SPI Bus)管理 spi_device

        我们在前面提到过,内核启动时默认只会去扫描根节点 / 以及带有 "simple-bus" 的通用片上总线。当内核扫描到 i2c1 控制器节点时,发现它的 compatible 可能是 "fsl,imx6ul-i2c",属于平台设备。于是内核为 i2c1 自身创建了一个 platform_device但是,内核随即就会止步,绝对不会主动去碰 i2c1 内部的子节点(如传感器)。
        / {
            soc {
                compatible = "simple-bus";
                
                i2c1: i2c@021a0000 {
                    compatible = "fsl,imx6ul-i2c"; /* 内核把它变成了 platform_device */
                    
                    /* 🌲 默认扫描到此为止,下面的子节点被内核无视、跳过 */
                    ap3216c@1e {
                        compatible = "alientek,ap3216c"; /* 默认不会变成 platform_device */
                    };
                };
            };
        };


        那它们正常是怎么被转换的?

        i2c1 控制器自己的驱动(I2C 总线核心驱动)加载并运行时,该驱动会自己去扫描自己肚子底下的子节点,然后调用 i2c_new_client_device() 等专用函数,把 ap3216c 节点转换成一个 struct i2c_client(属于 I2C 总线),而不是 platform_device

    2. 重点:驱动怎么“手动调用 API 强行转换”?
      既然正常流程下它们各回各家(变成 i2c_client),那什么情况下会发生你说的“强行注册为 platform_device”呢?
      💡 核心场景:复合型芯片 / 多功能设备(MFD)
      在复杂的嵌入式硬件中,有很多芯片是“二合一”甚至“多合一”的。 例如:你通过 I2C 总线连接了一个电源管理芯片(PMIC)或音频解码芯片(Codec)。这个芯片在物理上走 I2C 通信,但在逻辑上,它内部同时包含了:

      1. 一个电源调节器(Regulator)

      2. 一个时钟发生器(Clock)

      3. 一个音频控制接口(Audio Control)

        对于这种复杂的复合芯片,Linux 社区的规范写法是:将该芯片整体注册为一个 I2C 设备;但它内部的各个子功能部件,应该抽象为独立的 platform_device,以便复用内核中现成的通用平台驱动。

        强行转换的设备树写法:

        &i2c1 {
            status = "okay";

            /* 核心驱动主芯片:它首先会被 I2C 子系统正常转换为 i2c_client */
            pmic@4b {
                compatible = "rohm,bd71847";
                reg = <0x4b>;

                /* 🌟 重点:这些是藏在 I2C 节点底下的子子节点 */
                /* 它们在系统启动时被默认忽略,完全不会变成任何设备 */
                pmic_regulator {
                    compatible = "rohm,bd71847-regulator"; /* 我们希望它变成 platform_device */
                };

                pmic_clock {
                    compatible = "rohm,bd71847-clk";       /* 我们希望它变成 platform_device */
                };
            };
        };


        核心驱动中的“临门一脚”(手动调用 API)
        当这个 PMIC 芯片的 I2C 驱动(核心驱动)匹配成功并进入 probe 函数后,它会干一件史诗级的事情——主动帮它的子节点“逆天改命”,强行把它们塞进平台总线里。

        驱动代码通常是这样写的:
         

        static int bd71847_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
        {
            int ret;
            struct device *dev = &client->dev;

            printk("1. PMIC 主芯片作为 I2C 设备加载成功!\n");

            /* 🌟 核心 API:of_platform_populate
               这个函数的作用是:强行让内核去扫描指定节点(当前 pmic 节点)底下的子节点,
               不管它们身处何方(哪怕在 I2C 肚子里),只要带 compatible,统统强行转换成 platform_device!
            */
            ret = devm_of_platform_populate(dev);
            if (ret) {
                dev_err(dev, "强行转换子节点失败\n");
                return ret;
            }

            printk("2. 肚子里的 regulator 和 clk 子节点已被成功强行注册为 platform_device!\n");
            return 0;
        }

        运行后的最终结果
        当上面这段 of_platform_populate 执行完毕后:

        1. 藏在 I2C 节点下的 pmic_regulator 节点,会原地变身,在内核中生成一个 struct platform_device

        2. 随后,内核的平台总线开始干活,拿着 "rohm,bd71847-regulator" 这个兼容性字符串去匹配对侧的平台驱动。

        3. 最终,电源调节器的专属驱动(Platform Driver)被激活,开始工作。

    3. 总结
      你读到的这句话,指的就是这种“父节点属于外设总线(如 I2C/SPI),但为了软件架构的解耦,由父节点的驱动在运行时手动调用 of_platform_populate(),强行将其子节点越级注册到平台总线中”的高级技术。

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值