Linux I2C设备驱动

I2C核心:I2C 框架的 "中间调度层"

通俗理解:相当于 I2C 框架的 "总调度台",是 Linux 内核 I2C 子系统的核心抽象层,提供标准化 API,解耦适配器(平台驱动)和 I2C 驱动(从设备驱动),让两者无需关心对方的底层细节。

技术定义:一组通用的 API、数据结构和管理逻辑的集合,是连接适配器和 I2C 驱动的桥梁,统一管理所有 I2C 总线、适配器、从设备的生命周期。

  • 提供适配器注册i2c_add_adapter,I2C驱动注册i2c_register_driver等API
  • 当适配器 / I2C 驱动注册时,自动匹配:
    ✅ 适配器注册→遍历已注册的 I2C 驱动,匹配对应从设备;
    ✅ I2C 驱动注册→遍历已注册的适配器,匹配对应总线的从设备
  • 把 I2C 驱动的通信请求(如i2c_master_recv)转发给对应适配器的底层xfer函数,屏蔽不同适配器的硬件差异
  • 提供总线扫描(i2cdetect命令)、设备枚举、冲突处理、调试接口等通用功能

I2C适配器:I2C通信的"主控制硬件+平台驱动"

通俗理解:相当于 I2C 总线的 "总指挥",是 CPU/SoC 内部集成的 I2C 主控制器硬件(比如 STM32 的 I2C1、三星 S3C2440 的 I2C 控制器),也是管理这个硬件的平台驱动。

技术定义:也叫 "I2C 主控制器(Master)",是实现 I2C 协议物理层 / 链路层的硬件模块,唯一能发起 I2C 总线通信的组件;它由平台驱动适配(因为 I2C 控制器是焊死在 SoC 上的平台设备),负责把硬件接入内核 I2C 框架。

  1. 关键特征

是 "主设备":唯一能主动发起通信,从设备只能被动响应;

一个适配器对应一条物理 I2C 总线(如i2c-0、i2c-1);

依赖平台驱动:不同厂商的 I2C 控制器(如 STM32/IMX/S3C)需要不同的平台驱动适配,但都通过 I2C 核心的 API 接入框架。

I2C驱动:I2C 从设备的 "功能驱动"

通俗理解:相当于对接 I2C 从设备的 "专属操作员",针对具体 I2C 从设备(如 AT24C02 EEPROM、MPU6050 传感器、OLED 屏)开发的驱动,负责实现从设备的业务逻辑。

技术定义:也叫 "I2C 从设备驱动(Slave Driver)",面向具体从设备的功能开发,不关心底层 I2C 适配器的硬件细节,仅通过 I2C 核心提供的 API 和从设备交互。

声明支持的从设备:通过设备树compatible属性(如"atmel,24c02")或id_table(如从设备地址0x50),告诉 I2C 核心 "我能驱动哪些从设备";

实现业务逻辑:封装从设备的操作接口(如mpu6050_read_accel()读取加速度、at24c02_write()写入数据);

借助核心通信:调用 I2C 核心的标准化 API(如i2c_master_recv()/i2c_master_send())和从设备交互,无需直接操作适配器硬件;

生命周期管理:实现probe(设备匹配成功后初始化)、remove(设备卸载后清理)函数。

  1. 关键特征

与具体从设备强相关:一个 I2C 驱动只针对一类从设备(如 AT24C02 驱动只管 EEPROM);

通用化:同一 I2C 驱动可在不同适配器(不同 SoC 的 I2C 控制器)上运行,无需修改;

注册到 I2C 核心:通过i2c_register_driver()或module_i2c_driver()宏注册。

I2C框架抽象

struct i2c_adapter:抽象I2C主设备,用于标识物理I2C总线

内核使用struct i2c_adapter数据结构表示物理I2C总线以及访问该总线所需的算法

· owner:大多数情况下,可通过THIS_MODULE进行设置。这是所有者,用于引用计数。

· algo:这是控制器驱动程序用来驱动I2C总线的一组回调。这些回调允许生成I2C访问周期所需的信号。

struct i2c_algorithm:抽象I2C总线事务接口,即传输(读操作、写操作)接口

  • master_xfer:这是核心传递函数,必须为它的算法驱动程序提供纯I2C访问。该函数将在I2C设备驱动程序需要与底层I2C设备通信时被调用。但是,如果该函数没有实现(如果它为NULL),则调用smbus_xfer函数。

核心定义

