01-I2C驱动开发.md 32 KB


title: I2C驱动开发 tags: [Linux驱动, I2C, 嵌入式, 总线驱动, 设备树] created: 2026-09-16 updated: 2026-09-17

pdf_ref: "正点原子 I.MX6U嵌入式Linux驱动开发指南V2.0.1 第六十一章"

I2C驱动开发

关联知识: [[STM32学习笔记/07-I2C通信与EEPROM 24C02]] | [[03-Linux驱动开发核心/07-platform驱动模型]] | [[04-Linux总线与接口驱动/02-SPI驱动开发]]


一、I2C协议概述

1.1 I2C总线原理

I2C(Inter-Integrated Circuit)是Philips公司开发的两线式串行通信总线,广泛用于连接低速外设。

硬件特性

  • 双线制:SCL(串行时钟线)+ SDA(串行数据线)
  • 主从架构:一个Master可以控制多个Slave
  • 多设备挂载:同一总线可挂载多个I2C设备(通过器件地址区分)
  • 开漏输出:SCL和SDA均为开漏结构,需外接上拉电阻(通常4.7KΩ)
  • 电平标准:支持3.3V和5V电平(部分器件支持宽电压)

速率模式

模式 速率 说明
标准模式 100KHz 最基本的I2C通信速率
快速模式 400KHz 常用的高速模式
高速模式 3.4MHz 特定应用场景
超高速 5MHz 最新规范

1.2 起始/停止条件

I2C通信由主机产生起始和停止条件来控制总线:

起始条件(S):SCL为高电平时,SDA产生下降沿
停止条件(P):SCL为高电平时,SDA产生上升沿

时序图

       ___                                 ___
SDA  /   \_______________________________/   \___
     |   |                                   |
     | S |         数据传输                  | P |
SCL  \___/‾‾‾\___/‾‾‾\___/‾‾‾\___/‾‾‾\___/‾‾‾\___
      START       SCL时钟同步       STOP

1.3 数据传输格式

I2C数据传输遵循严格的格式:

起始位 → 7位从机地址 + R/W位 → ACK → 数据字节 → ACK → ... → 停止位

详细流程

  1. 主机发送起始条件(S)
  2. 发送7位从机地址 + 1位读写方向位(R/W=0写,R/W=1读)
  3. 被寻址的从机发送ACK应答
  4. 发送/接收数据字节
  5. 每个字节后接收ACK/NAK
  6. 主机发送停止条件(P)或重复起始条件(Sr)

数据有效性规则

  • SCL为高电平时,SDA上的数据必须保持稳定
  • SCL为低电平时,SDA上的数据可以改变
  • 每个SCL时钟脉冲传输一个bit

1.4 7位/10位寻址

7位寻址(常用)

  • 地址范围:0x08 ~ 0x77(有效地址112个)
  • 格式:[A6:A0] + R/W
  • 示例:AT24C02地址为0x50,AP3216C地址为0x1E

10位寻址

  • 地址范围:0x000 ~ 0x3FF(1024个地址)
  • 格式:第一字节 11110[A9:A8] + R/W,第二字节 [A7:A0]
  • 用于需要大量I2C设备的场景

保留地址

地址 用途
0x00 广播呼叫
0x01~0x07 保留
0x78~0x7F 保留

二、Linux I2C架构

2.1 I2C子系统架构图

Linux I2C子系统采用分层分离思想,分为三层:

graph TD
    A[用户空间] -->|read/write/ioctl| B[I2C设备驱动<br/>i2c_driver]
    B -->|i2c_transfer| C[I2C核心层<br/>i2c-core]
    C -->|调用master_xfer| D[I2C总线驱动<br/>i2c_adapter]
    D -->|寄存器操作| E[I2C控制器硬件<br/>SOC I2C外设]
    E -->|SDA/SCL| F[I2C从设备<br/>AP3216C/AT24C02]

    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#e8f5e9
    style D fill:#fff3e0
    style E fill:#fce4ec
    style F fill:#f5f5f5

各层职责

层级 组件 职责 开发者
设备驱动层 i2c_driver 实现具体设备的读写逻辑 应用开发者
核心层 i2c-core 提供通用API,管理设备/驱动匹配 内核维护者
总线驱动层 i2c_adapter 控制I2C控制器硬件 SoC厂商

