Linux I2C 驱动

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 总线特性

  1. 半双工通信:同一时刻只能单向传输数据(SDA 线共用)。
  2. 多主设备支持 :通过 仲裁机制 解决总线冲突(当多个主设备同时发送时,SDA 线电平低的主设备优先)。
  3. 速率等级
    • 标准模式(SM):100 kbps;
    • 快速模式(FM):400 kbps;
    • 高速模式(HS):3.4 Mbps(较少用)。
  4. 硬件地址 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 调试工具

  1. 用户空间工具 (Linux):
    • i2cdetect:扫描 I2C 总线设备(如 i2cdetect -y 0 扫描第 0 路 I2C 总线);
    • i2cget/i2cset:读取/写入从设备寄存器(如 i2cget -y 0 0x48 0x00 读取地址 0x48 设备的 0x00 寄存器)。
  2. 示波器逻辑分析:抓取 SDA/SCL 波形,验证时序是否符合协议(如起始/停止条件、ACK 信号)。
  3. 内核调试 :通过 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_xfersmbus_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. 初始化阶段:注册与匹配

  1. 适配器驱动注册 :内核启动时,I2C 适配器驱动(如 SOC 自带的 I2C 控制器驱动)通过 i2c_add_adapter() 向核心层注册 i2c_adapter,核心层将其加入 I2C 总线链表。

  2. 设备注册

    • 方式 1(设备树):设备树中配置 I2C 设备节点(指定 compatible 属性、I2C 地址),内核启动时解析节点,自动创建 i2c_client 并注册到核心层。
    • 方式 2(静态注册):通过 i2c_new_device() 手动创建 i2c_client(适用于无设备树的旧系统)。
  3. 驱动注册与匹配 :设备驱动通过 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 = &reg_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_driverremove 函数,释放申请的资源(如 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");

六、核心总结

  1. Linux I2C 驱动遵循 "总线 - 设备 - 驱动" 模型,核心是 i2c_adapter(总线)、i2c_client(设备)、i2c_driver(驱动)三者的匹配与交互;
  2. 设备树是现代 Linux 中 I2C 设备配置的标准方式,通过 compatible 属性实现驱动匹配;
  3. 驱动开发的核心流程:注册驱动 → 匹配设备 → 初始化 → 提供读写 API → 释放资源;
  4. 调试时优先用 i2cdetect/i2cget 等工具验证硬件连通性,再排查驱动逻辑。

掌握以上内容后,可快速适配绝大多数 I2C 设备(如传感器、EEPROM、RTC 等)。如需深入,可进一步学习 I2C 从模式驱动、SMBus 协议支持、DMA 传输优化等高级特性。

相关推荐
_OP_CHEN6 小时前
【Git原理与使用】(一)告别文件混乱!Git 初识:从版本灾难到高效管理的终极方案
linux·运维·git·github·运维开发·版本控制·企业级组件
装不满的克莱因瓶6 小时前
【Java架构 搭建环境篇三】Linux安装Git详细教程
java·linux·运维·服务器·git·架构·centos
进击大厂的小白6 小时前
45.申请一个GPIO中断
驱动开发
博语小屋6 小时前
线程同步与条件变量
linux·jvm·数据结构·c++
至善迎风6 小时前
Linux 服务器安全防护工具完全指南
linux·服务器·安全·防火墙
MC皮蛋侠客6 小时前
Linux安装go及环境配置教程
linux·运维·golang
满天点点星辰7 小时前
Linux命令大全-find命令
linux·运维·服务器
H_z_q24017 小时前
RHCE的条件测试
linux·运维·服务器
新青年.7 小时前
【Ubuntu】Ubuntu下解决Chrome不能输入中文
linux·chrome·ubuntu