一、I2C 总线简介与核心特性
I2C(Inter-Integrated Circuit)是一种由 NXP(原 Philips)公司开发的半双工、同步串行通信总线。它只需两根传输线即可实现主控芯片与多个从机设备之间的数据通信。
1. 物理引脚说明
SCL(Serial Clock Line):串行时钟线,由主机(Master)统一提供时钟脉冲。
SDA(Serial Data Line):串行数据线,用于主机与从机之间传输数据。
2. 核心特性
1.高位先行(MSB First):数据传输时,最高有效位(MSB)最先被发送(例如 0xAC->
1010 1100)。
2.寻址机制:起始信号后的第一个字节为设备地址(7 位从机地址 + 1 位数据流向位)。
7 位地址:理论上最多支持128个地址(除去保留地址,可挂载 127 个设备)。
1 位数据流向位:0 表示写(主机->从机);1 表示读(从机 ->主机)。
3.通信速率分类:
标准模式(Standard Mode):最高 100 kHz
快速模式(Fast Mode):最高 400 kHz
高速/超高速模式:1.x MHz、3.x MHz
二、I2C 基础时序信号拆解
1. 起始信号(Start Signal)与结束信号(Stop Signal)
起始信号(Start):当 SCL 处于高电平时,SDA 数据线出现一个下降沿(由高变低)。
停止信号(Stop):当 SCL 处于高电平时,SDA 数据线出现一个上升沿(由低变高)。
SCL : ───────────┐ ┌───────────
└─────────┘
SDA : ──────┐ ┌────────────────
└─────────┘
[ Start 信号 ] [ Stop 信号 ]
2. 数据传输与有效性规则
I2C 总线在传输数据时,SCL 的电平状态决定了 SDA 是处于采样状态还是允许改变状态:
数据采样:当 SCL 为高电平时,SDA 数据线上的电平必须保持稳定,此时接收方进行数据采样。
数据切换:当 SCL 为低电平时,SDA 数据线上的电平才可以发生改变,发送方在此阶段准备下一位数据。
SCL : ┌──────┐ ┌──────┐
────────┘ └───────┘ └──────
SDA : ... ╳ Data ╳ Data ╳ Data ╳ ...
[采样稳定] [允许改变]
3. 应答信号(ACK / NACK)
数据每传输完 1 个字节(8 位)后,在第 9 个时钟周期,接收方需要向发送方回复应答信号:
ACK(Acknowledge,有效应答):第 9 个时钟周期时,将 SDA 拉低(低电平 0)。
NACK(Not Acknowledge,非应答):第 9 个时钟周期时,将 SDA 保持或释放为高电平(高电平 1)。
三、I2C 核心读写时序详解
1. I2C 写数据时序(Write Sequence)
主机向从机指定的寄存器地址写入数据的完整过程如下:
1.主机发送起始信号(Start)。
2.主机发送设备地址 + 写指令(0x00 / 写位 0),等待从机回复ACK。
3.主机发送目标寄存器地址,等待从机回复ACK。
4.主机向从机发送待写入的数据字节,等待从机回复ACK(若发送多字节,可重复此步骤)。
5.主机发送停止信号(Stop),结束通信。
// I2CR #define IEN (7) #define MSTA (5) #define MTX (4) #define TXAK (3) #define RSTA (2) // I2SR #define ICF (7) #define IBB (5) #define IAL (4) #define IIF (1) void i2c_write(I2C_Type *base, unsigned char dev_address, unsigned char reg_address, const unsigned char *data, int len) { // 通信前 仲裁丢失标志位清零 + 中断标记位清零 base->I2SR &= ~((1 << IIF) | (1 << IAL)); // 通信前,等待数据传输完毕 while ((base->I2SR & (1 << ICF)) == 0); // 发送起始信号 base->I2CR |= (1 << MSTA); // 主机模式: 发送模式 base->I2CR |= (1 << MTX); // 后续监测从机ACK信号 base->I2CR &= ~(1 << TXAK); // 中断标记位清零 base->I2SR &= ~(1 << IIF); // 发送设备地址 + 写位 base->I2DR = dev_address << 1; // 等待从机ACK while ((base->I2SR & (1 << IIF)) == 0); // 中断标记位清零 base->I2SR &= ~(1 << IIF); // 发送寄存器地址 base->I2DR = reg_address; // 等待从机ACK while ((base->I2SR & (1 << IIF)) == 0); while (len--) { // 中断标记位清零 base->I2SR &= ~(1 << IIF); // 发送数据 base->I2DR = *data++; // 等待从机ACK while ((base->I2SR & (1 << IIF)) == 0); } // 发送停止信号 base->I2CR &= ~(1 << MSTA); // 等待总线空闲 while ((base->I2SR & (1 << IBB)) != 0) { delay_us(100); } }2. I2C 读数据时序(Read Sequence)
由于读取数据前需要先告知从机“要读取哪个寄存器”,因此 I2C 读时序通常包含一次伪写(指定寄存器地址)和一次真读:
1.主机发送起始信号(Start)。
2.主机发送设备地址 + 写指令(0x00 / 写位 0),等待从机回复ACK。
3.主机发送目标寄存器地址,等待从机回复ACK。
4.主机重新发送起始信号(ReStart)。
5.主机发送设备地址 + 读指令(0x01 / 读位 1),等待从机回复ACK。
6.从机开始向主机发送数据,主机每接收一个字节回复一个 ACK。
7.当主机接收完最后一个字节后,向从机回复NACK(告知从机停止发送)。
8.主机发送停止信号(Stop),结束通信。
// I2CR #define IEN (7) #define MSTA (5) #define MTX (4) #define TXAK (3) #define RSTA (2) // I2SR #define ICF (7) #define IBB (5) #define IAL (4) #define IIF (1) void i2c_read(I2C_Type *base, unsigned char dev_address,unsigned char reg_address,unsigned char *data, int len) { // 通信前 仲裁丢失标志位清零 + 中断标记位清零 base->I2SR &= ~((1 << IIF) | (1 << IAL)); // 通信前,等待数据传输完毕 while ((base->I2SR & (1 << ICF)) == 0); // 发送起始信号 base->I2CR |= (1 << MSTA); // 主机模式: 发送模式 base->I2CR |= (1 << MTX); // 后续监测从机ACK信号 base->I2CR &= ~(1 << TXAK); // 中断标记位清零 base->I2SR &= ~(1 << IIF); // 发送设备地址 + 写位 base->I2DR = dev_address << 1; // 等待从机ACK while ((base->I2SR & (1 << IIF)) == 0); // 中断标记位清零 base->I2SR &= ~(1 << IIF); // 发送寄存器地址 base->I2DR = reg_address; // 等待从机ACK while ((base->I2SR & (1 << IIF)) == 0); // 中断标记位清零 base->I2SR &= ~(1 << IIF); //主机重发起始信号+设备地址+读位 base->I2CR |= (1 << MSTA); base->I2DR = (dev_address << 1) | (1 << 0); // 等待从机ACK while ((base->I2SR & (1 << IIF)) == 0); // 中断标记位清零 base->I2SR &= ~(1 << IIF); // 切换为主机接收模式 base->I2CR &= ~(1 << MTX); while (len--) { //主机接收最后一个数据后,回复NACK if(len==0) { base->I2CR |= (1 << TXAK); } else { base->I2CR &= ~(1 << TXAK); } while ((base->I2SR & (1 << IIF)) == 0); //读取数据 *data++=base->I2DR; //主机回复ACK base->I2SR |= (1 << IIF); } // 发送停止信号 base->I2CR &= ~(1 << MSTA); // 等待总线空闲 while ((base->I2SR & (1 << IBB)) != 0) { delay_us(100); } }四、总结
操作类型 | 起始位 | 地址控制 | 寄存器定位 | 重复起始位 | 数据流向 | 结束条件 |
I2C 写 | Start | 设备地址 + 0 (写) | 发送寄存器地址 | 无 | 主机 从机 | 最后一个字节后 Stop |
I2C 读 | Start | 设备地址 + 0 (写) | 发送寄存器地址 | ReStart 设备地址 + 1 (读) | 从机 主机 | 接收完后主机发 NACK + Stop |
在实际开发中(例如基于 STM32 的软件模拟 I2C 或硬件 I2C 库驱动),牢记SCL 高电平采样、低电平切换以及第 9 位 ACK/NACK 交互是排查时序问题的关键所在。