关键理解:I2C总线驱动(适配器驱动)由SoC厂商编写(如NXP为IMX6ULL编写i2c-imx.c),用户只需编写I2C设备驱动。

2.2 核心数据结构

2.2.1 i2c_adapter — I2C适配器

表示SOC的I2C控制器,定义在include/linux/i2c.h

struct i2c_adapter {
    struct module *owner;
    unsigned int class;
    const struct i2c_algorithm *algo;  /* 总线访问算法 */
    void *algo_data;

    struct rt_mutex bus_lock;
    int timeout;                       /* 超时时间,单位jiffies */
    int retries;
    struct device dev;                 /* 适配器设备 */

    int nr;                            /* 总线号 */
    char name[48];
    struct completion dev_released;

    struct mutex userspace_clients_lock;
    struct list_head userspace_clients;

    struct i2c_bus_recovery_info *bus_recovery_info;
    const struct i2c_adapter_quirks *quirks;
};

关键成员说明

  • algo:指向i2c_algorithm,包含实际的传输函数master_xfer
  • nr:适配器编号,对应/dev/i2c-X
  • bus_lock:总线锁,保证同一时刻只有一个master操作总线

2.2.2 i2c_algorithm — 传输算法

struct i2c_algorithm {
    int (*master_xfer)(struct i2c_adapter *adap,
                       struct i2c_msg *msgs,
                       int num);        /* I2C传输函数 */

    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 *);  /* 返回支持的功能 */
};
  • master_xfer:核心传输函数,设备驱动通过i2c_transfer最终调用此函数
  • functionality:返回适配器支持的协议标志,如I2C_FUNC_I2C

2.2.3 i2c_client — I2C设备

描述一个挂载在I2C总线上的设备:

struct i2c_client {
    unsigned short flags;                /* 标志 */
    unsigned short addr;                 /* 7位芯片地址,存储在低7位 */
    char name[I2C_NAME_SIZE];            /* 设备名字 */
    struct i2c_adapter *adapter;         /* 对应的适配器 */
    struct device dev;                   /* 设备结构体 */
    int irq;                             /* 中断号 */
    struct list_head detected;
};

重要addr字段只存储7位地址值(不含R/W位),例如AP3216C的地址0x1E直接存储为0x1E

2.2.4 i2c_driver — I2C设备驱动

类似platform_driver,是设备驱动的核心结构体:

struct i2c_driver {
    unsigned int class;

    int (*probe)(struct i2c_client *,
                 const struct i2c_device_id *);   /* 匹配成功后执行 */
    int (*remove)(struct i2c_client *);            /* 移除时执行 */

    void (*shutdown)(struct i2c_client *);
    void (*alert)(struct i2c_client *, unsigned int data);
    int (*command)(struct i2c_client *client,
                   unsigned int cmd, void *arg);

    struct device_driver driver;              /* 驱动结构体 */
    const struct i2c_device_id *id_table;     /* 传统匹配ID表 */

    int (*detect)(struct i2c_client *,
                  struct i2c_board_info *);
    const unsigned short *address_list;
    struct list_head clients;
};

2.2.5 i2c_msg — I2C消息

描述一次I2C数据传输的基本单元:

struct i2c_msg {
    __u16 addr;     /* 从机地址 */
    __u16 flags;    /* 标志 */
    __u16 len;      /* 消息长度 */
    __u8 *buf;      /* 消息数据缓冲区 */
};

flags常用值

标志 说明
I2C_M_RD 0x0001 读数据
I2C_M_TEN 0x0010 10位地址模式
I2C_M_STOP 0x8000 传输后发送停止条件
I2C_M_NOSTART 0x4000 不发送起始条件
I2C_M_REV_DIR_ADDR 0x2000 反转读写方向

2.3 设备与驱动匹配过程

I2C总线的匹配由i2c_bus_type驱动,定义在drivers/i2c/i2c-core.c

struct bus_type i2c_bus_type = {
    .name = "i2c",
    .match = i2c_device_match,
    .probe = i2c_device_probe,
    .remove = i2c_device_remove,
    .shutdown = i2c_device_shutdown,
};

