I²C(Inter-Integrated Circuit)是两条线的低速串行总线:SCL (时钟)和 SDA(数据), 外加地址寻址。它的最大价值不是"快",而是"省引脚":一根总线上可以挂几十个芯片, 每个芯片有 7 位(或 10 位)地址。EEPROM、RTC、温湿度传感器、PMIC、触摸屏控制器、 音频 codec 几乎都用 I²C 连接。
本章分三层讲清 I²C:
- I²C 核心 (
drivers/i2c/i2c-core-*.c)------总线的"交通警察"。 - I²C 适配器驱动(adapter / bus driver)------控制 SoC 上的 I²C 控制器硬件。
- I²C 设备驱动(client driver)------操作挂在总线上的具体芯片。
15.1 Linux I2C 体系结构
Linux 的 I²C 子系统采用经典的三层分离架构:
+--------------------------------------------+
| 用户空间 |
| i2c-tools (i2cdetect/i2cget/i2cset) |
| /dev/i2c-N + ioctl(I2C_RDWR/I2C_SMBUS) |
+----------------------+---------------------+
|
+----------------------------v----------------------------+
| I2C 核心层 drivers/i2c/ |
| i2c-core-base.c 注册/匹配/设备模型 |
| i2c-core-smbus.c SMBus 兼容层 |
| i2c-core-of.c device tree 解析 |
| i2c-core-acpi.c ACPI 解析 |
| i2c-dev.c /dev/i2c-N 字符设备 |
+--------+---------------------------------+--------------+
| |
+--------v-----------+ +--------v--------------+
| I2C 适配器驱动 | | I2C 设备驱动 |
| (控制器/bus driver) | | (client driver) |
| i2c_algorithm | | i2c_driver |
| 真正操作 SCL/SDA | | 操作芯片寄存器 |
+--------------------+ +-----------------------+
三层各自的职责:
| 层次 | 关键结构 | 关键 API | 谁写 |
|---|---|---|---|
| 核心 | i2c_adapter / i2c_client / i2c_driver / i2c_algorithm |
i2c_transfer() |
内核自带 |
| 适配器驱动 | struct i2c_algorithm |
i2c_add_adapter() / devm_i2c_add_adapter() |
SoC 厂商/移植者 |
| 设备驱动 | struct i2c_driver |
module_i2c_driver() |
芯片驱动作者 |
7.2.5 的重要变化 :核心层被拆成 i2c-core-base.c、i2c-core-smbus.c、 i2c-core-of.c、i2c-core-acpi.c 等多个文件(4.0 时基本集中在 i2c-core.c)。 阅读源码时先看 drivers/i2c/Makefile 确认拆分。
15.2 Linux I2C 核心
核心层提供四件事:
- 注册与匹配 :把
i2c_driver和i2c_client配对(i2c_device_match())。 - 传输封装 :
i2c_transfer()、i2c_smbus_*()。 - 设备模型集成 :
/sys/bus/i2c/devices/、/sys/bus/i2c/drivers/。 - 设备实例化 :从 device tree、ACPI、
i2c_board_info、sysfs 的new_device创建 client。
15.2.1 四个核心数据结构
cpp
/* include/linux/i2c.h */
/* 一个物理 I2C 控制器(总线)在软件中的表示 */
struct i2c_adapter {
struct module *owner;
unsigned int class; /* 允许的地址类别 */
const struct i2c_algorithm *algo; /* 如何发一个传输 */
void *algo_data;
struct rt_mutex bus_lock;
int timeout;
int retries;
struct device dev; /* 设备模型中的父设备 */
int nr; /* 总线号,对应 /dev/i2c-N */
char name[48];
struct i2c_bus_recovery_info *bus_recovery_info;
const struct i2c_adapter_quirks *quirks;
...
};
/* 一个挂在总线上的从设备(芯片) */
struct i2c_client {
unsigned short flags;
unsigned short addr; /* 7 位从地址 */
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; /* 属于哪条总线 */
struct device dev;
int irq; /* 该芯片的中断号,可为 0 */
...
};
/* 芯片驱动 */
struct i2c_driver {
unsigned int class;
int (*probe)(struct i2c_client *client); /* 7.2.5:无 id 参数 */
void (*remove)(struct i2c_client *client); /* 7.2.5:返回 void */
void (*shutdown)(struct i2c_client *client);
struct device_driver driver;
const struct i2c_device_id *id_table;
...
};
/* 控制器如何"做一次传输" */
struct i2c_algorithm {
int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr,
unsigned short flags, char read_write,
u8 command, int size, union i2c_smbus_data *data);
u32 (*functionality)(struct i2c_adapter *adap);
...
};
15.2.2 核心层的关键函数(7.2.5)
| 函数 | 作用 |
|---|---|
i2c_transfer(adap, msgs, num) |
执行一组 i2c_msg,返回成功的消息数 |
i2c_master_send(client, buf, count) |
一条消息的写封装 |
i2c_master_recv(client, buf, count) |
一条消息的读封装 |
i2c_smbus_read_byte_data(client, cmd) |
SMBus 读字节 |
i2c_smbus_write_byte_data(client, cmd, val) |
SMBus 写字节 |
i2c_new_client_device(adap, &info) |
动态创建一个 client |
i2c_new_scanned_device(adap, &info, addr_list, &new) |
探测式创建 |
i2c_get_adapter(nr) / i2c_put_adapter(adap) |
按总线号引用/释放 adapter |
i2c_register_driver(owner, driver) |
注册驱动(module_i2c_driver 内部用) |
i2c_del_driver(driver) |
注销驱动 |
15.2.3 I²C 速度等级(7.2.5 常量)
cpp
/* include/linux/i2c.h */
#define I2C_MAX_STANDARD_MODE_FREQ 100000 /* 100 kHz */
#define I2C_MAX_FAST_MODE_FREQ 400000 /* 400 kHz */
#define I2C_MAX_FAST_MODE_PLUS_FREQ 1000000 /* 1 MHz */
#define I2C_MAX_TURBO_MODE_FREQ 1400000 /* 1.4 MHz */
#define I2C_MAX_HIGH_SPEED_MODE_FREQ 3400000 /* 3.4 MHz */
#define I2C_MAX_ULTRA_FAST_MODE_FREQ 5000000 /* 5 MHz */
设备树里用 clock-frequency = <400000>; 描述某条总线的速度。
15.3 Linux I2C 适配器驱动
适配器驱动 = 操作 SoC 上 I²C 控制器寄存器的代码。它实现 struct i2c_algorithm, 通常还实现时钟、DMA、中断、总线恢复(bus recovery)。
15.3.1 I2C 适配器驱动的注册与注销
最小骨架(基于 i2c_algorithm.master_xfer):
cpp
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/io.h>
struct my_i2c {
struct i2c_adapter adap;
void __iomem *base;
struct clk *clk;
int irq;
};
static int my_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
struct my_i2c *priv = i2c_get_adapdata(adap);
int i;
for (i = 0; i < num; i++) {
struct i2c_msg *m = &msgs[i];
u16 addr = m->addr;
int ret;
/* 1. 产生 START;2. 发出 addr<<1 | R/W;3. 收发 len 字节;
* 4. 产生 STOP(最后一条消息,或下一条是不同方向的读)
* 这些动作由你的控制器寄存器完成。
*/
ret = my_hw_do_one(priv, m);
if (ret)
return ret; /* 返回已完成的消息数或负错误码 */
}
return num;
}
static u32 my_i2c_func(struct i2c_adapter *adap)
{
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
}
static const struct i2c_algorithm my_i2c_algo = {
.master_xfer = my_i2c_xfer,
.functionality = my_i2c_func,
};
static int my_i2c_probe(struct platform_device *pdev)
{
struct my_i2c *priv;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
priv->clk = devm_clk_get_enabled(&pdev->dev, NULL); /* 7.2.5 推荐 */
if (IS_ERR(priv->clk))
return PTR_ERR(priv->clk);
i2c_set_adapdata(&priv->adap, priv);
priv->adap.owner = THIS_MODULE;
priv->adap.algo = &my_i2c_algo;
priv->adap.dev.parent = &pdev->dev;
priv->adap.dev.of_node = pdev->dev.of_node;
strscpy(priv->adap.name, "my-i2c", sizeof(priv->adap.name));
platform_set_drvdata(pdev, priv);
/* 动态分配总线号 */
return devm_i2c_add_adapter(&pdev->dev, &priv->adap);
}
static void my_i2c_remove(struct platform_device *pdev)
{
/* devm_ 自动回收 adapter、clk、ioremap */
}
static const struct of_device_id my_i2c_of_match[] = {
{ .compatible = "myvendor,my-i2c" },
{ }
};
MODULE_DEVICE_TABLE(of, my_i2c_of_match);
static struct platform_driver my_i2c_driver = {
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.driver = {
.name = "my-i2c",
.of_match_table = my_i2c_of_match,
},
};
module_platform_driver(my_i2c_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimal I2C adapter driver");
注册/注销 API 对比:
| 4.0 写法 | 7.2.5 写法 | 说明 |
|---|---|---|
i2c_add_adapter() |
i2c_add_adapter() 仍可用 |
动态总线号 |
i2c_add_numbered_adapter() |
同上 | 指定总线号 |
手动 i2c_del_adapter() |
devm_i2c_add_adapter() |
推荐,自动回收 |
clk_get() + clk_prepare_enable() |
devm_clk_get_enabled() |
一行搞定 |
request_irq() 手动释放 |
devm_request_irq() |
推荐 |
i2c_set_adapdata() |
不变 | 保存私有指针 |
15.3.2 I2C 总线的通信方法
方法一:master_xfer(原生 I²C)
控制器直接实现 I²C 时序,接收任意数量的 struct i2c_msg:
cpp
struct i2c_msg {
__u16 addr; /* 7 位从地址 */
__u16 flags; /* I2C_M_RD = 0x0001(读);I2C_M_TEN;I2C_M_NOSTART */
__u16 len;
__u8 *buf;
};
一次典型的"写寄存器地址再读数据"(EEPROM 随机读)需要两条消息, 中间不产生 STOP(I2C_M_NOSTART 或核心层自动处理):
cpp
u8 reg = 0x10;
u8 val;
struct i2c_msg msgs[2] = {
{ .addr = client->addr, .flags = 0, .len = 1, .buf = ® },
{ .addr = client->addr, .flags = I2C_M_RD, .len = 1, .buf = &val },
};
int ret = i2c_transfer(client->adapter, msgs, 2);
方法二:smbus_xfer(SMBus 兼容)
如果控制器只支持 SMBus 协议(或你想省事),实现 smbus_xfer, 核心层会把 i2c_smbus_*() 调用路由过来。如果控制器只 实现了 master_xfer, 核心层的 i2c_smbus_xfer_emulated() 会用原生 I²C 模拟 SMBus 时序 (前提是 functionality 里声明了 I2C_FUNC_SMBUS_EMUL)。
方法三:functionality
告诉核心层这块控制器能做什么:
| 标志 | 含义 |
|---|---|
I2C_FUNC_I2C |
支持原生 I²C(任意消息组合) |
I2C_FUNC_SMBUS_QUICK |
支持 SMBus quick 命令 |
I2C_FUNC_SMBUS_BYTE |
单字节读写 |
I2C_FUNC_SMBUS_BYTE_DATA |
寄存器 + 字节 |
I2C_FUNC_SMBUS_WORD_DATA |
寄存器 + 16 位 |
I2C_FUNC_SMBUS_BLOCK_DATA |
块读写 |
I2C_FUNC_SMBUS_EMUL |
核心层可用 I²C 模拟上述 SMBus |
15.3.3 总线恢复(bus recovery)
当从设备把 SDA 拉低不放("总线死锁")时,控制器可以发 9 个时钟脉冲把它"踢醒"。 7.2.5 用 struct i2c_bus_recovery_info 描述:
cpp
struct i2c_bus_recovery_info {
int (*recover_bus)(struct i2c_adapter *adap);
void (*set_scl)(struct i2c_adapter *adap, int val);
int (*get_scl)(struct i2c_adapter *adap);
void (*set_sda)(struct i2c_adapter *adap, int val);
int (*get_sda)(struct i2c_adapter *adap);
void (*prepare_recovery)(struct i2c_adapter *adap);
void (*unprepare_recovery)(struct i2c_adapter *adap);
struct gpio_desc *scl_gpiod;
struct gpio_desc *sda_gpiod;
...
};
把 priv->adap.bus_recovery_info = &my_recovery; 挂上即可,核心层在传输超时时自动调用。
15.4 Linux I2C 设备驱动
设备驱动 = 针对某颗芯片(EEPROM、RTC、传感器)的代码。它实现 struct i2c_driver。
15.4.1 模块加载与卸载
cpp
#include <linux/i2c.h>
#include <linux/module.h>
static const struct i2c_device_id mychip_id[] = {
{ "mychip", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, mychip_id);
static const struct of_device_id mychip_of_match[] = {
{ .compatible = "myvendor,mychip" },
{ }
};
MODULE_DEVICE_TABLE(of, mychip_of_match);
static int mychip_probe(struct i2c_client *client)
{
/* 检查适配器能力 */
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA))
return -EOPNOTSUPP;
dev_info(&client->dev, "mychip probed at 0x%02x\n", client->addr);
return 0;
}
static void mychip_remove(struct i2c_client *client)
{
dev_info(&client->dev, "mychip removed\n");
}
static struct i2c_driver mychip_driver = {
.driver = {
.name = "mychip",
.of_match_table = mychip_of_match,
},
.probe = mychip_probe,
.remove = mychip_remove,
.id_table = mychip_id,
};
module_i2c_driver(mychip_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Example I2C chip driver");
module_i2c_driver() 自动生成 module_init/module_exit,内部调用 i2c_register_driver(THIS_MODULE, drv) 和 i2c_del_driver(drv)。
7.2.5 签名变化(重要):
| 4.0 写法 | 7.2.5 写法 |
|---|---|
int probe(struct i2c_client *client, const struct i2c_device_id *id) |
int probe(struct i2c_client *client) |
int remove(struct i2c_client *client) |
void remove(struct i2c_client *client) |
struct i2c_driver.detect / .attach_adapter |
早已删除,不要使用 |
i2c_device_id 仍然保留,用于非设备树(ACPI/手工 new_device)的匹配; probe 里如果需要 id 的 driver_data,用 i2c_get_clientdata() 或 device_get_match_data(&client->dev)。
自动加载与匹配的三种来源:
- device tree :
compatible与of_match_table匹配。 - ACPI :
_HID与acpi_match_table匹配。 - 手工/id_table :sysfs 的
echo mychip 0x50 > /sys/bus/i2c/devices/i2c-0/new_device。
15.4.2 数据传输
(1)SMBus 便捷 API(最常用)
cpp
int i2c_smbus_read_byte_data(const struct i2c_client *client, u8 command);
int i2c_smbus_write_byte_data(const struct i2c_client *client,
u8 command, u8 value);
int i2c_smbus_read_word_data(const struct i2c_client *client, u8 command);
int i2c_smbus_write_word_data(const struct i2c_client *client,
u8 command, u16 value);
int i2c_smbus_read_i2c_block_data(const struct i2c_client *client,
u8 command, u8 length, u8 *values);
int i2c_smbus_write_i2c_block_data(const struct i2c_client *client,
u8 command, u8 length, const u8 *values);
(2)原生 I²C 传输
cpp
int i2c_master_send(const struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(const struct i2c_client *client, char *buf, int count);
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
(3)寄存器映射:regmap(7.2.5 强烈推荐)
现代 I²C/SPI 驱动几乎都用 regmap 而不是裸 SMBus 调用,好处是统一缓存、 统一读写、统一调试(/sys/kernel/debug/regmap/):
cpp
static const struct regmap_config mychip_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0xff,
.cache_type = REGCACHE_MAPLE, /* 7.2.5 使用 maple tree 缓存 */
};
static int mychip_probe(struct i2c_client *client)
{
struct regmap *regmap;
regmap = devm_regmap_init_i2c(client, &mychip_regmap_config);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
devm_regmap_add_irq_chip(&client->dev, regmap, client->irq,
IRQF_ONESHOT, 0, &mychip_irq_chip, NULL);
regmap_write(regmap, 0x10, 0x01);
return 0;
}
drivers/misc/eeprom/at24.c(AT24xx EEPROM)就是基于 regmap 的: 它用 client_regmaps[] 为不同型号选择不同的 regmap_config, 设备树匹配表 at24_of_match[] 覆盖 atmel,24c00、atmel,24c01、atmel,24cs01、 atmel,24c02、atmel,24cs02、atmel,24mac402、atmel,24mac602 等型号。
15.4.3 i2c-dev.c 文件分析
drivers/i2c/i2c-dev.c 把每条 I²C 总线暴露成一个字符设备 /dev/i2c-N, 让用户空间无需写内核模块就能访问 I²C 芯片。
关键点:
- 每个 adapter 对应一个
i2c_dev,通过i2c-devclass 在/dev/i2c-N创建设备节点。 open()时从i2c_get_adapter(minor)取得 adapter。ioctl()支持的核心命令(见include/uapi/linux/i2c-dev.h):
| ioctl | 值 | 作用 |
|---|---|---|
I2C_SLAVE |
0x0703 |
设置本次会话的从地址(若被占用则失败) |
I2C_SLAVE_FORCE |
0x0706 |
强制设置从地址(忽略占用) |
I2C_TENBIT |
0x0704 |
切换 10 位地址模式 |
I2C_RDWR |
0x0707 |
执行 struct i2c_rdwr_ioctl_data(原生 I²C) |
I2C_SMBUS |
0x0720 |
执行 struct i2c_smbus_ioctl_data |
I2C_RETRIES |
0x0701 |
设置重试次数 |
I2C_TIMEOUT |
0x0702 |
设置超时(10ms 为单位) |
I2C_RDWR 一次最多传 I2C_RDWR_IOCTL_MAX_MSGS(= 42)条消息。
用户空间示例(原生 I²C):
cpp
#include <fcntl.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
int fd = open("/dev/i2c-0", O_RDWR);
unsigned char reg = 0x00, val = 0;
struct i2c_msg msgs[2] = {
{ .addr = 0x50, .flags = 0, .len = 1, .buf = ® },
{ .addr = 0x50, .flags = I2C_M_RD, .len = 1, .buf = &val },
};
struct i2c_rdwr_ioctl_data rdwr = { .msgs = msgs, .nmsgs = 2 };
if (fd < 0) { perror("open"); return 1; }
if (ioctl(fd, I2C_RDWR, &rdwr) < 0) { perror("I2C_RDWR"); return 1; }
printf("reg[0] = 0x%02x\n", val);
close(fd);
return 0;
}
用户空间工具(i2c-tools):
bash
i2cdetect -l # 列出所有 I2C 总线
i2cdetect -y 0 # 扫描总线 0 上的所有地址
i2cget -y 0 0x50 0x00 # 读 0x50 的寄存器 0x00
i2cset -y 0 0x50 0x00 0xab # 写
i2cdump -y 0 0x50 # 转储整片寄存器
i2ctransfer -y 0 w1@0x50 0x00 r1 # 原生 I²C:写 1 字节再读 1 字节
15.5 适配器驱动实例:对照 Tegra I2C 与现代 virtio-i2c
原书用 Tegra I²C 控制器驱动(drivers/i2c/busses/i2c-tegra.c)讲解适配器驱动。 在 7.2.5 中该驱动仍然存在,并已改为 devm_* + i2c_add_adapter 的现代写法。 它在 QEMU 上无法运行(需要 Tegra 硬件),因此我们的实验用下面两种方式替代:
15.5.1 virtio-i2c(真机行为的虚拟控制器)
7.2.5 提供 drivers/i2c/busses/i2c-virtio.c(CONFIG_I2C_VIRTIO), 对应 QEMU 的 virtio-i2c 设备。它是一条"真的"总线,可以挂 virtio 从设备。
bash
# 检查你的 QEMU 是否支持
qemu-system-x86_64 -device help | grep -i i2c
# 期望看到 virtio-i2c-pci 或 virtio-i2c
15.5.2 i2c-stub(无硬件首选,本手册主推)
drivers/i2c/i2c-stub.c(CONFIG_I2C_STUB)是一个纯软件假 I²C 控制器 : 它不接任何硬件,只把 SMBus 命令读写到内存数组里,并打印内核日志。 Kconfig 中它 depends on m,只能编译成模块。
bash
# 内核配置
CONFIG_I2C=y
CONFIG_I2C_CHARDEV=y # 提供 /dev/i2c-N
CONFIG_I2C_STUB=m # 软件假控制器
CONFIG_EEPROM_AT24=m # AT24xx EEPROM 驱动(实验用)
CONFIG_REGMAP_I2C=y
CONFIG_DEBUG_FS=y # 看 regmap 调试信息
加载与实验(guest 内执行):
bash
# 1. 加载 i2c-stub,声明两个假芯片地址:0x50(EEPROM)、0x48(温度传感器)
modprobe i2c-stub chip_addr=0x50,0x48
# 2. 确认总线出现(i2c-stub 会占一个动态总线号,通常是 i2c-0)
i2cdetect -l
ls -l /dev/i2c-*
# 3. 扫描:0x50 和 0x48 应该应答
i2cdetect -y 0
# 4. 预置数据(i2c-stub 的寄存器数组)
i2cset -y 0 0x50 0x00 0x11
i2cset -y 0 0x50 0x01 0x22
# 5. 读回
i2cget -y 0 0x50 0x00
i2cdump -y 0 0x50
# 6. 观察内核日志(i2c-stub 会打印每条命令)
dmesg | tail -20
预期输出要点:
i2cdetect -l出现i2c-0 smbus SMBus stub driver。i2cdetect -y 0的 0x48、0x50 位置显示地址,其余显示--。i2cget返回刚写入的字节;dmesg中出现smbus_xfer日志。
注意 :i2c-stub 默认不支持 I2C_FUNC_SMBUS_BLOCK_DATA, 需要显式打开:加载时加 functionality=0x03000000(或对应位), 否则 i2c_smbus_read_block_data() 会返回 -EOPNOTSUPP。
15.6 设备驱动实例:AT24xx EEPROM
15.6.1 用现成的 at24 驱动(推荐先跑通)
bash
# guest 内(接上面 i2c-stub 的实验)
modprobe at24
echo at24 0x50 > /sys/bus/i2c/devices/i2c-0/new_device
# 或者用设备树/ACPI 自动实例化
dmesg | tail
ls -l /sys/bus/i2c/devices/
# 会出现 i2c-0-0050 目录
# 通过 sysfs 的 nvmem 接口读写 EEPROM
ls /sys/bus/i2c/devices/i2c-0-0050/
cat /sys/bus/i2c/devices/i2c-0-0050/eeprom # 可能是二进制
7.2.5 的 at24.c 通过 nvmem 子系统暴露 EEPROM,也提供 eeprom 二进制属性。 它内部用 regmap,并把 max_register、page_size、byte_len 等按型号设置。
15.6.2 自己写一个"AT24 风格"的 I²C 驱动
下面是一个可在 i2c-stub 上完整运行的驱动,它实现一个 256 字节 EEPROM 的 读/写,并暴露 sysfs 属性:
cpp
/* myat24.c ------ 在 i2c-stub 上可运行的简化 EEPROM 驱动 */
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/sysfs.h>
struct myat24 {
struct regmap *regmap;
struct i2c_client *client;
};
static const struct regmap_config myat24_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0xff,
.cache_type = REGCACHE_MAPLE,
};
static ssize_t addr_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct myat24 *chip = dev_get_drvdata(dev);
unsigned int val;
int ret = regmap_read(chip->regmap, 0x00, &val);
if (ret)
return ret;
return sysfs_emit(buf, "0x%02x\n", val);
}
static ssize_t addr_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
struct myat24 *chip = dev_get_drvdata(dev);
unsigned int val;
int ret;
if (kstrtouint(buf, 0, &val) || val > 0xff)
return -EINVAL;
ret = regmap_write(chip->regmap, 0x00, val);
return ret ? ret : count;
}
static DEVICE_ATTR_RW(addr);
static struct attribute *myat24_attrs[] = {
&dev_attr_addr.attr,
NULL,
};
ATTRIBUTE_GROUPS(myat24);
static int myat24_probe(struct i2c_client *client)
{
struct myat24 *chip;
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
return dev_err_probe(&client->dev, -EOPNOTSUPP,
"adapter lacks I2C_FUNC_I2C\n");
chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL);
if (!chip)
return -ENOMEM;
chip->client = client;
chip->regmap = devm_regmap_init_i2c(client, &myat24_regmap_config);
if (IS_ERR(chip->regmap))
return PTR_ERR(chip->regmap);
dev_set_drvdata(&client->dev, chip);
i2c_set_clientdata(client, chip);
dev_info(&client->dev, "myat24: 256-byte EEPROM at 0x%02x\n", client->addr);
return 0;
}
static void myat24_remove(struct i2c_client *client)
{
dev_info(&client->dev, "myat24 removed\n");
}
static const struct i2c_device_id myat24_id[] = {
{ "myat24", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, myat24_id);
static const struct of_device_id myat24_of_match[] = {
{ .compatible = "myvendor,myat24" },
{ }
};
MODULE_DEVICE_TABLE(of, myat24_of_match);
static struct i2c_driver myat24_driver = {
.driver = {
.name = "myat24",
.of_match_table = myat24_of_match,
.dev_groups = myat24_groups,
},
.probe = myat24_probe,
.remove = myat24_remove,
.id_table = myat24_id,
};
module_i2c_driver(myat24_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simplified AT24-style EEPROM I2C driver");
Makefile(外部模块,注意 7.2.5 的新写法):
bash
obj-m += myat24.o
KDIR ?= ~/works/study/build/linux-7.2.5-x86_64
PWD := $(shell pwd)
all:
$(MAKE) -f $(KDIR)/Makefile M=$(PWD) modules
clean:
$(MAKE) -f $(KDIR)/Makefile M=$(PWD) clean
运行实验(guest 内):
bash
modprobe i2c-stub chip_addr=0x50
modprobe regmap_i2c
insmod myat24.ko
echo myat24 0x50 > /sys/bus/i2c/devices/i2c-0/new_device
# 写、读
echo 0x5a > /sys/bus/i2c/devices/i2c-0-0050/addr
cat /sys/bus/i2c/devices/i2c-0-0050/addr # 期望 0x5a
# 直接看 regmap 调试信息
ls /sys/kernel/debug/regmap/
cat /sys/kernel/debug/regmap/i2c-0-0050/registers
预期输出 :cat addr 打印 0x5a;dmesg 中有 i2c-stub 的传输日志。
15.6.3 与设备树结合
cpp
&i2c0 {
status = "okay";
clock-frequency = <400000>;
eeprom@50 {
compatible = "myvendor,myat24";
reg = <0x50>;
pagesize = <8>;
};
};
驱动只靠 compatible 就能匹配,client->addr 由 reg 提供。
15.7 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
probe(client, id) 两参数 |
编译错误 | 7.2.5 是 probe(client) |
remove 返回 int |
编译错误/警告 | 7.2.5 返回 void |
用 .attach_adapter / .detect |
不存在 | 早已删除 |
忘记 i2c_check_functionality() |
运行时报 -EOPNOTSUPP |
probe 里先检查能力 |
| i2c-stub 上用 block 传输 | -EOPNOTSUPP |
加载时加 functionality 位 |
| 在原子上下文调用 SMBus | 睡眠警告 | I²C 传输可能睡眠,用工作队列/线程中断 |
i2c_transfer() 返回正数当成功 |
误判 | 返回成功消息数,判断 == num |
| 7 位地址写成 8 位 | 探测不到 | client->addr 是 7 位;写设备树 reg 也用 7 位 |
忘记 i2c_put_adapter() |
引用计数泄漏 | 用完必须配对释放 |
regmap 缓存与硬件不一致 |
读到旧值 | 必要时 regcache_drop_region() |
| 模块名/设备名不匹配 | new_device 后无 probe |
用 i2c_device_id 的名字 |
15.8 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| i2c-stub 基本读写 | 理解 I²C 核心与 i2c-dev | i2cdetect/i2cget/i2cset 成功 |
| i2c-dev 用户程序 | 理解 /dev/i2c-N |
自写 I2C_RDWR 程序读到数据 |
| at24 现成驱动 | 学习 regmap + nvmem | /sys/bus/i2c/devices/i2c-0-0050/ 出现 |
| 自写 myat24 | 学习完整设备驱动骨架 | sysfs addr 属性读写正确 |
| virtio-i2c(若可用) | 学习真实控制器驱动 | i2cdetect -l 出现 virtio 总线 |
| 设备树实例化 | 学习 compatible 匹配 |
去掉 new_device 也能 probe |
| regmap 调试 | 学习调试手段 | /sys/kernel/debug/regmap/ 有寄存器视图 |
| 总线恢复 | 理解 bus recovery | 构造死锁后自动恢复 |
15.9 总结
- I²C 子系统是"核心 + 适配器驱动 + 设备驱动"三层分离;7.2.5 的核心被拆成多个
i2c-core-*.c。 i2c_algorithm.master_xfer是控制器驱动的心脏;i2c_driver是芯片驱动的心脏。- 7.2.5 中
i2c_driver.probe只有一个i2c_client *参数,remove返回void。 i2c_transfer()返回成功消息数 ,必须与num比较;SMBus 用i2c_smbus_*()。- 现代驱动优先使用 regmap(统一 I²C/SPI、缓存、调试),AT24 就是范例。
/dev/i2c-N+ i2c-tools 让你不写内核模块就能调试 I²C 芯片。- 无开发板时,i2c-stub 是最可靠的 I²C 实验载体;virtio-i2c 在 QEMU 支持时提供更真实的控制器。
- 原书的 Tegra 适配器实例、AT24xx 设备实例、i2c-dev.c 分析在本章分别由 virtio-i2c/i2c-stub、at24/myat24、i2c-dev 源码阅读对应保留。