I2C子系统
I2C简介
串行、半双工的总线
I2C总线有两根双向的信号线,一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步
IIC是真正的多主机总线,(对比SPI在每次通信前都需要把主机定死,而IIC可以在通讯过程中,改变主机),如果两个或更多的主机同时请求总线,可以通过冲突检测和仲裁防止总线数据被破坏
起始和终止信号都是由主机发出的,连接到I2C总线上的器件,若具有I2C总线的硬件接口,则很容易检测到起始和终止信号
在起始信号后必须发送一个7位从机地址+1位方向位,用“0”表示主机发送数据,“1”表示主机接收数据。
每当主机向从机发送完一个字节的数据,主机总是需要等待从机给出一个应答信号,以确认从机是否成功接收到了数据
起始信号是必需的,结束信号和应答信号,都可以不要
注:实际使用中,一般是单片机作为主机,其它器件作为从机,单片机先向器件发送信息表示要读取数据,之后转变传输方向,器件发送数据到单片机。
I2C总线上可挂接的设备数量受总线的最大电容400pF限制。
串行的8位双向数据传输速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s。
IIC物理连接
使用IIC通信的IIC器件有很多,比如温度传感器,陀螺仪加速度计MPU6050,EEPROM存储芯片AT24C02等,通过IIC总线,可以与单片机之间进行数据传输。
IIC通信线只有只有两根,数据线SDA的高低电平传输2进制的数据,时钟线SCL通过方波信号提供时钟节拍
多个IIC器件可以并联在IIC总线上,每个器件有特定的地址,分时共享IIC总线
实际使用IIC当然还要连接电源以及共地哦

I2C时序图
SCL 时钟线
SDA 数据线
以下为I2C基础时序图。

起始:时钟线SCL为高时,数据线SDA由高到低
停止:时钟线SCL为高时,数据线SDA由低到高
注:SDA和SCL同时为高时,为IIC总线的空闲状态

开始标志(S)发出后,主设备会传送一个7 位的Slave 地址,并且后面跟着一个第8位,称为Read/Write 位。
R/W 位表示主设备是在接受从设备的数据还是在向其写数据。
然后,主设备释放SDA 线,等待从设备的应答信号(ACK)。每个字节的传输都要跟随有一个应答位。
应答产生时,从设备将SDA 线拉低并且在SCL 为高电平时保持低。
数据传输以停止标志(P)结束,然后释放总线。但主设备也可以产生重复的开始信号去操作另一台从设备,而不发出结束标志。
所有的SDA 信号变化都要在SCL 时钟为低电平时进行,除了开始和结束标志

S为起始位 黄 0为主机给从机发送数据 1为从机给主机发送数据
A为应答位 P为结束位
简化后:

写寄存器时,主设备除了发出开始标志和地址位,还要加一个R/W 位,0 为写,1 为读在第9 个时钟周期(高电平时),MPU6050 产生应答信号主设备开始传送寄存器地址,并接到应答然后开始传送寄存器数据,仍然要有应答信号最后主设备发送停止信号。

首先由主设备产生开始信号,然后发送从设备地址位和一个写数据位,等待应答然后发送寄存器地址,才能开始读寄存器收到应答信号后,主设备再发一个开始信号,然后发送从设备地址位和一个读数据位然后,作为从设备的MPU6050 产生应答信号并开始发送寄存器中的数据通信以主设备产生的拒绝应答信号(nACK)和结束标志(Stop)结束拒绝应答信号(nACK)产生定义为SDA 数据在第9 个时钟周期一直为高
I2C框架图

