1. 什么是I2C
I2C(Inter-Integrated Circuit,集成电路间总线)是一种短距离、低速、串行通信总线 ,由 Philips(现 NXP)公司开发,广泛用于嵌入式系统中连接微控制器与外设(如传感器、EEPROM、LCD、ADC/DAC 等)。其特点是引脚少(仅两根信号线)、协议简单、支持多设备挂载,非常适合板级设备间的通信。
一、I2C 总线物理层
I2C 总线仅需两根信号线,加上电源和地,即可实现多设备通信:
1. SDA(Serial Data):串行数据线
- 双向传输数据(高低电平表示 1/0),需通过上拉电阻(通常 4.7kΩ)接电源,总线空闲时默认高电平。
2. SCL(Serial Clock):串行时钟线
- 由 主设备(Master) 产生,控制数据传输的速率和时序,从设备(Slave)被动接收时钟。
3. 多设备挂载
- 总线上可同时连接多个 主设备 (但同一时刻仅一个主设备主导通信)和多个 从设备 ,每个从设备通过唯一的 7位/10位设备地址 区分(最常用 7位地址,支持最多 127 个设备)。
二、I2C 协议核心时序
I2C 通信基于 "起始条件""数据传输""应答信号""停止条件" 等基本时序单元,所有设备需严格遵循协议规则。
1. 起始条件(Start Condition, S)
- 时序:SCL 为高电平时,SDA 从高电平拉低(下降沿)。
- 作用:标志一次通信的开始,所有从设备开始监听总线,准备接收地址。
2. 停止条件(Stop Condition, P)
- 时序:SCL 为高电平时,SDA 从低电平拉高(上升沿)。
- 作用:标志一次通信的结束,总线释放,从设备停止监听。
3. 数据传输(Data Bit)
- 时序:SCL 为低电平时,SDA 可改变电平;SCL 为高电平时,SDA 电平保持稳定(此时数据有效)。
- 数据格式 :每次传输 1字节(8位),高位(MSB)先传。
4. 应答信号(ACK/NACK)
- 定义 :每传输 1字节后,接收方需发送 应答位(ACK) 表示数据接收成功。
- 时序:第 8位数据传输完成后,SCL 再次拉高时,发送方释放 SDA,接收方拉低 SDA 表示 ACK(若保持高电平则为 NACK,通信失败)。
三、I2C 通信流程(主设备 → 从设备)
以 主设备向从设备读写数据 为例,典型流程如下:
1. 写操作(主 → 从)
S → [从设备地址(7位)+ 写标志(0)] → ACK → [寄存器地址(N位)] → ACK → [数据1] → ACK → [数据2] → ACK → ... → P
步骤 :
① 主设备发送 起始条件(S) ;
② 发送 从设备地址+写标志(0) (共 8位),从设备地址匹配后返回 ACK;
③ 发送 目标寄存器地址 (如传感器的温度寄存器地址),从设备返回 ACK;
④ 发送 数据字节 (可连续多个),从设备每次接收后返回 ACK;
⑤ 主设备发送 停止条件(P),结束通信
2. 读操作(从 → 主)
需先发送寄存器地址,再切换为读模式:
// 阶段1:发送寄存器地址(写模式)
S → [从设备地址+写标志(0)] → ACK → [寄存器地址] → ACK →
// 阶段2:读取数据(读模式)
S → [从设备地址+读标志(1)] → ACK → [数据1] → ACK → [数据2] → NACK → P
① 先通过 写操作 发送目标寄存器地址(从设备内部地址指针指向该寄存器);
② 主设备发送 重复起始条件(Sr) (不释放总线,避免其他主设备抢占);
③ 发送 从设备地址+读标志(1) ,从设备返回 ACK;
④ 从设备依次发送数据字节,主设备接收后返回 ACK(最后一个数据返回 NACK,表示停止接收);
⑤ 主设备发送 停止条件(P),结束通信。
四、I2C 总线特性
- 半双工通信:同一时刻只能单向传输数据(SDA 线共用)。
- 多主设备支持 :通过 仲裁机制 解决总线冲突(当多个主设备同时发送时,SDA 线电平低的主设备优先)。
- 速率等级 :
- 标准模式(SM):100 kbps;
- 快速模式(FM):400 kbps;
- 高速模式(HS):3.4 Mbps(较少用)。
- 硬件地址 vs 软件地址 :
- 部分设备支持通过引脚配置硬件地址(如 A0/A1/A2 引脚接高低电平,组合出不同地址),避免地址冲突。
五、I2C 与其他总线对比
| 特性 | I2C | SPI | UART |
|---|---|---|---|
| 信号线 | SDA + SCL(2线) | SCK/MOSI/MISO/CS(4线+) | TX/RX(2线) |
| 设备数量 | 多设备(7位地址) | 多设备(需独立CS) | 点对点(或总线需额外协议) |
| 通信方向 | 半双工 | 全双工 | 全双工 |
| 时钟控制 | 主设备提供时钟 | 主设备提供时钟 | 无时钟(需波特率一致) |
| 协议复杂度 | 中等(带ACK/地址) | 简单(无协议,自定义) | 简单(起始/停止位) |
| 典型应用 | 传感器、EEPROM、LCD | 高速ADC、SPI Flash | 串口通信(如调试 |
六、I2C 调试工具
- 用户空间工具 (Linux):
i2cdetect:扫描 I2C 总线设备(如i2cdetect -y 0扫描第 0 路 I2C 总线);i2cget/i2cset:读取/写入从设备寄存器(如i2cget -y 0 0x48 0x00读取地址 0x48 设备的 0x00 寄存器)。
- 示波器逻辑分析:抓取 SDA/SCL 波形,验证时序是否符合协议(如起始/停止条件、ACK 信号)。
- 内核调试 :通过
dmesg | grep i2c查看 I2C 控制器和设备的枚举日志。
2. Linux I2C 驱动
Linux I2C 驱动是 Linux 内核中用于支持 I²C(Inter-Integrated Circuit)总线通信的核心子系统之一.遵循 "总线 - 设备 - 驱动"(Bus/Device/Driver) 模型,核心目标是屏蔽底层硬件差异,为上层应用提供统一的 I2C 通信接口。
一、Linux I2C 子系统架构
Linux 的 I2C 子系统采用分层设计,主要包括三层:
1. I2C 核心层(i2c-core)
- 提供统一接口给上层(client 驱动)和下层(adapter 驱动)
- 管理 I2C 总线、设备、驱动的注册与匹配
- 实现
i2c_transfer()、i2c_smbus_*()等通用函数
2. I2C 总线适配器驱动(Adapter / Controller Driver)
- 对应 SoC 中的 I2C 控制器硬件(如 i2c-gpio、i2c-imx、i2c-s3c2410 等)
- 实现
struct i2c_algorithm中的master_xfer或smbus_xfer - 注册为
struct i2c_adapter
3. I2C 设备驱动(Client Driver)
- 针对具体 I2C 从设备(如温度传感器 LM75、加速度计 MPU6050)
- 实现设备初始化、读写逻辑
- 通过
i2c_driver结构体注册到内核
匹配机制:通过设备树(Device Tree)、ACPI 或板级文件(旧式)指定 I2C 设备地址和 compatible 字符串,内核自动匹配 driver 和 device。
三、关键数据结构
1. struct i2c_adapter
表示一个 I2C 主控制器(物理总线):
struct i2c_adapter {
struct module *owner;
unsigned int class; // 支持的设备类(如 sensors, ddc)
const struct i2c_algorithm *algo; // 通信算法
void *algo_data;
int nr; // 总线编号
char name[48];
struct device dev;
// ...
};
2. struct i2c_algorithm
定义如何进行 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);
// ...
};
3. struct i2c_client
代表一个 I2C 从设备:
struct i2c_client {
u16 addr; // 7-bit 地址(左移1位,bit0为R/W)
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter;
struct device dev;
struct i2c_driver *driver;
// ...
};
4. struct i2c_driver
I2C 设备驱动模板:
struct i2c_driver {
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);
const struct i2c_device_id *id_table;
struct of_device_id *of_match_table; // 用于设备树匹配
struct device_driver driver;
};
四、I2C 驱动工作流程(从匹配到通信)
1. 初始化阶段:注册与匹配
-
适配器驱动注册 :内核启动时,I2C 适配器驱动(如 SOC 自带的 I2C 控制器驱动)通过
i2c_add_adapter()向核心层注册i2c_adapter,核心层将其加入 I2C 总线链表。 -
设备注册:
- 方式 1(设备树):设备树中配置 I2C 设备节点(指定
compatible属性、I2C 地址),内核启动时解析节点,自动创建i2c_client并注册到核心层。 - 方式 2(静态注册):通过
i2c_new_device()手动创建i2c_client(适用于无设备树的旧系统)。
- 方式 1(设备树):设备树中配置 I2C 设备节点(指定
-
驱动注册与匹配 :设备驱动通过
i2c_add_driver()注册i2c_driver,核心层会遍历总线上的i2c_client,通过以下两种方式匹配:- 设备树匹配:
i2c_driver.of_match_table中的compatible属性与设备节点一致; - ID 匹配:
i2c_driver.id_table中的设备名称与i2c_client.name一致。匹配成功后,核心层调用驱动的probe函数,完成设备初始化(如申请 GPIO、配置寄存器)。
- 设备树匹配:
2. 通信阶段:数据读写
设备驱动通过核心层提供的通用 API 与 I2C 设备通信,核心 API 如下:
(1)核心传输 API:i2c_transfer()
最底层的传输接口,直接调用适配器的 master_xfer 函数,支持多消息(struct i2c_msg)传输(如 "发送寄存器地址 + 读取数据"):
// 原型:int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
// 参数:
// adap:目标适配器(从 i2c_client->adapter 获取)
// msgs:I2C 消息数组(每个消息包含地址、方向、长度、数据缓冲区)
// num:消息数量
// 示例:读取 I2C 设备(地址 0x50)的 0x01 寄存器值
struct i2c_msg msgs[2];
uint8_t reg_addr = 0x01;
uint8_t data_buf[1];
// 消息 1:发送寄存器地址(写操作)
msgs[0].addr = 0x50;
msgs[0].flags = 0; // 0 = 写,I2C_M_RD = 读
msgs[0].len = 1;
msgs[0].buf = ®_addr;
// 消息 2:读取数据(读操作)
msgs[1].addr = 0x50;
msgs[1].flags = I2C_M_RD;
msgs[1].len = 1;
msgs[1].buf = data_buf;
// 执行传输(通过 client->adapter 获取适配器)
i2c_transfer(client->adapter, msgs, 2);
(2)便捷读写 API:i2c_master_send()/i2c_master_recv()
封装 i2c_transfer(),适用于单消息传输(仅写或仅读):
// 写数据:向地址 addr 发送 len 字节数据
int i2c_master_send(struct i2c_client *client, const char *buf, int len);
// 读数据:从地址 addr 读取 len 字节数据到 buf
int i2c_master_recv(struct i2c_client *client, char *buf, int len);
(3)寄存器读写 API:i2c_smbus_xxx()
针对 "寄存器 - 数据" 类设备(如传感器、EEPROM)的便捷 API,封装了 "发送寄存器地址 + 读写数据" 的逻辑:
// 读取 8 位寄存器值(最常用)
uint8_t i2c_smbus_read_byte_data(struct i2c_client *client, uint8_t reg);
// 写入 8 位寄存器值
int i2c_smbus_write_byte_data(struct i2c_client *client, uint8_t reg, uint8_t value);
// 其他:16 位寄存器、块读写(如 i2c_smbus_read_i2c_block_data)等
3. 卸载阶段:资源释放
当驱动被卸载或设备被移除时,核心层调用 i2c_driver 的 remove 函数,释放申请的资源(如 GPIO、中断、内存)。
五、I2C 驱动开发实战(设备树 + 驱动示例)
以 "适配 I2C 接口的 EEPROM 24C02" 为例,完整展示驱动开发流程(基于 Linux 5.10 内核,设备树场景)。
1. 步骤 1:设备树配置(I2C 设备节点)
在 SOC 的 I2C 总线节点下添加 24C02 设备节点,指定 compatible(用于驱动匹配)、I2C 地址(0x50):
// 假设 SOC 的 I2C 总线节点为 i2c@12340000(对应 i2c-0 适配器)
i2c@12340000 {
status = "okay";
clock-frequency = <100000>; // I2C 总线速度 100kHz(标准模式)
// 24C02 设备节点
eeprom@50 {
compatible = "atmel,24c02"; // 驱动匹配的 compatible 属性
reg = <0x50>; // I2C 设备地址(7 位)
pagesize = <8>; // 24C02 每页 8 字节(页写需要)
};
};
2. 步骤 2:编写 I2C 设备驱动
驱动核心逻辑:匹配设备树节点 → 初始化设备 → 提供 sysfs 接口供上层读取 EEPROM 数据。
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/sysfs.h>
#include <linux/kobject.h>
// 设备私有数据(存储驱动需要的信息)
struct eeprom_data {
struct i2c_client *client;
uint8_t buffer[256]; // 24C02 容量 256 字节
};
// sysfs 接口:读取 EEPROM 指定地址的数据
static ssize_t eeprom_read_show(struct device *dev, struct device_attribute *attr, char *buf) {
struct eeprom_data *priv = dev_get_drvdata(dev);
struct i2c_client *client = priv->client;
int addr, ret;
// 从用户输入获取要读取的地址(如 cat /sys/bus/i2c/devices/0-0050/read 0x10)
ret = kstrtoint(buf, 16, &addr);
if (ret < 0 || addr >= 256)
return -EINVAL;
// 读取 addr 地址的数据(调用核心层 API)
priv->buffer[addr] = i2c_smbus_read_byte_data(client, addr);
return sprintf(buf, "0x%02x\n", priv->buffer[addr]);
}
// 定义 sysfs 属性
static DEVICE_ATTR_RO(eeprom_read);
// 设备匹配成功后执行的 probe 函数
static int eeprom_probe(struct i2c_client *client, const struct i2c_device_id *id) {
struct eeprom_data *priv;
int ret;
// 申请私有数据内存
priv = devm_kzalloc(&client->dev, sizeof(struct eeprom_data), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->client = client;
dev_set_drvdata(&client->dev, priv); // 绑定私有数据到设备
// 创建 sysfs 接口(/sys/bus/i2c/devices/0-0050/eeprom_read)
ret = device_create_file(&client->dev, &dev_attr_eeprom_read);
if (ret < 0)
dev_err(&client->dev, "sysfs create failed\n");
dev_info(&client->dev, "EEPROM 24C02 probe success!\n");
return 0;
}
// 设备移除时执行的 remove 函数
static int eeprom_remove(struct i2c_client *client) {
// 自动释放 devm 申请的资源,无需手动释放
device_remove_file(&client->dev, &dev_attr_eeprom_read);
dev_info(&client->dev, "EEPROM 24C02 remove success!\n");
return 0;
}
// 设备树匹配表(与 DTB 中的 compatible 对应)
static const struct of_device_id eeprom_of_match[] = {
{ .compatible = "atmel,24c02" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, eeprom_of_match);
// I2C 驱动结构体
static struct i2c_driver eeprom_driver = {
.probe = eeprom_probe,
.remove = eeprom_remove,
.driver = {
.name = "eeprom-24c02-driver", // 驱动名称
.of_match_table = eeprom_of_match, // 设备树匹配表
},
};
// 注册/注销驱动(内核模块接口)
module_i2c_driver(eeprom_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("I2C EEPROM 24C02 Driver");
MODULE_AUTHOR("Your Name");
六、核心总结
- Linux I2C 驱动遵循 "总线 - 设备 - 驱动" 模型,核心是
i2c_adapter(总线)、i2c_client(设备)、i2c_driver(驱动)三者的匹配与交互; - 设备树是现代 Linux 中 I2C 设备配置的标准方式,通过
compatible属性实现驱动匹配; - 驱动开发的核心流程:注册驱动 → 匹配设备 → 初始化 → 提供读写 API → 释放资源;
- 调试时优先用
i2cdetect/i2cget等工具验证硬件连通性,再排查驱动逻辑。
掌握以上内容后,可快速适配绝大多数 I2C 设备(如传感器、EEPROM、RTC 等)。如需深入,可进一步学习 I2C 从模式驱动、SMBus 协议支持、DMA 传输优化等高级特性。