AI 技术博客
Android全栈11 分钟阅读5748

【全栈第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.chello-i2c.dtsiMakefile

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. 踩坑提醒

  1. reg 写成内存地址:I2C 节点 reg 是从设备地址(0x48),写成 platform 那种内存地址 → I2C 框架挂错地址,通信失败。
  2. probe 参数:现代单参数 (struct i2c_client*),老内核双参数。版本不对编不过。
  3. regmap_config 宽度错:reg_bits/val_bits 和芯片实际不符(比如 16 位地址写成 8 位)→ 读到的全是错的。
  4. CHIPID 读失败当驱动 bug:多半是硬件——地址错、没上电、排线松,先示波器看波形(L23),别盯着代码。
  5. i2c remove 是 void:别学 L15 platform 那样纠结(i2c 早就是 void)。

9. 常见问题分析与定位(I2C 实战)

9.1 常见问题清单

  1. probe 里 regmap_read 返回 -EIO / -ENXIO(读不到)
  2. probe 根本没被调用
  3. 读到的 CHIPID 不对(不是期望值)
  4. I2C 总线上多设备地址冲突
  5. 偶发读失败(时序/干扰)

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. 小结

  1. I2C 是外设最常见总线,驱动本质 = 读写 IC 寄存器。
  2. regmap 统一寄存器读写,屏蔽 I2C/SPI 差异(换 SPI 只改 init)。
  3. bring-up 第一步读 CHIPID 验证通信,读不到先怀疑硬件(用 i2cdetect/示波器)。
  4. I2C 节点 reg = 从设备地址(对比 platform 节点 reg = 内存地址)。
  5. 排查口诀:读不到先 i2cdetect 扫总线(分清硬件层 vs 驱动层);扫不到看波形;CHIPID 不对核 datasheet。

下节预告

L17:中断 + GPIO + workqueue —— IC 有数据了怎么通知 CPU?中断里为什么不能久留?

评论