这是 I2C 适配器最基础、最核心的传输函数,负责实现纯 I2C 协议的完整数据收发逻辑(起始信号、数据传输、停止信号、应答检测等)------ 是适配器的 "标配能力"。
"必须为它的算法驱动程序提供纯 I2C 访问":

算法驱动(i2c_algorithm)是适配器的 "通信规则",master_xfer是这个规则的核心 ------ 如果适配器想支持标准 I2C 通信,就必须实现这个函数(否则就是 "空壳适配器")。
"I2C 设备驱动程序需要与底层 I2C 设备通信时被调用":

当你写的 I2C 从设备驱动(比如 MPU6050 驱动)调用i2c_master_recv()/i2c_master_send()时,I2C 核心会直接调用适配器的master_xfer,完成物理总线上的数据收发。
"如果它为 NULL,则调用 smbus_xfer 函数":

这是反向兜底逻辑(极少出现):如果适配器没实现纯 I2C 的master_xfer,但实现了 SMBus 的smbus_xfer,那核心会用smbus_xfer来模拟 I2C 传输(但兼容性差,几乎没人这么做)。
通俗举例

比如你要给 I2C 总线上的 OLED 屏发数据(纯 I2C 协议),OLED 驱动调用i2c_master_send() → I2C 核心直接喊适配器:"用你的通用快递服务(master_xfer)把这包数据送过去!"

· smbus_xfer:这个函数由I2C控制器驱动程序设置,前提是它的算法驱动程序可以执行SMBus访问。当I2C设备驱动程序想要使用SMBus协议与芯片设备通信时,可以使用该函数。但如果它为NULL,则使用master_xfer函数,并模拟SMBus。

核心定义

这是适配 SMBus 协议的专用传输函数,只有当 I2C 控制器硬件原生支持 SMBus时,才需要在算法驱动中实现它 ------ 是适配器的 "增值服务"。
关键解读

"由 I2C 控制器驱动程序设置,前提是它的算法驱动程序可以执行 SMBus 访问":

只有适配器的硬件(I2C 控制器)能直接处理 SMBus 协议(比如硬件自带 SMBus 时序逻辑),驱动才会实现这个函数;如果硬件不支持,就不用设置(设为 NULL)。

"当 I2C 设备驱动程序想要使用 SMBus 协议与芯片设备通信时使用":

比如主板上的温度传感器(SMBus 协议)驱动调用i2c_smbus_read_byte()时,I2C 核心会优先调用smbus_xfer(如果不为 NULL)。

"如果它为 NULL,则使用 master_xfer 函数,并模拟 SMBus":

这是正向兜底逻辑(最常见):如果适配器没实现专用的smbus_xfer,I2C 核心会用通用的master_xfer,通过软件模拟 SMBus 协议的时序,完成数据传输(硬件不支持 SMBus 时,用软件凑)。
通俗举例

比如你要读主板 SMBus 总线上的内存温度传感器,传感器驱动调用i2c_smbus_read_word() → I2C 核心先看适配器有没有 "顺丰特惠服务(smbus_xfer)":

有 → 直接用硬件原生的 SMBus 传输(快、准);

没有 → 用通用快递服务(master_xfer),按 SMBus 的规则打包数据(软件模拟,稍慢但能用)。

· functionality:这个函数由I2C核心调用,以确定适配器的功能。该函数决定了I2C适配器驱动程序可以进行什么样的读取和写入。

核心定义

这是 I2C 核心用来 "查岗" 的函数 ------ 核心会主动调用它,问适配器:"你到底能提供哪些通信能力?",适配器返回自己的 "能力清单"。
关键解读

"由 I2C 核心调用,以确定适配器的功能":

I2C 核心在匹配驱动、执行传输前,会先调用这个函数,获取适配器的能力标识(比如I2C_FUNC_I2C表示支持纯 I2C,I2C_FUNC_SMBUS_BYTE表示支持 SMBus 字节传输)。

"决定了 I2C 适配器驱动程序可以进行什么样的读取和写入":

适配器返回的能力清单,会限制 I2C 驱动的操作 ------ 比如适配器不支持 SMBus 块传输,那 I2C 核心就会拒绝驱动调用i2c_smbus_read_block_data(),避免出现 "驱动要做的事,适配器做不了" 的错误。

