Linux I2C 子系统

I2C 与 SMBus 协议对比

SMBus 是 I2C 的简化与约束版本,在实际设备中更常被支持,Linux 也建议优先使用 SMBus,若硬件不支持,也可通过 I2C 协议软件模拟。

关键差异

特性 I2C 协议 SMBus 协议
VDD 极限值 高达 12V 1.8V ~ 5V
最小时钟频率 无限制 10KHz
Clock Stretching 时长无限制 最大时间有限制
地址回应 无强制回应要求 强制回应,用于感知设备状态
数据传输格式 仅定义传输方式,格式由设备自定义 明确多种数据格式
REPEATED START 支持 支持,写读操作间可直接发新 START 支持
低功耗版本 无特定低功耗版本定义 有 SMBus Low Power Version

总结

因多数设备实现 SMBus,Linux 建议优先使用。即使 I2C 控制器无 SMBus 硬件支持,也可通过软件模拟 SMBus 协议。

Linux I2C 子系统核心结构体

i2c_adapter:I2C 控制器抽象

代表物理 I2C 控制器(适配器),是 CPU 与 I2C 总线的硬件接口抽象。

c 复制代码
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;                      // I2C 总线编号(如 i2c-0、i2c-1)
    char name[48];               // 适配器名称
    // 其他成员...
};

作用

  • 管理一条 I2C 总线,是 I2C 通信的硬件载体。
  • 通过 algo 关联 i2c_algorithm,提供硬件相关操作接口。
  • 向上层(I2C 核心)提供总线访问能力,使设备驱动能与总线上设备通信。
    使用场景 :每个 I2C 控制器需注册一个 i2c_adapter 实例到内核,如嵌入式系统有 3 个 I2C 控制器,就会注册 i2c-0i2c-1i2c-2 三个实例。

i2c_algorithm:通信算法抽象

定义 I2C 适配器实现 I2C 协议的底层操作(起始、数据收发、停止等),是适配器与硬件交互的"驱动逻辑"。

c 复制代码
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 *);
    // 其他成员...
};

核心函数

  • master_xfer:核心函数,实现 I2C 主模式下的消息传输(发送/接收数据)。
  • smbus_xfer:用于 SMBus 协议的传输(可选)。
  • functionality:返回适配器支持的功能(如是否支持 10 位地址、SMBus 协议等)。
    使用场景 :每个 i2c_adapter 必须绑定一个 i2c_algorithm 才能实现实际 I2C 通信,不同芯片 I2C 控制器的 i2c_algorithm 实现不同,但都通过 master_xfer 提供统一接口。

i2c_client:I2C 从设备抽象

代表 I2C 总线上的具体从设备(如传感器、EEPROM 等),是设备驱动与硬件设备的桥梁。
addr:表示 I2C 从设备的地址(通常是 7 位地址,10 位地址需要通过flags中的I2C_CLIENT_TEN标志位来标识 )。
name:设备的名称,用于标识设备类型。
adapter:指向该设备所连接的i2c_adapter(I2C 适配器),通过它来和硬件总线交互。
dev:内嵌的struct device结构体,用于设备模型管理,涉及电源管理、热插拔等功能。
driver:指向绑定到该设备的i2c_driver,当设备和驱动匹配成功后,会设置此指针。

c 复制代码
struct i2c_client {
    unsigned short addr;         // 设备的 I2C 从地址(7 位或 10 位)
    char name[I2C_NAME_SIZE];    // 设备名称
    struct i2c_adapter *adapter; // 指向设备连接的 I2C 适配器
    struct device dev;           // 内核设备模型相关,用于电源、热插拔等管理
    struct i2c_driver *driver;   // 指向绑定的设备驱动
    int irq;                     // 设备中断号
    // 其他成员...
};

