I2C子系统
一、I2C总线基础
1. I2C总线特点
-
两线制:时钟线(SCL)、数据线(SDA)
-
空闲状态:SCL和SDA均为高电平
-
多主多从:支持多个主设备和多个从设备
-
地址寻址:每个从设备有唯一的7位地址
2. I2C通信时序
开始信号:SCL高电平期间,SDA由高变低
数据传送:SCL低电平时数据可以变化,SCL高电平时数据必须稳定
应答信号:每传送8位数据后,接收方在第9个时钟周期拉低SDA
停止信号:SCL高电平期间,SDA由低变高
二、Linux I2C子系统架构
1. 子系统分层结构
应用层
↓
I2C设备驱动层(lm75.c、at24c08等)
↓
I2C核心层(I2C Core)
↓
I2C总线驱动层(I2C Adapter)
↓
硬件层(I2C控制器)
2. 各层功能说明
(1) 硬件层
-
I2C控制器:硬件电路,产生I2C时序
-
I2C设备:如LM75温度传感器、AT24C08 EEPROM等
(2) I2C总线驱动层
-
I2C Adapter:I2C控制器驱动程序
-
主要功能:
-
初始化I2C控制器硬件
-
实现I2C时序收发算法
-
提供master_xfer()传输函数
-
(3) I2C核心层
-
I2C Core:核心管理层
-
主要功能:
-
管理I2C总线驱动和设备驱动
-
提供注册/注销接口
-
处理I2C设备匹配
-
(4) I2C设备驱动层
-
设备驱动:具体I2C设备的驱动程序
-
主要功能:
-
实现设备特定的读写逻辑
-
提供文件操作接口(open/read/write/close)
-
解析设备数据
-
三、I2C设备驱动分析 (lm75.c)
1. I2C驱动框架
(1) I2C驱动结构体
static struct i2c_driver lm75_driver =
{
.probe = probe, // 设备探测函数
.remove = remove, // 设备移除函数
.driver =
{
.name = DEV_NAME, // 驱动名称
.of_match_table = of_lm75_table // 设备树匹配表
},
.id_table = lm75_table // 设备ID表
};
(2) 设备匹配表
// 设备树匹配表
static const struct of_device_id of_lm75_table[] =
{
{.compatible = "ti,lm75"}, // 与设备树中的compatible匹配
{}
};
// 设备ID表
static const struct i2c_device_id lm75_table[] =
{
{.name = "ti,lm75"},
{}
};
2. 驱动注册与注销
// 注册I2C驱动
static int __init lm75_driver_init(void)
{
int ret = i2c_add_driver(&lm75_driver);
return ret;
}
// 注销I2C驱动
static void __exit lm75_driver_exit(void)
{
i2c_del_driver(&lm75_driver);
}
3. 设备探测函数 (probe)
static int probe(struct i2c_client * client, const struct i2c_device_id * device)
{
int ret = misc_register(&misc_dev); // 注册杂项设备
lm75_client = client; // 保存client指针
printk("lm75 probe\n");
return 0;
}
参数说明:
-
client:I2C客户端结构体,包含设备地址等信息 -
device:匹配的设备ID
4. I2C数据传输
(1) I2C消息结构体
struct i2c_msg msg =
{
.addr = lm75_client->addr, // 从设备地址
.flags = I2C_M_RD, // 读标志
.len = 2, // 数据长度
.buf = temp // 数据缓冲区
};
(2) 执行I2C传输
// 使用I2C控制器算法进行传输
lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &msg, 1);
流程:
-
填充i2c_msg结构体
-
调用adapter的master_xfer()函数
-
控制器硬件执行I2C时序
(3) 读取温度数据
static ssize_t read(struct file * file, char __user * buf, size_t size, loff_t * loff)
{
unsigned char temp[2] = {0};
// 1. 设置I2C消息
struct i2c_msg msg = {...};
// 2. 执行I2C读操作
lm75_client->adapter->algo->master_xfer(lm75_client->adapter, &msg, 1);
// 3. 将数据复制到用户空间
ret = copy_to_user(buf, temp, sizeof(temp));
return sizeof(temp);
}
四、I2C应用程序分析
1. 直接访问I2C设备 (iic_lm75.c)
(1) 打开I2C适配器
int fd = open("/dev/i2c-0", O_RDWR); // 打开I2C适配器
(2) 设置从设备地址
ioctl(fd, I2C_SLAVE, 0x48); // 设置LM75的从设备地址
地址说明:
-
LM75默认地址:0x48
-
通过A0-A2引脚可以改变地址
(3) 读取温度数据
// 1. 读取2字节温度数据
int ret = read(fd, temp, sizeof temp);
// 2. 温度值计算
float t = 0.5 * ((temp[0] << 8 | temp[1]) >> 7);
数据格式:
-
LM75温度数据为16位
-
高9位为温度值,每个单位代表0.5°C
-
需要右移7位后乘以0.5得到实际温度
2. 通过设备驱动访问 (lm75_app.c)
(1) 打开设备驱动
int fd = open("/dev/lm75", O_RDWR); // 打开LM75设备驱动
区别:
-
不需要设置从设备地址
-
不需要关心I2C时序细节
-
驱动已经封装了设备访问逻辑
(2) 读取温度数据
// 直接读取处理好的数据
int ret = read(fd, temp, sizeof temp);
float t = 0.5 * ((temp[0] << 8 | temp[1]) >> 7);
五、两种访问方式的对比
1. 直接访问I2C适配器
应用程序
↓
open("/dev/i2c-0") ← 打开I2C控制器
↓
ioctl(I2C_SLAVE, 0x48) ← 设置从设备地址
↓
read/write() ← 直接读写I2C总线
↓
硬件I2C控制器
特点:
-
需要应用程序了解I2C协议
-
需要设置从设备地址
-
需要处理原始数据
-
灵活性高,但编程复杂
2. 通过设备驱动访问
应用程序
↓
open("/dev/lm75") ← 打开设备驱动
↓
read/write() ← 调用设备操作函数
↓
LM75设备驱动 ← 封装I2C访问逻辑
↓
硬件I2C控制器
特点:
-
应用程序无需了解I2C协议细节
-
驱动处理数据解析
-
接口简单易用
-
代码复用性好
六、设备树配置示例
1. I2C控制器配置
i2c0: i2c@4000000 {
compatible = "fsl,imx6ul-i2c";
reg = <0x4000000 0x1000>;
interrupts = <GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "okay";
/* I2C设备列表 */
lm75: lm75@48 {
compatible = "ti,lm75";
reg = <0x48>;
};
eeprom: at24c08@50 {
compatible = "atmel,at24c08";
reg = <0x50>;
};
};
2. 关键属性说明
-
compatible:与驱动匹配的兼容字符串
-
reg:设备地址(7位地址)
-
每个I2C设备作为I2C控制器的子节点
七、I2C子系统工作流程
1. 驱动匹配流程
设备树:i2c0节点 → lm75子节点(compatible="ti,lm75")
↓
I2C核心:扫描设备树,创建i2c_client
↓
驱动注册:i2c_add_driver(&lm75_driver)
↓
匹配检查:compatible匹配
↓
执行probe:注册设备,初始化硬件
2. 数据传输流程
应用程序:read(/dev/lm75)
↓
设备驱动:read函数
↓
构造i2c_msg:地址、数据、方向
↓
调用master_xfer:I2C控制器算法
↓
硬件I2C控制器:产生时序波形
↓
LM75芯片:响应请求,返回数据
3. 温度值计算原理
// LM75温度数据格式
// 字节0:高8位,字节1:低8位
// 实际温度值在高9位中
temp[0]: D15 D14 D13 D12 D11 D10 D9 D8
temp[1]: D7 D6 D5 D4 D3 D2 D1 D0
// 合并两个字节
int16_t raw_data = (temp[0] << 8) | temp[1];
// 右移7位,保留高9位
int16_t temp_value = raw_data >> 7;
// 转换为实际温度(每个单位0.5°C)
float temperature = temp_value * 0.5;
// 简化公式
float t = 0.5 * ((temp[0] << 8 | temp[1]) >> 7);
总结:
-
I2C子系统分层:设备驱动层 → 核心层 → 总线驱动层 → 硬件层
-
两种访问方式:直接访问I2C适配器 vs 通过设备驱动访问
-
设备驱动框架:基于i2c_driver结构体,实现probe/remove函数
-
数据传输:通过i2c_msg结构体和master_xfer()函数
-
设备树配置:I2C设备作为I2C控制器的子节点,配置地址和兼容性
-
温度计算:LM75温度数据需要特殊格式转换