找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

979

积分

0

好友

111

主题
发表于 4 天前 | 查看: 15| 回复: 0

I2C (Inter-Integrated Circuit) 是嵌入式系统中最常用的串行通信协议之一,广泛应用于连接各类低速外设,如传感器、EEPROM存储器和RTC实时时钟芯片。它是一种同步、半双工、支持多主多从的串行总线,仅需两条信号线即可工作:由主设备控制的时钟线 SCL,以及用于双向数据传输的数据线 SDA。其核心工作机制基于地址寻址,每个从设备都拥有一个唯一的7位或10位地址。在通信中,主设备发起请求,从设备进行响应。

在 Linux 内核的设备驱动框架中,I2C子系统遵循典型的总线-设备-驱动模型,其具体实现被划分为三个核心角色:

  • I2C Adapter (总线驱动):代表SoC或CPU内部集成的I2C控制器硬件(即主设备)。它负责底层硬件时序、电平控制等物理层操作,并在内核中注册 struct i2c_adapter 结构。
  • I2C Client (设备):代表物理连接到I2C总线上的具体从设备(如一个温度传感器芯片)。它包含了该设备的唯一地址以及其所在总线的指针。在现代嵌入式Linux开发中,I2C Client的信息通常来源于设备树 (Device Tree)
  • I2C Driver (驱动):这是开发者需要编写的、用于操作特定芯片的逻辑。它通过名称或设备树中的 compatible 属性字符串与I2C Client进行匹配,并包含初始化、寄存器读写等具体功能。驱动必须围绕 struct i2c_driver 结构来构建。
struct i2c_driver {
    int (*probe) (struct i2c_client *client, const struct i2c_device_id *id);
    int (*remove) (struct i2c_client *client);
    const struct i2c_device_id *id_table; // 传统基于ID的匹配方式
    const struct of_device_id *of_match_table; // 基于设备树的匹配方式 (推荐)
    struct device_driver driver; // 包含驱动名称等信息
};

驱动与硬件的匹配

要让内核知道你的驱动需要控制哪个硬件设备,需要进行两步配置:在设备树中声明设备,在驱动代码中定义匹配表。

1. 在设备树 (DTS) 中添加节点
假设我们的传感器连接在SoC的 i2c1 总线上,设备地址为 0x48 (7位地址)。

&i2c1 {
    status = "okay";
    my_sensor@48 { // 节点名,@后通常跟设备地址
        compatible = "vendor,my-sensor-xyz"; // 用于驱动匹配的关键字符串
        reg = <0x48>; // 关键:指定设备的I2C从地址
    };
};

2. 在驱动中定义匹配表
驱动代码中需要定义一个of_device_id结构数组,其中的compatible属性值必须与设备树中的字符串完全一致。

static const struct of_device_id my_sensor_of_match[] = {
    { .compatible = "vendor,my-sensor-xyz" },
    { /* Sentinel */ } // 结束标志
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);

// ... 随后在 struct i2c_driver 的 driver 成员中关联此表
.driver = {
    .name = "my_i2c_drv",
    .of_match_table = my_sensor_of_match, // 关联匹配表
},

当内核启动并解析设备树时,发现一个I2C Client的compatible属性与驱动中定义的my_sensor_of_match匹配,便会自动调用该驱动的probe函数,并将对应的struct i2c_client结构指针传递给驱动。

I2C通信方式

