ESC
输入关键词搜索文章标题和内容

嵌入式Linux I2C子系统与驱动开发:架构与实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

嵌入式Linux I2C子系统与驱动开发:架构与实战

导读:I2C总线是嵌入式系统最常用的外设通信接口之一。本文从Linux内核I2C子系统架构出发,拆解核心数据结构与传输机制,配合EEPROM和温度传感器两个完整驱动示例,帮你从协议原理一路写到驱动代码。

一、原理简析

I2C协议基础

I2C(Inter-IC)是一种两线制串行总线协议,仅需SCL(时钟)和SDA(数据)两根信号线即可完成主从通信。协议核心特征:

  • 7位寻址为主流,扩展支持10位地址(需适配器声明I2C_FUNC_10BIT_ADDR)
  • 标准模式100kHz、快速模式400kHz、高速模式3.4MHz
  • 多主机总线,通过开漏信号仲裁冲突
  • 主从结构:只有主设备能发起通信,从设备被动响应

I2C子系统架构

Linux内核I2C子系统采用三层设计:控制器驱动管硬件操作,设备驱动管业务逻辑,中间的核心层负责桥接和协议模拟。

flowchart TB A["用户空间<br/>i2c-dev / i2ctools"] --> B["I2C设备驱动层<br/>i2c_driver"] B --> C["I2C核心层<br/>i2c-core"] C --> D["I2C控制器驱动层<br/>i2c_adapter + i2c_algorithm"] D --> E["硬件<br/>I2C控制器 + 从设备"] style A fill:#E8F5E9 style B fill:#E3F2FD style C fill:#F3E5F5 style D fill:#E3F2FD style E fill:#E8F5E9

核心层(i2c-core)干四件事:

  1. 管理适配器和驱动的注册/注销
  2. 完成设备与驱动的匹配(OF/ACPI/id_table)
  3. 将设备驱动的通信请求转发给对应适配器的底层传输函数
  4. 提供SMBus协议的软件模拟(当适配器不支持时)

二、核心数据结构

i2c_adapter — 总线适配器

每个i2c_adapter实例代表一条物理I2C总线,是控制器硬件的软件抽象:

struct i2c_adapter {
    struct module *owner;          // 模块所有者,通常设THIS_MODULE
    unsigned int class;            // 适配器类别(用于自动探测)
    const struct i2c_algorithm *algo; // 总线传输算法
    void *algo_data;               // 算法私有数据(已弃用,用dev_get_drvdata替代)
    struct device dev;             // 嵌入device结构
    int nr;                        // 总线编号(i2c-0, i2c-1...)
    char name[48];                 // 适配器名称
    u32 retries;                   // 重试次数
    struct mutex bus_lock;         // 总线锁
    struct mutex clk_lock;         // 时钟锁
};

i2c_algorithm — 传输算法

定义适配器如何收发数据,是控制器驱动的核心:

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);
    u32 (*functionality)(struct i2c_adapter *adap);
};
  • master_xfer:纯I2C传输,必须实现(否则回退到smbus_xfer)
  • smbus_xfer:SMBus协议传输,可选(i2c-core可基于master_xfer模拟)
  • functionality:声明适配器支持的功能标志

i2c_client — 从设备实例

每个i2c_client代表一个挂载在总线上的I2C从设备:

struct i2c_client {
    unsigned short addr;           // 从设备地址(7位或10位)
    char name[I2C_NAME_SIZE];      // 设备名称
    struct i2c_adapter *adapter;   // 所属适配器
    struct device dev;             // 嵌入device结构
    int irq;                       // 中断号
    struct list_head detected;     // 检测链表
};

i2c_driver — 设备驱动

struct i2c_driver {
    unsigned int class;
    int (*probe)(struct i2c_client *client);      // 新版probe(6.x内核)
    void (*remove)(struct i2c_client *client);
    struct device_driver driver;                   // 嵌入driver结构
    const struct i2c_device_id *id_table;          // 传统ID匹配表
    const struct of_device_id *driver.of_match_table; // 设备树匹配表
};

重要变更:内核6.x中,旧的probe(struct i2c_client *, const struct i2c_device_id *)已被移除,中间过渡的probe_new也被合并回probe,新签名只接收client参数。如需获取匹配数据,使用i2c_match_id()或device_get_match_data()。

