深入理解I2C通信协议

在嵌入式系统开发中,通信协议是连接各种外设的桥梁。除了之前介绍的USART串行通信协议,I2C(Inter-Integrated Circuit)作为另一种广泛使用的同步串行通信协议,以其简单的硬件连接和高效的通信方式,成为单片机与传感器、存储器、显示屏等外设通信的首选方案。本文将详细介绍I2C的工作原理、特点,并给出基于STM32单片机的代码实现示例。

什么是I2C?

I2CInter-Integrated Circuit,即集成电路互连总线。I2C最初由飞利浦半导体(现恩智浦NXP)在1982年开发,最初目的是为了简化电视机内部不同芯片之间的通信。如今,I2C已成为嵌入式领域中最常用的通信协议之一。

I2C的主要特点

特性说明
两线制通信只需要两根信号线:SCL(时钟线)和SDA(数据线)
多主多从支持多个主设备和多个从设备挂在同一总线上
半双工通信数据可以在两个方向上传输,但不能同时进行
可变位速率标准模式100kbps,快速模式400kbps,高速模式3.4Mbps
地址寻址每个从设备都有唯一的7位或10位地址
上拉电阻SCL和SDA线需要外部上拉电阻(通常4.7kΩ~10kΩ)

I2C的硬件连接

上拉电阻的选择

总线电容推荐上拉电阻(3.3V供电)
< 100pF2.2kΩ ~ 4.7kΩ
100pF ~ 200pF4.7kΩ ~ 10kΩ
200pF ~ 400pF10kΩ

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的通信过程

标准通信流程

  1. 主机发送起始条件
  2. 主机发送从设备地址 + 读/写位
  3. 从设备应答
  4. 主机发送/接收数据字节
  5. 每次传输后接收方应答
  6. 主机发送停止条件

STM32的I2C实现

硬件I2C配置(CubeMX配置)

使用STM32CubeMX配置I2C主要步骤:

  1. 打开对应I2C外设(I2C1/I2C2/I2C3)
  2. 配置I2C参数:
    • 时钟频率:100kHz(标准模式)或 400kHz(快速模式)
    • 地址模式:7位或10位
  3. 使能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
/**
* @brief 向I2C设备写入一个字节
* @param addr: I2C设备地址
* @param reg: 寄存器地址
* @param data: 要写入的数据
*/
void I2C_WriteByte(uint8_t addr, uint8_t reg, uint8_t data)
{
// 等待I2C总线空闲
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);
}

/**
* @brief 从I2C设备读取一个字节
* @param addr: I2C设备地址
* @param reg: 寄存器地址
* @return 读取到的数据
*/
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
/* 使用HAL库的简化写法 */
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
/* OLED初始化与显示示例 */
#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
/* AHT10传感器读取 */
#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的对比

特性I2CUSART
线路数量2根(SCL + SDA)2-4根(TX/RX/RTS/CTS)
通信模式同步串行异步串行
传输速度最高3.4Mbps最高10Mbps
多设备支持支持(通过地址)支持(但需要更多线路)
硬件复杂度简单中等
数据传输方向半双工全双工
典型应用传感器、存储器、显示屏调试通信、与电脑通信

常见问题与排查

1. 总线忙,无法通信

原因:上次通信异常未正确发送停止位 解决

1
2
3
4
5
6
7
/* 软件复位I2C总线 */
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
/* 使用HAL库检测设备是否存在 */
if(HAL_I2C_IsDeviceReady(&hi2c1, device_addr, 3, 1000) == HAL_OK)
{
// 设备存在
}
else
{
// 设备不存在,检查连接
}

3. 数据读取不稳定

可能原因: - 时钟频率过高 - 上拉电阻阻值不合适 - 总线电容过大

总结

I2C协议以其简单的硬件连接和灵活的通信方式,成为嵌入式开发中不可或缺的工具。通过本文的介绍,你应该已经掌握了:

  1. I2C的基本工作原理和时序
  2. I2C的数据传输格式
  3. STM32的I2C编程方法
  4. 常见I2C外设的应用
  5. 常见问题的排查方法

在实际开发中,建议多查阅芯片数据手册,了解具体外设的寄存器配置。随着学习的深入,你会发现I2C协议在各种嵌入式应用中扮演着越来越重要的角色。

参考资料