在上述代码中,functionality是一个健全的回调函数,I2C核心或设备驱动程序都可以调用它,并通过i2c_check_functionality()来检查给定的适配器是否能够提供启动访问之前所需的I2C访问。例如,不是所有适配器都支持10位寻址模式。因此需要在芯片驱动程序中调用i2c_check_functionality(client->adapter,I2C_FUNC_10BIT_ADDR),以确定适配器是否支持10位寻址模式以及访问是否安全。所有标志都采用I2C_FUNC_XXX的形式,虽然每个标志都可以单独检查,但是I2C核心已根据逻辑功能对它们进行了划分,如下所示:

C 复制代码
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    // 第一步:检查适配器是否支持10位寻址(当前传感器需要10位地址)
    if (!i2c_check_functionality(client->adapter, I2C_FUNC_10BIT_ADDR)) {
        dev_err(&client->dev, "适配器不支持10位寻址,无法通信!\n");
        return -EOPNOTSUPP; // 返回"操作不支持"错误
    }

    // 第二步:确认支持后,再执行10位寻址的通信操作
    i2c_master_send(client, buf, len); // 安全执行
    return 0;
}

struct i2c_client: 抽象I2C总线从设备

上述数据结构包含了I2C设备的属性。其中,flags是设备标志,用于表示其是否为10位芯片地址。addr表示芯片地址,对于7位地址芯片,只会存储低7位。name是设备名称,最多包含I2C_NAME_SIZE(在include/linux/mod_devicetable.h中设置为20)个字符。adapter是适配器(记住,它是I2C总线)​,设备将被连接到该适配器上。dev是设备模型的基础设备结构,irq是分配给设备的中断线。

struct i2c_driver:从设备驱动程序,包含一组特定的驱动函数用于处理从设备

C 复制代码
struct i2c_driver
{
	unsigned int class;
	int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
	int (*remove)(struct i2c_client *client);
	int (*probe_new)(struct i2c_client *client);
	void (*shutdown)(struct i2c_client *client);
	struct device_driver driver;
	const struct i2c_device_id *id_table;
};

unsigned int class:驱动所属的设备类(可选)

核心含义:给驱动标记 "设备类型分类",用于 I2C 核心按类筛选驱动,减少无意义的匹配。

常用取值(内核宏定义):

I2C_CLASS_SENSOR:传感器类(如温度、加速度传感器);

I2C_CLASS_EEPROM:EEPROM 存储类;

I2C_CLASS_DISPLAY:显示类(如 OLED 屏);

0:默认值(不分类,匹配所有类型设备)。

使用场景:当驱动只针对某一类 I2C 设备时设置,比如 "只匹配传感器类设备",避免驱动去匹配 EEPROM、显示屏等无关设备。

C 复制代码
// 驱动仅针对传感器类设备
unsigned int class = I2C_CLASS_SENSOR;

int (*probe)(struct i2c_client *client, const struct i2c_device_id *id):驱动初始化函数(核心)

核心含义:I2C 核心匹配到 "驱动 - 设备" 后调用的初始化函数,是驱动的核心逻辑入口。

参数解析:

struct i2c_client *client:匹配到的 I2C 从设备实例(包含设备地址、挂载的适配器、设备信息等,是驱动操作设备的核心句柄);

const struct i2c_device_id *id:匹配到的设备 ID(来自id_table,传统无设备树场景用)。

返回值:0表示初始化成功;负数(如-ENOMEM/-EOPNOTSUPP)表示失败,驱动加载终止。

作用:

检查适配器能力(i2c_check_functionality);

初始化硬件(读写设备寄存器配置);

申请资源(中断、内存、字符设备节点);

注册驱动的操作接口(如 sysfs、ioctl)。

C 复制代码
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id) {
    // 1. 检查适配器是否支持SMBus字节数据传输
    if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA)) {
        dev_err(&client->dev, "适配器不支持SMBus字节数据传输!\n");
        return -EOPNOTSUPP;
    }
    // 2. 初始化传感器(写配置寄存器)
    i2c_smbus_write_byte_data(client, 0x6B, 0x00); // 唤醒MPU6050
    // 3. 创建字符设备节点(供用户空间访问)
    dev_info(&client->dev, "MPU6050初始化成功!\n");
    return 0;
}

int (*remove)(struct i2c_client *client):驱动卸载函数

核心含义:当设备被移除(rmmod驱动、物理移除设备)时调用,用于释放资源、恢复硬件默认状态。

参数:client是要卸载的从设备实例(和 probe 的 client 一致)。

返回值:0 表示成功,负数表示清理失败(但不影响卸载流程)。

作用:

释放 probe 中申请的资源(中断、内存映射、字符设备);

恢复硬件默认配置(如关闭传感器电源);

