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)或 示波器直接看波形。