3、I2C总线驱动:I2C控制器驱动框架、设备树配置、I2C从设备驱动编写与调试

I2C总线,在嵌入式开发里太常见了。传感器、触摸屏、eeprom、音频codec……几乎每个板子上都能找到它的身影。RK平台上的I2C控制器驱动,其实内核已经帮我们写好了,我们更多的工作是配置设备树,然后编写从设备驱动。

说白了,你不需要从零写一个I2C控制器驱动。但你要搞清楚它的框架,否则出了问题你都不知道从哪查起。我刚开始搞RK的时候,就犯过这个错——以为I2C不通就是硬件问题,结果折腾半天发现是时钟配置错了。

3.1 I2C控制器驱动框架

RK平台的I2C控制器,本质上是一个总线控制器。它负责产生SCL时钟、处理SDA数据、处理起始/停止条件、应答等等。内核里,这部分代码在 drivers/i2c/busses/i2c-rk3x.c。

这个驱动主要干了三件事:

  • 初始化硬件:设置时钟频率、使能控制器、配置中断。
  • 实现master_xfer:这是核心,负责发送和接收数据。
  • 中断处理:处理传输完成、错误等中断。

我们来看一下它的核心结构体:

static const struct i2c_algorithm rk3x_i2c_algorithm = {
    .master_xfer    = rk3x_i2c_xfer,
    .functionality  = rk3x_i2c_func,
};

这个 master_xfer 就是总线控制器对外提供的接口。你调用 i2c_transfer() 的时候,最终就会走到这里。

关键点:RK的I2C控制器支持标准模式(100kHz)和快速模式(400kHz)。但实际能跑多快,跟你的PCB走线、上拉电阻、从设备能力都有关系。我遇到过一块板子,400kHz死活调不通,降到200kHz就稳了。别死磕频率。

3.2 设备树配置

设备树配置,是RK平台I2C开发里最常打交道的地方。你不需要改内核代码,改dts就行。

一个典型的I2C节点长这样:

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

    /* 从设备节点 */
    touchscreen@38 {
        compatible = "edt,edt-ft5406";
        reg = <0x38>;
        interrupt-parent = <&gpio0>;
        interrupts = <RK_PB5 IRQ_TYPE_EDGE_FALLING>;
    };
};

这里有几个关键属性:

属性 说明 注意事项
clock-frequency I2C总线时钟频率 不要超过从设备支持的最大值
pinctrl-0 引脚复用配置 必须与硬件原理图一致
reg 从设备地址(7位) 注意左对齐还是右对齐,RK用7位地址
status 使能状态 "okay" 或 "disabled"

我曾经踩过的坑:设备树里 reg 填的是7位地址,但有些芯片手册给的是8位地址(带读写位)。比如手册写0x70,实际7位地址是0x38。一定要确认清楚,否则设备根本枚举不到。

3.3 I2C从设备驱动编写

这部分才是我们真正要写的代码。一个I2C从设备驱动,本质上就是一个 i2c_driver。

核心结构体:

static const struct i2c_device_id ft5406_id[] = {
    { "edt,edt-ft5406", 0 },
    { }
};

static struct i2c_driver ft5406_driver = {
    .driver = {
        .name   = "edt-ft5406",
        .of_match_table = ft5406_of_match,
    },
    .probe      = ft5406_probe,
    .remove     = ft5406_remove,
    .id_table   = ft5406_id,
};

驱动加载流程:

  1. 匹配:内核根据设备树里的 compatible 和驱动的 of_match_table 进行匹配。
  2. probe:匹配成功后调用 probe 函数。在这里做初始化、注册中断、创建sysfs节点等。
  3. 读写:通过 i2c_smbus_read_byte_data()、i2c_master_send() 等API与设备通信。

一个简单的读操作示例:

static int ft5406_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
    struct i2c_msg msg[2];
    int ret;

    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].len = 1;
    msg[0].buf = &reg;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].len = 1;
    msg[1].buf = val;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret == 2)
        return 0;
    else
        return -EIO;
}