匹配函数i2c_device_match的执行顺序:

graph TD
    A[i2c_device_match] --> B{of_driver_match_device?}
    B -->|是| C[设备树匹配成功]
    B -->|否| D{acpi_driver_match_device?}
    D -->|是| E[ACPI匹配成功]
    D -->|否| F{driver->id_table?}
    F -->|是| G[i2c_match_id]
    F -->|否| H[匹配失败]
    G --> I[比较name字段]
    I -->|匹配| C
    I -->|不匹配| H

匹配方式优先级

  1. 设备树匹配:比较compatible属性(优先)
  2. ACPI匹配:用于ACPI平台
  3. 传统ID表匹配:比较i2c_device_idname字段

三、I2C设备树配置

3.1 设备树节点格式

I2C设备作为I2C总线节点的子节点,格式如下:

&i2c1 {
    clock-frequency = <100000>;          /* I2C频率100KHz */
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";

    设备名@器件地址 {
        compatible = "厂商,设备名";
        reg = <器件地址>;
        /* 其他属性 */
    };
};

3.2 关键属性说明

属性 说明 示例
reg I2C从设备7位地址 reg = <0x1e>;
compatible 匹配驱动的兼容字符串 "alientek,ap3216c"
clock-frequency I2C总线频率 <100000> (100KHz)
status 设备状态 "okay" / "disabled"

3.3 完整设备树示例

IMX6ULL I2C1控制器节点(来自imx6ull.dtsi):

i2c1: i2c@021a0000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
    reg = <0x021a0000 0x4000>;
    interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_I2C1>;
    status = "disabled";
};

AP3216C设备子节点(修改后):

&i2c1 {
    clock-frequency = <100000>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";

    ap3216c@1e {
        compatible = "alientek,ap3216c";
        reg = <0x1e>;
    };
};

pinctrl配置

pinctrl_i2c1: i2c1grp {
    fsl,pins = <
        MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
        MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
    >;
};

3.4 设备树验证

编译设备树后,检查I2C设备是否正确注册:

# 查看所有I2C设备
ls /sys/bus/i2c/devices/
# 输出示例:0-001e  i2c-0  i2c-1

# 查看设备名
cat /sys/bus/i2c/devices/0-001e/name
# 输出:ap3216c

四、I2C驱动API

4.1 驱动注册/注销

函数 说明
i2c_add_driver(driver) 注册I2C设备驱动(常用)
i2c_del_driver(driver) 注销I2C设备驱动
i2c_register_driver(owner, driver) 注册驱动(底层函数)

i2c_add_driver是宏定义,等价于:

#define i2c_add_driver(driver) \
    i2c_register_driver(THIS_MODULE, driver)

4.2 数据传输API

4.2.1 i2c_transfer — 核心传输函数

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

参数说明

参数 说明
adap I2C适配器,通过client->adapter获取
msgs I2C消息数组
num 消息数量
返回值 负值失败,非负值为发送的消息数

读操作示例(构建2条消息):

struct i2c_msg msg[2];
struct i2c_client *client = dev->private_data;

/* msg[0]: 发送要读取的寄存器地址 */
msg[0].addr = client->addr;
msg[0].flags = 0;                  /* 写标志 */
msg[0].buf = &reg;                 /* 寄存器地址 */
msg[0].len = 1;

/* msg[1]: 读取寄存器数据 */
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;          /* 读标志 */
msg[1].buf = val;                  /* 数据缓冲区 */
msg[1].len = len;                  /* 数据长度 */

ret = i2c_transfer(client->adapter, msg, 2);

写操作示例(构建1条消息):

struct i2c_msg msg;
u8 b[256];

b[0] = reg;                        /* 寄存器首地址 */
memcpy(&b[1], buf, len);          /* 拷贝写入数据 */

msg.addr = client->addr;
msg.flags = 0;                     /* 写标志 */
msg.buf = b;
msg.len = len + 1;                /* 地址 + 数据 */

ret = i2c_transfer(client->adapter, &msg, 1);

4.2.2 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);
参数 说明
client I2C设备客户端
buf 数据缓冲区
count 数据字节数(< 64KB)
返回值 负值失败,非负值为传输字节数

