I2C(Inter-Integrated Circuit)是一种简单高效的双向二线制同步串行总线,只需要两根线(SDA和SCL)就能实现设备间的通信。在嵌入式开发中,I2C常用于连接各种传感器、EEPROM等外设。STM32的标准外设库(Standard Peripheral Library)提供了一套完整的I2C驱动函数,封装了底层寄存器操作,大大简化了开发流程。
我第一次使用STM32的I2C接口时,被它丰富的功能所震撼。标准库提供了从初始化配置到数据传输、从DMA控制到中断处理的全套函数,几乎涵盖了I2C通信的所有环节。不过在实际项目中,我也踩过不少坑,比如时钟配置不当导致通信失败,或者忘记处理NACK错误导致程序卡死。
与SPI、USART等其他通信接口相比,I2C有几个显著特点:它是多主从结构的,支持总线仲裁;采用地址寻址方式,同一总线上可以挂载多个设备;通信速率从标准模式(100kHz)到快速模式(400kHz)甚至高速模式(3.4MHz)不等。STM32的I2C外设完全兼容这些特性,还支持SMBus和PMBus协议。
在使用I2C前,必须正确配置硬件。首先需要开启相关GPIO和I2C外设的时钟。以I2C1为例,通常使用PB6(SCL)和PB7(SDA),需要将这两个引脚配置为复用开漏输出模式:
c复制RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
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);
接下来是I2C外设本身的初始化。标准库提供了I2C_Init()函数,需要填充一个I2C_InitTypeDef结构体。这里有几个关键参数容易出错:
c复制I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 快速模式占空比
I2C_InitStructure.I2C_OwnAddress1 = 0xA0; // 主模式可设任意不冲突地址
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = 400000; // 400kHz快速模式
I2C_Init(I2C1, &I2C_InitStructure);
I2C_Cmd(I2C1, ENABLE);
在实际项目中,I2C初始化失败的原因主要有以下几种:
时钟配置错误:APB1总线时钟必须至少是I2C时钟速度的4倍。如果APB1时钟是36MHz,最大只能设置到400kHz。
GPIO模式错误:必须使用开漏输出(GPIO_Mode_AF_OD),并且外部需要上拉电阻(通常4.7kΩ)。
地址冲突:总线上每个设备的地址必须唯一,7位地址范围是0x08~0x77。
我曾经遇到一个棘手的问题:I2C通信偶尔会失败。经过示波器抓取波形发现,SCL信号上升沿太慢,导致时序不符合标准。解决方法是在GPIO初始化时提高GPIO_Speed,或者在硬件上减小上拉电阻值。
I2C通信的基本流程包括:起始条件→发送地址→数据传输→停止条件。标准库提供了对应的函数:
c复制// 主设备发送数据流程示例
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, 0x01); // 发送寄存器地址
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_SendData(I2C1, 0xAA); // 发送数据
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTOP(I2C1, ENABLE);
接收数据的过程类似,但需要注意在读取最后一个字节前要发送NACK:
c复制// 主设备接收数据流程示例
I2C_AcknowledgeConfig(I2C1, ENABLE); // 使能ACK
// ...前面的起始条件和地址发送类似...
// 读取多个字节
for(int i=0; i<length-1; i++) {
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
buffer[i] = I2C_ReceiveData(I2C1);
}
// 读取最后一个字节
I2C_AcknowledgeConfig(I2C1, DISABLE); // 最后一个字节发送NACK
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
buffer[length-1] = I2C_ReceiveData(I2C1);
I2C_GenerateSTOP(I2C1, ENABLE);
EEPROM读写是I2C的典型应用。以24C02为例,它的地址是0xA0(写)和0xA1(读)。写入数据时需要先发送内存地址,再发送数据:
c复制void EEPROM_WriteByte(uint8_t addr, uint8_t data) {
I2C_GenerateSTART(I2C1, ENABLE);
// ...等待EV5事件...
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
// ...等待EV6事件...
I2C_SendData(I2C1, addr); // 发送内存地址
// ...等待EV8事件...
I2C_SendData(I2C1, data); // 发送数据
// ...等待EV8事件...
I2C_GenerateSTOP(I2C1, ENABLE);
Delay(5); // 等待写入完成
}
传感器数据读取也很常见。以BMP280气压传感器为例,读取数据时需要先写入寄存器地址,再发起读取:
c复制uint8_t BMP280_ReadRegister(uint8_t reg) {
uint8_t value;
I2C_GenerateSTART(I2C1, ENABLE);
// ...等待EV5...
I2C_Send7bitAddress(I2C1, 0x76, I2C_Direction_Transmitter);
// ...等待EV6...
I2C_SendData(I2C1, reg); // 发送要读取的寄存器地址
// ...等待EV8...
I2C_GenerateSTART(I2C1, ENABLE); // 重复起始条件
// ...等待EV5...
I2C_Send7bitAddress(I2C1, 0x76, I2C_Direction_Receiver);
// ...等待EV6...
I2C_AcknowledgeConfig(I2C1, DISABLE); // 只读一个字节,发NACK
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
value = I2C_ReceiveData(I2C1);
I2C_GenerateSTOP(I2C1, ENABLE);
return value;
}
对于大量数据传输,使用DMA可以大幅提高效率并降低CPU负载。STM32的I2C支持DMA传输,配置步骤如下:
c复制// 先配置DMA通道
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_DeInit(DMA1_Channel6); // I2C1_TX用DMA1通道6
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&I2C1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)txBuffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = bufferSize;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel6, &DMA_InitStructure);
// 启用I2C的DMA功能
I2C_DMACmd(I2C1, ENABLE);
DMA_Cmd(DMA1_Channel6, ENABLE);
// 启动传输后,可以通过DMA中断或查询标志位判断传输完成
使用DMA时需要注意几个问题:
I2C中断可以大大提高程序的响应效率。STM32的I2C中断分为三类:
配置中断的步骤如下:
c复制// 启用I2C中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = I2C1_EV_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = I2C1_ER_IRQn;
NVIC_Init(&NVIC_InitStructure);
// 启用特定中断
I2C_ITConfig(I2C1, I2C_IT_EVT | I2C_IT_BUF | I2C_IT_ERR, ENABLE);
在中断服务函数中,需要先判断中断类型,再执行相应处理:
c复制void I2C1_EV_IRQHandler(void) {
uint32_t event = I2C_GetLastEvent(I2C1);
switch(event) {
case I2C_EVENT_MASTER_MODE_SELECTED:
// 起始条件发送完成
break;
case I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED:
// 地址发送完成(写模式)
break;
case I2C_EVENT_MASTER_BYTE_TRANSMITTED:
// 数据发送完成
break;
// 其他事件处理...
}
}
void I2C1_ER_IRQHandler(void) {
if(I2C_GetITStatus(I2C1, I2C_IT_AF)) {
// ACK错误处理
I2C_ClearITPendingBit(I2C1, I2C_IT_AF);
}
if(I2C_GetITStatus(I2C1, I2C_IT_BERR)) {
// 总线错误处理
I2C_ClearITPendingBit(I2C1, I2C_IT_BERR);
}
// 其他错误处理...
}
I2C通信中常见的错误包括:
一个健壮的I2C驱动应该包含错误恢复机制。当检测到错误时,建议执行以下步骤:
c复制void I2C_Recover(I2C_TypeDef* I2Cx) {
// 1. 禁用I2C
I2C_Cmd(I2Cx, DISABLE);
// 2. 软件复位
I2C_SoftwareResetCmd(I2Cx, ENABLE);
I2C_SoftwareResetCmd(I2Cx, DISABLE);
// 3. 重新初始化
I2C_Init(I2Cx, &I2C_InitStructure);
I2C_Cmd(I2Cx, ENABLE);
// 4. 清除所有错误标志
I2C_ClearFlag(I2Cx, I2C_FLAG_AF | I2C_FLAG_ARLO | I2C_FLAG_BERR);
}
调试I2C问题时,以下几个工具和方法非常有用:
我曾经遇到一个奇怪的问题:I2C偶尔会卡死。通过逻辑分析仪发现,有时从设备会拉低SCL线(时钟拉伸),但STM32的标准库默认禁用了时钟拉伸功能。解决方法是在初始化时启用时钟拉伸:
c复制I2C_StretchClockCmd(I2C1, ENABLE);
另一个常见问题是上拉电阻选择不当。理论上I2C总线需要上拉电阻,但阻值过大会导致上升沿太慢,阻值过小又会增加功耗。对于3.3V系统,通常选择4.7kΩ的电阻,但在高速模式下可能需要更小的阻值(如2.2kΩ)。