--- 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协议概述 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》第六十一章内容(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子系统采用分层分离思想,分为三层: ```mermaid graph TD A[用户空间] -->|read/write/ioctl| B[I2C设备驱动
i2c_driver] B -->|i2c_transfer| C[I2C核心层
i2c-core] C -->|调用master_xfer| D[I2C总线驱动
i2c_adapter] D -->|寄存器操作| E[I2C控制器硬件
SOC I2C外设] E -->|SDA/SCL| F[I2C从设备
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`: ```c 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 — 传输算法 ```c 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总线上的设备: ```c 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`,是设备驱动的核心结构体: ```c 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数据传输的基本单元: ```c 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 | 反转读写方向 | | `I2C_M_IGNORE_NAK` | 0x1000 | 忽略 NAK | | `I2C_M_NO_RD_ACK` | 0x0800 | 读操作不发送 ACK | | `I2C_M_RECV_LEN` | 0x0400 | 首字节为长度 | > 以上 8 个标志与《I.MX6U嵌入式Linux驱动开发指南》示例代码 61.3.2.1 中 `i2c_msg` 结构体内的定义一致。 #### 2.2.6 i2c_board_info — 非设备树下的I2C设备描述 在未使用设备树时,内核使用 `i2c_board_info` 描述一个具体的 I2C 设备(原书示例代码 61.3.1.1): ```c struct i2c_board_info { char type[I2C_NAME_SIZE]; /* I2C 设备名字 */ unsigned short flags; /* 标志 */ unsigned short addr; /* I2C 器件地址 */ void *platform_data; struct dev_archdata *archdata; struct device_node *of_node; struct fwnode_handle *fwnode; int irq; }; ``` 其中 `type` 和 `addr` 必须设置。原书用 `I2C_BOARD_INFO` 宏来初始化(示例代码 61.3.1.3): ```c #define I2C_BOARD_INFO(dev_type, dev_addr) \ .type = dev_type, .addr = (dev_addr) /* 原书示例:OV2640 摄像头,器件地址 0x30 */ static struct i2c_board_info mx27_3ds_i2c_camera = { I2C_BOARD_INFO("ov2640", 0x30), }; ``` 采用设备树后不再使用 `i2c_board_info`,改为在 I2C 控制器节点下创建子节点。 ### 2.3 设备与驱动匹配过程 I2C总线的匹配由`i2c_bus_type`驱动,定义在`drivers/i2c/i2c-core.c`: ```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`的执行顺序: ```mermaid 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_id`的`name`字段 --- ## 三、I2C设备树配置 ### 3.1 设备树节点格式 I2C设备作为I2C总线节点的子节点,格式如下: ```dts &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`): ```dts i2c1: i2c@021a0000 { #address-cells = <1>; #size-cells = <0>; compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c"; reg = <0x021a0000 0x4000>; interrupts = ; clocks = <&clks IMX6UL_CLK_I2C1>; status = "disabled"; }; ``` **AP3216C设备子节点**(修改后): ```dts &i2c1 { clock-frequency = <100000>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c1>; status = "okay"; ap3216c@1e { compatible = "alientek,ap3216c"; reg = <0x1e>; }; }; ``` > **重要**:`imx6ull-alientek-emmc.dts` 中 `&i2c1` 节点默认带有 NXP 官方 EVK 的两个子节点 `mag3110@0e`(磁力计,compatible 为 `"fsl,mag3110"`)和 `fxls8471@1e`(加速度计,compatible 为 `"fsl,fxls8471"`)。正点原子 I.MX6U-ALPHA 开发板**没有**这两个器件,必须先将它们删除,再添加 `ap3216c@1e` 子节点(原书示例代码 61.5.1.3)。否则 `fxls8471@1e` 会与 AP3216C 争夺同一地址 0x1e。 **pinctrl配置**: ```dts pinctrl_i2c1: i2c1grp { fsl,pins = < MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0 MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0 >; }; ``` ### 3.4 设备树验证 编译设备树后,检查I2C设备是否正确注册: ```bash # 查看所有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_adapter(adapter)` | 注册适配器(动态总线号)| | `i2c_add_numbered_adapter(adap)` | 注册适配器(静态总线号)| | `i2c_del_adapter(adap)` | 注销适配器 | **函数原型**(原书第 61.1.1/61.1.2 节): ```c int i2c_register_driver(struct module *owner, struct i2c_driver *driver); /* 0成功,负值失败 */ void i2c_del_driver(struct i2c_driver *driver); /* 无返回值 */ int i2c_add_adapter(struct i2c_adapter *adapter); /* 动态总线号,0成功,负值失败 */ int i2c_add_numbered_adapter(struct i2c_adapter *adap); /* 静态总线号,0成功,负值失败 */ void i2c_del_adapter(struct i2c_adapter *adap); /* 无返回值 */ ``` `i2c_add_driver`是宏定义,等价于: ```c #define i2c_add_driver(driver) \ i2c_register_driver(THIS_MODULE, driver) ``` ### 4.2 数据传输API #### 4.2.1 i2c_transfer — 核心传输函数 ```c int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); ``` **参数说明**: | 参数 | 说明 | | ------ | ------------------------------------ | | `adap` | I2C适配器,通过`client->adapter`获取 | | `msgs` | I2C消息数组 | | `num` | 消息数量 | | 返回值 | 负值失败,非负值为发送的消息数 | **读操作示例**(构建2条消息): ```c 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 = ® /* 寄存器地址 */ 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条消息): ```c 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 ```c /* 发送数据 */ 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 > ⚠️ **来源说明**:本节 API 列表不属于《I.MX6U嵌入式Linux驱动开发指南》第六十一章内容,为扩展知识(原书仅提到 `i2c_algorithm.smbus_xfer`)。 ```c /* 读取单个字节数据 */ 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 寄存器定义头文件 ```c /* 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 驱动源码(完整) ```c #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 = ® 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; #if 0 /* 也可以用 SMBus API 读取单个寄存器,效果等价 */ struct i2c_client *client = (struct i2c_client *)dev->private_data; return i2c_smbus_read_byte_data(client, reg); #endif } /* * ap3216c_write_reg - 向AP3216C指定寄存器写入值 */ static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data) { u8 buf = 0; 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); if (IS_ERR(ap3216cdev.class)) { return PTR_ERR(ap3216cdev.class); } /* 4. 创建设备节点 */ ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME); if (IS_ERR(ap3216cdev.device)) { return PTR_ERR(ap3216cdev.device); } /* 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`,简化驱动注册: ```c module_i2c_driver(ap3216c_driver); ``` --- ## 六、实验验证 ### 6.1 I2C调试工具 ```bash # 扫描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 完整测试流程 ```bash # 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"` | --- ## 七、跨平台对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 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配置生成代码,函数封装层次分明 - 裸机:直接操作寄存器,无标准框架,效率最高但不可移植 --- ## 八、面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 题目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`参数`msgs`是`i2c_msg`数组,每条消息包含从机地址、标志位、缓冲区和长度。 - **写操作**:构建1条`i2c_msg`,`flags=0`,`buf`包含寄存器地址+写入数据 - **读操作**:构建2条`i2c_msg`,`msg[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_id`的`name`字段 匹配函数`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 `:扫描总线上的设备地址 - `i2cget -y `:读取指定寄存器 - `i2cset -y `:写入指定寄存器 - `i2cdump -y `:转储所有寄存器 这些工具直接通过`/dev/i2c-X`设备节点操作I2C控制器,不需要编写驱动。前提是I2C适配器驱动已加载(SoC启动时自动加载)。通过读写关键寄存器可以验证硬件连接和基本通信是否正常。 --- **代码来源**: 正点原子 I.MX6U驱动开发指南 第六十一章, 例程 21_iic **内容来源**: 《I.MX6U嵌入式Linux驱动开发指南》第六十一章 Linux I2C驱动实验 **最后更新**: 2026-09-17