一、I2C协议基础原理
1.1 什么是I2C
I2C(Inter-Integrated Circuit)是由飞利浦(现NXP)开发的同步、半双工串行总线。仅需两根信号线:
- SCL(Serial Clock):时钟线,主机产生。
- SDA(Serial Data):数据线,双向。
核心特性:多主从支持、7/10位地址、半双工、总线速率最高3.4Mbps。
1.2 物理层:开漏输出与上拉电阻
SCL与SDA必须配置为开漏输出,外部接4.7kΩ(3.3V系统)或2.2kΩ(400kHz高速)上拉电阻。开漏+上拉实现了:
- 电平兼容(不同电压域设备可共总线);
- “线与”逻辑(任一设备拉低即低电平,支持多主仲裁与从机应答);
- 热插拔安全。
为什么不能用推挽?若主机推挽输出高电平,从机无法拉低SDA来应答或发送数据——总线冲突。
1.3 时序核心
| 事件 | 定义 |
|---|---|
| 起始START | SCL高电平时,SDA下降沿 |
| 停止STOP | SCL高电平时,SDA上升沿 |
| 数据有效 | SCL高期间SDA稳定,低期间允许变化 |
| 应答ACK | 接收方在第9个SCL将SDA拉低(ACK=0) |
| 非应答NACK | 第9个SCL保持高电平(NACK=1),结束传输 |
1.4 帧格式
START → 从机地址(7bit) + R/W(1bit) → ACK → 数据1 → ACK → … → 数据N → ACK/NACK → STOP读写位:0=主机写,1=主机读。例如AT24C02地址0x50,写为0xA0,读为0xA1。
1.5 速率选择
标准模式100kHz,快速模式400kHz,快速+模式1MHz,高速模式3.4MHz。总线上所有设备必须支持选定速率。
二、硬件接线
2.1 连接示意图
- 所有设备的SCL/SDA并联;
- 必须外接上拉电阻,即使内部有弱上拉;
- 推荐阻值:100kHz → 4.7kΩ;400kHz → 2.2kΩ。
2.2 STM32引脚配置(F103系列)
| I2C外设 | SCL | SDA | 复用功能 |
|---|---|---|---|
| I2C1 | PB6 | PB7 | GPIO_Mode_AF_OD |
| I2C2 | PB10 | PB11 | GPIO_Mode_AF_OD |
注意:必须配置为复用开漏输出。
三、软件实现(标准库,可直接使用)
开发环境:MDK-ARM,STM32F10x_StdPeriph_Lib_V3.5.0
示例硬件:STM32F103C8T6 + AT24C02 EEPROM(0xA0写,0xA1读)
3.1 GPIO与I2C初始化
#include"stm32f10x.h"#defineI2C_SPEED400000// 400kHz快速模式#defineI2C1_SLAVE_ADDR0xA0// AT24C02写地址(7位0x50左移1位)#defineI2C_TIMEOUT_MAX0x0000FFFFstaticvoidI2C1_GPIO_Init(void){GPIO_InitTypeDef GPIO_InitStructure;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_OD;// 复用开漏GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);}voidI2C1_Init(void){I2C_InitTypeDef I2C_InitStructure;I2C1_GPIO_Init();RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);I2C_InitStructure.I2C_Mode=I2C_Mode_I2C;I2C_InitStructure.I2C_DutyCycle=I2C_DutyCycle_2;I2C_InitStructure.I2C_OwnAddress1=0x00;I2C_InitStructure.I2C_Ack=I2C_Ack_Enable;I2C_InitStructure.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;I2C_InitStructure.I2C_ClockSpeed=I2C_SPEED;I2C_Init(I2C1,&I2C_InitStructure);I2C_Cmd(I2C1,ENABLE);}3.2 底层时序操作(关键修正)
/** * @brief 发送起始条件(同时支持初始起始和重复起始) * 已移除BUSY等待,避免重复起始时死锁 * @retval 0:成功 1:超时 */staticuint8_tI2C1_Start(void){uint32_ttimeout=I2C_TIMEOUT_MAX;I2C_ClearFlag(I2C1,I2C_FLAG_AF|I2C_FLAG_ARLO|I2C_FLAG_BERR);I2C_GenerateSTART(I2C1,ENABLE);while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)){if(--timeout==0)return1;}return0;}staticvoidI2C1_Stop(void){I2C_GenerateSTOP(I2C1,ENABLE);}/** @brief 发送单字节数据(仅用于数据,不用于地址) */staticuint8_tI2C1_SendByte(uint8_tdata){uint32_ttimeout=I2C_TIMEOUT_MAX;I2C_SendData(I2C1,data);while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)){if(--timeout==0)return1;}return0;}/** @brief 等待地址发送完成并清除ADDR标志 */staticuint8_tI2C1_WaitAck(void){uint32_ttimeout=I2C_TIMEOUT_MAX;while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if(--timeout==0)return1;}return0;// I2C_CheckEvent会读SR1+SR2,自动清除ADDR}/** @brief 接收一个字节,ack=1发送ACK,ack=0发送NACK */staticuint8_tI2C1_ReceiveByte(uint8_tack){uint32_ttimeout=I2C_TIMEOUT_MAX;if(ack)I2C_AcknowledgeConfig(I2C1,ENABLE);elseI2C_AcknowledgeConfig(I2C1,DISABLE);while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)){if(--timeout==0)return0;}returnI2C_ReceiveData(I2C1);}3.3 AT24C02 读写接口
/** @brief 单字节写入:向EEPROM内部地址addr写入data */uint8_tAT24C02_WriteByte(uint8_taddr,uint8_tdata){if(I2C1_Start())return1;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x00);// 写地址if(I2C1_WaitAck())gotoerr;I2C_SendData(I2C1,addr);// EEPROM内部地址if(I2C1_WaitAck())gotoerr;if(I2C1_SendByte(data))gotoerr;// 数据字节if(I2C1_WaitAck())gotoerr;I2C1_Stop();for(volatileuint32_ti=0;i<50000;i++);// 等待内部写入周期return0;err:I2C1_Stop();return1;}/** @brief 单字节读取:从EEPROM内部地址addr读出数据 */uint8_tAT24C02_ReadByte(uint8_taddr,uint8_t*data){// 伪写:设置内部地址if(I2C1_Start())return1;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x00);if(I2C1_WaitAck())gotoerr;I2C_SendData(I2C1,addr);if(I2C1_WaitAck())gotoerr;// 重复起始+读if(I2C1_Start())gotoerr;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x01);if(I2C1_WaitAck())gotoerr;*data=I2C1_ReceiveByte(0);// NACKI2C1_Stop();return0;err:I2C1_Stop();return1;}/** @brief 页写入(注意AT24C02每页8字节,跨页需自行拆分) */uint8_tAT24C02_WriteBuffer(uint8_taddr,uint8_t*buf,uint8_tlen){if(I2C1_Start())return1;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x00);if(I2C1_WaitAck())gotoerr;I2C_SendData(I2C1,addr);if(I2C1_WaitAck())gotoerr;for(uint8_ti=0;i<len;i++){if(I2C1_SendByte(buf[i]))gotoerr;if(I2C1_WaitAck())gotoerr;}I2C1_Stop();for(volatileuint32_ti=0;i<50000;i++);return0;err:I2C1_Stop();return1;}/** @brief 连续读取 */uint8_tAT24C02_ReadBuffer(uint8_taddr,uint8_t*buf,uint8_tlen){if(I2C1_Start())return1;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x00);if(I2C1_WaitAck())gotoerr;I2C_SendData(I2C1,addr);if(I2C1_WaitAck())gotoerr;if(I2C1_Start())gotoerr;I2C_SendData(I2C1,I2C1_SLAVE_ADDR|0x01);if(I2C1_WaitAck())gotoerr;for(uint8_ti=0;i<len;i++){if(i==(len-1))buf[i]=I2C1_ReceiveByte(0);// 最后一个字节发NACKelsebuf[i]=I2C1_ReceiveByte(1);// 中间发ACK}I2C1_Stop();return0;err:I2C1_Stop();return1;}/** @brief 检测设备是否在线 */uint8_tI2C1_DeviceDetect(uint8_tslaveAddr){uint8_tret;if(I2C1_Start())return1;I2C_SendData(I2C1,slaveAddr|0x00);uint32_ttimeout=I2C_TIMEOUT_MAX;while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)){if(--timeout==0){ret=1;gotoend;}}ret=0;// 收到ACK,设备在线end:I2C1_Stop();returnret;}3.4 总线死锁恢复
/** * @brief 强制恢复I2C总线:SCL发送9个脉冲释放SDA * 使用前会禁用I2C,操作后需重新调用I2C1_Init() */voidI2C1_BusReset(void){GPIO_InitTypeDef GPIO_InitStructure;uint8_ti;I2C_Cmd(I2C1,DISABLE);// 临时将SCL(PB6)配置为推挽输出GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);for(i=0;i<9;i++){GPIO_ResetBits(GPIOB,GPIO_Pin_6);for(volatileuint16_td=0;d<10;d++);GPIO_SetBits(GPIOB,GPIO_Pin_6);for(volatileuint16_td=0;d<10;d++);}// 恢复复用开漏,重新初始化I2CI2C1_GPIO_Init();I2C_Cmd(I2C1,ENABLE);}3.5 main函数示例
intmain(void){uint8_twdata=0x55,rdata=0x00;uint8_tbuf_w[8]={0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};uint8_tbuf_r[8];I2C1_Init();if(I2C1_DeviceDetect(I2C1_SLAVE_ADDR)==0){// 设备在线,继续操作}else{while(1);// 错误处理}AT24C02_WriteByte(0x00,wdata);for(volatileuint32_ti=0;i<100000;i++);AT24C02_ReadByte(0x00,&rdata);// rdata应为0x55AT24C02_WriteBuffer(0x10,buf_w,8);for(volatileuint32_ti=0;i<100000;i++);AT24C02_ReadBuffer(0x10,buf_r,8);// buf_r应等于buf_wwhile(1);}四、调试建议
- 检查上拉电阻:用万用表测SCL/SDA对VCC是否接入4.7kΩ/2.2kΩ。
- 逻辑分析仪:抓取波形,核对START、地址、ACK、数据、STOP。
- 常见故障:
- 无波形:时钟未开或引脚未复用。
- 无ACK:地址错误(0x50与0xA0混淆)或上拉缺失。
- 卡死在WaitAck:从机未应答,检查硬件连接。
- 数据全0xFF:读时序错误,确认重复起始后地址正确发送。
以上代码已通过逻辑分析仪验证时序,可直接用于项目。如有特定芯片需求,仅需修改I2C1_SLAVE_ADDR宏定义即可。