i2c_msg — 传输消息

struct i2c_msg {
    __u16 addr;     // 从设备地址
    __u16 flags;    // 标志位(I2C_M_RD=读, I2C_M_TEN=10位地址等)
    __u16 len;      // 数据长度(最大65535)
    __u8 *buf;      // 数据缓冲区
};

常用flags:

标志 值 含义
I2C_M_RD 0x0001 读操作
I2C_M_TEN 0x0010 10位地址
I2C_M_STOP 0x8000 发送STOP
I2C_M_NOSTART 0x4000 跳过重复START

三、I2C消息传输

i2c_transfer — 通用传输

int i2c_transfer(struct i2c_adapter *adap,
                 struct i2c_msg *msgs, int num);

返回值(务必注意):

返回值 含义 处理方式
负数 错误码(-ENXIO/-EIO/-ETIMEDOUT等) 直接返回错误
正数(=num) 全部消息传输成功 正常处理
正数(<num) 部分消息成功(异常) 视为错误

关键点:i2c_transfer返回的是成功传输的消息数,不是0/1。必须检查ret != num来判断是否全部成功,不能简单判断ret < 0。

常见错误码:

错误码 含义
-ENXIO 从设备地址未响应(设备不存在或未上电)
-EIO 数据传输未收到ACK
-ETIMEDOUT 操作超时
-EAGAIN 总线仲裁失败,需重试
-EAFNOSUPPORT 不支持10位地址

i2c_master_send / i2c_master_recv

单消息收发的便捷封装:

// 发送:返回发送字节数或负错误码
int i2c_master_send(const struct i2c_client *client,
                    const char *buf, int count);

// 接收:返回接收字节数或负错误码
int i2c_master_recv(const struct i2c_client *client,
                    char *buf, int count);

SMBus操作函数

SMBus是I2C的子集协议,提供标准化的寄存器读写操作:

// 字节读写
s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_byte_data(struct i2c_client *client,
                               u8 command, u8 value);

// 字读写(16位,SMBus字节序:低字节在前)
s32 i2c_smbus_read_word_data(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_word_data(struct i2c_client *client,
                               u8 command, u16 value);

// 字读写(字节交换版:高字节在前,适用于LM75等传感器)
s32 i2c_smbus_read_word_swapped(struct i2c_client *client, u8 command);
s32 i2c_smbus_write_word_swapped(struct i2c_client *client,
                                  u8 command, u16 value);

i2c_smbus_read_word_swapped是内核提供的便捷函数,专门处理MSB-first的字序,避免手动swab16。

i2c_check_functionality

使用SMBus函数前,必须检查适配器是否支持:

// 检查适配器是否支持字节和字数据操作
if (!i2c_check_functionality(client->adapter,
                             I2C_FUNC_SMBUS_BYTE_DATA |
                             I2C_FUNC_SMBUS_WORD_DATA)) {
    dev_err(&client->dev, "adapter lacks required SMBus functionality\n");
    return -EIO;
}

常用功能标志:

标志 含义
I2C_FUNC_I2C 支持纯I2C协议(非SMBus适配器通常不支持)
I2C_FUNC_10BIT_ADDR 支持10位地址
I2C_FUNC_SMBUS_BYTE_DATA 支持SMBus字节读写
I2C_FUNC_SMBUS_WORD_DATA 支持SMBus字读写
I2C_FUNC_SMBUS_EMUL i2c-core可模拟全部SMBus操作

四、设备树配置

I2C从设备在设备树中定义为控制器节点的子节点:

来自 linuxros.cn · linuxROS
&i2c1 {
    status = "okay";
    clock-frequency = <100000>;  /* 100kHz */

    /* EEPROM AT24C02 */
    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;            /* I2C 7位地址 */
        pagesize = <8>;
    };

    /* LM75温度传感器 */
    temperature-sensor@48 {
        compatible = "national,lm75";
        reg = <0x48>;
    };
};

关键字段说明:

字段 说明
compatible 驱动匹配字符串,必须与驱动of_match_table一致
reg I2C从设备地址(7位),不含读写位
clock-frequency 总线时钟频率(在控制器节点设置)

内核启动时,I2C核心层解析设备树,为每个子节点创建i2c_client实例,并通过compatible属性匹配对应的i2c_driver。

五、完整驱动示例