清理驱动创建的 sysfs 节点、数据结构。

C 复制代码
static int mpu6050_remove(struct i2c_client *client) {
    // 1. 关闭传感器(写寄存器)
    i2c_smbus_write_byte_data(client, 0x6B, 0x40); // 休眠MPU6050
    // 2. 释放字符设备节点
    dev_info(&client->dev, "MPU6050卸载成功!\n");
    return 0;
}

int (*probe_new)(struct i2c_client *client):新版 probe 函数(简化版)

核心含义:内核新版本推出的简化版 probe 函数,仅保留 client 参数,去掉了id参数。

设计目的:适配设备树匹配场景(设备树匹配时不需要id_table,id参数无意义),简化驱动写法。

使用规则:probe和probe_new二选一,不能同时实现;内核优先调用probe_new(如果定义),否则调用probe。

C 复制代码
static int mpu6050_probe_new(struct i2c_client *client) {
    // 设备树场景下,无需id参数,直接初始化
    i2c_smbus_write_byte_data(client, 0x6B, 0x00);
    return 0;
}

void (*shutdown)(struct i2c_client *client):关机 / 断电回调函数

核心含义:系统关机、重启或设备断电前调用的 "紧急清理函数",无返回值(必须保证执行成功)。

区别于 remove:

remove:正常卸载驱动时的 "优雅清理";

shutdown:系统关机 / 断电时的 "紧急清理"(比如保存传感器配置到 EEPROM、关闭硬件电源)。

C 复制代码
static void mpu6050_shutdown(struct i2c_client *client) {
    // 关机前保存传感器校准数据
    i2c_smbus_write_byte_data(client, 0x10, 0x01);
    dev_info(&client->dev, "MPU6050关机清理完成!\n");
}

struct device_driver driver:内嵌的通用设备驱动结构体(核心)

核心含义:struct i2c_driver是对 Linux 通用device_driver的 "特化",这个成员让 I2C 驱动融入 Linux 通用设备模型(支持设备树、sysfs、uevent 等)。

核心子成员(最常用):

name:驱动名称(用于传统匹配、sysfs 节点命名);

of_match_table:设备树匹配表(关键!设备树场景下,通过compatible属性匹配设备);

owner:驱动所属模块(固定设为THIS_MODULE)。

C 复制代码
// 设备树匹配表(和设备树中compatible="invensense,mpu6050"匹配)
static const struct of_device_id mpu6050_of_match[] = {
    { .compatible = "invensense,mpu6050" },
    { } // 结束标记
};
MODULE_DEVICE_TABLE(of, mpu6050_of_match);

struct device_driver driver = {
    .name = "mpu6050",
    .of_match_table = mpu6050_of_match, // 设备树匹配表
    .owner = THIS_MODULE,
};

const struct i2c_device_id *id_table:传统匹配 ID 表(兼容无设备树)

核心含义:无设备树场景下,驱动声明 "支持哪些设备" 的 ID 列表(字符串数组),I2C 核心通过对比i2c_client->name和id_table中的 ID 完成匹配。

C 复制代码
struct i2c_device_id {
    char name[I2C_NAME_SIZE]; // 设备名称(如"mpu6050")
    kernel_ulong_t driver_data; // 驱动私有数据(可选)
};

使用规则:

设备树场景下,id_table可选(但建议保留,兼容无设备树系统);

必须以空元素{}结尾;

需配合MODULE_DEVICE_TABLE(i2c, xxx_id_table)导出到内核,让内核知道驱动支持的设备。

struct i2c_msg:定义设备地址、事务标志(发送或接受),指向要发送/接受的数据指针以及数据大小

C 复制代码
struct i2c_msg {
	__u16 addr;
	__u16 flags;
#define I2C_M_TEN	0x0010
#define I2C_M_RD		0x0001
	__u16 len;
	__u8 *buf;
};

· addr:这是从地址。

· flags:一个事务可以由多个操作组成,这个字段是操作的标志。在写操作(主设备发送给从设备的操作)的情况下,它应该设置为0。但是,对于读操作(主设备从从设备读取)​,则可以使用I2C_M_RD或I2C_M_TEN(假设设备是一个10位地址芯片)​。

· len:这是缓冲区中数据的大小。在读操作中,它对应于要从设备上读取的字节数,并存储在buf中;在写操作中,它对应于buf中要写入设备的字节数。

· buf:这是读/写缓冲区,必须按长度分配。

I2C通信接口

普通I2C通信

