嵌入式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 号引脚,低电平点亮)。

    5. 设备树的两种编写模式

      在实际工程中,芯片厂商(如 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 从机设备地址

        };

        };

    6. 实战:从零编写一个控制 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>; // 传入具体的引脚和有效电平
        };
        };

    7. 驱动程序(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;

      }

    8. 避坑黄金法则

      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. 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(),强行将其子节点越级注册到平台总线中"的高级技术。

相关推荐
峥嵘life2 小时前
Android 蓝牙设备连接广播详解-2026
android·python·学习
lihao lihao2 小时前
软硬链接
linux·运维·服务器
TOWE technology2 小时前
智能安防监控系统如何做好防雷?——视频信号SPD综合应用方案解析
运维·服务器·防雷产品·信号保护·信号防雷·spd
楼田莉子2 小时前
Docker学习:Docker介绍及其架构介绍
运维·后端·学习·docker·容器·架构
YY&DS2 小时前
Qt 嵌入 CEF 在 Linux 下必须设置 `QT_XCB_GL_INTEGRATION=xcb_egl才能加载网页
linux·开发语言·qt
辰风沐阳2 小时前
ThinkPHP8.1 + think-swoole 4.1 使用指南(保姆级教程)
linux·后端·swoole
星夜夏空993 小时前
FreeRTOS学习(7)——任务列表
java·前端·学习
不羁的木木3 小时前
Form Kit(卡片开发服务)学习笔记01-核心概念与架构设计
笔记·学习·harmonyos
Mikowoo0073 小时前
神经网络 替代 线性模型_进行模型学习
人工智能·神经网络·学习