作用

  • 描述 I2C 从设备,记录硬件信息(I2C 地址、挂载总线等)。
  • 通过内核设备模型,将自身与 i2c_driver(设备驱动)绑定,实现驱动对设备的控制。
  • 提供通信入口,设备驱动通过其关联的 i2c_adapter 与硬件设备进行 I2C 通信。
    生命周期
  • 创建注册 :常由设备树解析(i2c_new_device())或用户空间通过 sysfs 动态创建。
  • 驱动绑定i2c_driver 注册时,内核根据设备名或 ID 表,将 i2c_client 与匹配的 i2c_driver 绑定。
  • 通信控制 :绑定后,驱动通过 i2c_transfer() 等接口,借助关联的 i2c_adapter 与硬件通信。
  • 注销 :设备移除时,i2c_client 被注销,释放资源。

测试

在用户态生成

示例:

shell 复制代码
// 在I2C BUS0下创建i2c_client
# echo ap3216c 0x1e > /sys/bus/i2c/devices/i2c-0/new_device

// 删除i2c_client
# echo 0x1e > /sys/bus/i2c/devices/i2c-0/delete_device
编写代码
  • i2c_new_device
  • i2c_new_probed_device
  • i2c_register_board_info
    • 内核没有EXPORT_SYMBOL(i2c_register_board_info)
    • 使用这个函数的驱动必须编进内核里去
使用设备树生成

在某个I2C控制器的节点下,添加如下代码:

shell 复制代码
		ap3216c@1e {
			compatible = "lite-on,ap3216c";
			reg = <0x1e>;
		};

i2c_msg:I2C 传输消息

表示一次 I2C 传输的消息单元,包含地址、方向、数据等信息。

c 复制代码
struct i2c_msg {
    __u16 addr;                  // 从设备地址
    __u16 flags;                 // 传输标志,如 I2C_M_RD 表示读
    __u16 len;                   // 数据长度
    __u8 *buf;                   // 数据缓冲区
};

关键标志I2C_M_RD,bit 0 为该值时表示读操作,否则为写操作。一个 i2c_msg 要么读,要么写。

i2c_driver

probe:当 I2C 驱动与 I2C 设备匹配成功后调用,用于设备的初始化工作,比如申请设备资源、注册设备文件操作接口等。
remove:在设备移除时调用,用于释放设备相关资源。
of_match_table:设备树匹配表,用于通过设备树进行驱动和设备的匹配。
acpi_match_table:ACPI 匹配表,用于通过 ACPI 进行驱动和设备的匹配。
id_table:定义了该驱动支持的设备 ID 列表,用于传统的 ID 匹配方式。
driver:内嵌的struct device_driver结构体,提供驱动的通用属性和操作。
i2c_driver表明能支持哪些设备:

  • 使用of_match_table来判断
    • 设备树中,某个I2C控制器节点下可以创建I2C设备的节点
      • 如果I2C设备节点的compatible属性跟of_match_table的某项兼容,则匹配成功
    • i2c_client.name跟某个of_match_table[i].compatible值相同,则匹配成功
  • 使用id_table来判断
    • i2c_client.name跟某个id_table[i].name值相同,则匹配成功

i2c_driver跟i2c_client匹配成功后,就调用i2c_driver.probe函数。

I2C 数据传输核心流程与代码

内核数据传输函数:i2c_transfer

用于在 I2C 总线上传输一个或多个 i2c_msg,是内核中 I2C 数据传输的核心接口。

c 复制代码
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
    int ret;
    // 检查参数有效性
    if (adap == NULL || msgs == NULL || num <= 0)
        return -EINVAL;
    // 调用适配器关联的 algorithm 中的 master_xfer 函数进行传输
    if (adap->algo->master_xfer) {
        ret = adap->algo->master_xfer(adap, msgs, num);
    } else {
        ret = -ENOSYS;
    }
    return ret;
}

作用 :APP 或驱动通过 i2c_adapter,以 i2c_msg 为数据单元,与 i2c_client 对应的从设备传输数据。

I2C 设备驱动开发核心步骤与代码

以一个简单的 I2C 传感器驱动为例,展示核心步骤。

定义设备 ID 表

用于匹配 i2c_clienti2c_driver

