设备树其实只描述三件事

0 阅读6分钟

前两天调一块 imx6ull 的板子,外挂了一颗 I2C 的温湿度传感器,驱动是从别处拷过来的,按理说改个地址就能跑。结果 probe 死活不进来,dmesg 里干干净净,连个报错都没有。我盯着 arch/arm/boot/dts/imx6ull-myboard.dts 看了半天,越看越迷糊——这文件里一堆 &i2c1、pinctrl-0、status = "okay",到底哪些是必须的,哪些是我抄多了?

后来我把设备树这块重新捋了一遍,发现其实没那么玄。设备树说白了就描述三件事:有什么设备、设备挂在哪、设备用什么引脚和参数。 想通这一点,再看那些 dts 文件就顺眼多了。

一、背景

板子是 imx6ull,跑的 Linux 4.1.15(NXP 那套 BSP)。传感器是 SHT30,I2C 地址 0x44,挂在 I2C1 上,SDA 用的是 UART4_TX_DATA 那个复用脚,SCL 是 UART4_RX_DATA。

我一开始是这么写的,从别的板子的 dts 里抄了一段:

&i2c1 {
    status = "okay";
    clock-frequency = <100000>;

    sht30@44 {
        compatible = "sensirion,sht30";
        reg = <0x44>;
    };
};

编译、烧录、重启,i2cdetect -y 1 能看到 0x44 有响应,说明硬件通了。但 ls /sys/bus/i2c/devices/ 里就是没有 1-0044 这个目录,驱动的 probe 函数一次都没进。

二、相关知识点

先别急着改代码,把设备树的作用想清楚。内核启动的时候,CPU 只知道自己是 ARM 核、内存多大,板子上焊了什么芯片、芯片接在哪根总线上、用哪个引脚,内核是不知道的。以前这些信息全塞在 arch/arm/mach-xxx/ 的 C 代码里,板子一多就成一锅粥。设备树就是把这些"板级信息"抽出来,变成一份给内核看的硬件清单。

所以一份 dts 里,你写的每一段,本质上都在回答三个问题:

1、有什么设备 —— 靠 compatible 字符串。这是设备和驱动配对的唯一钥匙,驱动里 of_match_table 写什么,dts 里就得写什么,一个字符都不能差。

2、设备挂在哪 —— 靠设备树的层级关系。sht30@44 写在 &i2c1 里面,内核就知道它是 I2C1 总线上的设备,reg = <0x44> 就是它的从机地址。写在 &spi1 里面,那就是 SPI 设备,reg 变成片选号。位置决定总线类型,这是设备树最省事的地方。

3、设备用什么引脚、什么参数 —— 靠 pinctrl 和自定义属性。引脚复用要单独定义 pinctrl 节点,然后在设备节点里用 pinctrl-0 引用;频率、中断脚、复位脚这些,就写成自定义 property,驱动里用 of_property_read_u32() 去读。

至于 status = "okay",那只是"这个节点要不要启用"的开关,跟设备本身没关系。clock-frequency 是 I2C 控制器的总线速率,也是控制器的事,不是传感器的事。

想明白这三层,再回头看我的 dts——设备有了(compatible 写了),位置有了(挂在 i2c1 下),但引脚复用压根没配。imx6ull 的 I2C1 默认引脚和 UART4 那两个脚不是同一组,不配 pinctrl,I2C 控制器根本出不来波形。

三、解决过程

1、先确认 compatible 有没有对上

别笑,这一步最容易翻车。我 grep 了一下驱动源码:

grep -rn "sht30" drivers/hwmon/

驱动里写的是:

static const struct of_device_id sht30_of_match[] = {
    { .compatible = "sensirion,sht30" },
    { }
};

跟我 dts 里写的一致,这条没问题。顺手提醒一句,compatible 建议用 厂商,型号 的格式,全小写,别自己造。

2、确认 I2C 控制器真的使能了

ls /sys/bus/i2c/devices/

输出里有 i2c-0、i2c-1,说明控制器起来了。再看一眼:

cat /sys/bus/i2c/devices/i2c-1/name

输出 21a0000.i2c,对得上。控制器 OK。

3、补 pinctrl,这是真正的坑

去 arch/arm/boot/dts/imx6ul-pinfunc.h 里查引脚宏,找到:

#define MX6UL_PAD_UART4_TX_DATA__I2C1_SCL  0x01b4 0x0440 0x0000 0x2 0x0
#define MX6UL_PAD_UART4_RX_DATA__I2C1_SDA  0x01b8 0x0444 0x0000 0x2 0x0

在 dts 里加 pinctrl 节点:

&iomuxc {
    pinctrl_i2c1_sht30: i2c1sht30grp {
        fsl,pins = <
            MX6UL_PAD_UART4_TX_DATA__I2C1_SCL  0x4001b8b0
            MX6UL_PAD_UART4_RX_DATA__I2C1_SDA  0x4001b8b0
        >;
    };
};

&i2c1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1_sht30>;
    status = "okay";
    clock-frequency = <100000>;

    sht30@44 {
        compatible = "sensirion,sht30";
        reg = <0x44>;
    };
};

那个 0x4001b8b0 是 pad 配置值,具体含义查参考手册的 IOMUXC 章节,我一般直接抄同板子其他 I2C 的配置,改一下电气属性。

4、编译、烧录、重启

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx6ull-myboard.dtb

把 dtb 拷到板子 /boot/ 下,重启。

实际测试还是不行呀,/sys/bus/i2c/devices/1-0044 还是没有,probe 依然不进。我当时的第一反应是 pinctrl 又写错了,把 dts 反编译回来对了一遍:

dtc -I dtb -O dts /boot/imx6ull-myboard.dtb | grep -A5 sht30

节点确实在。又 cat /proc/device-tree/soc/aips-bus@02100000/i2c@021a0000/status,输出 okay。设备树这边看着都对。

折腾了大半天才发现是 内核里的 SHT30 驱动压根没编进去。make menuconfig 里那个选项是 <M>,但我烧的是最小根文件系统,modules 目录是空的,insmod 也没得 insmod。改成 <*> 编进内核,重新烧 zImage,重启,1-0044 目录终于出现了。

cat /sys/bus/i2c/devices/1-0044/name

输出 sht30。再读一下温度:

cat /sys/class/hwmon/hwmon0/temp1_input

输出 25300,25.3 摄氏度,通了。

四、结果和一点遗留

现在回头看,设备树那三个问题,我一开始只答对了前两个——写了 compatible,挂对了位置,但引脚复用没配。等这个补上,又栽在"驱动没编进内核"这种跟设备树八竿子打不着的地方。所以说设备树配对了不等于驱动能跑,probe 不进的时候,dts、驱动、内核配置三边都得查,别一上来就怀疑设备树。

遗留一个小问题:clock-frequency 我写的 100kHz,但实测波形上升沿有点软,估计是上拉电阻偏大。这个跟设备树没关系了,得改板子,先放着。

参考资料:

  • 内核文档 Documentation/devicetree/bindings/i2c/i2c.txt
  • NXP《i.MX 6ULL Applications Processor Reference Manual》第 32 章 IOMUXC
  • 感谢网上那些把 pinfunc 宏整理成表格的网友,省了我不少事

我这方面也就学了点皮毛,上面有说错的地方,欢迎留言指正,一起交流。