欢迎光临
我们一直在努力

Linux内核学习16--I2C子系统

1 Linux I2C子系统

对于Linux i2c子系统来说,整个子系统由以下三个核心抽象层组成:

1.1 I2C 核心(I2C Core)

这是总线的核心中枢(由 i2c-core-base.c 实现)。它不依赖于任何具体的硬件平台,专门负责:

  • 维护总线上所有的驱动链表和设备链表。

  • 提供统一的、与硬件无关的传输接口(如 i2c_transfer)。

  • 负责撮合“设备”和“驱动”配对。

1.2 I2C 适配器驱动(I2C Adapter)(控制器)

对应芯片(如高通、树莓派、全志)上的 I2C 硬件控制器(SOC I2C IP Core)。这部分一般是原厂提供。

  • 数据结构:struct i2c_adapter

  • 职责:它负责产生真正的 I2C 物理电平信号(START、STOP、ACK、时钟信号)。每一个 adapter 内部都绑定了一个算法结构体 struct i2c_algorithm,里面实现了具体的物理读写逻辑(如 .master_xfer 函数)。

  • 在系统中的呈现:你在 Linux 终端运行 ls /sys/class/i2c-adapter/ 看到的 i2c-0、i2c-1,每一个都代表一个独立的物理 I2C 控制器。

1.3 I2C 设备与驱动(I2C Client & Driver)

这是作为驱动开发者接触最多的一层,专门用来控制挂在 I2C 总线上的外部芯片(如 EEPROM、传感器、触摸屏、摄像头等)。

  • I2C 设备 (struct i2c_client):代表一个真实的、具体的物理芯片个体。它包含了这个芯片的物理信息,比如:它挂在哪个 I2C 控制器上(adapter)、它的 I2C 从机地址(Slave Address)是多少、它叫什么名字。

  • I2C 驱动 (struct i2c_driver):代表控制这类芯片的通用软件剧本。它实现了具体的业务逻辑,比如怎么初始化传感器、怎么读写寄存器,里面包含核心的 .probe 和 .remove 函数。

2 一个驱动例子

DTS,tommy_i2c.dts

/dts-v1/;
/plugin/;

/ {
compatible = "raspberrypi,raspberrypi5", "brcm,bcm2712";

fragment@0 {
/* 目标正确指向 RP1 上的 I2C1 总线 */
target = <&i2c1>;

__overlay__ {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;

tommy_i2c_node@50 {
compatible = "tommy,i2c";
reg = <0x50>;

/* =======================================================
* 【核心修正】在树莓派5上,控制通用40-Pin排针的物理GPIO控制器
* 它的标准设备树标签名字叫:&gio_RP1
* =======================================================
*/
reset-gpios = <&gio_RP1 6 1>; /* 6代表BCM GPIO 6,1代表低电平有效 */
};
};
};
};

编译

dtc -@ -I dts -O dtb -o tommy_i2c.dtbo tommy_i2c.dts

驱动代码tommy_i2c_driver.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/gpio/consumer.h> /* 核心:引入现代 GPIO 子系统 (gpiod) */

/* 驱动自定义的私有结构体,用来存放我们申请到的硬件资源 */
struct tommy_i2c_priv {
struct gpio_desc *reset_gpio;
};