i2c_transfer()

让我们从最底层开始。i2c_transfer()是用于传输I2C消息的核心函数。其他API封装了这个函数,该函数由适配器的algo->master_xfer支持。其原型如下:

使用i2c_transfer()函数时,在同一事务的读/写操作中,字节之间不会发送停止位。这对于那些在地址写入和数据读取之间不需要停止位的设备非常有用。以下代码展示了它的用法:

C 复制代码
// 读MPU6050 0x3B寄存器(无停止位,推荐)
u8 reg = 0x3B;
u8 data[2];
struct i2c_msg msgs[2] = {
    // 第一个msg:写寄存器地址(无停止位)
    {
        .addr = 0x68,
        .flags = 0, // 写操作
        .len = 1,
        .buf = &reg
    },
    // 第二个msg:读数据(重复起始,最后发停止位)
    {
        .addr = 0x68,
        .flags = I2C_M_RD, // 读操作
        .len = 2,
        .buf = data
    }
};
// 单次调用:两个msg用重复起始,无中间停止位
i2c_transfer(client->adapter, msgs, 2);

如果设备在读取序列的中间需要停止位,则应该将传输事务拆分为两部分(即两个操作)------使用一个i2c_transfer()(带有单个写操作)进行地址写入,并使用另一个i2c_transfer()(带有单个读操作)进行数据读取,如下所示:

C 复制代码
// 读老旧传感器 0x01寄存器(需要中间停止位)
u8 reg = 0x01;
u8 data;
struct i2c_msg msg;

// 第一次调用:写寄存器地址,结束后发停止位
msg.addr = 0x48;
msg.flags = 0;
msg.len = 1;
msg.buf = ®
i2c_transfer(client->adapter, &msg, 1); // 写地址,发停止位

// (可选:总线释放,其他设备可使用)

// 第二次调用:读数据,重新起始→读→发停止位
msg.flags = I2C_M_RD;
msg.len = 1;
msg.buf = &data;
i2c_transfer(client->adapter, &msg, 1); // 读数据,发停止位

示例场景:读 MPU6050 的加速度 X 轴数据(0x3B 寄存器)

不需要停止位:先写 0x3B(寄存器地址)→ 读 2 字节数据(重复起始,无停止位),设备能正确返回 0x3B 寄存器的数据;

若插入停止位:写 0x3B 后发停止位,设备清空地址缓存,后续读数据时会读默认寄存器(比如 0x00),得到错误数据。

  • 判断方法:
    看设备手册:这是最权威的!设备 datasheet 会明确写 "Read Sequence" 是否需要 Stop 位(比如标注 "Repeated Start"= 不需要,"Stop after Write"= 需要);
    先试无停止位(单次 i2c_transfer):99% 的设备都适用,若读数据错误(比如全 0 / 全 FF),再试拆分事务加停止位;
    看总线行为:用逻辑分析仪抓 I2C 总线,若无停止位时设备无应答,拆分后有应答,说明需要停止位。

i2c_master_send()/i2c_master_recv()

i2c_master_send() = 「一键寄包裹」:只支持寄 1 个包裹(单写 i2c_msg),系统自动帮你填好 "寄件" 相关信息,直接调用通用下单系统;

i2c_master_recv() = 「一键收包裹」:只支持收 1 个包裹(单读 i2c_msg),系统自动帮你填好 "收件" 相关信息,直接调用通用下单系统。

C 复制代码
// 内核简化版实现,仅保留核心逻辑
int i2c_master_send(const struct i2c_client *client, const char *buf, int count)
{
    struct i2c_msg msg;
    // 1. 自动填充"单个写操作"的i2c_msg
    msg.addr = client->addr;    // 从设备地址(复用client里的地址)
    msg.flags = 0;              // 写操作(无I2C_M_RD)
    msg.len = count;            // 要发送的字节数
    msg.buf = (u8 *)buf;        // 要发送的数据缓冲区

    // 2. 调用底层的i2c_transfer,仅传1个msg(单个写操作)
    return i2c_transfer(client->adapter, &msg, 1);
}

// 内核简化版实现,仅保留核心逻辑
int i2c_master_recv(const struct i2c_client *client, char *buf, int count)
{
    struct i2c_msg msg;
    // 1. 自动填充"单个读操作"的i2c_msg
    msg.addr = client->addr;    // 从设备地址
    msg.flags = I2C_M_RD;       // 读操作(设置I2C_M_RD)
    msg.len = count;            // 要接收的字节数
    msg.buf = (u8 *)buf;        // 接收数据的缓冲区

    // 2. 调用底层的i2c_transfer,仅传1个msg(单个读操作)
    return i2c_transfer(client->adapter, &msg, 1);
}