c 复制代码
static const struct i2c_device_id sensor_id_table[] = {
    {"sensor_demo", 0},
    {},
};
MODULE_DEVICE_TABLE(i2c, sensor_id_table);
初始化 I2C 设备(probe 函数与 i2c_client 处理)

probe 函数在驱动与设备匹配时调用,完成设备初始化。

c 复制代码
static int sensor_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret;
    // 检查设备是否支持
    if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
        dev_err(&client->dev, "adapter does not support I2C\n");
        return -EOPNOTSUPP;
    }
    // 可在这里进行设备初始化操作,如读取设备 ID 等
    // 示例:读取设备某个寄存器(假设设备 ID 寄存器地址为 0x00)
    u8 dev_id;
    ret = i2c_smbus_read_byte_data(client, 0x00);
    if (ret < 0) {
        dev_err(&client->dev, "failed to read device ID\n");
        return ret;
    }
    dev_id = ret;
    dev_info(&client->dev, "sensor device ID: 0x%x\n", dev_id);
    // 后续可进行设备相关资源申请、中断注册等操作
    return 0;
}
I2C 读写函数编写

实现对从设备寄存器的读写操作。

指定寄存器读(单字节)
c 复制代码
static int sensor_read_reg(struct i2c_client *client, u8 reg, u8 *val)
{
    int ret;
    *val = i2c_smbus_read_byte_data(client, reg);
    if (*val < 0) {
        dev_err(&client->dev, "failed to read reg 0x%x\n", reg);
        ret = -EIO;
    } else {
        ret = 0;
    }
    return ret;
}
指定寄存器写(单字节)
c 复制代码
static int sensor_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
    int ret;
    ret = i2c_smbus_write_byte_data(client, reg, val);
    if (ret < 0) {
        dev_err(&client->dev, "failed to write reg 0x%x val 0x%x\n", reg, val);
        ret = -EIO;
    }
    return ret;
}
读取从设备多个寄存器数据
c 复制代码
static int sensor_read_multi_regs(struct i2c_client *client, u8 reg, u8 *buf, int len)
{
    struct i2c_msg msgs[2];
    int ret;
    // 第一个消息:写寄存器地址
    msgs[0].addr = client->addr;
    msgs[0].flags = 0; // 写操作
    msgs[0].len = 1;
    msgs[0].buf = &reg;
    // 第二个消息:读多个寄存器数据
    msgs[1].addr = client->addr;
    msgs[1].flags = I2C_M_RD; // 读操作
    msgs[1].len = len;
    msgs[1].buf = buf;
    // 传输消息
    ret = i2c_transfer(client->adapter, msgs, 2);
    if (ret != 2) {
        dev_err(&client->dev, "i2c transfer failed, ret = %d\n", ret);
        ret = -EIO;
    } else {
        ret = 0;
    }
    return ret;
}
向从设备多个寄存器写入数据
c 复制代码
static int sensor_write_multi_regs(struct i2c_client *client, u8 reg, u8 *buf, int len)
{
    struct i2c_msg msgs[1];
    u8 *tmp_buf;
    int ret, i;
    tmp_buf = kmalloc(len + 1, GFP_KERNEL);
    if (!tmp_buf)
        return -ENOMEM;
    tmp_buf[0] = reg;
    for (i = 0; i < len; i++) {
        tmp_buf[i + 1] = buf[i];
    }
    // 消息:写寄存器地址和多个数据
    msgs[0].addr = client->addr;
    msgs[0].flags = 0; // 写操作
    msgs[0].len = len + 1;
    msgs[0].buf = tmp_buf;
    // 传输消息
    ret = i2c_transfer(client->adapter, msgs, 1);
    if (ret != 1) {
        dev_err(&client->dev, "i2c transfer failed, ret = %d\n", ret);
        ret = -EIO;
    } else {
        ret = 0;
    }
    kfree(tmp_buf);
    return ret;
}
设备操作函数编写

定义字符设备等的操作函数,供应用层调用。