i2c_master_send/recv内部调用i2c_transfer,只适用于简单的单次读写,复杂场景建议直接使用i2c_transfer

4.2.3 SMBus API

/* 读取单个字节数据 */
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);

/* 读取2字节数据 */
s32 i2c_smbus_read_word_data(struct i2c_client *client, u8 command);

/* 写入2字节数据 */
s32 i2c_smbus_write_word_data(struct i2c_client *client,
                              u8 command, u16 value);

/* 读取块数据 */
s32 i2c_smbus_read_block_data(struct i2c_client *client,
                              u8 command, u8 *values);

/* 读取I2C数据 */
s32 i2c_smbus_read_i2c_block_data(struct i2c_client *client,
                                   u8 command, u8 length, u8 *values);

4.3 API选择指南

简单单字节读写 → i2c_smbus_read/write_byte_data
简单收发       → i2c_master_send/recv
多寄存器读写   → i2c_transfer(推荐,最灵活)
SMBus兼容设备  → SMBus系列API

五、完整源码分析:AP3216C驱动

5.1 寄存器定义头文件

/* ap3216creg.h - AP3216C寄存器地址 */
#ifndef AP3216C_H
#define AP3216C_H

#define AP3216C_SYSTEMCONG   0x00   /* 配置寄存器 */
#define AP3216C_INTSTATUS    0x01   /* 中断状态寄存器 */
#define AP3216C_INTCLEAR     0x02   /* 中断清除寄存器 */
#define AP3216C_IRDATALOW    0x0A   /* IR数据低字节 */
#define AP3216C_IRDATAHIGH   0x0B   /* IR数据高字节 */
#define AP3216C_ALSDATALOW   0x0C   /* ALS数据低字节 */
#define AP3216C_ALSDATAHIGH  0x0D   /* ALS数据高字节 */
#define AP3216C_PSDATALOW    0x0E   /* PS数据低字节 */
#define AP3216C_PSDATAHIGH   0x0F   /* PS数据高字节 */

#endif

5.2 驱动源码(完整)

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"

#define AP3216C_CNT    1
#define AP3216C_NAME   "ap3216c"

/* 设备结构体 */
struct ap3216c_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    int major;
    void *private_data;        /* 保存i2c_client指针 */
    unsigned short ir, als, ps; /* 三合一传感器数据 */
};

static struct ap3216c_dev ap3216cdev;

/*
 * ap3216c_read_regs - 从AP3216C读取多个寄存器数据
 * @dev: AP3216C设备
 * @reg: 要读取的寄存器首地址
 * @val: 读取到的数据
 * @len: 要读取的数据长度
 * 返回: 0成功,负值失败
 */
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg,
                             void *val, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev->private_data;

    /* msg[0]: 发送要读取的寄存器首地址 */
    msg[0].addr = client->addr;
    msg[0].flags = 0;            /* 写标志 */
    msg[0].buf = &reg;
    msg[0].len = 1;

    /* msg[1]: 读取数据 */
    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;    /* 读标志 */
    msg[1].buf = val;
    msg[1].len = len;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret == 2)
        ret = 0;
    else {
        printk("i2c rd failed=%d reg=%06x len=%d\n", ret, reg, len);
        ret = -EREMOTEIO;
    }
    return ret;
}

/*
 * ap3216c_write_regs - 向AP3216C多个寄存器写入数据
 * @dev: AP3216C设备
 * @reg: 要写入的寄存器首地址
 * @buf: 要写入的数据缓冲区
 * @len: 要写入的数据长度
 * 返回: 0成功,负值失败
 */
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg,
                              u8 *buf, u8 len)
{
    u8 b[256];
    struct i2c_msg msg;
    struct i2c_client *client = (struct i2c_client *)dev->private_data;

    b[0] = reg;                  /* 寄存器首地址 */
    memcpy(&b[1], buf, len);    /* 拷贝写入数据 */

    msg.addr = client->addr;
    msg.flags = 0;               /* 写标志 */
    msg.buf = b;
    msg.len = len + 1;          /* 地址 + 数据 */

    return i2c_transfer(client->adapter, &msg, 1);
}