SMBus兼容API

SMBus是由Intel公司开发的双向总线,非常类似于I2C总线。此外,SMBus是I2C总线的一个子集,这意味着I2C设备兼容SMBus设备,但反过来不一定成立。SMBus是I2C总线的一个子集,这意味着I2C控制器支持大多数SMBus操作。然而,对于SMBus控制器来说并不一定兼容I2C控制器提供的所有协议选项。

纯字节操作
C 复制代码
// 读1字节,有应答则设备在线
int ret = i2c_smbus_read_byte(client);
if (ret >= 0) {
    dev_info(&client->dev, "设备在线,读到字节:0x%02x\n", ret);
} else {
    dev_err(&client->dev, "设备离线!错误码:%d\n", ret);
}
带寄存器字节操作
C 复制代码
// 第一步:检查适配器是否支持该功能
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA)) {
    dev_err(&client->dev, "适配器不支持SMBus字节数据操作!\n");
    return -EOPNOTSUPP;
}

// 第二步:写0x6B寄存器为0x00(唤醒MPU6050)
int ret = i2c_smbus_write_byte_data(client, 0x6B, 0x00);
if (ret != 0) {
    dev_err(&client->dev, "写寄存器失败!\n");
    return ret;
}

// 第三步:读0x6B寄存器,确认是否写入成功
u8 val = i2c_smbus_read_byte_data(client, 0x6B);
dev_info(&client->dev, "0x6B寄存器值:0x%02x\n", val); // 应输出0x00
字数据操作(双字节)
C 复制代码
// 检查功能支持
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WORD_DATA)) {
    return -EOPNOTSUPP;
}

// 读0x3B寄存器(加速度X轴,2字节)
u16 accel_x = i2c_smbus_read_word_data(client, 0x3B);
// SMBus默认大端序,MPU6050也是大端,直接拼接即可
dev_info(&client->dev, "加速度X轴:%d\n", (s16)accel_x); // 转有符号数
块数据操作
C 复制代码
u8 write_buf[8] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
u8 read_buf[32] = {0};

// 检查功能支持
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BLOCK_DATA)) {
    return -EOPNOTSUPP;
}

// 写0x00寄存器开始的8字节数据(AT24C02页写上限8字节)
int ret = i2c_smbus_write_block_data(client, 0x00, 8, write_buf);
if (ret != 0) return ret;

// 读0x00寄存器开始的8字节数据
int len = i2c_smbus_read_block_data(client, 0x00, read_buf);
dev_info(&client->dev, "读取%d字节:%02x %02x ...\n", len, read_buf[0], read_buf[1]);
I2C兼容块数据操作
C 复制代码
u8 buf[128] = {0};

// 检查功能支持
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_I2C_BLOCK)) {
    return -EOPNOTSUPP;
}

// 读0x40寄存器开始的128字节显存数据
int len = i2c_smbus_read_i2c_block_data(client, 0x40, 128, buf);
if (len == 128) {
    dev_info(&client->dev, "显存数据读取成功!\n");
}

I2C设备驱动程序抽象和结构

struct i2c_driver数据结构包含了处理它所负责的I2C设备所需的驱动方法。一旦将设备添加到总线上,设备就需要进行探测,这使得i2c_ driver.probe_new方法成为驱动程序的入口点。

探测I2C设备

每当在总线上实例化I2C设备并声明使用其驱动程序时,探测函数就会被调用。它可以执行以下操作。

· 使用i2c_check_functionality()函数检查I2C总线控制器(即I2C适配器)是否支持设备所需的功能。

· 检查设备是否符合预期。

· 初始化设备。

· 如有必要,设置设备特定数据。

· 向适当的内核框架进行注册。

其中,i2c_client结构体类型的指针表示I2C设备本身。参数client由核心预先构建和初始化(根据设备的描述)​,这不是在设备树中完成,就是在板级文件中完成。

在上面的例子中,我们检查了底层适配器是否支持设备所需的类型/命令。只有在成功进行完整的检查之后,我们才能安全地访问设备,并分配所有类型的资源,在必要时向其他框架进行注册。

实现remove回调

其中,client是传递给探测函数的相同I2C设备数据结构,这意味着探测时存储的任何数据都可以在此处检索。例如,可能需要根据探测函数中设置的私有数据来执行一些清理操作或其他操作:

