4. 设备树实例:I2C控制器

I2C(Inter-Integrated Circuit)总线是嵌入式Linux系统中使用最广泛的低速外设总线之一,连接着触摸屏、传感器、EEPROM、RTC等大量从设备。在设备树中正确描述I2C控制器及其从设备,是驱动正常工作的前提。本节将以实际项目为例,详细讲解I2C控制器设备树节点的编写方法,涵盖时钟频率、从设备地址、时序参数等关键属性。

4.1 I2C控制器设备树节点结构

一个完整的I2C控制器节点通常包含以下层次结构:

  • 控制器节点:描述I2C总线控制器本身的硬件信息(寄存器基址、中断号、时钟频率等)。
  • 从设备子节点:挂载在该总线上的每个从设备,包含设备地址、时序参数、中断引脚等。
  • 引脚控制(pinctrl):描述I2C总线使用的硬件引脚(SCL、SDA)及其复用功能。
关键原则:设备树中的I2C节点描述的是硬件连接关系,而非驱动行为。驱动通过读取设备树属性来获取硬件参数,从而完成初始化与通信配置。

4.2 核心属性详解

I2C控制器节点及其从设备子节点中,以下属性最为关键:

属性名 所属节点 说明 示例值
compatible 控制器 匹配驱动,格式为"厂商,型号" "fsl,imx6q-i2c"
reg 控制器 寄存器基地址与长度 <0x021a0000 0x4000>
interrupts 控制器 中断号与触发类型 <0 36 IRQ_TYPE_LEVEL_HIGH>
clock-frequency 控制器 I2C总线时钟频率(单位Hz) <100000>
reg 从设备 7位或10位从设备地址 <0x1a>
i2c-scl-rising-time-ns 从设备 SCL上升时间(纳秒) <300>
i2c-scl-falling-time-ns 从设备 SCL下降时间(纳秒) <100>

4.3 完整设备树实例

以下是一个基于i.MX6Q平台的I2C控制器节点实例,挂载了触摸屏控制器和温度传感器两个从设备:

/* I2C1控制器节点 */
&i2c1 {
    compatible = "fsl,imx6q-i2c", "fsl,imx21-i2c";
    reg = <0x021a0000 0x4000>;
    interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6QDL_CLK_I2C1>;
    clock-names = "ipg";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";

    /* 总线时钟频率:100kHz标准模式 */
    clock-frequency = <100000>;

    /* 从设备1:触摸屏控制器 FT5x06 */
    touchscreen@38 {
        compatible = "edt,ft5x06";
        reg = <0x38>;
        interrupt-parent = <&gpio1>;
        interrupts = <9 IRQ_TYPE_EDGE_FALLING>;
        reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
        touchscreen-size-x = <1024>;
        touchscreen-size-y = <600>;

        /* 时序参数:根据硬件手册设定 */
        i2c-scl-rising-time-ns = <300>;
        i2c-scl-falling-time-ns = <100>;
    };

    /* 从设备2:温度传感器 LM75 */
    temperature@48 {
        compatible = "national,lm75";
        reg = <0x48>;
        vs-supply = <®_3v3>;

        /* 时序参数:快速模式 */
        i2c-scl-rising-time-ns = <150>;
        i2c-scl-falling-time-ns = <50>;
    };
};
常见错误:clock-frequency 属性必须放在控制器节点内,而不是从设备子节点中。从设备节点中的时序参数(如 i2c-scl-rising-time-ns)用于帮助控制器驱动计算时序寄存器值,而非设置总线频率。

4.4 时序参数的作用与配置

I2C时序参数直接影响总线通信的可靠性,尤其在高速模式(400kHz)或快速模式+(1MHz)下更为关键。控制器驱动根据这些参数计算SCL高电平和低电平的保持时间:

  • i2c-scl-rising-time-ns:SCL信号从低电平上升到高电平所需时间。该值越大,驱动需要插入的等待周期越多。
  • i2c-scl-falling-time-ns:SCL信号从高电平下降到低电平所需时间。通常比上升时间短。
  • i2c-sda-hold-time-ns(可选):SDA数据保持时间,用于满足从设备的建立时间要求。

时序参数的典型取值范围如下:

总线模式 时钟频率 典型上升时间 典型下降时间
标准模式 100 kHz 300-1000 ns 100-300 ns
快速模式 400 kHz 100-300 ns 30-100 ns
快速模式+ 1 MHz 50-150 ns 20-50 ns
调试技巧:如果I2C通信出现随机失败或数据错误,首先检查时序参数是否与硬件手册匹配。使用示波器测量SCL/SDA的实际上升/下降时间,然后更新设备树中的对应值。不匹配的时序参数会导致总线时序违规,表现为ACK丢失或数据位错误。

