15- Linux I2C 核心、总线与设备驱动

I²C(Inter-Integrated Circuit)是两条线的低速串行总线:SCL (时钟)和 SDA(数据), 外加地址寻址。它的最大价值不是"快",而是"省引脚":一根总线上可以挂几十个芯片, 每个芯片有 7 位(或 10 位)地址。EEPROM、RTC、温湿度传感器、PMIC、触摸屏控制器、 音频 codec 几乎都用 I²C 连接。

本章分三层讲清 I²C:

  1. I²C 核心 (drivers/i2c/i2c-core-*.c)------总线的"交通警察"。
  2. I²C 适配器驱动(adapter / bus driver)------控制 SoC 上的 I²C 控制器硬件。
  3. 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 核心

核心层提供四件事:

  1. 注册与匹配 :把 i2c_driver 和 i2c_client 配对(i2c_device_match())。
  2. 传输封装 :i2c_transfer()、i2c_smbus_*()。
  3. 设备模型集成 :/sys/bus/i2c/devices/、/sys/bus/i2c/drivers/。
  4. 设备实例化 :从 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 = &reg },
	{ .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)。

自动加载与匹配的三种来源:

  1. device tree :compatible 与 of_match_table 匹配。
  2. ACPI :_HID 与 acpi_match_table 匹配。
  3. 手工/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-dev class 在 /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 = &reg },
		{ .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 总结

  1. I²C 子系统是"核心 + 适配器驱动 + 设备驱动"三层分离;7.2.5 的核心被拆成多个 i2c-core-*.c。
  2. i2c_algorithm.master_xfer 是控制器驱动的心脏;i2c_driver 是芯片驱动的心脏。
  3. 7.2.5 中 i2c_driver.probe 只有一个 i2c_client * 参数,remove 返回 void。
  4. i2c_transfer() 返回成功消息数 ,必须与 num 比较;SMBus 用 i2c_smbus_*()。
  5. 现代驱动优先使用 regmap(统一 I²C/SPI、缓存、调试),AT24 就是范例。
  6. /dev/i2c-N + i2c-tools 让你不写内核模块就能调试 I²C 芯片。
  7. 无开发板时,i2c-stub 是最可靠的 I²C 实验载体;virtio-i2c 在 QEMU 支持时提供更真实的控制器。
  8. 原书的 Tegra 适配器实例、AT24xx 设备实例、i2c-dev.c 分析在本章分别由 virtio-i2c/i2c-stub、at24/myat24、i2c-dev 源码阅读对应保留。
相关推荐
Snasph2 小时前
附录-API 速查与术语表
linux 驱动学习
Snasph3 小时前
11-内存与 I/O 访问
linux 驱动学习
Snasph4 小时前
14-Linux 网络设备驱动
linux 驱动学习
Snasph5 小时前
18-ARM Linux 设备树
linux 驱动学习
Snasph6 小时前
20-Linux 芯片级移植及底层驱动
linux 驱动学习
Snasph6 小时前
12- Linux 设备驱动的软件架构思想
linux 驱动学习
Snasph7 小时前
23-从 Linux 4.0 到 7.2.5 的迁移
linux 驱动学习
Snasph11 天前
01-设备驱动概述与开发环境构建
linux 驱动学习
Snasph11 天前
05-Linux 文件系统与设备文件
linux 驱动学习