第一次接触IIC总线时,我被它的简洁设计震惊了——两根线就能搞定主从设备之间的通信?这比需要片选线的SPI省了多少IO口!但真正动手写代码时才发现,简单的物理层背后藏着不少门道。IIC全称Inter-Integrated Circuit,我们常读作"I方C",是飞利浦在1980年代为连接低速外设设计的协议。现在常见的OLED屏、MPU6050陀螺仪、DS3231时钟模块都在用它。
物理连接上确实简单:SCL时钟线和SDA数据线都要接上拉电阻,默认保持高电平。这里有个新手容易踩的坑——上拉电阻取值。我在驱动0.96寸OLED时,最初用的10K电阻导致波形畸变,后来换成4.7K才稳定。原理很简单:电阻太大导致上升沿过缓,在高速模式下可能无法满足时序要求。
多主机特性是IIC的亮点。理论上可以多个MCU共享总线,但实际项目中我建议谨慎使用。有次尝试用两个STM32通过IIC互传数据,结果因为仲裁逻辑没处理好导致总线锁死。后来改用单主机+多从机架构就稳定多了,这也是大多数嵌入式项目的首选方案。
IIC的7位地址机制堪称经典设计。地址字节实际占用8位,最低位是读写标志(0写1读),剩下7位才是设备地址。这意味着理论上有128个地址空间,但实际可用的更少。比如MPU6050的地址固定为0x68或0x69,很多传感器通过引脚电平来选择最后一位地址。
我在智能家居项目中遇到过地址冲突问题。两个光照传感器都支持0x23地址,最后只能飞线修改其中一个的地址选择引脚。现在选型时我都会特别注意器件是否支持地址配置,以及有多少位可配置——这决定了同型号设备的最大并联数量。
地址分配还有个冷知识:0x00到0x07是保留地址,0x78到0x7F用于10位寻址。实际可用的7位地址范围是0x08到0x77。曾经有同事误用0x02地址导致通信异常,排查了半天才发现问题所在。
看协议文档时觉得起始/停止条件很简单:SCL高电平时SDA的下降沿是起始,上升沿是停止。但第一次用示波器抓波形时,我发现实际间隔比想象中微妙得多。标准模式下,起始条件后需要保持至少4.7us的低电平才能开始传输,这个细节很多文档都没强调。
数据有效性规则是另一个容易出错的地方。必须在SCL低电平时改变SDA,高电平时保持稳定。有次调试发现数据错位,最后发现是SCL还没拉低就改变了SDA。后来我养成了习惯——每个bit操作后都插入短暂延时:
c复制void IIC_WriteBit(uint8_t bit) {
SCL_LOW();
delay_us(2); // 确保SCL稳定为低
if(bit) SDA_HIGH();
else SDA_LOW();
delay_us(2); // 数据稳定时间
SCL_HIGH();
delay_us(4); // 保持高电平时间
SCL_LOW();
}
ACK/NACK机制也值得细说。主机发送完8位数据后,第9个时钟周期需要释放SDA线(设为输入模式),从机则通过拉低SDA来应答。常见错误是主机忘记切换IO方向,导致永远收不到ACK。我的经验是封装专用函数:
c复制uint8_t IIC_Wait_Ack(void) {
SDA_INPUT(); // 关键步骤!
SCL_HIGH();
delay_us(1);
if(SDA_READ()) { // 检测高电平表示NACK
SCL_LOW();
return 1; // 应答失败
}
SCL_LOW();
return 0; // 应答成功
}
硬件IIC外设虽然方便,但在STM32上常遇到兼容性问题。我更喜欢软件模拟,灵活可控。以驱动SSD1306 OLED为例,完整通信流程包括:起始条件→发送地址+写标志→发送控制字节→发送数据→停止条件。每个步骤都需要严格遵循时序。
引脚初始化有讲究:除了配置推挽输出,还要特别注意上电时的默认状态。有次项目在复位期间IIC引脚产生毛刺,导致OLED显示乱码。后来在初始化代码中加入引脚强制拉高:
c复制void IIC_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能时钟
__HAL_RCC_GPIOB_CLK_ENABLE();
// 先强制拉高
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6|GPIO_PIN_7, GPIO_PIN_SET);
// 再配置推挽输出
GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
速度优化是另一个重点。标准模式100kHz用延时函数还能应付,快速模式400kHz就需要精细调校。我的经验是先用示波器测量实际波形,逐步减少延时直到满足最短时间要求。以下是经过实测的延时参数(STM32F103@72MHz):
c复制#define IIC_DELAY_FAST 1 // 快速模式延时(us)
#define IIC_DELAY_STD 4 // 标准模式延时
void IIC_Delay(uint8_t mode) {
volatile uint32_t count = mode;
while(count--);
}
当总线上挂载多个设备时,软件架构需要特别设计。我的做法是封装统一的IIC底层驱动,再为每个设备编写专属驱动层。例如MPU6050和BMP280可以这样组织:
code复制/iic
├── iic_soft.c // 基础IO操作
├── iic_core.c // 协议封装
/devices
├── mpu6050.c // 陀螺仪驱动
└── bmp280.c // 气压计驱动
地址管理也很关键。建议在头文件中用宏定义所有设备地址:
c复制#define DEV_MPU6050_ADDR (0x68 << 1) // 注意左移一位
#define DEV_BMP280_ADDR (0x76 << 1)
#define DEV_OLED_ADDR (0x3C << 1)
复合格式传输是高级技巧。比如先写寄存器地址再读数据,需要用到重复起始条件(Sr)。以读取MPU6050为例:
c复制uint8_t MPU6050_Read(uint8_t reg) {
uint8_t data;
IIC_Start();
IIC_WriteByte(DEV_MPU6050_ADDR | 0); // 写模式
IIC_Wait_Ack();
IIC_WriteByte(reg); // 寄存器地址
IIC_Wait_Ack();
IIC_Start(); // 重复起始条件
IIC_WriteByte(DEV_MPU6050_ADDR | 1); // 读模式
IIC_Wait_Ack();
data = IIC_ReadByte();
IIC_NAck();
IIC_Stop();
return data;
}
调试IIC时,逻辑分析仪比示波器更高效。我常用Saleae抓取协议数据,几个典型故障现象:
有个隐蔽的坑是IO模式设置。STM32的GPIO在输出模式下读取IDR寄存器可能不准,所以检测ACK时要先切换为输入模式。这也是为什么我的SDA_IN()/SDA_OUT()函数分开实现:
c复制void SDA_IN(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
void SDA_OUT(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}
在实时性要求高的场景,IIC通信需要特别优化。我的经验是:
有个特殊案例是IIC从机模式。STM32的硬件IIC作为从机时,对时钟拉伸(clock stretching)的支持有限。遇到需要处理时间的从设备(如EEPROM写入),软件模拟反而更可靠。这时可以这样处理:
c复制void IIC_SlaveProcess(void) {
if(检测到起始条件) {
uint8_t addr = IIC_ReadByte();
if((addr>>1) == OUR_ADDRESS) {
if(addr & 0x01) {
// 主机要读数据
while(数据未准备好) {
SCL_LOW(); // 时钟拉伸
delay_us(10);
}
SCL_HIGH(); // 释放时钟
IIC_WriteByte(数据);
} else {
// 主机要写数据
IIC_SendAck();
数据 = IIC_ReadByte();
}
}
}
}
电磁干扰环境下的稳定性也值得关注。在工业现场,我给IIC总线加上TVS二极管防护,并用双绞线连接设备,通信成功率明显提升。如果线缆超过30cm,建议将速率降到100kHz以下。