一、I2C 核心概念I2C也写作 IICInter-Integrated Circuit协议的核心可以用一句话概括两根线SCLSDA 严格时序 主从寻址 半双工同步传输是嵌入式系统中最经典的低速串行总线协议。1.1 总线结构与物理基础1.1.1 总线信号线I2C 总线仅由两根双向信号线组成所有挂载在总线上的设备共享这两根线SCLSerial Clock Line串行时钟线由主机产生时钟信号用于同步通信双方的时序从机仅被动跟随时钟。SDASerial Data Line串行数据线双向传输数据主机和从机都通过这根线发送 / 接收数据。1.1.2 开漏输出与上拉电阻核心电气特性I2C 总线的 SDA、SCL 引脚必须配置为开漏输出模式且必须外接上拉电阻部分 MCU 可配置内部上拉这是多设备共享总线的根本保障开漏模式的特性只能主动拉低总线电平无法主动输出高电平高电平完全依靠外部上拉电阻拉到 VCC。总线空闲状态无设备拉低总线时上拉电阻将 SDA、SCL 维持在高电平作为总线的空闲状态。线与逻辑只要有一个设备拉低总线整个总线就会呈现低电平只有所有设备都释放总线开漏高阻总线才会回到高电平完美支持多设备共享不会出现电平冲突。当多个设备同时连接到 SDA/SCL 总线时总线的实际电平由所有设备输出的最小值决定逻辑与运算规则如下表设备 A 输出设备 B 输出总线实际电平说明高电平 (1)高电平 (1)高电平 (1)无设备拉低总线总线保持空闲高电平高电平 (1)低电平 (0)低电平 (0)任一设备拉低总线立即变为低电平低电平 (0)高电平 (1)低电平 (0)任一设备拉低总线立即变为低电平低电平 (0)低电平 (0)低电平 (0)多设备同时拉低无电平冲突1.1.3 推挽 vs 开漏I2C 必须用开漏核心原因是推挽模式不支持多设备共享总线二者的底层原理对比如下模式硬件原理核心特性适用场景推挽模式PMOS推高 NMOS拉低两个 MOS 管交替工作能主动推高、拉低电平驱动能力强、速度快单设备 GPIO 输出如 LED 控制绝对不能用于总线共享开漏模式仅 NMOS 管工作PMOS 始终关闭只能拉低无法主动推高需上拉电阻维持高电平支持线与逻辑I2C、CAN 等多设备共享总线场景1.2 通信核心特性1.2.1 半双工同步传输同步通信所有数据传输都由主机产生的 SCL 时钟同步收发双方严格按照时钟节拍采样数据保证时序一致。半双工通信同一时刻通信双方只能一个发送、一个接收不能同时收发总线型组网天然只能是半双工全双工仅适合点对点通信。补充全双工需要两条独立的收发通道如以太网、USB如果用全双工做总线组网会出现数据碰撞、电平冲突通信完全混乱。1.2.2 主从架构与寻址I2C 总线采用主从应答式通信架构所有通信都由主机发起从机被动响应总线上有 1 个多主模式下可多个主机其余为从机从机之间无法直接通信必须通过主机中转。每个从机都有唯一的 7 位标准/10 位扩展从机地址主机发起通信时先发送从机地址 1 位读写位读写位0写操作主发从收主机向从机发送数据读写位1读操作主收从发主机从从机读取数据总线上所有设备都会监听地址帧只有地址匹配的从机才会响应主机实现精准寻址。1.2.3 总线仲裁多主模式下当多个主机同时发起通信时I2C 会通过线与逻辑 地址仲裁判断优先级每个主机在发送地址时会同时监听总线电平如果自己发送的电平与总线实际电平不一致说明有更高优先级的主机占用总线该主机立即停止发送释放总线。最终只有优先级最高的主机能完成通信保证总线不冲突。1.3 核心通信时序I2C 通信的可靠性完全依赖严格的时序规则一次完整通信包含「空闲状态 → 起始信号 → 数据传输 → 应答 → 停止信号」核心时序规则如下1.3.1 空闲状态总线空闲时SDA 和 SCL 都由上拉电阻维持在高电平等待主机发起通信。1.3.2 起始信号Start Condition时序规则SCL 保持高电平时SDA 由高电平向低电平跳变产生下降沿。信号意义主机通知总线上所有从机即将开始通信所有从机进入监听状态。1.3.3 数据传输规则数据位每帧传输 8 位数据先传最高位MSB后传最低位LSB。时序同步SCL 为低电平时发送方可以改变 SDA 电平数据准备窗口接收方不得采样。SCL 为高电平时发送方必须保证 SDA 电平稳定接收方采样 SDA 数据数据采样窗口。核心逻辑SCL 低电平时切换数据高电平时采样数据避免电平跳变导致采样错误。1.3.4 应答机制ACK/NACK每发送 1 字节8 位数据后会跟随 1 个应答位第 9 个时钟周期ACK应答接收方在第 9 个时钟周期内将 SDA 拉低表示成功接收该字节。NACK非应答接收方保持 SDA 高电平表示接收失败或读操作最后一个字节通知从机结束传输。时序细节主机发送完 8 位数据后会释放 SDA 总线切换为输入由从机控制 SDA 电平主机在 SCL 高电平时读取应答状态。1.3.5 停止信号Stop Condition时序规则SCL 保持高电平时SDA 由低电平向高电平跳变产生上升沿。信号意义主机通知所有从机本次通信结束总线恢复空闲状态可被其他主机占用。1.4 总线组网核心逻辑I2C 总线的组网核心是多设备共享同一根总线 线与逻辑 载波监听所有设备共享一根数据线任何设备都能拉低总线也能监听总线电平天然支持多设备挂载。半双工 开漏的组合从根本上避免了电平短路所有设备只能拉低总线不会出现推高 vs 拉低的短路问题。多设备同时发送时通过总线电平判断优先级实现总线仲裁保证通信有序进行。二、IMX6ULL 中的 I2C 模块2.1 I2C 硬件配置特性参数代码 / 硬件说明通信速率100KHz (标准模式)代码中base-IFDR 0x16;配置分频系数适配 IPG 时钟66MHz生成 100KHz SCL 时钟信号线数量2 线制SDA(UART4_RX/UART5_RX)串行数据线双向传输SCL(UART4_TX/UART5_TX)串行时钟线主机产生电气特性开漏输出 上拉电阻代码引脚配置0x98b0实现开漏逻辑配合外部上拉电阻实现线与逻辑支持多设备共享总线工作模式主机模式 (Master)代码中通过设置I2CR[MSTA] 1主动发起通信主导时序与数据流向数据位宽8 bit / 帧每次传输 1 字节数据先发送最高位 (MSB)后发送最低位 (LSB)地址格式7 bit 从机地址 1 bit 读写位从机地址唯一标识挂载设备最低位0表示写1表示读附加功能自动应答 (ACK/NACK)、总线仲裁、多主支持代码内置RXAK应答检查、MSTA主机状态检测、IAL仲裁丢失处理2.2 I2C 关键寄存器寄存器名称位定义功能实战配置代码对应操作I2C 控制寄存器 (I2CR)IICEN (bit7)I2C 模块使能初始化时先清 0关闭模块配置完成后置 1使能模块MSTA (bit5)主机模式控制起始 / 停止信号置 1 发送起始信号清 0 发送停止信号MTX (bit4)传输方向选择置 1 发送模式主机发数据清 0 接收模式主机收数据TXAK (bit3)接收应答控制清 0 发送 ACK应答置 1 发送 NACK非应答RSTA (bit2)重复起始信号置 1 发送重复起始信号读操作必备I2C 状态寄存器 (I2SR)IBB (bit5)I2C 总线忙标志1 总线忙0 总线空闲通信前必须等待 IBB0IIF (bit1)中断标志字节传输完成11 字节数据收发完成每次收发后必须清 0IAL (bit4)仲裁丢失标志1 总线仲裁失败通信前清 0失败后清 0RXAK (bit0)接收应答标志0 收到 ACK从机应答1 收到 NACK无应答I2C 数据寄存器 (I2DR)DATA (bit[7:0])数据收发寄存器写 发送数据 / 地址读 接收数据I2C 频率分频寄存器 (IFDR)IC (bit[5:0])I2C 时钟分频系数代码配置为0x16对应标准 100KHz I2C 时钟三、I2C 驱动实现i2c.h#ifndef __I2C_H #define __I2C_H #define I2C_RD 0 // 读操作 #define I2C_WR 1 // 写操作 typedef struct i2c_msg { uint8_t slave_addr; // 从机地址7位 uint16_t reg_addr; // 寄存器地址 uint8_t reg_len; // 寄存器地址长度1字节/2字节 uint8_t* data; // 数据缓冲区 uint32_t len; // 数据长度 uint8_t flag; // 操作标志0读 1写 }i2c_msg_t; void i2c_init(I2C_Type *base); int i2c_write(I2C_Type * base, uint8_t slave_addr, uint16_t reg_addr, unsigned char reg_len, const uint8_t * data, uint32_t len); int i2c_read(I2C_Type * base, uint8_t slave_addr, uint16_t reg_addr, unsigned char reg_len, uint8_t * data, uint32_t len); int i2c_master_xfer(I2C_Type * base, i2c_msg_t * msg); #endif1. slave_addrI2C 从机设备地址7 位如 OLED0x3CAP3216C0x1ELM750x482. reg_addr要访问的寄存器地址I2C 设备必须先写寄存器地址再读写数据3. reg_len寄存器地址的字节长度大部分设备18 位地址部分 EEPROM 216 位地址4. data读写数据的缓冲区指针写存放要发送的数据读存放读取回来的数据5. len要读写的数据字节数6. flag操作类型I2C_RD(0) 读I2C_WR(1) 写结构体意义总结把一次 I2C 通信打包成一个消息上层调用只需填充结构体不用关心底层时序标准 I2C 驱动通用设计模式i2c.c1.i2c_init—— I2C 初始化函数功能引脚复用配置 → 电气属性配置 → 控制器初始化关闭→分频→使能void i2c_init(I2C_Type *base) { // 1. I2C1 引脚复用与电气配置 if (I2C1 base) { // 引脚复用UART4_RX/TX 复用为 I2C1_SDA/SCL IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA, 1); IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL, 1); // 电气配置0x98b0 为I2C开漏、上拉、高速模式标准配置 IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA, 0x98b0); IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL, 0x98b0); } // 2. I2C2 引脚复用与电气配置逻辑同I2C1 else if (I2C2 base) { IOMUXC_SetPinMux(IOMUXC_UART5_RX_DATA_I2C2_SDA, 1); IOMUXC_SetPinMux(IOMUXC_UART5_TX_DATA_I2C2_SCL, 1); IOMUXC_SetPinConfig(IOMUXC_UART5_RX_DATA_I2C2_SDA, 0x98b0); IOMUXC_SetPinConfig(IOMUXC_UART5_TX_DATA_I2C2_SCL, 0x98b0); } // 3. 关闭I2C控制器确保配置安全 base-I2CR ~(1 7); // IICEN0 // 4. 配置时钟分频0x16 对应100KHz标准I2C速率 base-IFDR 0x16; // 5. 使能I2C控制器 base-I2CR | (1 7); // IICEN1 }引脚必须配置为开漏输出配合上拉电阻实现线与逻辑初始化必须先关闭模块再配置分频最后使能避免时序混乱0x16分频值对应 I.MX6ULL 的 IPG 时钟66MHz计算后得到标准 100KHz SCL 时钟2. 辅助工具函数等待 / 状态检查// 等待总线忙实际等待IIF中断1字节传输完成 static inline int i2c_wait_bus_busy(I2C_Type * base) { int times 50; while ((!(base-I2SR (1 1))) times--) delay_us(1); return (times 0) ? -1 : 0; } // 等待总线空闲代码逻辑同上面可优化为同一函数 static inline int i2c_wait_bus_idle(I2C_Type * base) { int times 50; while ((!(base-I2SR (1 1))) times--) delay_us(1); return (times 0) ? -1 : 0; } // 等待中断标志标准功能等待1字节收发完成 static inline int i2c_wait_irq(I2C_Type * base) { int times 50; while ((!(base-I2SR (1 1))) times--) delay_us(1); return (times 0) ? -1 : 0; }所有等待都基于I2SR[1] (IIF)1 字节数据收发完成后该位自动置 1带超时机制50 次循环防止总线卡死导致程序死循环三个函数逻辑完全一致实际工程中可合并为一个i2c_wait_irq3.i2c_assert_msta—— 主机状态检查static inline int i2c_assert_msta(I2C_Type * base) { // 检查MSTA位1仍为主机模式0失去主机权限 if (!(base-I2CR (1 5))) { base-I2SR ~(1 4); // 清除仲裁丢失标志IAL return -1; // 仲裁失败返回错误 } return 0; }多主模式下总线仲裁失败会导致 MSTA 位清 0主机失去总线控制权每次收发数据后必须检查该位确保通信过程中主机权限不丢失失败后清除 IAL 标志为下一次通信做准备4.i2c_write—— I2C 主机写数据函数功能严格遵循 I2C 写时序空闲状态 → 起始信号 → 从机地址写位 → ACK → 寄存器地址 → ACK → 数据字节1 → ACK → ... → 停止信号int i2c_write(I2C_Type * base, uint8_t slave_addr, uint16_t reg_addr, unsigned char reg_len, const uint8_t * data, uint32_t len) { int ret 0; // 1. 清除状态仲裁丢失(IAL) 中断标志(IIF) base-I2SR ~((1 4) | (1 1)); // 2. 等待总线空闲IBB0 while(base-I2SR (1 5)); // 3. 发送起始信号MSTA1设置为发送模式MTX1 base-I2CR | (1 5); base-I2CR | (1 4); // 4. 等待总线忙IBB1确认起始信号已发出 while(!(base-I2SR (1 5))); // 5. 发送从机地址写位(0) base-I2DR (slave_addr 1) | (0 0); // 等待发送完成清中断标志 while (!(base-I2SR (1 1))); base-I2SR ~(1 1); // 检查主机状态、应答位 ret i2c_assert_msta(base); if (-1 ret) return -1; if ((base-I2SR (1 0))) goto i2c_stop; // 收到NACK直接停止 // 6. 发送寄存器地址支持8/16位大端发送高字节先传 int i reg_len; for (i reg_len - 1; i 0; i--) { base-I2DR (reg_addr (8 * i)) 0xff; while (!(base-I2SR (1 1))); base-I2SR ~(1 1); ret i2c_assert_msta(base); if (-1 ret) return -1; if ((base-I2SR (1 0))) goto i2c_stop; } // 7. 发送待写入数据 for (i 0; i len; i) { base-I2DR data[i]; while (!(base-I2SR (1 1))); base-I2SR ~(1 1); ret i2c_assert_msta(base); if (-1 ret) return -1; if ((base-I2SR (1 0))) goto i2c_stop; } i2c_stop: // 8. 发送停止信号MSTA0 base-I2CR ~(1 5); // 等待总线空闲确认通信结束 while(base-I2SR (1 5)); return 0; }从机地址必须左移 1 位最低位为 0 表示写操作寄存器地址支持 1/2 字节大端发送高字节先传兼容 EEPROM 等 16 位地址设备每发送 1 字节必须等待 IIF 置 1、清标志检查 ACK 和主机状态确保传输可靠收到 NACK 直接跳转到停止流程避免总线挂死5.i2c_read—— I2C 主机读数据函数功能严格遵循 I2C 读时序空闲状态 → 起始信号 → 从机地址写位 → ACK → 寄存器地址 → ACK → 重复起始信号 → 从机地址读位 → ACK → 数据字节1 → ACK → ... → 数据字节N → NACK → 停止信号int i2c_read(I2C_Type * base, uint8_t slave_addr, uint16_t reg_addr, unsigned char reg_len, uint8_t * data, uint32_t len) { int ret 0; // 1. 清标志 等总线空闲 base-I2SR ~((1 4) | (1 1)); while (base-I2SR (1 5)); // 2. 设置发送模式 发送起始信号 base-I2CR | (1 4); base-I2CR | (1 5); // 3. 等待总线忙带超时 int tm 50; while ((!(base-I2SR (1 5))) tm--) delay_us(5); if(tm 0) return -1; // 4. 发送从机地址写位(0)等待应答 base-I2DR (slave_addr 1) | (0 0); while (!(base-I2SR (1 1))); base-I2SR ~(1 1); ret i2c_assert_msta(base); if (-1 ret) return -2; if ((base-I2SR (1 0))) goto i2c_stop; // 5. 发送寄存器地址同写操作 int i reg_len; for (i reg_len - 1; i 0; i--) { base-I2DR (reg_addr (8 * i)) 0xff; while (!(base-I2SR (1 1))); base-I2SR ~(1 1); ret i2c_assert_msta(base); if (-1 ret) return -3; if ((base-I2SR (1 0))) goto i2c_stop; } // 6. 发送重复起始信号RSTA1读操作核心步骤 base-I2CR | (1 2); // 7. 发送从机地址读位(1) base-I2DR (slave_addr 1) | (1 0); while (!(base-I2SR (1 1))); base-I2SR ~(1 1); ret i2c_assert_msta(base); if (-1 ret) return -4; if ((base-I2SR (1 0))) goto i2c_stop; // 8. 切换为接收模式MTX0 dummy read 启动从机发送 base-I2CR ~(1 4); data[0] base-I2DR; // 9. 默认发送ACK准备接收数据 base-I2CR ~(1 3); // 10. 循环读取数据 for(i 0; i len; i) { while (!(base-I2SR (1 1))); base-I2SR ~(1 1); // 倒数第2个字节设置TXAK1最后1字节发送NACK if(i len - 2) base-I2CR | (1 3); // 最后1个字节发送停止信号 else if(i len - 1) base-I2CR ~(1 5); // 读取数据 data[i] base-I2DR; } i2c_stop: base-I2CR ~(1 5); // 11. 确保发送停止信号等待总线空闲 base-I2CR ~(1 5); tm 50; while(((base-I2SR (1 5))) tm--) delay_us(5); if(tm 0) return -5; return 0; }读操作必须先写寄存器地址再发重复起始信号切换为读模式这是 I2C 读时序的核心最后 1 个字节必须发送 NACKTXAK1通知从机停止发送数据dummy read 是为了清空 I2DR启动从机的发送流程每个错误分支返回不同错误码方便调试定位问题6.i2c_master_xfer—— 统一上层接口功能封装 I2C 消息结构体统一读写入口简化上层调用int i2c_master_xfer(I2C_Type * base, i2c_msg_t * msg) { int ret 0; // 根据flag选择读/写操作 if(I2C_RD msg-flag) ret i2c_read(base, msg-slave_addr, msg-reg_addr, msg-reg_len, msg-data, msg-len); else if(I2C_WR msg-flag) ret i2c_write(base, msg-slave_addr, msg-reg_addr, msg-reg_len, msg-data, msg-len); else ret -1; return ret; }上层只需填充i2c_msg_t结构体无需关心底层时序统一入口代码可维护性强是工业级驱动的标准写法完美适配i2c.h中定义的消息结构体实现一次 I2C 事务的完整封装三、I2C 学习核心重点 完整配置流程写操作核心逻辑解析起始信号 设备地址I2C 通信的第一步由主机发送起始信号SCL 高电平时 SDA 拉低随后发送 7 位从机地址左移 1 位最低位为读写位0 写、1 读完成总线寻址。寄存器地址发送代码支持多字节寄存器地址reg_len可配置 1/2 字节大端顺序发送高字节→低字节适配 8/16 位地址的 I2C 设备如 AT24C02 EEPROM。数据发送与应答校验每发送 1 字节数据等待中断标志IIF置 1 后清标志同时检查从机应答位RXAK收到 NACK 直接终止通信保证传输可靠。停止信号通信结束后主机发送停止信号SCL 高电平时 SDA 拉高释放 I2C 总线等待总线空闲IBB0后完成本次事务。读操作核心难点解析伪写阶段I2C 读操作必须先执行「写寄存器地址」流程告知从机要读取的寄存器位置这是读时序的前置步骤。重发起始信号从「写模式」切换到「读模式」时必须发送重复起始信号RSTA 置 1不释放总线直接切换传输方向避免总线冲突。应答策略多字节读取时前 N-1 个字节主机发送 ACKTXAK 清 0通知从机继续发送最后 1 个字节必须发送 NACKTXAK 置 1通知从机停止发送否则从机将持续输出数据。伪读操作I.MX6ULL 的 I2C 控制器硬件要求切换到接收模式后必须先执行一次「dummy read」读取 I2DR 寄存器才能触发后续的正常数据接收流程。I2C 完整配置思路流程 :1. 硬件初始化阶段引脚复用配置将对应 GPIO 引脚复用为 I2C 的 SDA/SCL 功能如 I2C1 对应 UART4_RX/TX。电气属性配置配置引脚为开漏输出0x98b0标准参数配合上拉电阻实现线与逻辑支持多设备共享总线。控制器初始化关闭 I2C 模块IICEN 清 0→ 配置时钟分频IFDR0x16生成 100KHz 标准时钟→ 使能 I2C 模块IICEN 置 1。2. 写操作流程清状态标志 → 等待总线空闲 → 发送起始信号 → 发送从机地址写位 → 发送寄存器地址 → 循环发送数据 → 检查应答 → 发送停止信号 → 等待总线空闲3. 读操作流程清状态标志 → 等待总线空闲 → 发送起始信号 → 发送从机地址写位 → 发送寄存器地址 → 发送重复起始信号 → 发送从机地址读位 → 切换接收模式 → 伪读启动 → 循环读取数据ACK/NACK 控制 → 发送停止信号 → 等待总线空闲4. 上层封装通过i2c_msg_t结构体封装一次完整 I2C 通信从机地址、寄存器地址、数据、读写标志用i2c_master_xfer统一入口简化上层调用实现驱动复用。