c 复制代码
static struct file_operations sensor_fops = {
    .owner = THIS_MODULE,
    .read = sensor_read,
    .write = sensor_write,
    .open = sensor_open,
    .release = sensor_release,
    // 其他操作函数...
};
static struct miscdevice sensor_miscdev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "sensor_demo",
    .fops = &sensor_fops,
};
// 在 probe 函数中注册 misc 设备
static int sensor_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    // ... 其他初始化操作 ...
    misc_register(&sensor_miscdev);
    return 0;
}
// 在 remove 函数中注销 misc 设备
static int sensor_remove(struct i2c_client *client)
{
    misc_deregister(&sensor_miscdev);
    // ... 释放资源等操作 ...
    return 0;
}
定义 i2c_driver 并注册

将驱动与设备匹配逻辑、proberemove 等函数关联,并注册到内核。

c 复制代码
static struct i2c_driver sensor_driver = {
    .driver = {
        .name = "sensor_demo",
        .owner = THIS_MODULE,
    },
    .probe = sensor_probe,
    .remove = sensor_remove,
    .id_table = sensor_id_table,
};
static int __init sensor_init(void)
{
    return i2c_add_driver(&sensor_driver);
}
static void __exit sensor_exit(void)
{
    i2c_del_driver(&sensor_driver);
}
module_init(sensor_init);
module_exit(sensor_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("I2C Sensor Demo Driver");

怎么使用I2C-GPIO

设置设备树,在里面添加一个节点即可:

  • compatible = "i2c-gpio";
  • 使用pinctrl把 SDA、SCL所涉及引脚配置为GPIO、开极
    • 可选
  • 指定SDA、SCL所用的GPIO
  • 指定频率(2种方法):
    • i2c-gpio,delay-us = <5>; /* ~100 kHz */
    • clock-frequency = <400000>;
  • #address-cells = <1>;
  • #size-cells = <0>;
  • i2c-gpio,sda-open-drain:
    • 它表示其他驱动、其他系统已经把SDA设置为open drain了
    • 在驱动里不需要在设置为open drain
    • 如果需要驱动代码自己去设置SDA为open drain,就不要提供这个属性
  • i2c-gpio,scl-open-drain:
    • 它表示其他驱动、其他系统已经把SCL设置为open drain了
    • 在驱动里不需要在设置为open drain
    • 如果需要驱动代码自己去设置SCL为open drain,就不要提供这个属性
编写设备树
shell 复制代码
i2c_gpio_100ask {
	compatible = "i2c-gpio";
	gpios = <&gpio4 20 0 /* sda */
		     &gpio4 21 0 /* scl */
		    >;
	i2c-gpio,delay-us = <5>;	/* ~100 kHz */
	#address-cells = <1>;
	#size-cells = <0>;
};

把上述代码,放入dts的根节点下面。

相关推荐
无敌最俊朗@3 小时前
Qt 多线程与并发编程详解
linux·开发语言·qt
DrugOne3 小时前
Amber24 安装指南:Ubuntu 22.04 + CUDA 12.4 环境
linux·运维·ubuntu·drugone
至善迎风3 小时前
Ubuntu 24.04 SSH 多端口监听与 ssh.socket 配置详解
linux·ubuntu·ssh
wdfk_prog4 小时前
[Linux]学习笔记系列 -- lib/timerqueue.c Timer Queue Management 高精度定时器的有序数据结构
linux·c语言·数据结构·笔记·单片机·学习·安全
大聪明-PLUS4 小时前
如何从 USB 闪存驱动器安装 Debian Linux
linux·嵌入式·arm·smarc
报错小能手4 小时前
linux学习笔记(18)进程间通讯——共享内存
linux·服务器·前端
第四维度45 小时前
【全志V821_FoxPi】6-2 IMX219 MIPI摄像头适配
linux·ipc·tina·v821·imx219
杜子不疼.5 小时前
【Linux】进程的初步探险:基本概念与基本操作
linux·人工智能·ai
de之梦-御风5 小时前
【Linux】 开启关闭MediaMTX服务
linux·运维·服务器