I2C (Inter-Integrated Circuit) 是嵌入式系统中最常用的串行通信协议之一,广泛应用于连接各类低速外设,如传感器、EEPROM存储器和RTC实时时钟芯片。它是一种同步、半双工、支持多主多从的串行总线,仅需两条信号线即可工作:由主设备控制的时钟线 SCL,以及用于双向数据传输的数据线 SDA。其核心工作机制基于地址寻址,每个从设备都拥有一个唯一的7位或10位地址。在通信中,主设备发起请求,从设备进行响应。
在 Linux 内核的设备驱动框架中,I2C子系统遵循典型的总线-设备-驱动模型,其具体实现被划分为三个核心角色:
- I2C Adapter (总线驱动):代表SoC或CPU内部集成的I2C控制器硬件(即主设备)。它负责底层硬件时序、电平控制等物理层操作,并在内核中注册
struct i2c_adapter 结构。
- I2C Client (设备):代表物理连接到I2C总线上的具体从设备(如一个温度传感器芯片)。它包含了该设备的唯一地址以及其所在总线的指针。在现代嵌入式Linux开发中,I2C Client的信息通常来源于设备树 (Device Tree)。
- I2C Driver (驱动):这是开发者需要编写的、用于操作特定芯片的逻辑。它通过名称或设备树中的
compatible 属性字符串与I2C Client进行匹配,并包含初始化、寄存器读写等具体功能。驱动必须围绕 struct i2c_driver 结构来构建。
struct i2c_driver {
int (*probe) (struct i2c_client *client, const struct i2c_device_id *id);
int (*remove) (struct i2c_client *client);
const struct i2c_device_id *id_table; // 传统基于ID的匹配方式
const struct of_device_id *of_match_table; // 基于设备树的匹配方式 (推荐)
struct device_driver driver; // 包含驱动名称等信息
};
驱动与硬件的匹配
要让内核知道你的驱动需要控制哪个硬件设备,需要进行两步配置:在设备树中声明设备,在驱动代码中定义匹配表。
1. 在设备树 (DTS) 中添加节点
假设我们的传感器连接在SoC的 i2c1 总线上,设备地址为 0x48 (7位地址)。
&i2c1 {
status = "okay";
my_sensor@48 { // 节点名,@后通常跟设备地址
compatible = "vendor,my-sensor-xyz"; // 用于驱动匹配的关键字符串
reg = <0x48>; // 关键:指定设备的I2C从地址
};
};
2. 在驱动中定义匹配表
驱动代码中需要定义一个of_device_id结构数组,其中的compatible属性值必须与设备树中的字符串完全一致。
static const struct of_device_id my_sensor_of_match[] = {
{ .compatible = "vendor,my-sensor-xyz" },
{ /* Sentinel */ } // 结束标志
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);
// ... 随后在 struct i2c_driver 的 driver 成员中关联此表
.driver = {
.name = "my_i2c_drv",
.of_match_table = my_sensor_of_match, // 关联匹配表
},
当内核启动并解析设备树时,发现一个I2C Client的compatible属性与驱动中定义的my_sensor_of_match匹配,便会自动调用该驱动的probe函数,并将对应的struct i2c_client结构指针传递给驱动。
I2C通信方式
在probe函数中获取到i2c_client后,即可通过它来操作硬件。Linux内核提供了两种不同抽象层次的通信函数:
- I2C原始传输 (
i2c_transfer):这是最灵活、最底层的接口,允许构造任意复杂的读写序列(例如:先写入寄存器地址,再连续读取多个数据字节)。它使用struct i2c_msg数组来描述传输消息。对于大多数驱动开发,内核提供了更易用的封装函数:
i2c_master_send(client, buf, count): 向设备发送数据。
i2c_master_recv(client, buf, count): 从设备读取数据。
- SMBus传输协议:SMBus是I2C的一个子集,定义了一些标准的、格式固定的传输函数(如读写一个字节、一个字)。对于支持SMBus的设备(许多EEPROM和传感器),使用这些函数代码更简洁。
- 例如:
i2c_smbus_read_byte_data(client, reg) 读取8位寄存器数据。
- 建议:除非芯片数据手册明确要求使用SMBus,否则优先使用
i2c_master_send/recv来构造自定义的读写序列,以获得更好的兼容性。
I2C设备驱动实战示例
以下是一个完整的驱动示例,展示如何在probe函数中进行设备功能检查和基本的寄存器读写操作。
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/slab.h>
// 假设的芯片ID寄存器地址
#define MY_CHIP_ID_REG 0x05
struct my_i2c_data {
u8 chip_id;
// ... 可在此添加其他驱动私有数据
};
/* I2C 驱动的 PROBE 函数 */
static int my_i2c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct my_i2c_data *data;
int ret;
u8 reg_addr = MY_CHIP_ID_REG;
u8 reg_val;
// 1. 检查I2C适配器是否支持标准I2C功能
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
dev_err(&client->dev, "I2C适配器不支持标准I2C协议\n");
return -EIO;
}
// 2. 分配并关联驱动私有数据结构
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
i2c_set_clientdata(client, data);
dev_info(&client->dev, "设备探测成功! 地址: 0x%02x\n", client->addr);
/*
* 3. 典型寄存器读取示例:先写地址,再读数据
* 大多数传感器/外设都采用这种“写-读”两步操作。
*/
// Step A: 发送要读取的寄存器地址 (0x05)
ret = i2c_master_send(client, ®_addr, 1);
if (ret < 0)
goto err;
// Step B: 从该地址读取一个字节的数据(假设为芯片ID)
ret = i2c_master_recv(client, ®_val, 1);
if (ret < 0)
goto err;
data->chip_id = reg_val;
dev_info(&client->dev, "芯片ID读取成功: 0x%02x\n", data->chip_id);
// TODO: 后续驱动初始化工作(例如:注册字符设备、input子系统、IIO框架等)
return 0;
err:
dev_err(&client->dev, "I2C通信失败,错误码: %d\n", ret);
return ret;
}
static int my_i2c_remove(struct i2c_client *client)
{
// struct my_i2c_data *data = i2c_get_clientdata(client);
dev_info(&client->dev, "设备已移除\n");
return 0;
}
/* 4. 定义设备树匹配表 */
static const struct of_device_id my_i2c_match_table[] = {
{ .compatible = "vendor,my-sensor-xyz" },
{ /* 结束标志 */ }
};
MODULE_DEVICE_TABLE(of, my_i2c_match_table);
/* 5. 定义并注册 I2C 驱动 */
static struct i2c_driver my_i2c_driver = {
.probe = my_i2c_probe,
.remove = my_i2c_remove,
.id_table = NULL, // 本例仅使用设备树匹配,此字段可为空
.driver = {
.name = "my_i2c_drv",
.of_match_table = my_i2c_match_table, // 关键:指向匹配表
},
};
module_i2c_driver(my_i2c_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("示例:一个简单的I2C传感器驱动");
这个示例清晰地展示了Linux下I2C设备驱动开发的核心流程:从设备树匹配、适配器功能检查,到使用标准I2C函数进行寄存器操作。开发者可以在此基础上,根据具体外设的数据手册,实现更完整的读写逻辑和功能集成。