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 传输优化等高级特性。

相关推荐
梁洪飞2 分钟前
通过链接文件和Start.S学习armv7
linux·arm开发·嵌入式硬件·学习·arm
DN金猿13 分钟前
使用ubuntu安装nginx时报错
linux·nginx·ubuntu
小赵还有头发34 分钟前
安装Ceres与glog
linux·学习·无人机·ceres·glog
负二代0.01 小时前
Linux下的网络管理
linux·网络
自由的好好干活1 小时前
PCI9x5x驱动移植支持PCI9054在win7下使用2
驱动开发
s_daqing1 小时前
ubuntu(arm)安装redis
linux·redis·ubuntu
林鸿群2 小时前
ubuntu 26.04 安装mysql-server
linux·mysql·ubuntu
betazhou2 小时前
rsync使用案例分析
linux·运维·服务器·rsync·同步数据
安静的技术开发者2 小时前
Linux Ubuntu学习笔记
linux·ubuntu
geshifei3 小时前
Sched ext回调1——init_task (linux 6.15.7)
linux·ebpf