/*
 * ap3216c_read_reg - 读取AP3216C单个寄存器值
 */
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
    u8 data = 0;
    ap3216c_read_regs(dev, reg, &data, 1);
    return data;
}

/*
 * ap3216c_write_reg - 向AP3216C指定寄存器写入值
 */
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
    u8 buf = data;
    ap3216c_write_regs(dev, reg, &buf, 1);
}

/*
 * ap3216c_readdata - 读取AP3216C的IR/ALS/PS原始数据
 * 同时打开ALS和IR+PS时,两次读取间隔需大于112.5ms
 */
void ap3216c_readdata(struct ap3216c_dev *dev)
{
    unsigned char i = 0;
    unsigned char buf[6];

    /* 循环读取6字节传感器数据 */
    for (i = 0; i < 6; i++)
        buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);

    if (buf[0] & 0x80)          /* IR_OF位为1,数据无效 */
        dev->ir = 0;
    else
        dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0x03);

    dev->als = ((unsigned short)buf[3] << 8) | buf[2];

    if (buf[4] & 0x40)          /* PS数据无效标志 */
        dev->ps = 0;
    else
        dev->ps = ((unsigned short)(buf[5] & 0x3F) << 4) |
                  (buf[4] & 0x0F);
}

/* 字符设备操作函数 */
static int ap3216c_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &ap3216cdev;

    /* 初始化AP3216C: 软复位 */
    ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x04);
    mdelay(50);                 /* 复位至少需要10ms */
    /* 启动ALS+IR+PS */
    ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x03);
    return 0;
}

static ssize_t ap3216c_read(struct file *filp, char __user *buf,
                            size_t cnt, loff_t *off)
{
    short data[3];
    long err = 0;
    struct ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;

    ap3216c_readdata(dev);

    data[0] = dev->ir;
    data[1] = dev->als;
    data[2] = dev->ps;
    err = copy_to_user(buf, data, sizeof(data));
    return 0;
}

static int ap3216c_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 文件操作结构体 */
static const struct file_operations ap3216c_ops = {
    .owner = THIS_MODULE,
    .open = ap3216c_open,
    .read = ap3216c_read,
    .release = ap3216c_release,
};

/* I2C设备匹配表 */
static const struct i2c_device_id ap3216c_id[] = {
    {"alientek,ap3216c", 0},
    {}
};

/* 设备树匹配表 */
static const struct of_device_id ap3216c_of_match[] = {
    { .compatible = "alientek,ap3216c" },
    { /* Sentinel */ }
};

/* I2C驱动结构体 */
static struct i2c_driver ap3216c_driver = {
    .probe = ap3216c_probe,
    .remove = ap3216c_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "ap3216c",
        .of_match_table = ap3216c_of_match,
    },
    .id_table = ap3216c_id,
};

/*
 * ap3216c_probe - 驱动与设备匹配成功后执行
 * 完成字符设备注册和私有数据初始化
 */
static int ap3216c_probe(struct i2c_client *client,
                         const struct i2c_device_id *id)
{
    /* 1. 构建设备号 */
    if (ap3216cdev.major) {
        ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
        register_chrdev_region(ap3216cdev.devid, AP3216C_CNT,
                               AP3216C_NAME);
    } else {
        alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT,
                            AP3216C_NAME);
        ap3216cdev.major = MAJOR(ap3216cdev.devid);
    }

    /* 2. 注册字符设备 */
    cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
    cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);

    /* 3. 创建设备类 */
    ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);

    /* 4. 创建设备节点 */
    ap3216cdev.device = device_create(ap3216cdev.class, NULL,
                                      ap3216cdev.devid, NULL,
                                      AP3216C_NAME);

    /* 5. 保存i2c_client指针 */
    ap3216cdev.private_data = client;

    return 0;
}

/*
 * ap3216c_remove - 移除I2C设备时执行
 * 注销字符设备和设备节点
 */
static int ap3216c_remove(struct i2c_client *client)
{
    cdev_del(&ap3216cdev.cdev);
    unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);
    device_destroy(ap3216cdev.class, ap3216cdev.devid);
    class_destroy(ap3216cdev.class);
    return 0;
}