总结:
设备驱动层:实现具体I2C外设的驱动程序并生成设备节点,
核心层:承上启下
适配器驱动层:实现I2C时序
i2c实现温度传感器数据读取的框架项目
一、整体框架结构
┌─────────────────────────────────────────────────────────────┐
│ 用户空间 (Userspace) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 应用程序 (read/write, sysfs, IIO) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 内核空间 (Kernel) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 传感器驱动 (I2C Client Driver) │ │
│ │ - probe() / remove() │ │
│ │ - i2c_transfer() 读写 │ │
│ │ - 数据解析与转换 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ I2C 核心子系统 (I2C Core) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 硬件层 (Hardware) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ I2C 控制器 (I2C Adapter) │ 温度传感器 (Slave) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
二、部分驱动代码编写
probe 函数与设备注册
/* I2C 设备 ID 匹配表 */
static const struct i2c_device_id temp_sensor_id[] = {
{ "temp_sensor", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, temp_sensor_id);
/* 设备树匹配表 */
static const struct of_device_id temp_sensor_of_match[] = {
{ .compatible = "vendor,temp-sensor", },
{ }
};
/* probe 函数:驱动与设备匹配时调用 */
static int temp_sensor_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct temp_sensor_data *data;
int ret;
/* 1. 检查 I2C 通信是否正常 */
ret = i2c_smbus_read_byte(client);
if (ret < 0) {
dev_err(&client->dev, "i2c check failed\n");
return ret;
}
/* 2. 分配设备私有数据 */
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
mutex_init(&data->lock);
i2c_set_clientdata(client, data);
/* 3. 注册到 IIO 或 HWMON 子系统 */
/* 具体注册方式取决于选择的上层子系统 */
dev_info(&client->dev, "sensor probed successfully\n");
return 0;
}
/* remove 函数 */
static void temp_sensor_remove(struct i2c_client *client)
{
/* 清理资源,注销子系统接口 */
}
/* I2C 驱动结构 */
static struct i2c_driver temp_sensor_driver = {
.driver = {
.name = "temp_sensor",
.of_match_table = temp_sensor_of_match,
},
.probe = temp_sensor_probe,
.remove = temp_sensor_remove,
.id_table = temp_sensor_id,
};
module_i2c_driver(temp_sensor_driver);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("I2C Temperature Sensor Driver");
设备树配置
&i2c2 {
status = "okay";
clock-frequency = <400000>; /* 400kHz 高速模式 */
temp-sensor@38 {
compatible = "vendor,temp-sensor";
reg = <0x38>; /* I2C 设备地址 */
status = "okay";
};
};
传感器驱动核心是封装 I2C 传输
/* I2C 写操作 */
static int temp_sensor_write(struct i2c_client *client, u8 reg, u8 *data, u8 len)
{
struct i2c_msg msg;
u8 buf[32];
buf[0] = reg;
memcpy(&buf[1], data, len);
msg.addr = client->addr;
msg.flags = 0; /* 写操作 */
msg.buf = buf;
msg.len = len + 1;
return i2c_transfer(client->adapter, &msg, 1);
}
/* I2C 读操作 */
static int temp_sensor_read(struct i2c_client *client, u8 reg, u8 *data, u8 len)
{
struct i2c_msg msgs[2];
/* 发送寄存器地址 */
msgs[0].addr = client->addr;
msgs[0].flags = 0;
msgs[0].buf = ®
msgs[0].len = 1;
/* 读取数据 */
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD;
msgs[1].buf = data;
msgs[1].len = len;
return i2c_transfer(client->adapter, msgs, 2);
}
温度读取实现
/* 触发测量并读取结果 */
static int temp_sensor_get_temperature(struct temp_sensor_data *data, int *temp_milli)
{
struct i2c_client *client = data->client;
u8 cmd = 0xAC; /* 启动测量命令 */
u8 raw_data[3]; /* 原始数据(含CRC可选) */
int ret;
mutex_lock(&data->lock);
/* 1. 发送启动测量命令 */
ret = temp_sensor_write(client, cmd, NULL, 0);
if (ret < 0) {
dev_err(&client->dev, "write cmd failed\n");
goto out;
}
/* 2. 等待测量完成(典型 10-80ms) */
msleep(80);
/* 3. 读取测量结果 */
ret = temp_sensor_read(client, 0x00, raw_data, sizeof(raw_data));
if (ret < 0) {
dev_err(&client->dev, "read data failed\n");
goto out;
}
/* 4. 数据转换(以 AHT30 为例:20bit 温度数据) */
*temp_milli = ((raw_data[1] & 0x0F) << 16) | (raw_data[2] << 8) | raw_data[3];
*temp_milli = (*temp_milli * 2000) / 1048576 - 500; /* 转换为毫摄氏度 */
out:
mutex_unlock(&data->lock);
return ret;
}
三、调试与验证方法
- I2C 总线扫描:
i2cdetect -y 0确认设备地址 - 逻辑分析仪:抓取 SCL/SDA 波形验证时序
- 内核日志:
dmesg | grep i2c查看通信错误
常见错误与调试
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
| 返回负数 | I2C 通信失败 | 检查设备地址、上拉电阻 |
| 返回1(只有第一个消息成功) | 传感器无应答 | 检查电源、时序 |
| 读到全0xFF | 未连接或未上拉 | 检查硬件连接 |
| 数据错乱 | 寄存器地址错误 | 核对数据手册 |
四、总结
| 层级 | 职责 | 关键接口 |
|---|---|---|
| 硬件层 | I2C 总线物理连接 | SCL/SDA |
| I2C 核心 | 总线管理与传输 ACK START信号管理 | i2c_transfer() |
| 驱动层 | I2C通信协议实现、数据解析 | i2c_driver 结构 |
| 应用层 | 数据获取 | sysfs、字符设备、IIO |
五、关于I2C 读操作的本质
I2C 读操作需要两次传输(大多数传感器):
第1步:主机发送要读取的寄存器地址(写操作)
/* 发送寄存器地址 */
msgs[0].addr = client->addr;
msgs[0].flags = 0;
msgs[0].buf = ®
msgs[0].len = 1;
起始条件(S) → 设备地址(7位) + 写标志(0) → 应答(ACK) → 寄存器地址(8位) → 应答(ACK)
第2步:主机重新发送设备地址 + 读标志,读取数据
/* 读取数据 */
msgs[1].addr = client->addr;
msgs[1].flags = I2C_M_RD;
msgs[1].buf = data;
msgs[1].len = len;
重复起始条件(Sr) → 设备地址(7位) + 读标志(1) → 应答(ACK) → 数据字节0 → 应答 → 数据字节1 → ... → 非应答(NACK) → 停止条件(P)
为什么需要两个消息?
大多数温度传感器内部有多个寄存器:
传感器内部寄存器映射:
┌─────────────────┐
│ 0x00: 温度高字节 │
│ 0x01: 温度低字节 │
│ 0x02: 配置寄存器 │
│ 0x03: 状态寄存器 │
└─────────────────┘
如果不发送寄存器地址,传感器不知道你要读取哪个寄存器。
例外:某些传感器有命令即地址的设计,一条读消息即可
某些传感器只有一个功能:测量温度。没有多个寄存器需要选择。
具体传感器需要查看数据手册的 I2C 通信协议章节。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 351134995@qq.com