深入理解I2C通信协议
在嵌入式系统开发中,通信协议是连接各种外设的桥梁。除了之前介绍的USART串行通信协议,I2C(Inter-Integrated Circuit)作为另一种广泛使用的同步串行通信协议,以其简单的硬件连接和高效的通信方式,成为单片机与传感器、存储器、显示屏等外设通信的首选方案。本文将详细介绍I2C的工作原理、特点,并给出基于STM32单片机的代码实现示例。
什么是I2C?
I2C:Inter-Integrated Circuit,即集成电路互连总线。I2C最初由飞利浦半导体(现恩智浦NXP)在1982年开发,最初目的是为了简化电视机内部不同芯片之间的通信。如今,I2C已成为嵌入式领域中最常用的通信协议之一。
I2C的主要特点
| 特性 | 说明 |
|---|
| 两线制通信 | 只需要两根信号线:SCL(时钟线)和SDA(数据线) |
| 多主多从 | 支持多个主设备和多个从设备挂在同一总线上 |
| 半双工通信 | 数据可以在两个方向上传输,但不能同时进行 |
| 可变位速率 | 标准模式100kbps,快速模式400kbps,高速模式3.4Mbps |
| 地址寻址 | 每个从设备都有唯一的7位或10位地址 |
| 上拉电阻 | SCL和SDA线需要外部上拉电阻(通常4.7kΩ~10kΩ) |
I2C的硬件连接
上拉电阻的选择
| 总线电容 | 推荐上拉电阻(3.3V供电) |
|---|
| < 100pF | 2.2kΩ ~ 4.7kΩ |
| 100pF ~ 200pF | 4.7kΩ ~ 10kΩ |
| 200pF ~ 400pF | 10kΩ |
I2C的通信时序
起始条件和停止条件
- 起始条件(Start):当SCL为高电平时,SDA从高电平变为低电平,表示通信开始
- 停止条件(Stop):当SCL为高电平时,SDA从低电平变为高电平,表示通信结束
1 2 3 4 5 6 7
| SCL ────┬───┬───┬───┬───┬───┬───┬───┬─── │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ SDA ────┐ ┌───┐ ┌───┐ ┌───┐ ┌─── Start │ │ │ │ │ │ │ │ Stop └───┘ └───┘ └───┘ └───┘ 数据传输区间
|
数据传输
- 每一位数据在SCL为高电平时采样
- SDA线上的数据必须在SCL低电平期间变化
- 数据按字节传输,先传输高位(MSB)
1 2 3 4 5 6 7 8
| SCL ────┬───┬───┬───┬───┬───┬───┬───┬─── │ 1 │ 0 │ 1 │ 0 │ 1 │ 0 │ 1 │ │ │ │ │ │ │ │ │ SDA ────┐ ┌───┐ ┌───┐ ┌───┐ ┌─── │ │ │ │ │ │ │ │ └───┘ └───┘ └───┘ └───┘ ↑ ↑ ↑ ↑ ↑ ↑ ↑ 采样点(数据有效)
|
应答信号(ACK/NACK)
- 发送完一个字节后,接收方会拉低SDA线表示应答(ACK)
- 如果接收方没有拉低SDA,则表示非应答(NACK)
I2C的数据帧格式
写操作
Start | 从设备地址(7位) | R/W=0 | ACK | 数据1 | ACK | 数据2 | ACK | … | Stop |
读操作
Start | 从设备地址(7位) | R/W=1 | ACK | 数据1 | ACK | 数据2 | ACK | … | NACK | Stop |
I2C的通信过程
标准通信流程
- 主机发送起始条件
- 主机发送从设备地址 + 读/写位
- 从设备应答
- 主机发送/接收数据字节
- 每次传输后接收方应答
- 主机发送停止条件
STM32的I2C实现
硬件I2C配置(CubeMX配置)
使用STM32CubeMX配置I2C主要步骤:
- 打开对应I2C外设(I2C1/I2C2/I2C3)
- 配置I2C参数:
- 时钟频率:100kHz(标准模式)或 400kHz(快速模式)
- 地址模式:7位或10位
- 使能I2C外设
I2C读写寄存器函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
|
void I2C_WriteByte(uint8_t addr, uint8_t reg, uint8_t data) { while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, reg); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1, data); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); }
uint8_t I2C_ReadByte(uint8_t addr, uint8_t reg) { uint8_t data; I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, reg); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Receiver); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data = I2C_ReceiveData(I2C1); I2C_GenerateSTOP(I2C1, ENABLE); return data; }
|
使用HAL库的I2C操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| uint8_t I2C_ReadByte_HAL(uint8_t addr, uint8_t reg) { uint8_t data; HAL_I2C_Mem_Read(&hi2c1, addr, reg, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000); return data; }
void I2C_WriteByte_HAL(uint8_t addr, uint8_t reg, uint8_t data) { HAL_I2C_Mem_Write(&hi2c1, addr, reg, I2C_MEMADD_SIZE_8BIT, &data, 1, 1000); }
void I2C_ReadBytes_HAL(uint8_t addr, uint8_t reg, uint8_t *buffer, uint16_t len) { HAL_I2C_Mem_Read(&hi2c1, addr, reg, I2C_MEMADD_SIZE_8BIT, buffer, len, 1000); }
|
常见I2C外设应用
1. OLED显示屏(SSD1306)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #define OLED_ADDR 0x78
void OLED_Init(void) { I2C_WriteByte_HAL(OLED_ADDR, 0x00, 0xAE); I2C_WriteByte_HAL(OLED_ADDR, 0x00, 0xD5); I2C_WriteByte_HAL(OLED_ADDR, 0x00, 0x80); I2C_WriteByte_HAL(OLED_ADDR, 0x00, 0xAF); }
void OLED_ShowString(uint8_t x, uint8_t y, char *str) { I2C_WriteByte_HAL(OLED_ADDR, 0x00, 0xB0 + y); I2C_WriteByte_HAL(OLED_ADDR, 0x00, ((x & 0xF0) >> 4) | 0x10); I2C_WriteByte_HAL(OLED_ADDR, 0x00, (x & 0x0F) | 0x01); while(*str) { I2C_WriteByte_HAL(OLED_ADDR, 0x40, *str++); } }
|
2. 温湿度传感器(AHT10)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #define AHT10_ADDR 0x38
float AHT10_ReadTemperature(void) { uint8_t data[6]; uint8_t cmd[3] = {0xAC, 0x33, 0x00}; HAL_I2C_Master_Transmit(&hi2c1, AHT10_ADDR, cmd, 3, 1000); HAL_Delay(80); HAL_I2C_Master_Receive(&hi2c1, AHT10_ADDR, data, 6, 1000); uint32_t temp = ((uint32_t)data[3] >> 4) | ((uint32_t)data[2] << 4); float temperature = ((float)temp * 200.0 / 1048576.0) - 50.0; return temperature; }
|
I2C与USART的对比
| 特性 | I2C | USART |
|---|
| 线路数量 | 2根(SCL + SDA) | 2-4根(TX/RX/RTS/CTS) |
| 通信模式 | 同步串行 | 异步串行 |
| 传输速度 | 最高3.4Mbps | 最高10Mbps |
| 多设备支持 | 支持(通过地址) | 支持(但需要更多线路) |
| 硬件复杂度 | 简单 | 中等 |
| 数据传输方向 | 半双工 | 全双工 |
| 典型应用 | 传感器、存储器、显示屏 | 调试通信、与电脑通信 |
常见问题与排查
1. 总线忙,无法通信
原因:上次通信异常未正确发送停止位 解决:
1 2 3 4 5 6 7
| void I2C_SoftReset(void) { HAL_I2C_DeInit(&hi2c1); HAL_Delay(10); HAL_I2C_Init(&hi2c1); }
|
2. 从设备不应答
可能原因: - 设备地址错误 - 硬件连接问题(上拉电阻、断线) - 从设备未上电
排查方法:
1 2 3 4 5 6 7 8 9
| if(HAL_I2C_IsDeviceReady(&hi2c1, device_addr, 3, 1000) == HAL_OK) { } else { }
|
3. 数据读取不稳定
可能原因: - 时钟频率过高 - 上拉电阻阻值不合适 - 总线电容过大
总结
I2C协议以其简单的硬件连接和灵活的通信方式,成为嵌入式开发中不可或缺的工具。通过本文的介绍,你应该已经掌握了:
- I2C的基本工作原理和时序
- I2C的数据传输格式
- STM32的I2C编程方法
- 常见I2C外设的应用
- 常见问题的排查方法
在实际开发中,建议多查阅芯片数据手册,了解具体外设的寄存器配置。随着学习的深入,你会发现I2C协议在各种嵌入式应用中扮演着越来越重要的角色。
参考资料