/* 驱动入口 */
static int __init ap3216c_init(void)
{
    int ret = 0;
    ret = i2c_add_driver(&ap3216c_driver);
    return ret;
}

/* 驱动出口 */
static void __exit ap3216c_exit(void)
{
    i2c_del_driver(&ap3216c_driver);
}

module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");

5.3 关键代码解读

probe函数执行流程

ap3216c_probe()
  ├── register_chrdev_region()    // 静态分配设备号
  ├── cdev_init()                // 初始化cdev
  ├── cdev_add()                 // 注册字符设备
  ├── class_create()             // 创建设备类
  ├── device_create()            // 创建设备节点 /dev/ap3216c
  └── private_data = client      // 保存i2c_client指针

数据读取流程

用户read() → ap3216c_read() → ap3216c_readdata()
  └── 循环6次调用ap3216c_read_reg()
        └── ap3216c_read_regs()
              └── i2c_transfer(client->adapter, msg, 2)
                    └── i2c_imx_xfer()  // SOC适配器传输函数
                          ├── i2c_imx_start()
                          ├── i2c_imx_write()  // 写寄存器地址
                          ├── i2c_imx_read()   // 读数据
                          └── i2c_imx_stop()

module_i2c_driver:可替代手写module_init/exit,简化驱动注册:

module_i2c_driver(ap3216c_driver);

六、实验验证

6.1 I2C调试工具

# 扫描I2C总线上的设备(显示所有响应的地址)
i2cdetect -y 0
# 输出示例:
#      0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
# 00:          -- -- -- -- -- -- -- --
# 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- 1e --

# 读取设备寄存器值
i2cget -y 0 0x1e 0x00
# 读取地址0x1e设备的0x00寄存器

# 写入设备寄存器
i2cset -y 0 0x1e 0x00 0x03
# 向地址0x1e设备的0x00寄存器写入0x03

# 转储设备所有寄存器
i2cdump -y 0 0x1e

参数说明

参数 说明
-y 跳过交互确认
0 I2C总线号(对应i2c-0)
0x1e 设备7位地址
0x00 寄存器地址

6.2 完整测试流程

# 1. 编译驱动模块
make -j32

# 2. 编译测试APP
arm-linux-gnueabihf-gcc ap3216cApp.c -o ap3216cApp

# 3. 拷贝文件到开发板
scp ap3216c.ko ap3216cApp root@192.168.1.200:/lib/modules/4.1.15/

# 4. 开发板上加载驱动
depmod                              # 首次加载需执行
modprobe ap3216c.ko                 # 加载驱动模块

# 5. 运行测试
./ap3216cApp /dev/ap3216c
# 输出示例:ir = 285, als = 1024, ps = 512

# 6. 卸载驱动
rmmod ap3216c

6.3 常见问题排查

问题 可能原因 解决方法
i2c_transfer失败 设备地址错误 i2cdetect扫描确认地址
读取数据全0 设备未正确初始化 检查probe函数,确认设备树配置
读取数据全0xFF SDA/SCL被拉低 检查上拉电阻和IO复用配置
模块加载失败 compatible不匹配 检查设备树compatible与驱动匹配表
设备节点不存在 设备树status不正确 确认status = "okay"

七、跨平台对比

7.1 IMX6ULL vs STM32 vs RK3568

特性 IMX6ULL STM32F4 RK3568
I2C控制器 I2C1/I2C2/I2C3 I2C1~I2C4 I2C0~I2C4
适配器驱动 i2c-imx.c i2c-stm32.c i2c-rk3x.c
匹配属性 "fsl,imx6ul-i2c" "st,stm32f4-i2c" "rockchip,rk3x-i2c"
最大速率 400KHz 400KHz 400KHz
DMA支持 支持 支持 支持
寄存器基地址 0x021A0000 多组 多组
设备树写法 相同 相同 相同

7.2 I2C驱动框架对比