4.5 I2C控制器设备树结构图

下图展示了I2C控制器节点、从设备子节点以及引脚控制之间的层次关系:

I2C控制器设备树节点层次结构 根节点 / soc aips-bus@02000000 i2c1@021a0000 clock-frequency = <100000> touchscreen@38 reg = <0x38> i2c-scl-rising-time-ns = <300> temperature@48 reg = <0x48> i2c-scl-rising-time-ns = <150> 引脚控制(pinctrl) pinctrl-0 = <&pinctrl_i2c1> 定义SCL/SDA引脚复用功能

4.6 从设备地址的编码规则

I2C从设备地址在设备树中使用 reg 属性表示,需要注意以下规则:

  • 7位地址:直接写入十六进制值,如 reg = <0x48>。这是最常用的格式。
  • 10位地址:需要设置 reg 为两个单元格,第一个单元格包含地址格式标志。例如 reg = <0x00000002 0x01a0>,其中 0x00000002 表示10位地址格式。
  • 地址左移:某些硬件手册将地址写为8位格式(左移1位),但设备树中必须使用7位格式。例如手册写 0x90,设备树中应写 0x48。
地址陷阱:7位地址范围是 0x08 到 0x77(保留地址除外)。地址 0x00 到 0x07 以及 0x78 到 0x7F 是I2C规范保留的通用呼叫和特殊用途地址,不应分配给普通从设备。

4.7 驱动中读取设备树属性的方法

在I2C控制器驱动中,通过以下API读取设备树中的时序参数:

#include <linux/i2c.h>
#include <linux/of.h>

static int my_i2c_probe(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    struct i2c_adapter *adap;
    u32 clock_freq;
    int ret;

    /* 读取总线时钟频率 */
    ret = of_property_read_u32(np, "clock-frequency", &clock_freq);
    if (ret) {
        /* 未设置时使用默认值100kHz */
        clock_freq = 100000;
        dev_warn(&pdev->dev, "clock-frequency not set, using 100kHz\n");
    }

    /* 读取时序参数(从设备子节点中获取) */
    struct device_node *child;
    u32 rise_time, fall_time;

    for_each_child_of_node(np, child) {
        if (!of_property_read_u32(child, "i2c-scl-rising-time-ns", &rise_time)) {
            dev_info(&pdev->dev, "child %s: rise_time=%dns\n",
                     child->name, rise_time);
        }
        if (!of_property_read_u32(child, "i2c-scl-falling-time-ns", &fall_time)) {
            dev_info(&pdev->dev, "child %s: fall_time=%dns\n",
                     child->name, fall_time);
        }
    }

    /* 根据时序参数配置I2C控制器硬件寄存器 */
    configure_i2c_timing(adap, clock_freq, rise_time, fall_time);

    return 0;
}

4.8 调试I2C设备树节点的方法

当I2C设备树节点配置不正确时,驱动可能无法正常探测从设备。以下是常用的调试方法:

调试方法 命令/操作 预期输出
查看设备树节点 ls /proc/device-tree/soc/aips-bus@*/i2c@*/ 显示所有I2C控制器节点
检查节点属性 cat /proc/device-tree/.../clock-frequency 输出时钟频率值(如 100000)
扫描I2C总线 i2cdetect -y <bus-number> 显示所有检测到的从设备地址
查看驱动绑定 ls /sys/bus/i2c/devices/ 显示已绑定的I2C设备列表
动态调试输出 echo 'file i2c-core-of.c +p' > /sys/kernel/debug/dynamic_debug/control 打印设备树解析过程的调试信息
实战案例:某项目中使用FT5x06触摸屏,I2C通信偶尔失败。通过示波器测量发现SCL上升时间约为450ns,而设备树中设置为300ns。将 i2c-scl-rising-time-ns 更新为450后,通信稳定性显著提升。这个案例说明时序参数必须与实际硬件测量值匹配,不能随意照搬参考设计。

4.9 本章小结

I2C控制器设备树节点的编写需要关注三个核心层面:控制器本身的硬件资源描述、从设备地址与中断配置、以及时序参数的精确设定。时钟频率决定了总线的工作模式,从设备地址必须使用7位格式且避开保留地址,时序参数则直接影响通信的可靠性。通过本节的学习,你应该能够独立编写和调试I2C控制器的设备树节点,为后续的驱动开发打下坚实基础。