【全栈第16课】I2C 从设备驱动
Android 全栈工程师进阶教程 第16课。I2C 从设备驱动。基于公开 AOSP 知识整理,含原理、可编译示例、常见问题定位。
> 本文是《Android 全栈工程师进阶教程》系列第 16 课。完整 26 课见 GitHub 仓库。
【全栈第16课】I2C 从设备驱动
> ✅ 本机 Linux 6.17 真 make modules 编出 .ko(i2c_driver 单参数 probe、void remove 新 API 经真编译确认)
0. 这节课你将做出什么
写一个 I2C 从设备驱动:probe 里通过 regmap 读芯片 ID 寄存器,验证和 IC 的通信。 学完你能回答:触控/传感器/指纹这些挂在 I2C 总线上的硬件,驱动怎么写?regmap 是什么、为什么用它?bring-up 一颗新 IC 第一步该干什么?I2C 节点的 reg 和 platform 节点的 reg 有什么不同?
1. 背景:I2C 是外设最常见的总线
手机里 SoC 和外围小芯片(触控 IC、各种传感器、指纹、PMIC)通信,最常用 I2C 总线(两根线:SCL 时钟、SDA 数据)。一条 I2C 总线上可以挂多个从设备,每个有唯一的 7 位地址。
驱动外设的本质 = 通过 I2C 总线读写 IC 的寄存器。读它的状态寄存器拿数据,写它的配置寄存器控制它。学会 I2C 驱动 = 掌握和绝大多数外围芯片打交道的方式。
2. 全局图:I2C 驱动结构
DTS: &i2c1 { hello_i2c@48 { compatible="xiaomi,hello-i2c"; reg=<0x48>; } }
│ reg=I2C 从设备地址(不是内存地址!)
│ 内核 I2C 框架按 compatible 匹配 + 在 0x48 地址挂上从设备
▼
i2c_driver.probe(client)
├─ devm_regmap_init_i2c(client) 建 regmap(统一寄存器读写抽象)
├─ regmap_read(regmap, CHIPID_REG, &id) 读芯片 ID(bring-up 第一步)
└─ 注册 input/iio 子系统 / 申请中断(L17) / 上报数据
3. 某真实机型 / 真实印证
真实触控/传感器驱动(如 drivers/input/touchscreen/、drivers/iio/)都是这套:
- DTS 节点挂在某条 i2c 控制器下,
reg是 IC 的 I2C 地址 - probe 里先读 CHIPID 确认 IC 在、通信正常
- regmap 屏蔽 I2C/SPI 差异(换 SPI 只改 init)
4. 前置准备
- 依赖 L13/L14/L15(内核、驱动、DTS、compatible 匹配)。
- 真编译:本机 Linux 6.17(已验证)。
- 文件:
hello_i2c.c、hello-i2c.dtsi、Makefile。
5. 动手:写 I2C 驱动
5.1 DTS 节点 hello-i2c.dtsi
&i2c1 { // 挂到 i2c1 控制器(SoC 上某条 I2C 总线)
status = "okay";
hello_i2c@48 {
compatible = "xiaomi,hello-i2c";
reg = <0x48>; // ★ I2C 从设备地址 0x48(7位地址),不是内存地址!
interrupt-parent = <&tlmm>;
interrupts = <42 2>; // 数据就绪中断(L17 用)
};
};
> reg 在 I2C 节点里 = 从设备地址(0x48),这是 I2C 节点和 platform 节点(L15 里 reg 是内存地址)最大的区别,新手极易混。
5.2 I2C 驱动 hello_i2c.c
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/regmap.h> // regmap 统一寄存器读写
#include <linux/of.h>
#include <linux/mod_devicetable.h>
#define HELLO_REG_CHIPID 0x00 // 假设芯片 ID 在寄存器 0x00
#define HELLO_EXPECT_ID 0x42 // 期望读到 0x42
struct hello_i2c_data {
struct i2c_client *client;
struct regmap *regmap;
};
// regmap 配置:告诉 regmap 寄存器地址宽度、值宽度(I2C 常见 8 位地址 8 位值)
static const struct regmap_config cfg = {
.reg_bits = 8, .val_bits = 8, .max_register = 0xFF,
};
// probe:I2C 设备匹配上(compatible 或 i2c_device_id)后调用
// ★ 注意:现代内核 probe 是单参数 (struct i2c_client*),老内核有第二个 const i2c_device_id* 参数
static int hello_probe(struct i2c_client *client) {
struct hello_i2c_data *d;
unsigned int id = 0;
int ret;
d = devm_kzalloc(&client->dev, sizeof(*d), GFP_KERNEL); // devm_ 自动释放,不用手动 free
if (!d) return -ENOMEM;
d->client = client;
d->regmap = devm_regmap_init_i2c(client, &cfg); // 基于 i2c client 建 regmap
if (IS_ERR(d->regmap)) return PTR_ERR(d->regmap);
// ★ bring-up 第一步:读 CHIPID 验证通信
ret = regmap_read(d->regmap, HELLO_REG_CHIPID, &id);
if (ret) {
dev_err(&client->dev, "读 CHIPID 失败 ret=%d(硬件没接好?I2C地址错?上电时序?)\n", ret);
return ret;
}
dev_info(&client->dev, "CHIPID=0x%02x(期望 0x%02x)\n", id, HELLO_EXPECT_ID);
i2c_set_clientdata(client, d);
// 真实驱动:注册 input/iio 设备、申请中断(L17)、上报数据
return 0;
}
static void hello_remove(struct i2c_client *client) { // i2c remove 早就是 void(对比 L15 platform)
dev_info(&client->dev, "remove\n");
}
// DTS 匹配
static const struct of_device_id hello_of_match[] = {
{ .compatible = "xiaomi,hello-i2c" }, { }
};
MODULE_DEVICE_TABLE(of, hello_of_match);
// 非 DTS 匹配(兼容老板子)
static const struct i2c_device_id hello_id[] = { { "hello-i2c", 0 }, { } };
MODULE_DEVICE_TABLE(i2c, hello_id);
static struct i2c_driver hello_driver = {
.driver = { .name = "hello-i2c", .of_match_table = hello_of_match },
.probe = hello_probe,
.remove = hello_remove,
.id_table = hello_id,
};
module_i2c_driver(hello_driver); // 宏自动注册/注销
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Lesson16 i2c driver");
逐块讲为什么:
- regmap 是重点:它是统一的寄存器读写抽象,
regmap_read/write屏蔽底层是 I2C 还是 SPI。换成 SPI 芯片只需把devm_regmap_init_i2c改成devm_regmap_init_spi,业务代码一行不动。这是现代驱动的标准做法。 - 读 CHIPID 是 bring-up 第一步:接一颗新 IC,第一件事就是读它的 ID 寄存器。读到期望值 = I2C 通了、地址对了、芯片活着、上电正常。读失败 = 硬件/地址/时序有问题,马上能定位。
- devm_ 前缀:
devm_kzalloc/devm_regmap_init_i2c是"设备管理"版本,设备移除时自动释放,不用在 remove 里手动清,减少泄漏。
6. 看底层:一次 regmap_read 走了什么
regmap_read(regmap, 0x00, &val)
→ regmap 框架知道这是 i2c regmap
→ i2c_smbus_read_byte_data(client, 0x00) 或 i2c_transfer
→ I2C 控制器驱动:在 SCL/SDA 上发 [START][从地址0x48+W][寄存器0x00][START][从地址0x48+R][读一字节][STOP]
→ 读回的字节放进 val
所以 regmap_read 底层就是 I2C 总线上的一次"先写寄存器地址,再读数据"的标准时序。读不到时用示波器看 SCL/SDA 波形(L23)。
7. 编译 & 运行(✅ 真编译过)
make # 真编译出 hello_i2c.ko
# DTS 挂到真实 i2c 总线后,probe 触发:
adb shell dmesg | grep -i "hello.*chipid\|hello-i2c"
真编译结果(本机 6.17):
CC [M] hello_i2c.o
LD [M] hello_i2c.ko ← 单参数 probe、void remove 都编过
> 验证状态(诚实):本机 6.17 真编译通过。i2c_driver 的单参数 probe(6.x 去掉了第二个 const i2c_device_id* 参数)、void remove 经真编译确认正确。
8. 踩坑提醒
- reg 写成内存地址:I2C 节点 reg 是从设备地址(0x48),写成 platform 那种内存地址 → I2C 框架挂错地址,通信失败。
- probe 参数:现代单参数
(struct i2c_client*),老内核双参数。版本不对编不过。 - regmap_config 宽度错:reg_bits/val_bits 和芯片实际不符(比如 16 位地址写成 8 位)→ 读到的全是错的。
- CHIPID 读失败当驱动 bug:多半是硬件——地址错、没上电、排线松,先示波器看波形(L23),别盯着代码。
- i2c remove 是 void:别学 L15 platform 那样纠结(i2c 早就是 void)。
9. 常见问题分析与定位(I2C 实战)
9.1 常见问题清单
- probe 里 regmap_read 返回 -EIO / -ENXIO(读不到)
- probe 根本没被调用
- 读到的 CHIPID 不对(不是期望值)
- I2C 总线上多设备地址冲突
- 偶发读失败(时序/干扰)
9.2 分析与定位
① regmap_read 返回 -EIO/-ENXIO
-ENXIO:该地址没有设备应答(NAK)——地址错 / IC 没上电 / 排线没接好。-EIO:通信错——时序、干扰、IC 异常。- 定位:
adb shell dmesg | grep -iE "i2c|hello" # 驱动报的错
# 用 i2c 工具直接读(绕过驱动,确认硬件层):
adb shell i2cdetect -y <bus> # 扫总线,看 0x48 在不在
adb shell i2cget -y <bus> 0x48 0x00 # 直接读 0x00 寄存器
i2cdetect看不到 0x48 → 硬件层问题(上电/地址/排线),示波器看 SCL/SDA(L23);看得到但驱动读不到 → regmap 配置/驱动逻辑。
② probe 没调用 — 同 L15:compatible 不一致 / DTS status≠okay / 节点没挂对 i2c 控制器。ls /sys/bus/i2c/devices/ 看从设备有没有创建。
③ CHIPID 不对 — regmap 宽度配置错,或读错了寄存器,或真的是另一颗芯片。对照 datasheet 确认 ID 寄存器地址和值。
④ 地址冲突 — 一条总线两个 0x48。看原理图,有的 IC 有地址选择脚(ADDR pin)改地址。
⑤ 偶发失败 — 时序裕量不够 / 上拉电阻不合适 / 干扰。示波器看波形质量;调 I2C 频率。
9.3 定位决策树
I2C 出问题
├─ regmap_read -ENXIO → 没设备应答 → i2cdetect 扫总线
│ ├─ 扫不到 0x48 → 硬件:上电/地址/排线 → 示波器看 SCL/SDA(L23)
│ └─ 扫得到但驱动读不到 → regmap 配置/驱动逻辑
├─ probe 没调 → compatible/status/挂对总线(同 L15)+ ls /sys/bus/i2c/devices/
├─ CHIPID 不对 → 对 datasheet 核 ID 寄存器+regmap 宽度
├─ 地址冲突 → 原理图/ADDR pin 改地址
└─ 偶发失败 → 示波器看波形/上拉/频率/干扰
10. 小结
- I2C 是外设最常见总线,驱动本质 = 读写 IC 寄存器。
- regmap 统一寄存器读写,屏蔽 I2C/SPI 差异(换 SPI 只改 init)。
- bring-up 第一步读 CHIPID 验证通信,读不到先怀疑硬件(用 i2cdetect/示波器)。
- I2C 节点 reg = 从设备地址(对比 platform 节点 reg = 内存地址)。
- 排查口诀:读不到先 i2cdetect 扫总线(分清硬件层 vs 驱动层);扫不到看波形;CHIPID 不对核 datasheet。
下节预告
L17:中断 + GPIO + workqueue —— IC 有数据了怎么通知 CPU?中断里为什么不能久留?