示例1:AT24C02 EEPROM驱动

EEPROM的特点是需要先写寄存器地址再读数据,适合用i2c_transfer组合消息:

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

struct at24_data {
    struct i2c_client *client;
    u8 pagesize;
};

/* 从EEPROM读取数据 */
static int at24_read(struct at24_data *at24, u8 offset,
                     u8 *buf, int len)
{
    struct i2c_client *client = at24->client;
    struct i2c_msg msgs[2];
    int ret;

    /* 第一条消息:写偏移地址(无STOP) */
    msgs[0].addr = client->addr;
    msgs[0].flags = 0;            /* 写操作 */
    msgs[0].len = 1;
    msgs[0].buf = &offset;

    /* 第二条消息:读数据(带重复START) */
    msgs[1].addr = client->addr;
    msgs[1].flags = I2C_M_RD;    /* 读操作 */
    msgs[1].len = len;
    msgs[1].buf = buf;

    ret = i2c_transfer(client->adapter, msgs, 2);
    if (ret != 2)
        return ret < 0 ? ret : -EIO;

    return len;
}

/* 向EEPROM写入数据 */
static int at24_write(struct at24_data *at24, u8 offset,
                      const u8 *buf, int len)
{
    struct i2c_client *client = at24->client;
    u8 write_buf[33];  /* 1字节偏移 + 最多32字节数据 */
    int ret;

    write_buf[0] = offset;
    memcpy(&write_buf[1], buf, len);

    ret = i2c_master_send(client, write_buf, len + 1);
    if (ret != len + 1)
        return ret < 0 ? ret : -EIO;

    return len;
}

static int at24_probe(struct i2c_client *client)
{
    struct at24_data *at24;

    if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
        return -EIO;

    at24 = devm_kzalloc(&client->dev, sizeof(*at24), GFP_KERNEL);
    if (!at24)
        return -ENOMEM;

    at24->client = client;
    at24->pagesize = 8;  /* AT24C02页大小8字节 */
    i2c_set_clientdata(client, at24);

    dev_info(&client->dev, "AT24C02 probed at 0x%02x\n", client->addr);
    return 0;
}

static void at24_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "AT24C02 removed\n");
}

static const struct of_device_id at24_of_match[] = {
    { .compatible = "atmel,24c02" },
    {}
};
MODULE_DEVICE_TABLE(of, at24_of_match);

static const struct i2c_device_id at24_id[] = {
    { "24c02", 0 },
    {}
};
MODULE_DEVICE_TABLE(i2c, at24_id);

static struct i2c_driver at24_driver = {
    .driver = {
        .name = "at24",
        .of_match_table = at24_of_match,
    },
    .probe = at24_probe,
    .remove = at24_remove,
    .id_table = at24_id,
};
module_i2c_driver(at24_driver);

MODULE_AUTHOR("Embedded Linux Developer");
MODULE_DESCRIPTION("AT24C02 EEPROM driver");
MODULE_LICENSE("GPL");

示例2:LM75温度传感器驱动

LM75寄存器简单,温度值为16位MSB-first,适合用SMBus操作:

#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/hwmon.h>
#include <linux/err.h>

struct lm75_data {
    struct i2c_client *client;
};

/* 读取温度值(毫度) */
static int lm75_read_temp(struct lm75_data *data)
{
    struct i2c_client *client = data->client;
    int ret;

    /* LM75温度寄存器0x00,MSB-first,用swapped版本 */
    ret = i2c_smbus_read_word_swapped(client, 0x00);
    if (ret < 0)
        return ret;

    /* 11位有符号数,右移5位得到实际温度 */
    return (s16)ret >> 5;
}

static int lm75_probe(struct i2c_client *client)
{
    struct lm75_data *data;
    int temp;

    if (!i2c_check_functionality(client->adapter,
                                  I2C_FUNC_SMBUS_BYTE_DATA |
                                  I2C_FUNC_SMBUS_WORD_DATA))
        return -EIO;

    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->client = client;
    i2c_set_clientdata(client, data);

    /* 验证设备:尝试读取温度 */
    temp = lm75_read_temp(data);
    if (temp < 0) {
        dev_err(&client->dev, "failed to read temperature: %d\n", temp);
        return temp;
    }

    dev_info(&client->dev, "LM75 at 0x%02x, temp=%d\n",
             client->addr, temp * 125);
    return 0;
}