上面的示例很简单,可能代表了你将在驱动程序中看到的大多数情况。由于此回调函数在成功时返回零,因此失败原因可能包括设备无法关闭电源、设备仍在使用等。这意味着在此回调函数中,可能存在需要查询设备并执行一些额外操作的情况。

驱动程序初始化和注册

I2C设备驱动程序是分别使用i2c_add_driver()和i2c_del_driver()函数在I2C核心中进行注册和注销的。其中,前者是由i2c_register_driver()函数支持的宏。以下代码展示了它们的原型:

在这两个函数中,drv是先前设置好的i2c_driver结构体。注册API在成功时返回零,在失败时返回错误码。驱动程序的注册大多在模块初始化时进行,驱动程序的注销则通常在模块退出函数中完成。以下是注册I2C设备驱动程序的典型示例:

如果驱动程序在模块初始化/清除期间除了注册/注销之外不需要执行其他操作,则可以使用module_i2c_driver()宏来简化上述代码,如下所示:

驱动程序配置设备

其中,name是设备的名称;而driver_data是驱动程序状态数据,它是私有于驱动程序的,它可以使用指向每个设备数据结构的指针进行设置。此外,为了进行设备匹配和模块(自动)加载,需要将同一设备id数组传递给MODULE_ DEVICE_TABLE宏。

然而,这与设备树匹配无关。对于设备树中的设备节点来说,为了与驱动程序匹配,i2c_driver.device.of_match_table必须设置为of_device_id结构体类型的元素列表,该列表中的每个条目将描述可以从设备树中匹配的I2C设备。of_device_id结构体定义如下:

其中,compatible是描述性的字符串,可用于在设备树中匹配驱动程序;而data可以指向任何东西,例如每个设备的资源。同样,为了因设备树匹配而进行模块(自动)加载,必须将of_device_id结构体类型的元素列表传递给MODULE_DEVICE_TABLE宏。

C 复制代码
static struct i2c_driver foo_driver = {
	.driver = {
		.name = "foo",
		.of_match_table = of_match_ptr(foobar_of_match),
	},
	.probe = fake_i2c_probe,
	.remove = fake_i2c_remove,
	.id_table = foo_idtable,
};

实例化I2C设备

· reg:表示设备在总线上的地址。· compatible:这是一个字符串,用于对设备与驱动程序进行匹配。它必须与驱动程序的of_match_table中的条目匹配。以下是在同一适配器上声明两个I2C设备的示例:

上面的示例声明了同一总线(SoC的I2C总线编号为2)上地址分别为0x68和0x55的RTC芯片和EEPROM。I2C核心将依赖于compatible字符串属性和i2c_device_id表来绑定设备和驱动程序。请尝试使用兼容字符串(OF样式,即设备树)匹配设备;如果失败,I2C核心将尝试通过id表格来匹配设备。

如何避免编写I2C设备驱动程序

I2C适配器由内核在用户空间中作为字符设备公开,格式为/dev/i2c--,其中是总线编号。一旦打开与所使用的设备所在的适配器对应的字符设备文件,就可以执行一系列命令。

C 复制代码
#include <linux/i2c-dev.h>
#include <i2c/smbus.h>
#include <linux/i2c.h>

ioctl(file, I2C_FUNCS, unsigned long *funcs)

它相当于内核中的i2c_check_functionality()函数,用于返回所需的适配器功能(在*funcs参数中)​。返回的标志也以I2C_FUNC_*的形式表示:

C 复制代码
unsigned long funcs;
if (ioctl(file, I2C_FUNCS, &funcs) < 0)
	return -errno;
if (!(funcs & I2C_FUNC_SMBUS_QUICK)) {
	/*SMBUS write quick not available*/
	exit(1);
}

ioctl(file, I2C_TENBIT, long select)

用于设置需要通信的从设备是不是一个10位地址芯片,如果是select=1,如果不是select=0

ioctl(file, I2C_SLAVE, long addr)

用于设置你需要在该适配器上通信的芯片地址。地址存储在addr的7个低位(对于10位地址,传递的是10个低位)​。这个芯片可能已经在使用,此时可以使用I2C_SLAVE_FORCE来强制使用。

ioctl(file, I2C_RDWR, struct i2c_rdwr_ioctl_data *msgset)

用于在不间断的情况下执行组合的普通I2C读/写事务。比较有趣的数据结构是struct i2c_rdwr_ ioctl_data,其定义如下:

