嵌入式Linux I2C子系统与驱动开发:架构与实战
导读:I2C总线是嵌入式系统最常用的外设通信接口之一。本文从Linux内核I2C子系统架构出发,拆解核心数据结构与传输机制,配合EEPROM和温度传感器两个完整驱动示例,帮你从协议原理一路写到驱动代码。
一、原理简析
I2C协议基础
I2C(Inter-IC)是一种两线制串行总线协议,仅需SCL(时钟)和SDA(数据)两根信号线即可完成主从通信。协议核心特征:
- 7位寻址为主流,扩展支持10位地址(需适配器声明
I2C_FUNC_10BIT_ADDR) - 标准模式100kHz、快速模式400kHz、高速模式3.4MHz
- 多主机总线,通过开漏信号仲裁冲突
- 主从结构:只有主设备能发起通信,从设备被动响应
I2C子系统架构
Linux内核I2C子系统采用三层设计:控制器驱动管硬件操作,设备驱动管业务逻辑,中间的核心层负责桥接和协议模拟。
核心层(i2c-core)干四件事:
- 管理适配器和驱动的注册/注销
- 完成设备与驱动的匹配(OF/ACPI/id_table)
- 将设备驱动的通信请求转发给对应适配器的底层传输函数
- 提供SMBus协议的软件模拟(当适配器不支持时)
二、核心数据结构
i2c_adapter — 总线适配器
每个i2c_adapter实例代表一条物理I2C总线,是控制器硬件的软件抽象:
struct i2c_adapter {
struct module *owner; // 模块所有者,通常设THIS_MODULE
unsigned int class; // 适配器类别(用于自动探测)
const struct i2c_algorithm *algo; // 总线传输算法
void *algo_data; // 算法私有数据(已弃用,用dev_get_drvdata替代)
struct device dev; // 嵌入device结构
int nr; // 总线编号(i2c-0, i2c-1...)
char name[48]; // 适配器名称
u32 retries; // 重试次数
struct mutex bus_lock; // 总线锁
struct mutex clk_lock; // 时钟锁
};
i2c_algorithm — 传输算法
定义适配器如何收发数据,是控制器驱动的核心:
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);
};
master_xfer:纯I2C传输,必须实现(否则回退到smbus_xfer)smbus_xfer:SMBus协议传输,可选(i2c-core可基于master_xfer模拟)functionality:声明适配器支持的功能标志
i2c_client — 从设备实例
每个i2c_client代表一个挂载在总线上的I2C从设备:
struct i2c_client {
unsigned short addr; // 从设备地址(7位或10位)
char name[I2C_NAME_SIZE]; // 设备名称
struct i2c_adapter *adapter; // 所属适配器
struct device dev; // 嵌入device结构
int irq; // 中断号
struct list_head detected; // 检测链表
};
i2c_driver — 设备驱动
struct i2c_driver {
unsigned int class;
int (*probe)(struct i2c_client *client); // 新版probe(6.x内核)
void (*remove)(struct i2c_client *client);
struct device_driver driver; // 嵌入driver结构
const struct i2c_device_id *id_table; // 传统ID匹配表
const struct of_device_id *driver.of_match_table; // 设备树匹配表
};
重要变更:内核6.x中,旧的
probe(struct i2c_client *, const struct i2c_device_id *)已被移除,中间过渡的probe_new也被合并回probe,新签名只接收client参数。如需获取匹配数据,使用i2c_match_id()或device_get_match_data()。
i2c_msg — 传输消息
struct i2c_msg {
__u16 addr; // 从设备地址
__u16 flags; // 标志位(I2C_M_RD=读, I2C_M_TEN=10位地址等)
__u16 len; // 数据长度(最大65535)
__u8 *buf; // 数据缓冲区
};
常用flags:
| 标志 | 值 | 含义 |
|---|---|---|
I2C_M_RD |
0x0001 | 读操作 |
I2C_M_TEN |
0x0010 | 10位地址 |
I2C_M_STOP |
0x8000 | 发送STOP |
I2C_M_NOSTART |
0x4000 | 跳过重复START |
三、I2C消息传输
i2c_transfer — 通用传输
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num);
返回值(务必注意):
| 返回值 | 含义 | 处理方式 |
|---|---|---|
| 负数 | 错误码(-ENXIO/-EIO/-ETIMEDOUT等) | 直接返回错误 |
| 正数(=num) | 全部消息传输成功 | 正常处理 |
| 正数(<num) | 部分消息成功(异常) | 视为错误 |
关键点:
i2c_transfer返回的是成功传输的消息数,不是0/1。必须检查ret != num来判断是否全部成功,不能简单判断ret < 0。
常见错误码:
| 错误码 | 含义 |
|---|---|
-ENXIO |
从设备地址未响应(设备不存在或未上电) |
-EIO |
数据传输未收到ACK |
-ETIMEDOUT |
操作超时 |
-EAGAIN |
总线仲裁失败,需重试 |
-EAFNOSUPPORT |
不支持10位地址 |
i2c_master_send / i2c_master_recv
单消息收发的便捷封装:
// 发送:返回发送字节数或负错误码
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);
SMBus操作函数
SMBus是I2C的子集协议,提供标准化的寄存器读写操作:
// 字节读写
s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_byte_data(struct i2c_client *client,
u8 command, u8 value);
// 字读写(16位,SMBus字节序:低字节在前)
s32 i2c_smbus_read_word_data(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_word_data(struct i2c_client *client,
u8 command, u16 value);
// 字读写(字节交换版:高字节在前,适用于LM75等传感器)
s32 i2c_smbus_read_word_swapped(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_word_swapped(struct i2c_client *client,
u8 command, u16 value);
i2c_smbus_read_word_swapped是内核提供的便捷函数,专门处理MSB-first的字序,避免手动swab16。
i2c_check_functionality
使用SMBus函数前,必须检查适配器是否支持:
// 检查适配器是否支持字节和字数据操作
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA |
I2C_FUNC_SMBUS_WORD_DATA)) {
dev_err(&client->dev, "adapter lacks required SMBus functionality\n");
return -EIO;
}
常用功能标志:
| 标志 | 含义 |
|---|---|
I2C_FUNC_I2C |
支持纯I2C协议(非SMBus适配器通常不支持) |
I2C_FUNC_10BIT_ADDR |
支持10位地址 |
I2C_FUNC_SMBUS_BYTE_DATA |
支持SMBus字节读写 |
I2C_FUNC_SMBUS_WORD_DATA |
支持SMBus字读写 |
I2C_FUNC_SMBUS_EMUL |
i2c-core可模拟全部SMBus操作 |
四、设备树配置
I2C从设备在设备树中定义为控制器节点的子节点:
&i2c1 {
status = "okay";
clock-frequency = <100000>; /* 100kHz */
/* EEPROM AT24C02 */
eeprom@50 {
compatible = "atmel,24c02";
reg = <0x50>; /* I2C 7位地址 */
pagesize = <8>;
};
/* LM75温度传感器 */
temperature-sensor@48 {
compatible = "national,lm75";
reg = <0x48>;
};
};
关键字段说明:
| 字段 | 说明 |
|---|---|
compatible |
驱动匹配字符串,必须与驱动of_match_table一致 |
reg |
I2C从设备地址(7位),不含读写位 |
clock-frequency |
总线时钟频率(在控制器节点设置) |
内核启动时,I2C核心层解析设备树,为每个子节点创建i2c_client实例,并通过compatible属性匹配对应的i2c_driver。
五、完整驱动示例
示例1:AT24C02 EEPROM驱动
EEPROM的特点是需要先写寄存器地址再读数据,适合用i2c_transfer组合消息:
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of_device.h>
struct at24_data {
struct i2c_client *client;
u8 pagesize;
};
/* 从EEPROM读取数据 */
static int at24_read(struct at24_data *at24, u8 offset,
u8 *buf, int len)
{
struct i2c_client *client = at24->client;
struct i2c_msg msgs[2];
int ret;
/* 第一条消息:写偏移地址(无STOP) */
msgs[0].addr = client->addr;
msgs[0].flags = 0; /* 写操作 */
msgs[0].len = 1;
msgs[0].buf = &offset;
/* 第二条消息:读数据(带重复START) */
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)
return ret < 0 ? ret : -EIO;
return len;
}
/* 向EEPROM写入数据 */
static int at24_write(struct at24_data *at24, u8 offset,
const u8 *buf, int len)
{
struct i2c_client *client = at24->client;
u8 write_buf[33]; /* 1字节偏移 + 最多32字节数据 */
int ret;
write_buf[0] = offset;
memcpy(&write_buf[1], buf, len);
ret = i2c_master_send(client, write_buf, len + 1);
if (ret != len + 1)
return ret < 0 ? ret : -EIO;
return len;
}
static int at24_probe(struct i2c_client *client)
{
struct at24_data *at24;
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
return -EIO;
at24 = devm_kzalloc(&client->dev, sizeof(*at24), GFP_KERNEL);
if (!at24)
return -ENOMEM;
at24->client = client;
at24->pagesize = 8; /* AT24C02页大小8字节 */
i2c_set_clientdata(client, at24);
dev_info(&client->dev, "AT24C02 probed at 0x%02x\n", client->addr);
return 0;
}
static void at24_remove(struct i2c_client *client)
{
dev_info(&client->dev, "AT24C02 removed\n");
}
static const struct of_device_id at24_of_match[] = {
{ .compatible = "atmel,24c02" },
{}
};
MODULE_DEVICE_TABLE(of, at24_of_match);
static const struct i2c_device_id at24_id[] = {
{ "24c02", 0 },
{}
};
MODULE_DEVICE_TABLE(i2c, at24_id);
static struct i2c_driver at24_driver = {
.driver = {
.name = "at24",
.of_match_table = at24_of_match,
},
.probe = at24_probe,
.remove = at24_remove,
.id_table = at24_id,
};
module_i2c_driver(at24_driver);
MODULE_AUTHOR("Embedded Linux Developer");
MODULE_DESCRIPTION("AT24C02 EEPROM driver");
MODULE_LICENSE("GPL");
示例2:LM75温度传感器驱动
LM75寄存器简单,温度值为16位MSB-first,适合用SMBus操作:
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/hwmon.h>
#include <linux/err.h>
struct lm75_data {
struct i2c_client *client;
};
/* 读取温度值(毫度) */
static int lm75_read_temp(struct lm75_data *data)
{
struct i2c_client *client = data->client;
int ret;
/* LM75温度寄存器0x00,MSB-first,用swapped版本 */
ret = i2c_smbus_read_word_swapped(client, 0x00);
if (ret < 0)
return ret;
/* 11位有符号数,右移5位得到实际温度 */
return (s16)ret >> 5;
}
static int lm75_probe(struct i2c_client *client)
{
struct lm75_data *data;
int temp;
if (!i2c_check_functionality(client->adapter,
I2C_FUNC_SMBUS_BYTE_DATA |
I2C_FUNC_SMBUS_WORD_DATA))
return -EIO;
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
/* 验证设备:尝试读取温度 */
temp = lm75_read_temp(data);
if (temp < 0) {
dev_err(&client->dev, "failed to read temperature: %d\n", temp);
return temp;
}
dev_info(&client->dev, "LM75 at 0x%02x, temp=%d\n",
client->addr, temp * 125);
return 0;
}
static void lm75_remove(struct i2c_client *client)
{
dev_info(&client->dev, "LM75 removed\n");
}
static const struct of_device_id lm75_of_match[] = {
{ .compatible = "national,lm75" },
{}
};
MODULE_DEVICE_TABLE(of, lm75_of_match);
static const struct i2c_device_id lm75_id[] = {
{ "lm75", 0 },
{}
};
MODULE_DEVICE_TABLE(i2c, lm75_id);
static struct i2c_driver lm75_driver = {
.driver = {
.name = "lm75",
.of_match_table = lm75_of_match,
},
.probe = lm75_probe,
.remove = lm75_remove,
.id_table = lm75_id,
};
module_i2c_driver(lm75_driver);
MODULE_AUTHOR("Embedded Linux Developer");
MODULE_DESCRIPTION("LM75 temperature sensor driver");
MODULE_LICENSE("GPL");
I2C控制器驱动骨架
编写控制器驱动需要实现i2c_algorithm:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
struct my_i2c_dev {
struct i2c_adapter adapter;
void __iomem *base;
struct clk *clk;
};
static int my_i2c_xfer(struct i2c_adapter *adap,
struct i2c_msg *msgs, int num)
{
struct my_i2c_dev *dev = i2c_get_adapdata(adap);
int i, ret;
for (i = 0; i < num; i++) {
/* 硬件相关:操作寄存器收发每个msg */
if (msgs[i].flags & I2C_M_RD)
ret = my_i2c_read_bytes(dev, &msgs[i]);
else
ret = my_i2c_write_bytes(dev, &msgs[i]);
if (ret < 0)
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_dev *dev;
int ret;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(dev->base))
return PTR_ERR(dev->base);
/* 初始化adapter */
dev->adapter.owner = THIS_MODULE;
dev->adapter.algo = &my_i2c_algo;
dev->adapter.dev.parent = &pdev->dev;
snprintf(dev->adapter.name, sizeof(dev->adapter.name),
"my-i2c-%d", pdev->id);
i2c_set_adapdata(&dev->adapter, dev);
platform_set_drvdata(pdev, dev);
ret = devm_i2c_add_adapter(&pdev->dev, &dev->adapter);
if (ret)
return ret;
dev_info(&pdev->dev, "I2C adapter registered\n");
return 0;
}
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,
.driver = {
.name = "my-i2c",
.of_match_table = my_i2c_of_match,
},
};
module_platform_driver(my_i2c_driver);
MODULE_AUTHOR("Embedded Linux Developer");
MODULE_DESCRIPTION("Custom I2C controller driver");
MODULE_LICENSE("GPL");
六、对比表格
I2C vs SPI
| 维度 | I2C | SPI |
|---|---|---|
| 信号线 | 2根(SCL+SDA) | 4根(SCLK+MOSI+MISO+CS) |
| 寻址方式 | 地址寻址(7/10位) | 片选线选择 |
| 通信模式 | 半双工 | 全双工 |
| 速率 | 最高3.4MHz | 可达数十MHz |
| 拓扑 | 多设备总线 | 一主多从 |
| 适用场景 | 传感器、EEPROM、低速外设 | Flash、ADC、高速外设 |
| Linux子系统 | i2c-core | spi-core |
i2c_transfer vs SMBus操作
| 维度 | i2c_transfer | SMBus操作 |
|---|---|---|
| 灵活性 | 任意消息组合 | 固定协议格式 |
| 复杂度 | 需手动构造i2c_msg | 一行函数调用 |
| 适配器要求 | 需I2C_FUNC_I2C | 需对应SMBus功能标志 |
| 典型场景 | EEPROM多字节读写 | 传感器寄存器读写 |
| START/STOP | 手动控制 | 协议自动管理 |
| 软件模拟 | 不可模拟 | i2c-core可基于I2C模拟 |
七、核心流程图
I2C子系统架构全景
I2C传输流程
八、常见问题解决
1. probe失败:设备树compatible不匹配
现象:驱动注册成功但probe不被调用。
排查:
# 查看总线上的设备
ls /sys/bus/i2c/devices/
# 查看驱动绑定
cat /sys/bus/i2c/drivers/at24/bind
# 对比compatible字符串
cat /sys/firmware/devicetree/base/i2c1/eeprom@50/compatible
解决:确保设备树compatible与驱动of_match_table完全一致。
2. i2c_transfer返回-ENXIO
现象:通信失败,返回-6(ENXIO)。
原因:从设备地址无响应。检查硬件连线、设备供电、地址是否正确(注意7位地址不含读写位,0x50不是0xA0)。
3. SMBus操作返回-EOPNOTSUPP
现象:调用i2c_smbus_read_word_data返回不支持。
原因:适配器不支持该SMBus操作,且i2c-core无法模拟(适配器未声明I2C_FUNC_I2C)。
解决:用i2c_check_functionality提前检查,必要时改用i2c_transfer。
4. 10位地址通信失败
现象:使用10位地址设备时返回-EAFNOSUPPORT。
原因:适配器未声明I2C_FUNC_10BIT_ADDR。SMBus控制器通常不支持10位地址,软件GPIO模拟(i2c-algo-bit)支持。
5. module_i2c_driver宏使用
// 这一行等价于 module_init + module_exit + i2c_register_driver
module_i2c_driver(at24_driver);
无需手动写__init和__exit函数,减少模板代码。
九、总结
I2C子系统的核心思路就三个字:分着写。控制器驱动只管硬件操作,设备驱动只管业务逻辑,核心层负责桥接和模拟。记住这几条就够了:
- 选对传输方式:简单寄存器操作用SMBus,复杂时序用i2c_transfer
- 检查适配器能力:始终用
i2c_check_functionality前置校验 - 正确处理返回值:
i2c_transfer返回消息数,不是0/1 - 设备树优先:新内核推荐通过
compatible匹配,id_table作为兼容回退 - 善用devm_函数:
devm_kzalloc/devm_i2c_add_adapter自动释放资源
下期写I2C控制器驱动的寄存器级实现。