static void lm75_remove(struct i2c_client *client)
{
    dev_info(&client->dev, "LM75 removed\n");
}

static const struct of_device_id lm75_of_match[] = {
    { .compatible = "national,lm75" },
    {}
};
MODULE_DEVICE_TABLE(of, lm75_of_match);

static const struct i2c_device_id lm75_id[] = {
    { "lm75", 0 },
    {}
};
MODULE_DEVICE_TABLE(i2c, lm75_id);

static struct i2c_driver lm75_driver = {
    .driver = {
        .name = "lm75",
        .of_match_table = lm75_of_match,
    },
    .probe = lm75_probe,
    .remove = lm75_remove,
    .id_table = lm75_id,
};
module_i2c_driver(lm75_driver);

MODULE_AUTHOR("Embedded Linux Developer");
MODULE_DESCRIPTION("LM75 temperature sensor driver");
MODULE_LICENSE("GPL");

I2C控制器驱动骨架

编写控制器驱动需要实现i2c_algorithm:

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

struct my_i2c_dev {
    struct i2c_adapter adapter;
    void __iomem *base;
    struct clk *clk;
};

static int my_i2c_xfer(struct i2c_adapter *adap,
                        struct i2c_msg *msgs, int num)
{
    struct my_i2c_dev *dev = i2c_get_adapdata(adap);
    int i, ret;

    for (i = 0; i < num; i++) {
        /* 硬件相关:操作寄存器收发每个msg */
        if (msgs[i].flags & I2C_M_RD)
            ret = my_i2c_read_bytes(dev, &msgs[i]);
        else
            ret = my_i2c_write_bytes(dev, &msgs[i]);
        if (ret < 0)
            return ret;
    }
    return num;
}

static u32 my_i2c_func(struct i2c_adapter *adap)
{
    return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
}

static const struct i2c_algorithm my_i2c_algo = {
    .master_xfer   = my_i2c_xfer,
    .functionality = my_i2c_func,
};

static int my_i2c_probe(struct platform_device *pdev)
{
    struct my_i2c_dev *dev;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(dev->base))
        return PTR_ERR(dev->base);

    /* 初始化adapter */
    dev->adapter.owner = THIS_MODULE;
    dev->adapter.algo = &my_i2c_algo;
    dev->adapter.dev.parent = &pdev->dev;
    snprintf(dev->adapter.name, sizeof(dev->adapter.name),
             "my-i2c-%d", pdev->id);

    i2c_set_adapdata(&dev->adapter, dev);
    platform_set_drvdata(pdev, dev);

    ret = devm_i2c_add_adapter(&pdev->dev, &dev->adapter);
    if (ret)
        return ret;

    dev_info(&pdev->dev, "I2C adapter registered\n");
    return 0;
}

static const struct of_device_id my_i2c_of_match[] = {
    { .compatible = "myvendor,my-i2c" },
    {}
};
MODULE_DEVICE_TABLE(of, my_i2c_of_match);

static struct platform_driver my_i2c_driver = {
    .probe = my_i2c_probe,
    .driver = {
        .name = "my-i2c",
        .of_match_table = my_i2c_of_match,
    },
};
module_platform_driver(my_i2c_driver);

MODULE_AUTHOR("Embedded Linux Developer");
MODULE_DESCRIPTION("Custom I2C controller driver");
MODULE_LICENSE("GPL");

六、对比表格

I2C vs SPI

维度 I2C SPI
信号线 2根(SCL+SDA) 4根(SCLK+MOSI+MISO+CS)
寻址方式 地址寻址(7/10位) 片选线选择
通信模式 半双工 全双工
速率 最高3.4MHz 可达数十MHz
拓扑 多设备总线 一主多从
适用场景 传感器、EEPROM、低速外设 Flash、ADC、高速外设
Linux子系统 i2c-core spi-core

i2c_transfer vs SMBus操作

维度 i2c_transfer SMBus操作
灵活性 任意消息组合 固定协议格式
复杂度 需手动构造i2c_msg 一行函数调用
适配器要求 需I2C_FUNC_I2C 需对应SMBus功能标志
典型场景 EEPROM多字节读写 传感器寄存器读写
START/STOP 手动控制 协议自动管理
软件模拟 不可模拟 i2c-core可基于I2C模拟