我的习惯:调试I2C设备时,先用 i2c-tools 在用户空间验证通信是否正常。命令 i2cdetect -y 1 可以扫描总线上的设备,i2cget 和 i2cset 可以直接读写寄存器。等用户空间调通了,再写内核驱动,能省很多时间。

3.4 调试技巧与常见问题

I2C调试,说白了就是看波形、看日志、看返回值。

  • 看波形:逻辑分析仪是神器。抓一下SCL和SDA,看看起始条件、地址、数据、应答是否正常。我习惯用Saleae,便宜好用。
  • 看日志:打开内核的I2C调试日志:echo 7 > /sys/module/i2c_core/parameters/debug。能看到每次传输的细节。
  • 看返回值:驱动里每个 i2c_transfer 的返回值都要检查。返回负数就是出错,返回小于消息数量也是出错。

常见问题:

现象 可能原因 排查方法
设备枚举不到 地址错误、硬件没上电、引脚复用不对 量电压、查原理图、确认设备树
通信超时 时钟频率太高、从设备没应答、总线被拉死 降频率、抓波形、复位从设备
数据错位 地址左对齐/右对齐搞错、寄存器地址长度不对 仔细看芯片手册的时序图
中断不触发 中断号配置错、中断类型不对、GPIO没配成中断模式 检查设备树interrupts属性

重要提醒:I2C总线如果被拉死(SCL或SDA一直低电平),很多时候是因为从设备异常。可以尝试给从设备硬件复位,或者重新上电。如果还不行,检查上拉电阻是否焊好。我遇到过一例,是PCB上拉电阻虚焊,折腾了两天。

3.5 I2C驱动框架与数据流

下面这张图,是我自己总结的I2C数据流。从用户空间到硬件,每一层都有对应的接口。

I2C驱动框架与数据流 用户空间 (应用程序 / i2c-tools) I2C核心层 (i2c-core) I2C控制器驱动 (i2c-rk3x) I2C控制器硬件 (RK芯片内部) I2C从设备 (传感器/触摸屏等) 从设备驱动位置 从设备驱动注册到 I2C核心层 通过i2c_client 与控制器交互 设备树配置 &i2c1 { clock-frequency; pinctrl-0; 从设备节点... };

从这张图可以看得很清楚:你的从设备驱动工作在I2C核心层之上,通过 i2c_client 与控制器驱动交互。控制器驱动再操作硬件寄存器。设备树则贯穿整个配置过程。

3.6 实战:编写一个简单的I2C从设备驱动

假设我们要驱动一个eeprom(AT24C02),设备地址0x50。驱动代码框架如下:

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

static int at24_probe(struct i2c_client *client)
{
    dev_info(&client->dev, "AT24 eeprom probed\n");
    /* 在这里注册misc设备或sysfs接口 */
    return 0;
}

static void at24_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "AT24 eeprom removed\n");
}

static const struct of_device_id at24_of_match[] = {
    { .compatible = "atmel,24c02" },
    { }
};
MODULE_DEVICE_TABLE(of, at24_of_match);

static struct i2c_driver at24_driver = {
    .driver = {
        .name   = "at24",
        .of_match_table = at24_of_match,
    },
    .probe  = at24_probe,
    .remove = at24_remove,
};
module_i2c_driver(at24_driver);

设备树里加上:

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

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
    };
};

编译、烧录、加载驱动。用 i2cdetect -y 2 应该能看到地址0x50的UU标记(表示有驱动绑定)。

小技巧:调试的时候,可以在 probe 函数里加 dev_err 打印,这样即使驱动加载失败,你也能看到错误信息。我习惯在probe开头和结尾各加一句打印,确认流程走到了哪里。

嗯,I2C这部分内容其实不难,但细节很多。设备树配对了,驱动框架搭好了,剩下的就是读写时序的问题。多抓波形,多看手册,慢慢就熟了。