3. I2C驱动开发:I2C总线协议、RK I2C控制器驱动、外设驱动适配与调试

I2C,说白了就是两根线搞定一堆外设。SCL(时钟)和SDA(数据),就这么简单。但真要在RK平台上把它跑稳,坑还真不少。我最早接触I2C是在一个温湿度传感器项目上,当时觉得协议简单,结果被ACK信号折腾了一整天。嗯,咱们今天就把这事彻底聊透。

3.1 I2C总线协议速览

先过一遍基础,不然后面代码你看着会懵。

  • 主从结构:RK芯片永远是Master,外设是Slave。一个总线上可以挂多个Slave,靠7位地址区分。
  • 起始条件:SCL高电平时,SDA从高拉低。停止条件:SCL高电平时,SDA从低拉高。
  • 数据传输:每字节8位,后面跟一个ACK位。Slave拉低SDA表示应答,不拉就是NACK。
  • 速率模式:标准模式100kHz,快速模式400kHz,高速模式3.4MHz。RK平台一般用100k或400k。

关键点:I2C的ACK机制是硬件自动完成的,但驱动层必须正确处理NACK。我曾经遇到一个EEPROM,写入后需要等待内部擦除,如果驱动不处理NACK,数据就丢了。

3.2 RK I2C控制器驱动架构

RK的I2C控制器,在芯片内部是一个独立的硬件模块。驱动代码在Linux内核里,路径是drivers/i2c/busses/i2c-rk3x.c。这个文件,我建议你直接打开看一眼,结构很清晰。

3.2.1 硬件框图

先看一张图,理解RK I2C控制器在系统里的位置。

RK I2C控制器驱动架构 用户空间应用 I2C核心层 (i2c-core) RK I2C控制器驱动 (i2c-rk3x.c) RK I2C硬件控制器 Slave 1 Slave 2 Slave N 应用通过 /dev/i2c-N 或 sysfs 访问外设 核心驱动:处理总线 仲裁、地址匹配

3.2.2 驱动核心数据结构

驱动里最重要的结构体是struct rk3x_i2c。它管理着硬件寄存器、时钟、DMA、完成量等。我挑几个关键字段说说:

字段 类型 说明
regs void __iomem * 寄存器基地址,ioremap后的虚拟地址
clk struct clk * I2C模块时钟,频率决定SCL速率
msg struct i2c_msg * 当前正在处理的消息
state enum rk3x_i2c_state 状态机:IDLE, START, READ, WRITE, STOP
error int 错误码,用于中断处理中传递错误

个人习惯:调试I2C时,我第一件事就是检查clk的频率。很多奇怪问题,比如SCL波形不对,都是时钟配置错了。用clk_get_rate打印一下,心里就有底了。

3.3 外设驱动适配实战

这部分是重头戏。你拿到一个新外设,比如一个温度传感器,怎么在RK平台上把驱动写出来?

3.3.1 设备树配置

RK平台用设备树描述硬件。I2C外设的节点要挂在对应的I2C总线节点下。举个例子:

&i2c1 {
    status = "okay";
    clock-frequency = <400000>;  /* 400kHz */

    temperature_sensor: tmp117@48 {
        compatible = "ti,tmp117";
        reg = <0x48>;
        status = "okay";
    };
};

这里要注意几点:

  • clock-frequency:我建议先从100kHz开始,稳定后再提频。直接上400kHz,如果PCB走线不好,容易丢数据。
  • reg:7位地址,左移1位填入。比如0x48,实际发送时是0x90(写)和0x91(读)。
  • compatible:驱动匹配的关键字符串,必须和驱动里of_match_table一致。

3.3.2 驱动编写模板

一个标准的I2C外设驱动,骨架是这样的:

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

static int tmp117_probe(struct i2c_client *client)
{
    struct device *dev = &client->dev;
    dev_info(dev, "TMP117 detected at 0x%02x\n", client->addr);
    /* 初始化外设,注册硬件监控接口等 */
    return 0;
}