七、核心流程图

I2C子系统架构全景

flowchart TB subgraph 用户空间 A["i2c-dev<br/>/dev/i2c-N"] B["i2c-tools<br/>i2cdetect/i2cget/i2cset"] end subgraph 内核空间 C["i2c_driver<br/>设备驱动"] D["i2c-core<br/>核心层"] E["i2c_adapter<br/>控制器适配器"] F["i2c_algorithm<br/>传输算法"] end subgraph 硬件 G["I2C控制器"] H["从设备1"] I["从设备2"] end A --> D B --> A C -->|"i2c_smbus_xxx<br/>i2c_transfer"| D D -->|"algo->master_xfer<br/>algo->smbus_xfer"| F F --> E E --> G G --- H G --- I style A fill:#E8F5E9 style B fill:#E8F5E9 style C fill:#E3F2FD style D fill:#F3E5F5 style E fill:#E3F2FD style F fill:#FFF8E1 style G fill:#FFEBEE style H fill:#E8F5E9 style I fill:#E8F5E9

I2C传输流程

flowchart TB A(["驱动调用传输函数"]) --> B{"选择传输方式"} B -->|"i2c_transfer"| C["构造i2c_msg数组"] B -->|"i2c_smbus_xxx"| D["SMBus协议封装"] C --> E["i2c-core转发"] D --> F{"适配器支持<br/>SMBus?"} F -->|"是"| G["algo->smbus_xfer"] F -->|"否"| H["i2c-core模拟<br/>转为i2c_transfer"] E --> I["algo->master_xfer"] H --> I G --> J(["硬件完成传输"]) I --> J J --> K{"返回值检查"} K -->|"ret < 0"| L["错误处理<br/>-ENXIO/-EIO等"] K -->|"ret == num"| M(["传输成功"]) K -->|"0 <= ret < num"| N["部分传输<br/>视为错误"] style A fill:#E8F5E9 style L fill:#FFEBEE style M fill:#E8F5E9 style N fill:#FFEBEE style F fill:#FFF8E1 style K fill:#FFF8E1

八、常见问题解决

1. probe失败:设备树compatible不匹配

现象:驱动注册成功但probe不被调用。

排查:

# 查看总线上的设备
ls /sys/bus/i2c/devices/
# 查看驱动绑定
cat /sys/bus/i2c/drivers/at24/bind
# 对比compatible字符串
cat /sys/firmware/devicetree/base/i2c1/eeprom@50/compatible

解决:确保设备树compatible与驱动of_match_table完全一致。

2. i2c_transfer返回-ENXIO

现象:通信失败,返回-6(ENXIO)。

原因:从设备地址无响应。检查硬件连线、设备供电、地址是否正确(注意7位地址不含读写位,0x50不是0xA0)。

3. SMBus操作返回-EOPNOTSUPP

现象:调用i2c_smbus_read_word_data返回不支持。

原因:适配器不支持该SMBus操作,且i2c-core无法模拟(适配器未声明I2C_FUNC_I2C)。

解决:用i2c_check_functionality提前检查,必要时改用i2c_transfer。

4. 10位地址通信失败

现象:使用10位地址设备时返回-EAFNOSUPPORT。

原因:适配器未声明I2C_FUNC_10BIT_ADDR。SMBus控制器通常不支持10位地址,软件GPIO模拟(i2c-algo-bit)支持。

5. module_i2c_driver宏使用

// 这一行等价于 module_init + module_exit + i2c_register_driver
module_i2c_driver(at24_driver);

无需手动写__init和__exit函数,减少模板代码。

九、总结

I2C子系统的核心思路就三个字:分着写。控制器驱动只管硬件操作,设备驱动只管业务逻辑,核心层负责桥接和模拟。记住这几条就够了:

  1. 选对传输方式:简单寄存器操作用SMBus,复杂时序用i2c_transfer
  2. 检查适配器能力:始终用i2c_check_functionality前置校验
  3. 正确处理返回值:i2c_transfer返回消息数,不是0/1
  4. 设备树优先:新内核推荐通过compatible匹配,id_table作为兼容回退
  5. 善用devm_函数:devm_kzalloc/devm_i2c_add_adapter自动释放资源

下期写I2C控制器驱动的寄存器级实现。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页