probe函数中获取到i2c_client后,即可通过它来操作硬件。Linux内核提供了两种不同抽象层次的通信函数:

  • I2C原始传输 (i2c_transfer):这是最灵活、最底层的接口,允许构造任意复杂的读写序列(例如:先写入寄存器地址,再连续读取多个数据字节)。它使用struct i2c_msg数组来描述传输消息。对于大多数驱动开发,内核提供了更易用的封装函数:
    • i2c_master_send(client, buf, count): 向设备发送数据。
    • i2c_master_recv(client, buf, count): 从设备读取数据。
  • SMBus传输协议:SMBus是I2C的一个子集,定义了一些标准的、格式固定的传输函数(如读写一个字节、一个字)。对于支持SMBus的设备(许多EEPROM和传感器),使用这些函数代码更简洁。
    • 例如:i2c_smbus_read_byte_data(client, reg) 读取8位寄存器数据。
    • 建议:除非芯片数据手册明确要求使用SMBus,否则优先使用i2c_master_send/recv来构造自定义的读写序列,以获得更好的兼容性。

I2C设备驱动实战示例

以下是一个完整的驱动示例,展示如何在probe函数中进行设备功能检查和基本的寄存器读写操作。

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/slab.h>

// 假设的芯片ID寄存器地址
#define MY_CHIP_ID_REG 0x05

struct my_i2c_data {
    u8 chip_id;
    // ... 可在此添加其他驱动私有数据
};

/* I2C 驱动的 PROBE 函数 */
static int my_i2c_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct my_i2c_data *data;
    int ret;
    u8 reg_addr = MY_CHIP_ID_REG;
    u8 reg_val;

    // 1. 检查I2C适配器是否支持标准I2C功能
    if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
        dev_err(&client->dev, "I2C适配器不支持标准I2C协议\n");
        return -EIO;
    }

    // 2. 分配并关联驱动私有数据结构
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;
    i2c_set_clientdata(client, data);
    dev_info(&client->dev, "设备探测成功! 地址: 0x%02x\n", client->addr);

    /*
     * 3. 典型寄存器读取示例:先写地址,再读数据
     * 大多数传感器/外设都采用这种“写-读”两步操作。
     */
    // Step A: 发送要读取的寄存器地址 (0x05)
    ret = i2c_master_send(client, ®_addr, 1);
    if (ret < 0)
        goto err;

    // Step B: 从该地址读取一个字节的数据(假设为芯片ID)
    ret = i2c_master_recv(client, ®_val, 1);
    if (ret < 0)
        goto err;

    data->chip_id = reg_val;
    dev_info(&client->dev, "芯片ID读取成功: 0x%02x\n", data->chip_id);

    // TODO: 后续驱动初始化工作(例如:注册字符设备、input子系统、IIO框架等)
    return 0;

err:
    dev_err(&client->dev, "I2C通信失败,错误码: %d\n", ret);
    return ret;
}

static int my_i2c_remove(struct i2c_client *client)
{
    // struct my_i2c_data *data = i2c_get_clientdata(client);
    dev_info(&client->dev, "设备已移除\n");
    return 0;
}

/* 4. 定义设备树匹配表 */
static const struct of_device_id my_i2c_match_table[] = {
    { .compatible = "vendor,my-sensor-xyz" },
    { /* 结束标志 */ }
};
MODULE_DEVICE_TABLE(of, my_i2c_match_table);

/* 5. 定义并注册 I2C 驱动 */
static struct i2c_driver my_i2c_driver = {
    .probe    = my_i2c_probe,
    .remove   = my_i2c_remove,
    .id_table = NULL, // 本例仅使用设备树匹配,此字段可为空
    .driver = {
        .name = "my_i2c_drv",
        .of_match_table = my_i2c_match_table, // 关键:指向匹配表
    },
};

module_i2c_driver(my_i2c_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("示例:一个简单的I2C传感器驱动");

这个示例清晰地展示了Linux下I2C设备驱动开发的核心流程:从设备树匹配、适配器功能检查,到使用标准I2C函数进行寄存器操作。开发者可以在此基础上,根据具体外设的数据手册,实现更完整的读写逻辑和功能集成。




上一篇:Logback配置错误导致磁盘空间告警:一次Java生产环境日志管理踩坑实录
下一篇:Doris MPP引擎内存管理深度解析:从OLAP内存结构到OOM排查运维实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 20:12 , Processed in 0.106634 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表