学习笔记——I2C子系统

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);

流程

  1. 填充i2c_msg结构体

  2. 调用adapter的master_xfer()函数

  3. 控制器硬件执行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);

总结

  1. I2C子系统分层:设备驱动层 → 核心层 → 总线驱动层 → 硬件层

  2. 两种访问方式:直接访问I2C适配器 vs 通过设备驱动访问

  3. 设备驱动框架:基于i2c_driver结构体,实现probe/remove函数

  4. 数据传输:通过i2c_msg结构体和master_xfer()函数

  5. 设备树配置:I2C设备作为I2C控制器的子节点,配置地址和兼容性

  6. 温度计算:LM75温度数据需要特殊格式转换

相关推荐
小白同学_C8 小时前
Lab4-Lab: traps && MIT6.1810操作系统工程【持续更新】 _
linux·c/c++·操作系统os
今天只学一颗糖8 小时前
1、《深入理解计算机系统》--计算机系统介绍
linux·笔记·学习·系统架构
不做无法实现的梦~10 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
陌上花开缓缓归以10 小时前
W25N01KVZEIR flash烧写
arm开发
默|笙11 小时前
【Linux】fd_重定向本质
linux·运维·服务器
陈苏同学12 小时前
[已解决] Solving environment: failed with repodata from current_repodata.json (python其实已经被AutoDL装好了!)
linux·python·conda
“αβ”12 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping
不爱学习的老登13 小时前
Windows客户端与Linux服务器配置ssh无密码登录
linux·服务器·windows
熊猫_豆豆14 小时前
同步整流 Buck 降压变换器
单片机·嵌入式硬件·matlab
小王C语言14 小时前
进程状态和进程优先级
linux·运维·服务器