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 总线的硬件接口抽象。
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-0
、i2c-1
、i2c-2
三个实例。
i2c_algorithm
:通信算法抽象
定义 I2C 适配器实现 I2C 协议的底层操作(起始、数据收发、停止等),是适配器与硬件交互的“驱动逻辑”。
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,当设备和驱动匹配成功后,会设置此指针。
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
被注销,释放资源。
测试
在用户态生成
示例:
// 在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控制器的节点下,添加如下代码:
ap3216c@1e {compatible = "lite-on,ap3216c";reg = <0x1e>;};
i2c_msg
:I2C 传输消息
表示一次 I2C 传输的消息单元,包含地址、方向、数据等信息。
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值相同,则匹配成功
- 设备树中,某个I2C控制器节点下可以创建I2C设备的节点
- 使用id_table来判断
- i2c_client.name跟某个id_table[i].name值相同,则匹配成功
i2c_driver跟i2c_client匹配成功后,就调用i2c_driver.probe函数。
I2C 数据传输核心流程与代码
内核数据传输函数:i2c_transfer
用于在 I2C 总线上传输一个或多个 i2c_msg
,是内核中 I2C 数据传输的核心接口。
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_client
与 i2c_driver
。
static const struct i2c_device_id sensor_id_table[] = {{"sensor_demo", 0},{},
};
MODULE_DEVICE_TABLE(i2c, sensor_id_table);
初始化 I2C 设备(probe
函数与 i2c_client
处理)
probe
函数在驱动与设备匹配时调用,完成设备初始化。
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 读写函数编写
实现对从设备寄存器的读写操作。
指定寄存器读(单字节)
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;
}
指定寄存器写(单字节)
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;
}
读取从设备多个寄存器数据
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 = ®// 第二个消息:读多个寄存器数据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;
}
向从设备多个寄存器写入数据
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;
}
设备操作函数编写
定义字符设备等的操作函数,供应用层调用。
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
并注册
将驱动与设备匹配逻辑、probe
、remove
等函数关联,并注册到内核。
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,就不要提供这个属性
编写设备树
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
的根节点下面。