Linux I2C框架:              STM32 HAL框架:              裸机开发:
┌──────────────┐            ┌──────────────┐            ┌──────────────┐
│ 用户空间     │            │ 应用层       │            │ 应用层       │
│ read/write   │            │ HAL_I2C_*    │            │ 直接寄存器   │
├──────────────┤            ├──────────────┤            ├──────────────┤
│ I2C设备驱动  │            │ HAL层        │            │ 寄存器操作   │
│ i2c_driver   │            │ I2C Handle   │            │ 直接配置     │
├──────────────┤            ├──────────────┤            ├──────────────┤
│ I2C核心层    │            │ LL层/注册器  │            │ 硬件         │
│ i2c-core     │            │ 直接操作     │            │              │
├──────────────┤            ├──────────────┤            └──────────────┘
│ I2C适配器    │            │ I2C外设      │
│ i2c_adapter  │            │ STM32 I2C    │
├──────────────┤            └──────────────┘
│ I2C控制器    │
│ SOC硬件      │
└──────────────┘

核心差异

  • Linux:设备树描述硬件,驱动与设备分离,标准化框架
  • STM32 HAL:CubeMX配置生成代码,函数封装层次分明
  • 裸机:直接操作寄存器,无标准框架,效率最高但不可移植

八、面试精选

题目1:Linux I2C驱动架构分为哪几层?各层职责是什么?

考察点:I2C架构理解

参考答案: Linux I2C驱动分为三层:

  • I2C设备驱动层i2c_driver):针对具体I2C设备编写,实现设备的读写逻辑,注册字符设备提供用户空间接口
  • I2C核心层i2c-core):提供与硬件无关的通用API,管理设备与驱动的匹配,维护I2C总线i2c_bus_type
  • I2C总线驱动层i2c_adapter):由SoC厂商编写,控制I2C控制器硬件,提供master_xfer传输函数

这种分层实现了驱动与设备的分离,与platform总线思想一致。

题目2:i2c_transfer函数的参数msgs如何构建?读操作和写操作有什么区别?

考察点:I2C数据传输机制

参考答案i2c_transfer参数msgsi2c_msg数组,每条消息包含从机地址、标志位、缓冲区和长度。

  • 写操作:构建1条i2c_msgflags=0buf包含寄存器地址+写入数据
  • 读操作:构建2条i2c_msgmsg[0].flags=0(写寄存器地址),msg[1].flags=I2C_M_RD(读数据)

读操作需要两条消息是因为I2C协议要求先写寄存器地址再读数据,且每次方向切换需要重新发送起始条件。

题目3:I2C设备和驱动的匹配有哪几种方式?优先级如何?

考察点:设备驱动匹配机制

参考答案: 三种匹配方式,按优先级:

  1. 设备树匹配(优先):比较设备节点的compatible属性与of_device_id表中的compatible字段
  2. ACPI匹配:用于ACPI平台
  3. 传统ID表匹配:比较设备名与i2c_device_idname字段

匹配函数i2c_device_match按上述顺序依次尝试,一旦匹配成功即停止。

题目4:I2C适配器驱动为什么用platform驱动模型实现?

考察点:驱动模型理解

参考答案: I2C适配器(控制器)是SOC的片上外设,其寄存器地址、中断号、时钟等资源都通过设备树描述,天然符合platform总线的设备/驱动分离模型。

NXP的i2c-imx.c就是一个标准的platform驱动,通过of_device_id匹配设备树节点,在probe中获取IO资源、初始化i2c_adapter、设置i2c_algorithm,最后调用i2c_add_numbered_adapter注册适配器。

这体现了Linux驱动的统一思想:所有SOC外设驱动都用platform模型,I2C/SPI等总线只是其上层抽象。

题目5:如何在不加载驱动的情况下调试I2C设备?

考察点:调试能力

参考答案: 使用内核自带的I2C调试工具:

  • i2cdetect -y <bus>:扫描总线上的设备地址
  • i2cget -y <bus> <addr> <reg>:读取指定寄存器
  • i2cset -y <bus> <addr> <reg> <val>:写入指定寄存器
  • i2cdump -y <bus> <addr>:转储所有寄存器

这些工具直接通过/dev/i2c-X设备节点操作I2C控制器,不需要编写驱动。前提是I2C适配器驱动已加载(SoC启动时自动加载)。通过读写关键寄存器可以验证硬件连接和基本通信是否正常。


代码来源: 正点原子 I.MX6U驱动开发指南 第六十一章, 例程 21_iic 最后更新: 2026-09-17