C 复制代码
int ret;
uint8_t buf[5] = {regaddr, '0x55', '0x65', '0x88', '0x14'};

struct i2c_msg messages[] = {
	{
		.addr = dev,
		.buf = buf,
		.len = 5,
	},
};

struct i2c_rdwr_ioctl_data payload = {
	.msgs = messages,
	.nmsgs = sizeof(messages)/sizeof(messages[0]),
};

ret = ioctl(file, I2C_RDWR, &payload);

也可以使用read()系统和write()系统调用来完成普通的I2C事务(假设用I2C_SLAVE设置了地址)​。

ioctl(file, I2C_SMBUS, struct i2c_smbus_ioctl_data *args)

· ioctl(file,I2C_SMBUS,struct i2c_smbus_ioctl_data *args):用于发出SMBus传输。数据结构struct i2c_ smbus_ioctl_data具有以下原型:

C 复制代码
struct i2c_smbus_ioctl_data {
	__u8 read_write;
	__u8 command;
	__u32 size;
	union i2c_smbus_data __user *data;
}

在上述数据结构中,read_write决定传输的方向------使用I2C_SMBUS_READ读取还是使用I2C_SMBUS_WRITE写入。command是可以被芯片解释的命令,例如寄存器地址。size是消息的长度,buf是消息缓冲区。请注意,标准化大小已经由I2C核心公开,它们分别是I2C_SMBUS_BYTE、I2C_SMBUS_BYTE_DATA、I2C_SMBUS_WORD_DATA、I2C_SMBUS_BLOCK_DATA和I2C_SMBUS_I2C_ BLOCK_DATA,分别对应1字节大小、2字节大小、3字节大小、5字节大小和8字节大小。完整列表可在include/uapi/linux/i2c.h中找到。下面的例子展示了如何在用户空间中执行SMBus传输:

C 复制代码
uint8_t buf[5] = {'0x55', '0x65', '0x88'};
struct i2c_smbus_ioctl_data payload = {
	.read_write = I2C_SMBUS_WRITE,
	.size = I2C_SMBUS_WORD_DATA,
	.command = regaddr,
	.data = (void *)buf,
};

ret = ioctl(fd, I2C_SLAVE_FORCE, dev);
if (ret < 0)
	/*handle_errors*/
ret = ioctl(fd, I2C_SMBUS, &payload);
if (ret < 0)
	/*handle errors*/

可以使用简单read()/write()系统调用来进行普通的I2C传输(尽管在每次传输后会发送一个停止位)​,I2C核心提供了以下API来执行SMBus传输:

write_quick 是 "快速应答检测"(仅传读写方向位,无有效数据),write_byte 是 "纯字节写"(传 1 字节有效数据,无寄存器地址)。

建议使用这些API而不是ioctl。如果发生故障,所有这些事务都将返回-1,可以检查errno以更好地了解发生了什么错误。成功时,*write 函数将返回0;而 read 函数将返回读取的值,除了 read_block*函数,它将返回已读取的值的数量。在面向块的操作中,缓冲区不需要超过32字节。

除了使用需要编写一些代码的API,还可以使用一个名为i2ctools的CLI(CommandLine Interface,命令行接口)包,其中包含以下工具。

· i2cdetect:用于枚举给定适配器上的I2C设备。

· i2cget:用于转储设备寄存器的内容。

· i2cset:用于设置设备寄存器的内容。

相关推荐
zhixingheyi_tian2 小时前
Linux 之 memory 碎片
linux
邂逅星河浪漫2 小时前
【域名解析+反向代理】配置与实现(步骤)-SwitchHosts-Nginx
linux·nginx·反向代理·域名解析·switchhosts
梅尔文.古2 小时前
RaspberryPi-如何启用看门狗
linux·运维·服务器
木子欢儿2 小时前
Ubuntu 24 安装 fcitx5 + rime + 雾凇配置
linux·运维·服务器·ubuntu
sg_knight2 小时前
Nuxt 4 生产环境部署指南 (Node.js + Nginx)
运维·nginx·node.js·nuxt·ssr
Alice2 小时前
linux scripts
java·linux·服务器
企微自动化3 小时前
自动化报表生成:将 RPA 采集的群聊数据自动整理为可视化周报
运维·自动化·rpa
代码游侠3 小时前
学习笔记——IPC(进程间通信)
linux·运维·网络·笔记·学习·算法
txzz88883 小时前
CentOS-Stream-10 YUM配置文件
linux·运维·centos·yum配置文件