static void tmp117_remove(struct i2c_client *client)
{
    /* 清理资源 */
}

static const struct of_device_id tmp117_of_match[] = {
    { .compatible = "ti,tmp117" },
    { }
};
MODULE_DEVICE_TABLE(of, tmp117_of_match);

static struct i2c_driver tmp117_driver = {
    .driver = {
        .name = "tmp117",
        .of_match_table = tmp117_of_match,
    },
    .probe = tmp117_probe,
    .remove = tmp117_remove,
};
module_i2c_driver(tmp117_driver);

写驱动时,我习惯先实现probe函数,只打印一条日志,确认设备树匹配成功。然后再逐步添加读写功能。这样能快速定位问题。

3.3.3 读写操作

I2C的读写,内核提供了i2c_transfer接口。但更常用的是封装好的i2c_smbus_read_byte_data和i2c_smbus_write_byte_data。举个例子:

/* 读取TMP117温度寄存器(0x00) */
int tmp117_read_temp(struct i2c_client *client)
{
    int ret;
    s16 raw;

    ret = i2c_smbus_read_word_data(client, 0x00);
    if (ret < 0) {
        dev_err(&client->dev, "read temp failed: %d\n", ret);
        return ret;
    }

    raw = (s16)ret;
    /* TMP117分辨率0.0078125°C */
    return (raw * 78125) / 10000;
}

我曾经踩过的坑:i2c_smbus_read_word_data返回的数据是CPU字节序,但外设可能是大端。TMP117就是大端,直接赋值会得到错误值。一定要根据数据手册确认字节序,必要时用be16_to_cpu转换。

3.4 调试方法与工具

驱动写好了,怎么验证?我有一套固定的调试流程。

3.4.1 硬件层面

示波器是王道。抓SCL和SDA波形,看起始条件、地址、数据、ACK。我常用的检查点:

  1. SCL频率对不对?用示波器测周期,算频率。
  2. 地址对不对?Slave地址是否和设备树一致。
  3. ACK有没有?Slave没应答,说明地址错或外设没上电。

3.4.2 软件层面

内核提供了i2cdetect、i2cget、i2cset等工具。在RK板子上,直接敲命令:

# 扫描I2C1总线上的设备
i2cdetect -y 1

# 读取TMP117 0x00寄存器
i2cget -y 1 0x48 0x00 w

如果i2cdetect能扫到设备,但驱动probe不执行,那问题大概率在设备树compatible字符串不匹配。我遇到过好几次,都是因为少打了个逗号。

3.4.3 内核调试开关

打开I2C的调试日志,能看到每次传输的细节:

# echo 'file i2c-core.c +p' > /sys/kernel/debug/dynamic_debug/control
# echo 'file i2c-rk3x.c +p' > /sys/kernel/debug/dynamic_debug/control

然后看dmesg输出。你会看到类似这样的信息:

[   12.345] i2c-core: i2c_transfer: msg[0] addr=0x48, flags=0x0, len=1
[   12.346] i2c-core: i2c_transfer: msg[1] addr=0x48, flags=0x1, len=2

这能帮你确认数据是否真的发到了总线上。

3.5 常见问题与避坑

问题 现象 解决方案
SCL无波形 示波器看SCL一直高 检查I2C时钟是否enable,clk_prepare_enable调了没
Slave无ACK 第九个SCL脉冲后SDA仍高 确认地址、外设供电、IO电压是否匹配
数据错位 读回来的值不对 检查字节序、寄存器地址是否要拼接
总线锁死 SDA一直被拉低 复位外设,或给SCL发9个脉冲恢复

避坑指南:我曾经在一个项目里,I2C总线莫名其妙锁死。查了半天,发现是Slave在启动时拉低了SDA。解决办法是在驱动probe之前,先通过GPIO模拟9个SCL脉冲,强制释放总线。这个技巧,你记一下,迟早用得上。

好了,I2C这块的内容就这些。从协议到驱动,再到调试,每一步都有坑,但也都有解法。你按这个流程走,大部分问题都能搞定。