本文是个人项目记录五I2C子系统控制器驱动基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发—I2C控制器驱动部分的完整开发记录。涵盖框架分析、硬件知识、代码实现、设备树配置、测试验证、调试分析全过程。项目及文档已开源在我的Github会随着逐步做完这个项目的BSP部分和驱动部分一直更新有完整的文档和源码欢迎Star⭐仓库地址 [https://github.com/illusOwo-ff/i.MX6ULL-bsp-driver-project]目录一、I2C子系统框架框架三层结构通信特点字节传输机制START/STOP/Repeated STARTACK/NACK的两个方向Dummy Read时钟系统关键结构体自定义的私有数据结构体i2c_adapter结构体i2c_algorithm结构体二、NXP官方手册硬件结构取自NXP官方芯片手册1447页寄存器列表I2CR控制寄存器bit定义I2SR状态寄存器bit定义三、函数实现逻辑注册流程ISR中断处理函数trx_complete 等待传输完成start发送START 从机地址write按字节写数据read按字节读数据master_xfer调度函数functionality四、并发保护五、设备树配置官方I2C1节点imx6ull.dtsiEVK覆盖imx6ull-14x14-evk.dts引脚选择我的板级覆盖imx6ull-my14x14-emmc.dts六、测试验证加载检查功能测试编辑测试结果七、调试分析——strace追踪I2C系统调用链I2C ioctl命令i2cget调用链读操作i2cset调用链写操作i2cdetect调用链扫描八、遇到的问题与解决问题1RXAK检查读到的始终是0问题2read函数进入循环后不等待直接返回一、I2C子系统框架框架三层结构I2C 核心层内核已实现 ↑ 提供 i2c_add_adapter / i2c_transfer 等API I2C 适配器驱动本项目要写的 ↑ 实现 master_xfer操作硬件寄存器完成传输 I2C 设备驱动如传感器驱动、EEPROM驱动APP通过I2C Controller与I2C Device传输数据。APP通过i2c_adapter与i2c_client传输i2c_msg通信特点I2C是同步串行总线使用两根线SDA数据和SCL时钟。主机产生SCL时钟决定通信速度。两根线都是开漏结构外接上拉电阻。这决定了ACK/NACK的物理实现拉低SDAACK不拉SDA上拉电阻自然拉高NACK。因此I2SR寄存器的RXAK位0ACK1NACK。字节传输机制I2C所有传输都是按字节进行的8个SCL时钟传输8个数据bit高位先发1个SCL时钟回应ACK/NACK接收方控制SDA共9个SCL时钟 传输一个完整字节START/STOP/Repeated STARTSTARTSCL高电平时SDA从高拉低。控制寄存器I2CR的MSTA位写1自动产生。STOPSCL高电平时SDA从低变高。I2CR的MSTA位写0自动产生。Repeated START不经过STOP直接发新START保持总线占用。I2CR的RSTA位写1产生。START和STOP不是字节传输不产生中断。ACK/NACK的两个方向主机写时从机决定发ACK/NACK硬件自动采样存到I2SR的RXAK位。驱动检查RXAK判断从机是否应答。主机读时主机决定发ACK/NACK驱动通过I2CR的TXAK位控制0ACK1NACK。最后一个字节发NACK告诉从机别再发了。Dummy Readi.MX6ULL的I2C控制器设计读I2DR同时做两件事——取出已接收的数据 触发硬件开始接收下一个字节。切换到接收模式后硬件不会自动开始接收需要读一次I2DR作为开始信号。但此时I2DR里是之前残留的值垃圾所以第一次读到的数据要丢弃——这就是dummy read。时钟系统IPG时钟66MHz→ IFDR寄存器分频查表64种离散值→ SCL时钟100kHz/400kHzI2C分频是查预定义表选最接近的离散值。关键结构体自定义的私有数据结构体成员类型作用adapterstruct i2c_adapterI2C适配器嵌在私有结构体里通过container_of反推basevoid __iomem *寄存器基地址ioremap后的虚拟地址。i2c_adapter本身没有base成员不像uart_port有membase所以必须在私有结构体中保存clkstruct clk *控制器时钟probe中获取并使能irqint中断号queuewait_queue_head_t等待队列master_xfer和ISR之间的同步机制i2csrunsigned long缓存的I2SR状态寄存器值——ISR中读出后存到这里master_xfer中检查ISR为了及时清除中断标志会将硬件I2SR清零但master_xfer需要检查传输结果如RXAK位判断ACK/NACK。ISR在清零前先把I2SR的值存到i2csr相当于清除前的状态记录。i2c_adapter结构体成员内容说明ownerTHIS_MODULE固定algoi2c_bus_my_algo指向算法结构体dev.parentpdev-dev父设备namezxr-i2c适配器名字i2cdetect -l 显示的就是这个dev.of_nodepdev-dev.of_node关键内核靠这个自动扫描设备树子节点创建i2c_client注册函数i2c_add_adapter()动态分配编号或i2c_add_numbered_adapter()。动态分配时I2C核心层内部会通过of_alias_get_id()从设备树aliases节点获取正确编号。i2c_algorithm结构体成员是否实现说明master_xfer必须核心传输函数smbus_xfer不需要不实现时核心层自动用master_xfer模拟SMBus传输functionality必须返回控制器支持的功能flags比如返回I2C_FUNC_I2C |I2C_FUNC_SMBUS_EMUL二、NXP官方手册硬件结构取自NXP官方芯片手册1447页寄存器列表I2C控制器只有5个寄存器访问宽度为8位用readb/writeb。I2CR控制寄存器bit定义Bit名字说明使用7IEN模块使能xfer开头置1结尾清06IIEN中断使能start中置1stop中清05MSTA主从模式写1START写0STOPstart中置1stop中清04MTX发送/接收1发0收发送时置1read开头清03TXAKACK控制0ACK1NACKread中控制最后字节发NACK2RSTARepeated START写1产生自动清零xfer中非最后msg之间使用I2SR状态寄存器bit定义Bit名字说明使用5IBB总线忙bus_busy函数轮询等待4IAL仲裁丢失写0清除可在xfer中检查1IIF中断标志写0清除ISR中判断和清除0RXAK收到的ACK0ACK1NACKstart发完地址后检查write发完每字节检查三、函数实现逻辑注册流程使用平台总线驱动模型init函数中platform_driver_register()注册platform_driverprobe函数中获取资源 → 初始化同步机制 → 配置硬件 → 注册adapterISR中断处理函数读I2SRtemp readb(base I2SR) 如果 temp IIF中断标志置位 清除硬件IIFwriteb(0, base I2SR) ← 必须在ISR里清因为中断是电平触发 存状态快照i2csr temp ← 清除前的值包含RXAK等信息 唤醒等待进程wake_up(queue) return IRQ_HANDLED return IRQ_NONEIIF必须在ISR里清除i.MX6ULL的I2C中断是电平触发的IIF1时中断线一直有效。如果ISR不清IIF就返回硬件会立刻再次触发中断造成中断风暴。trx_complete 等待传输完成清除上次残留状态i2csr 0 ← 让wait_event条件为假 等待中断唤醒wait_event_timeout(queue, i2csr IIF, 500ms) 如果超时 → return -ETIMEDOUT return 0这个函数被start、write、read共用流程是清i2csr → 等中断 → ISR存新的i2csr并唤醒 → 返回后检查i2csr中的RXAK等状态。start发送START 从机地址设置控制寄存器产生START等IBB1确认START成功发送从机地址 7位地址左移1位 | 读写位等待地址传输完成 →检查 i2csr RXAK → NACK(1)说明从机不存在返回-ENXIOwrite按字节写数据循环 msg-len 个字节 { 写数据到I2DRwriteb(msg-buf[i], base I2DR) → 硬件产生9个SCL时钟发送数据从机回ACK/NACK 等待传输完成trx_complete() 检查 i2csr RXAK RXAK1(NACK) → return -EIO传输失败 RXAK0(ACK) → 继续下一字节 } return 0read按字节读数据注意最后一个字节必须先STOP再读I2DR因为读I2DR会触发硬件开始接收下一个字节。如果先读再STOP硬件已经开始产生不该有的SCL时钟了。先STOP让硬件停止再读I2DR只是取数据不会触发接收。第一步切换到接收模式 → 读I2CR清MTX位接收模式 → 如果只读1个字节设TXAK1收完发NACK → 否则清TXAK0收完发ACK → 写回I2CR 第二步dummy read触发接收 → i2csr 0清start残留状态 → readb(base I2DR) ← 丢弃但触发第1个字节的接收 第三步循环读取每个字节 for (i 0; i msg-len; i) { 等待传输完成trx_complete() → 1个字节接收完成硬件已自动发了ACK或NACK 如果是倒数第2个字节(i len-2) → I2CR置TXAK1下一个字节要发NACK 如果是最后一个字节(i len-1) 如果is_lastmsg整个传输的最后一个msg → I2CR清MSTA → 产生STOP必须在读I2DR之前 否则后面还有msg → I2CR置MTX → 切回发送模式为Repeated START准备 读数据msg-buf[i] readb(base I2DR) → 取出数据 触发下一个字节接收最后字节除外因为已STOP } return 0master_xfer调度函数使能模块 → 等总线空闲 → 发START从机地址 → 遍历每个i2c_msg写msg调write按字节发送读msg调read按字节接收msg之间用Repeated START→ 发STOP → 禁用模块。每个字节传输后通过中断等待队列同步。functionalityreturn I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL | I2C_FUNC_SMBUS_READ_BLOCK_DATAI2C_FUNC_SMBUS_EMUL表示支持SMBus模拟因为有master_xfer核心层能用它模拟SMBus。四、并发保护I2C控制器驱动本身不需要自己加锁。I2C核心层在调用master_xfer之前已经对adapter加了bus_lockmutex保证同一时间只有一个线程在操作同一个控制器。这跟SPI类似——总线子系统的框架设计就是核心层管并发驱动层只管硬件操作。master_xfer和ISR之间的同步通过等待队列实现master_xfer的wait_event_timeout等待ISR的wake_up唤醒。i2csr作为共享变量由ISR写、master_xfer读由于ISR中先写i2csr再wake_upmaster_xfer醒来后读到的一定是最新值不需要额外加锁。五、设备树配置官方I2C1节点imx6ull.dtsii2c1: i2c021a0000 { #address-cells 1; /* 子节点reg用1个cell表示I2C从机地址 */ #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; };EVK覆盖imx6ull-14x14-evk.dtsi2c1 { clock-frequency 100000; /* 目标SCL频率100kHz */ pinctrl-names default; pinctrl-0 pinctrl_i2c1; /* UART4_TX→I2C1_SCL, UART4_RX→I2C1_SDA */ status okay; mag31100e { ... }; /* EVK板上的传感器 */ fxls84711e { ... }; };引脚选择开发板转接板J7引出了I2C1_SDA和I2C1_SCL对应的pinctrl已在EVK的dts中定义UART4_TX/RX引脚复用为I2C1功能直接复用无需重新定义。我的板级覆盖imx6ull-my14x14-emmc.dts其他所有属性reg、interrupts、clocks、clock-frequency、pinctrl、statusokay从dtsi和EVK继承不需要重复写。六、测试验证加载检查功能测试测试使用板载AP3216C红外光强距离传感器I2C地址0x1e和i2c-tools。i2c-tools通过i2c-dev.c/dev/i2c-X直接操作I2C总线。# 扫描总线 i2cdetect -y 0 # 预期0x1e位置显示1e其他显示-- # 验证master_xfer → start → 发地址 → 检查ACK 链路正常 # 写入配置软复位 i2cset -f -y 0 0x1e 0 0x4 # 验证write函数能正确发送多字节数据 # 写入配置开启ALSPS模式 i2cset -f -y 0 0x1e 0 0x3 # 验证写入后传感器开始采集数据 # 读取光强数据word模式2字节 i2cget -f -y 0 0x1e 0x0c w # 预期返回光强数值遮挡/曝光时变化 # 验证Repeated START 多字节read dummy read TXAK切换 链路正常 # 读取距离数据 i2cget -f -y 0 0x1e 0x0e w # 预期返回距离数值靠近/远离时变化 # 验证读取实时变化的传感器数据证明每次都是真实I2C传输 # 中断计数验证 cat /proc/interrupts | grep my_i2c # 预期计数显著增加每个字节传输触发一次中断测试结果i2cdetect成功检测到0x1e设备 → START 地址 ACK检测链路通过i2cset写入配置寄存器成功传感器模式切换 → write链路通过i2cget读到实时变化的传感器数据 → read链路通过包括Repeated START、dummy read、多字节读/proc/interrupts中断计数持续增长1148次 → 中断机制正常工作七、调试分析——strace追踪I2C系统调用链I2C ioctl命令strace编码ioctl命令含义_IOC(0x7, 0x3)I2C_SLAVE (0x0703)设置从机地址_IOC(0x7, 0x5)I2C_FUNCS (0x0705)查询适配器功能_IOC(0x7, 0x6)I2C_SLAVE_FORCE (0x0706)强制设置从机地址-f参数_IOC(0x7, 0x20)I2C_SMBUS (0x0720)执行SMBus事务i2cget调用链读操作strace -o i2c_trace.log i2cget -f -y 0 0x1e 0x0c w核心系统调用open(/dev/i2c-0, O_RDWR) → 打开I2C适配器设备节点 ioctl(3, I2C_FUNCS, ...) → 查适配器能力调functionality回调 ioctl(3, I2C_SLAVE_FORCE, 0x1e) → 设从机地址 ioctl(3, I2C_SMBUS, ...) → 执行SMBus读word事务 close(3) → 关闭I2C_SMBUS在内核中的路径ioctl(I2C_SMBUS) → i2c-dev.c: i2cdev_ioctl_smbus() → I2C核心: i2c_smbus_xfer() → 未实现smbus_xfer → i2c_smbus_xfer_emulated() → 构造2个i2c_msg: msg[0]写寄存器地址, msg[1]读2字节 → master_xfer → start → write → Repeated START → start → read → stopi2cset调用链写操作open(/dev/i2c-0, O_RDWR) → 打开设备节点 ioctl(3, I2C_FUNCS, ...) → 查能力 ioctl(3, I2C_SLAVE_FORCE, 0x1e) → 设从机地址 ioctl(3, I2C_SMBUS, ...) → 执行SMBus写字节事务 close(3) → 关闭i2cdetect调用链扫描open(/dev/i2c-0, O_RDWR) → 打开设备节点 ioctl(3, I2C_FUNCS, ...) → 查能力 循环0x03~0x77每个地址 ioctl(3, I2C_SLAVE, addr) → 设地址 ioctl(3, I2C_SMBUS, ...) → 尝试通信 0x1e返回0设备存在其他返回-ENXIO无设备 close(3) → 关闭八、遇到的问题与解决I2C部分比较简单遇到的问题不多主要是函数编写的时候逻辑上有问题以下是问题记录问题1RXAK检查读到的始终是0现象write函数中发完数据后检查从机ACK即使从机不存在应该NACK也检测不到。原因trx_complete中writeb(0, I2SR)清除了整个I2SR包括RXAK位。清除后再读硬件I2SRRXAK已经是0了。我的解决将IIF清除移到ISR中ISR读I2SR后立即清除检查RXAK时使用ISR存的i2csr状态而非直接读硬件寄存器。这也解决了电平触发中断反复调用ISR的问题。问题2read函数进入循环后不等待直接返回现象read函数进入循环后trx_complete立即返回不等待读到的数据错误。原因从start返回时i2csr里还残留着地址字节传输的状态IIF1。read进入循环调trx_completewait_event_timeout检查i2csr IIF仍然为真直接返回不睡眠。我的解决在trx_complete开头加my_i2c-i2csr 0统一清除上次残留状态确保wait_event_timeout条件初始为假。
个人项目记录(五)I2C子系统控制器驱动:基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发—I2C控制器驱动开发
本文是个人项目记录五I2C子系统控制器驱动基于i.MX6ULL的嵌入式Linux终端系统构建与多子系统控制器驱动开发—I2C控制器驱动部分的完整开发记录。涵盖框架分析、硬件知识、代码实现、设备树配置、测试验证、调试分析全过程。项目及文档已开源在我的Github会随着逐步做完这个项目的BSP部分和驱动部分一直更新有完整的文档和源码欢迎Star⭐仓库地址 [https://github.com/illusOwo-ff/i.MX6ULL-bsp-driver-project]目录一、I2C子系统框架框架三层结构通信特点字节传输机制START/STOP/Repeated STARTACK/NACK的两个方向Dummy Read时钟系统关键结构体自定义的私有数据结构体i2c_adapter结构体i2c_algorithm结构体二、NXP官方手册硬件结构取自NXP官方芯片手册1447页寄存器列表I2CR控制寄存器bit定义I2SR状态寄存器bit定义三、函数实现逻辑注册流程ISR中断处理函数trx_complete 等待传输完成start发送START 从机地址write按字节写数据read按字节读数据master_xfer调度函数functionality四、并发保护五、设备树配置官方I2C1节点imx6ull.dtsiEVK覆盖imx6ull-14x14-evk.dts引脚选择我的板级覆盖imx6ull-my14x14-emmc.dts六、测试验证加载检查功能测试编辑测试结果七、调试分析——strace追踪I2C系统调用链I2C ioctl命令i2cget调用链读操作i2cset调用链写操作i2cdetect调用链扫描八、遇到的问题与解决问题1RXAK检查读到的始终是0问题2read函数进入循环后不等待直接返回一、I2C子系统框架框架三层结构I2C 核心层内核已实现 ↑ 提供 i2c_add_adapter / i2c_transfer 等API I2C 适配器驱动本项目要写的 ↑ 实现 master_xfer操作硬件寄存器完成传输 I2C 设备驱动如传感器驱动、EEPROM驱动APP通过I2C Controller与I2C Device传输数据。APP通过i2c_adapter与i2c_client传输i2c_msg通信特点I2C是同步串行总线使用两根线SDA数据和SCL时钟。主机产生SCL时钟决定通信速度。两根线都是开漏结构外接上拉电阻。这决定了ACK/NACK的物理实现拉低SDAACK不拉SDA上拉电阻自然拉高NACK。因此I2SR寄存器的RXAK位0ACK1NACK。字节传输机制I2C所有传输都是按字节进行的8个SCL时钟传输8个数据bit高位先发1个SCL时钟回应ACK/NACK接收方控制SDA共9个SCL时钟 传输一个完整字节START/STOP/Repeated STARTSTARTSCL高电平时SDA从高拉低。控制寄存器I2CR的MSTA位写1自动产生。STOPSCL高电平时SDA从低变高。I2CR的MSTA位写0自动产生。Repeated START不经过STOP直接发新START保持总线占用。I2CR的RSTA位写1产生。START和STOP不是字节传输不产生中断。ACK/NACK的两个方向主机写时从机决定发ACK/NACK硬件自动采样存到I2SR的RXAK位。驱动检查RXAK判断从机是否应答。主机读时主机决定发ACK/NACK驱动通过I2CR的TXAK位控制0ACK1NACK。最后一个字节发NACK告诉从机别再发了。Dummy Readi.MX6ULL的I2C控制器设计读I2DR同时做两件事——取出已接收的数据 触发硬件开始接收下一个字节。切换到接收模式后硬件不会自动开始接收需要读一次I2DR作为开始信号。但此时I2DR里是之前残留的值垃圾所以第一次读到的数据要丢弃——这就是dummy read。时钟系统IPG时钟66MHz→ IFDR寄存器分频查表64种离散值→ SCL时钟100kHz/400kHzI2C分频是查预定义表选最接近的离散值。关键结构体自定义的私有数据结构体成员类型作用adapterstruct i2c_adapterI2C适配器嵌在私有结构体里通过container_of反推basevoid __iomem *寄存器基地址ioremap后的虚拟地址。i2c_adapter本身没有base成员不像uart_port有membase所以必须在私有结构体中保存clkstruct clk *控制器时钟probe中获取并使能irqint中断号queuewait_queue_head_t等待队列master_xfer和ISR之间的同步机制i2csrunsigned long缓存的I2SR状态寄存器值——ISR中读出后存到这里master_xfer中检查ISR为了及时清除中断标志会将硬件I2SR清零但master_xfer需要检查传输结果如RXAK位判断ACK/NACK。ISR在清零前先把I2SR的值存到i2csr相当于清除前的状态记录。i2c_adapter结构体成员内容说明ownerTHIS_MODULE固定algoi2c_bus_my_algo指向算法结构体dev.parentpdev-dev父设备namezxr-i2c适配器名字i2cdetect -l 显示的就是这个dev.of_nodepdev-dev.of_node关键内核靠这个自动扫描设备树子节点创建i2c_client注册函数i2c_add_adapter()动态分配编号或i2c_add_numbered_adapter()。动态分配时I2C核心层内部会通过of_alias_get_id()从设备树aliases节点获取正确编号。i2c_algorithm结构体成员是否实现说明master_xfer必须核心传输函数smbus_xfer不需要不实现时核心层自动用master_xfer模拟SMBus传输functionality必须返回控制器支持的功能flags比如返回I2C_FUNC_I2C |I2C_FUNC_SMBUS_EMUL二、NXP官方手册硬件结构取自NXP官方芯片手册1447页寄存器列表I2C控制器只有5个寄存器访问宽度为8位用readb/writeb。I2CR控制寄存器bit定义Bit名字说明使用7IEN模块使能xfer开头置1结尾清06IIEN中断使能start中置1stop中清05MSTA主从模式写1START写0STOPstart中置1stop中清04MTX发送/接收1发0收发送时置1read开头清03TXAKACK控制0ACK1NACKread中控制最后字节发NACK2RSTARepeated START写1产生自动清零xfer中非最后msg之间使用I2SR状态寄存器bit定义Bit名字说明使用5IBB总线忙bus_busy函数轮询等待4IAL仲裁丢失写0清除可在xfer中检查1IIF中断标志写0清除ISR中判断和清除0RXAK收到的ACK0ACK1NACKstart发完地址后检查write发完每字节检查三、函数实现逻辑注册流程使用平台总线驱动模型init函数中platform_driver_register()注册platform_driverprobe函数中获取资源 → 初始化同步机制 → 配置硬件 → 注册adapterISR中断处理函数读I2SRtemp readb(base I2SR) 如果 temp IIF中断标志置位 清除硬件IIFwriteb(0, base I2SR) ← 必须在ISR里清因为中断是电平触发 存状态快照i2csr temp ← 清除前的值包含RXAK等信息 唤醒等待进程wake_up(queue) return IRQ_HANDLED return IRQ_NONEIIF必须在ISR里清除i.MX6ULL的I2C中断是电平触发的IIF1时中断线一直有效。如果ISR不清IIF就返回硬件会立刻再次触发中断造成中断风暴。trx_complete 等待传输完成清除上次残留状态i2csr 0 ← 让wait_event条件为假 等待中断唤醒wait_event_timeout(queue, i2csr IIF, 500ms) 如果超时 → return -ETIMEDOUT return 0这个函数被start、write、read共用流程是清i2csr → 等中断 → ISR存新的i2csr并唤醒 → 返回后检查i2csr中的RXAK等状态。start发送START 从机地址设置控制寄存器产生START等IBB1确认START成功发送从机地址 7位地址左移1位 | 读写位等待地址传输完成 →检查 i2csr RXAK → NACK(1)说明从机不存在返回-ENXIOwrite按字节写数据循环 msg-len 个字节 { 写数据到I2DRwriteb(msg-buf[i], base I2DR) → 硬件产生9个SCL时钟发送数据从机回ACK/NACK 等待传输完成trx_complete() 检查 i2csr RXAK RXAK1(NACK) → return -EIO传输失败 RXAK0(ACK) → 继续下一字节 } return 0read按字节读数据注意最后一个字节必须先STOP再读I2DR因为读I2DR会触发硬件开始接收下一个字节。如果先读再STOP硬件已经开始产生不该有的SCL时钟了。先STOP让硬件停止再读I2DR只是取数据不会触发接收。第一步切换到接收模式 → 读I2CR清MTX位接收模式 → 如果只读1个字节设TXAK1收完发NACK → 否则清TXAK0收完发ACK → 写回I2CR 第二步dummy read触发接收 → i2csr 0清start残留状态 → readb(base I2DR) ← 丢弃但触发第1个字节的接收 第三步循环读取每个字节 for (i 0; i msg-len; i) { 等待传输完成trx_complete() → 1个字节接收完成硬件已自动发了ACK或NACK 如果是倒数第2个字节(i len-2) → I2CR置TXAK1下一个字节要发NACK 如果是最后一个字节(i len-1) 如果is_lastmsg整个传输的最后一个msg → I2CR清MSTA → 产生STOP必须在读I2DR之前 否则后面还有msg → I2CR置MTX → 切回发送模式为Repeated START准备 读数据msg-buf[i] readb(base I2DR) → 取出数据 触发下一个字节接收最后字节除外因为已STOP } return 0master_xfer调度函数使能模块 → 等总线空闲 → 发START从机地址 → 遍历每个i2c_msg写msg调write按字节发送读msg调read按字节接收msg之间用Repeated START→ 发STOP → 禁用模块。每个字节传输后通过中断等待队列同步。functionalityreturn I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL | I2C_FUNC_SMBUS_READ_BLOCK_DATAI2C_FUNC_SMBUS_EMUL表示支持SMBus模拟因为有master_xfer核心层能用它模拟SMBus。四、并发保护I2C控制器驱动本身不需要自己加锁。I2C核心层在调用master_xfer之前已经对adapter加了bus_lockmutex保证同一时间只有一个线程在操作同一个控制器。这跟SPI类似——总线子系统的框架设计就是核心层管并发驱动层只管硬件操作。master_xfer和ISR之间的同步通过等待队列实现master_xfer的wait_event_timeout等待ISR的wake_up唤醒。i2csr作为共享变量由ISR写、master_xfer读由于ISR中先写i2csr再wake_upmaster_xfer醒来后读到的一定是最新值不需要额外加锁。五、设备树配置官方I2C1节点imx6ull.dtsii2c1: i2c021a0000 { #address-cells 1; /* 子节点reg用1个cell表示I2C从机地址 */ #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; };EVK覆盖imx6ull-14x14-evk.dtsi2c1 { clock-frequency 100000; /* 目标SCL频率100kHz */ pinctrl-names default; pinctrl-0 pinctrl_i2c1; /* UART4_TX→I2C1_SCL, UART4_RX→I2C1_SDA */ status okay; mag31100e { ... }; /* EVK板上的传感器 */ fxls84711e { ... }; };引脚选择开发板转接板J7引出了I2C1_SDA和I2C1_SCL对应的pinctrl已在EVK的dts中定义UART4_TX/RX引脚复用为I2C1功能直接复用无需重新定义。我的板级覆盖imx6ull-my14x14-emmc.dts其他所有属性reg、interrupts、clocks、clock-frequency、pinctrl、statusokay从dtsi和EVK继承不需要重复写。六、测试验证加载检查功能测试测试使用板载AP3216C红外光强距离传感器I2C地址0x1e和i2c-tools。i2c-tools通过i2c-dev.c/dev/i2c-X直接操作I2C总线。# 扫描总线 i2cdetect -y 0 # 预期0x1e位置显示1e其他显示-- # 验证master_xfer → start → 发地址 → 检查ACK 链路正常 # 写入配置软复位 i2cset -f -y 0 0x1e 0 0x4 # 验证write函数能正确发送多字节数据 # 写入配置开启ALSPS模式 i2cset -f -y 0 0x1e 0 0x3 # 验证写入后传感器开始采集数据 # 读取光强数据word模式2字节 i2cget -f -y 0 0x1e 0x0c w # 预期返回光强数值遮挡/曝光时变化 # 验证Repeated START 多字节read dummy read TXAK切换 链路正常 # 读取距离数据 i2cget -f -y 0 0x1e 0x0e w # 预期返回距离数值靠近/远离时变化 # 验证读取实时变化的传感器数据证明每次都是真实I2C传输 # 中断计数验证 cat /proc/interrupts | grep my_i2c # 预期计数显著增加每个字节传输触发一次中断测试结果i2cdetect成功检测到0x1e设备 → START 地址 ACK检测链路通过i2cset写入配置寄存器成功传感器模式切换 → write链路通过i2cget读到实时变化的传感器数据 → read链路通过包括Repeated START、dummy read、多字节读/proc/interrupts中断计数持续增长1148次 → 中断机制正常工作七、调试分析——strace追踪I2C系统调用链I2C ioctl命令strace编码ioctl命令含义_IOC(0x7, 0x3)I2C_SLAVE (0x0703)设置从机地址_IOC(0x7, 0x5)I2C_FUNCS (0x0705)查询适配器功能_IOC(0x7, 0x6)I2C_SLAVE_FORCE (0x0706)强制设置从机地址-f参数_IOC(0x7, 0x20)I2C_SMBUS (0x0720)执行SMBus事务i2cget调用链读操作strace -o i2c_trace.log i2cget -f -y 0 0x1e 0x0c w核心系统调用open(/dev/i2c-0, O_RDWR) → 打开I2C适配器设备节点 ioctl(3, I2C_FUNCS, ...) → 查适配器能力调functionality回调 ioctl(3, I2C_SLAVE_FORCE, 0x1e) → 设从机地址 ioctl(3, I2C_SMBUS, ...) → 执行SMBus读word事务 close(3) → 关闭I2C_SMBUS在内核中的路径ioctl(I2C_SMBUS) → i2c-dev.c: i2cdev_ioctl_smbus() → I2C核心: i2c_smbus_xfer() → 未实现smbus_xfer → i2c_smbus_xfer_emulated() → 构造2个i2c_msg: msg[0]写寄存器地址, msg[1]读2字节 → master_xfer → start → write → Repeated START → start → read → stopi2cset调用链写操作open(/dev/i2c-0, O_RDWR) → 打开设备节点 ioctl(3, I2C_FUNCS, ...) → 查能力 ioctl(3, I2C_SLAVE_FORCE, 0x1e) → 设从机地址 ioctl(3, I2C_SMBUS, ...) → 执行SMBus写字节事务 close(3) → 关闭i2cdetect调用链扫描open(/dev/i2c-0, O_RDWR) → 打开设备节点 ioctl(3, I2C_FUNCS, ...) → 查能力 循环0x03~0x77每个地址 ioctl(3, I2C_SLAVE, addr) → 设地址 ioctl(3, I2C_SMBUS, ...) → 尝试通信 0x1e返回0设备存在其他返回-ENXIO无设备 close(3) → 关闭八、遇到的问题与解决I2C部分比较简单遇到的问题不多主要是函数编写的时候逻辑上有问题以下是问题记录问题1RXAK检查读到的始终是0现象write函数中发完数据后检查从机ACK即使从机不存在应该NACK也检测不到。原因trx_complete中writeb(0, I2SR)清除了整个I2SR包括RXAK位。清除后再读硬件I2SRRXAK已经是0了。我的解决将IIF清除移到ISR中ISR读I2SR后立即清除检查RXAK时使用ISR存的i2csr状态而非直接读硬件寄存器。这也解决了电平触发中断反复调用ISR的问题。问题2read函数进入循环后不等待直接返回现象read函数进入循环后trx_complete立即返回不等待读到的数据错误。原因从start返回时i2csr里还残留着地址字节传输的状态IIF1。read进入循环调trx_completewait_event_timeout检查i2csr IIF仍然为真直接返回不睡眠。我的解决在trx_complete开头加my_i2c-i2csr 0统一清除上次残留状态确保wait_event_timeout条件初始为假。