/* 1. 核心业务:当设备树和驱动匹配成功时,内核自动调用此函数 */
static int tommy_i2c_probe(struct i2c_client *client)
{
struct tommy_i2c_priv *priv;
int ret;
u8 reg_addr = 0x00; /* 假设要读取外设的 0x00 寄存器 */

dev_info(&client->dev, "=========================================\\n");
dev_info(&client->dev, "[Tommy-Debug] I2C Driver Probe STARTED!\\n");
dev_info(&client->dev, "[Tommy-Debug] Matched Device Addr: 0x%02x\\n", client->addr);

/* 为我们的小驱动申请一块内存,用来存放私有数据 */
priv = devm_kzalloc(&client->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
i2c_set_clientdata(client, priv);

/* ————————————————————-
* 【核心联动】从你的设备树插件中,盲抓 "reset-gpios" 属性对应的物理引脚
* GPIOD_OUT_LOW 表示申请成功后,默认将该引脚电平拉低(复位状态)
* ————————————————————-
*/
priv->reset_gpio = devm_gpiod_get(&client->dev, "reset", GPIOD_OUT_LOW);
if (IS_ERR(priv->reset_gpio)) {
dev_err(&client->dev, "[Tommy-Debug] Failed to get reset-gpios from Device Tree!\\n");
/* 注意:如果你只是玩玩动态 new_device,没走设备树,这里会报错退出。
* 如果想强行测试,可以把 return 注释掉 */
return PTR_ERR(priv->reset_gpio);
}
dev_info(&client->dev, "[Tommy-Debug] Successfully mapped reset-gpios (GPIO 6)!\\n");

/* 体验一下控制硬件:在 Probe 期间把复位引脚强行拉高(释放复位) */
gpiod_set_value(priv->reset_gpio, 1);
dev_info(&client->dev, "[Tommy-Debug] GPIO 6 pulled HIGH!\\n");

/* ————————————————————-
* 【I2C 寄存器通信测试】
* 尝试利用 SMBus 核心接口,读取物理芯片上 0x00 寄存器的值
* ————————————————————-
*/
ret = i2c_smbus_read_byte_data(client, reg_addr);
if (ret < 0) {
/* 如果你没接真实的 I2C 硬件芯片,这里一定会报 -121 (EREMOTEIO,无应答) 错误 */
dev_warn(&client->dev, "[Tommy-Debug] Register read test skipped or failed (Error: %d)\\n", ret);
} else {
dev_info(&client->dev, "[Tommy-Debug] Read Register 0x%02x Value: 0x%02x\\n", reg_addr, (u8)ret);
}

dev_info(&client->dev, "=========================================\\n");
return 0;
}

/* 2. 当驱动解绑或卸载时执行 */
static void tommy_i2c_remove(struct i2c_client *client)
{
struct tommy_i2c_priv *priv = i2c_get_clientdata(client);

/* 优雅离场:走之前把引脚电平重新拉低,让外设休眠 */
if (priv && priv->reset_gpio) {
gpiod_set_value(priv->reset_gpio, 0);
}
dev_info(&client->dev, "[Tommy-Debug] I2C Driver Removed.\\n");
}

/* 3. 设备树匹配表:【重中之重】必须和你的 DTS 插件里的 compatible 完全一样! */
static const struct of_device_id tommy_i2c_of_match[] = {
{ .compatible = "tommy,i2c" },
{ /* 哨兵,必须留空 */ }
};
MODULE_DEVICE_TABLE(of, tommy_i2c_of_match);

/* 4. 传统 I2C 匹配表(非设备树调试流使用) */
static const struct i2c_device_id tommy_i2c_id[] = {
{ "tommy_i2c_dev", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, tommy_i2c_id);

/* 5. 驱动骨架描述 */
static struct i2c_driver tommy_i2c_driver = {
.driver = {
.name = "tommy_i2c_panel_driver", /* 在 /sys/bus/i2c/drivers/ 下生成的目录名 */
.of_match_table = tommy_i2c_of_match, /* 注入设备树匹配表 */
},
.probe = tommy_i2c_probe,
.remove = tommy_i2c_remove,
.id_table = tommy_i2c_id,
};

/* 6. 向 Linux 内核宣告并注册该 I2C 驱动 */
module_i2c_driver(tommy_i2c_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Tommy");
MODULE_DESCRIPTION("A Perfect Match I2C Driver with GPIO Control");

使用make编译,生成tommy_i2c_driver.ko之后insmod。

直接加载内核节点。

echo "tommy_i2c_dev 0x50" | sudo tee /sys/class/i2c-dev/i2c-1/device/new_device

就可以看到log了。

tom@raspberrypi:~/i2ctest$ dmesg | tail -n 10
[ 580.478863] tommy_i2c_driver: loading out-of-tree module taints kernel.
[ 688.355492] tommy_i2c_panel_driver 1-0050: =========================================
[ 688.355502] tommy_i2c_panel_driver 1-0050: [Tommy-Debug] I2C Driver Probe STARTED!
[ 688.355505] tommy_i2c_panel_driver 1-0050: [Tommy-Debug] Matched Device Addr: 0x50
[ 688.355510] tommy_i2c_panel_driver 1-0050: [Tommy-Debug] Failed to get reset-gpios from Device Tree!
[ 688.355513] tommy_i2c_panel_driver 1-0050: probe with driver tommy_i2c_panel_driver failed with error -2
[ 688.355563] i2c i2c-1: new_device: Instantiated device tommy_i2c_dev at 0x50

如果用设备树,则io不会报错,不过好像输出还是没有,不过这个不重要了。

om@raspberrypi:~/i2ctest$ sudo dtoverlay tommy_i2c.dtbo
om@raspberrypi:~/i2ctest$ dmesg | tail -n 10
[ 883.093562] OF: overlay: WARNING: memory leak will occur if overlay removed, property: /axi/pcie@1000120000/rp1/i2c@70000/#size-cells
[ 883.094289] tommy_i2c_panel_driver 0-0050: =========================================
[ 883.094294] tommy_i2c_panel_driver 0-0050: [Tommy-Debug] I2C Driver Probe STARTED!
[ 883.094296] tommy_i2c_panel_driver 0-0050: [Tommy-Debug] Matched Device Addr: 0x50
[ 883.094319] tommy_i2c_panel_driver 0-0050: [Tommy-Debug] Successfully mapped reset-gpios (GPIO 6)!
[ 883.094322] tommy_i2c_panel_driver 0-0050: [Tommy-Debug] GPIO 6 pulled HIGH!
[ 883.094778] tommy_i2c_panel_driver 0-0050: [Tommy-Debug] Register read test skipped or failed (Error: -121)
[ 883.094782] tommy_i2c_panel_driver 0-0050: =========================================

3 i2c的内核接口 

3.1 I2C 控制器(Adapter)层面的调试接口

位置是/sys/class/i2c-dev/

tom@raspberrypi:~/i2ctest$ ls /sys/class/i2c-dev/
i2c-1 i2c-13 i2c-14
tom@raspberrypi:/sys/class/i2c-dev$ cd i2c-1
tom@raspberrypi:/sys/class/i2c-dev/i2c-1$ ls
dev device name power subsystem uevent

这里i2c-1,就是由RP1南桥芯片引出的通用物理 I2C1接口,也就是最熟悉的树莓派外侧40-Pin引脚排针上的物理引脚。

i2c-13,挂在树莓派 5 主 SoC(BCM2712)内部的低速外设总线,专门映射给了CSI/DSI 0号接口(排针附近的显示器/摄像头软排线接口)。

所属层级 文件/目录名称 读/写 内核核心作用 💡 实用排查技巧与典型调试命令

直属层

 

(i2c-dev 字符驱动)

name 只读 显示底层 I2C 硬件控制器的 IP 核身世。

cat name

 

• 树莓派5上会输出 Synopsys DesignWare I2C adapter,用来确认硬件 IP 来源。

dev 只读 显示该字符设备的主次设备号。

cat dev

 

• 输出 89:1。89 是 I2C 字符设备的内核通行证,1 代表总线编号。

uevent 只写 触发内核事件,通知应用层 udev 创建 /dev/i2c-X 节点。 常用于写自动化脚本或自定义物理节点权限。

硬件纽带层

 

(device/ 软链接)

1-0050/

 

(或类似外设目录)

目录 证明硬件节点已活在内核中。代表总线 1 上地址为 0x50 的设备。

ls device/1-0050/

 

• 如果能进去且里面有 driver 软链接,说明 Probe 成功;如果没有 driver,说明设备单身(没配对成功)。

new_device 只写 无中生有:绕过设备树,在应用层强行实例化一个硬件节点。

echo "tommy_i2c_dev 0x50" | sudo tee device/new_device

 

• 用于快速调通 C 驱动的 probe 函数。

delete_device 只写 斩草除根:动态销毁通过 new_device 创建的硬件节点。

echo 0x50 | sudo tee device/delete_device

 

• 卸载驱动前清理战场专用。

of_node/ 目录 直通设备树:直接映射该控制器在内存中的实时设备树数据。

cat device/of_node/status

 

• 检查该 I2C 控制器本身在硬件上有没有被启用(是否为 okay)。

waiting_for_supplier 只读 依赖等待看板。显示该 I2C 是否在等待时钟、电源等上游供应商。

cat device/waiting_for_supplier

 

• 如果输出不为 0,说明控制器因为欠压或缺时钟挂起了。

功耗管理层

 

(power/ 目录)

control 读写 运行时电源管理(Runtime PM)总开关。

cat device/power/control

 

• auto: 空闲时自动对 I2C 控制器断电省电。

 

• on: 强制永久供电(调试高频通信或排查时序时建议切到 on)。

runtime_status 只读 查看控制器当前的生死状态。

cat device/power/runtime_status

 

• active: 正在供电工作。

 

• suspended: 正在假死冬眠。

runtime_suspended_time 只读 统计开机以来,该 I2C 控制器摸鱼睡觉的总时间(毫秒)。

cat device/power/runtime_suspended_time

 

• 做超低功耗产品(如电池供电网关)时,用来对账续航的核心指标。

i2c-14,同样是挂在主 SoC(BCM2712)内部的总线,专门映射给了CSI/DSI 1号接口(另一个软排线接口)。

关于i2c-13,i2c-14。如果要适配新的屏幕或者Camera,才会动这边。

tom@raspberrypi:~/i2ctest$ ls -l /sys/class/i2c-dev/i2c-1/device/1-0050/
total 0
lrwxrwxrwx 1 root root 0 Jun 13 16:27 driver -> ../../../../../../../bus/i2c/drivers/tommy_i2c_panel_driver
-r–r–r– 1 root root 16384 Jun 13 16:20 modalias
-r–r–r– 1 root root 16384 Jun 13 16:20 name
drwxr-xr-x 2 root root 0 Jun 13 16:20 power
lrwxrwxrwx 1 root root 0 Jun 13 15:53 subsystem -> ../../../../../../../bus/i2c
-rw-r–r– 1 root root 16384 Jun 13 15:53 uevent

3.2 I2C 已匹配设备(Client)层面的调试接口

这里就是/sys/bus/i2c/devices/下面。和上面的/sys/class/i2c-dev/i2c-1/device/1-0050/其实也是一个东西。

这里有内核所有硬件。

4 I2C工具

4.1 i2cdetect

用法

sudo i2cdetect -y -r 1

参数含义:-y 代表自动确认避免交互提示;-r 代表使用标准不破坏芯片的 I2C SMBus 命令去探测;1 代表扫描 i2c-1 总线

执行后会弹出一个 16 * 16 的矩阵网格,代表 0x00 到 0x7F 的 7 位 I2C 地址空间。

0 1 2 3 4 5 6 7 8 9 a b c d e f
00: — — — — — — — —
10: — — — — — — — — — — — — — — — —
20: — — — — — — — — — — — — — — — —
30: — — — — — — — — — — — — — — — —
40: — — — — — — — — — — — — — — — —
50: 50 — — — — — — — — — — — — — — —
60: — — — — — — — — — — — — — — — —
70: — — — — — — — —

看到 50:代表在 0x50 地址上真的收到了硬件芯片的 ACK 应答!硬件通了!

看到 UU:代表这个地址上虽然有芯片,但它已经被内核里的某个 C 驱动给占了(Claimed)。此时用户态工具无法直接读写它,必须先卸载驱动或 unbind 释放它。

满屏幕全是数字:通常是因为硬件上 SDA 或 SCL 引脚断线/虚焊、或者忘记接 4.7K 的上拉电阻,导致总线电平死锁,误让软件以为处处有应答。

4.2 i2cdump

导出芯片全部寄存器。

sudo i2cdump -y 1 0x50 b

参数含义:1 为总线,0x50 为芯片地址,b 代表按字节 byte 连续读取。

常用于对照着芯片的 Datasheet(数据手册)去核对配置寄存器,看是否有某些控制位(如休眠位、使能位)没有被正确写入。

4.3 i2cget

读取单个寄存器。在驱动运行或排除故障时,想知道某一个特定寄存器此时此刻的值是多少(比如读取温度传感器的原始数据寄存器、或者读取中断状态寄存器)。

sudo i2cget -y 1 0x50 0x02 b

参数含义:读取 i2c-1 上 0x50 芯片的 0x02 号寄存器,以 b 字节格式返回。

4.4 i2cset

修改特定寄存器。这在调屏、调摄像头、调电源管理芯片(PMIC)时是绝对的王炸工具。不需要为了测试一个寄存器参数而去改 C 代码、重新编译、挂载模块。直接在终端敲命令,就能实时改写芯片内部的硬件行为。

sudo i2cset -y 1 0x50 0x02 0xFF b

参数含义:向 i2c-1 上的 0x50 芯片的 0x02 寄存器,强行写入数据 0xFF。

比如调一块液晶屏,厂家给了一堆初始化寄存器参数,可以写一个简单的 Shell 脚本,里面全是用 i2cset 一行行去砸寄存器。只要屏幕亮了,说明这套参数百分之百正确,再把它移植到 C 驱动里。

4.5 i2ctransfer

主要是处理多字节、大容量存储芯片的。这里暂时不写太多吧。

5 问题处理

如果某个I2C设备有问题,处理的思路是:

  • 注册对没有?(软件坑位)

  • 引脚对不对?(物理通道)

  • 数据发出去没有?(信号落地)

  • # I2C 看这里:有没有对应的设备节点蹦出来
    ls -l /sys/bus/i2c/devices/

    # SPI 看这里:
    ls -l /sys/bus/spi/devices/

    # 看看对应的 compatible 暗号到底绑定了哪个 ko
    cat /sys/bus/spi/devices/spi10.0/of_node/compatible

    如果在这里连名字都没看到,或者绑定的驱动不对,那就是软件的锅(设备树语法写错、别名没对上、或者 .ko 压根没加载进来)。直接在软件层面解决,根本不需要去动硬件。

    软件配置的是 GPIO 6,但硬件工程师画板子时可能错看成了 GPIO 16。如果这时候软件疯狂发数据,但引脚配错了,信号就会在芯片内部走错路。这时候的技能就是看懂原理图的引脚网络名(Net Name),然后在设备树(Pinctrl / GPIO)里纠正。

    最后就是逻辑分析仪(Logic Analyzer)或 示波器直接看波形。

    赞(0)
    未经允许不得转载:171主机测评 » Linux内核学习16--I2C子系统
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址