Skip to content

I2C 总线通信

I2C(Inter-Integrated Circuit)是一种半双工、同步、多主从的串行通信总线。它只需要两根线就可以连接多个设备,广泛应用于传感器、EEPROM、OLED 显示屏等模块。

I2C 物理层

I2C 总线由两条开漏输出线组成:

  • SCL:串行时钟线,由主机产生时钟信号。
  • SDA:串行数据线,用于传输数据。

由于两条线都是开漏输出,所以必须外接上拉电阻。多个设备的 SCL 和 SDA 分别连接在一起,通过唯一的 7 位或 10 位地址区分不同的从机。

为什么用开漏输出

开漏输出配合上拉电阻可以实现“线与”逻辑:

  • 只要有一个设备把 SDA 拉低,总线就是低电平。
  • 只有当所有设备都释放 SDA,总线才被上拉电阻拉到高电平。

这一特性使得 I2C 支持多主设备和总线仲裁。

I2C 通信过程

一次完整的 I2C 通信包含四个阶段:起始位、寻址、数据传输、停止位。

起始位

当 SCL 为高电平时,SDA 从高电平跳变为低电平,表示通信开始。

text
SCL:  ____
SDA:      \___

寻址

起始位之后,主机发送 7 位从机地址和 1 位方向位:

  • R/W# = 0:写操作,主机向从机发送数据。
  • R/W# = 1:读操作,主机从从机接收数据。

发送完地址和方向位后,主机释放 SDA,从机在第九个时钟周期拉低 SDA 表示应答(ACK)。如果从机不应答(NAK),说明寻址失败或从机不存在。

数据传输

  • 每个字节 8 位,先传最高位(MSB)。
  • 每发送完一个字节,接收方都要回复一个应答位。
  • 写操作时,主机发送数据,从机应答。
  • 读操作时,从机发送数据,主机应答;主机想结束读取时,发送 NAK。

停止位

当 SCL 为高电平时,SDA 从低电平跳变为高电平,表示通信结束。

text
SCL:  ____
SDA:  ___/

HAL 库 I2C 常用函数

向从机发送数据

c
HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c,
                        uint16_t DevAddress,
                        uint8_t *pData,
                        uint16_t Size,
                        uint32_t Timeout);

示例:向地址为 0x50 的 EEPROM 发送 1 字节数据。

c
uint8_t data[] = {0x00, 0xAB};  // 寄存器地址 + 数据
HAL_I2C_Master_Transmit(&hi2c1, 0x50 << 1, data, 2, 1000);

从从机接收数据

c
HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c,
                       uint16_t DevAddress,
                       uint8_t *pData,
                       uint16_t Size,
                       uint32_t Timeout);

示例:

c
uint8_t reg = 0x00;
uint8_t rxBuf[4];

HAL_I2C_Master_Transmit(&hi2c1, 0x50 << 1, &reg, 1, 1000);
HAL_I2C_Master_Receive(&hi2c1, 0x50 << 1, rxBuf, 4, 1000);

写入指定寄存器

c
HAL_I2C_Mem_Write(&hi2c1, 0x50 << 1, 0x00,
                  I2C_MEMADD_SIZE_8BIT, txBuf, len, 1000);

读取指定寄存器

c
HAL_I2C_Mem_Read(&hi2c1, 0x50 << 1, 0x00,
                 I2C_MEMADD_SIZE_8BIT, rxBuf, len, 1000);

I2C 地址

HAL 库的 I2C 地址参数需要传入 8 位地址,即把 7 位从机地址左移 1 位,最低位由 HAL 自动根据读写方向填充。

例如从机 7 位地址是 0x50,则传入参数为 0x50 << 1 = 0xA0

读写 OLED 示例

SSD1306 是常见的 I2C 接口 OLED 驱动芯片,默认地址为 0x3C

写命令

c
void OLED_WriteCommand(uint8_t cmd)
{
    uint8_t data[2] = {0x00, cmd};  // 0x00 表示命令
    HAL_I2C_Master_Transmit(&hi2c1, 0x3C << 1, data, 2, 1000);
}

写数据

c
void OLED_WriteData(uint8_t dat)
{
    uint8_t data[2] = {0x40, dat};  // 0x40 表示数据
    HAL_I2C_Master_Transmit(&hi2c1, 0x3C << 1, data, 2, 1000);
}

实战案例

I2C 发送命令并读取从机状态

I2CTest 项目演示了如何通过 I2C 向从机发送初始化命令,然后读取从机返回的状态字节,并根据状态位控制 LED。

本例中从机地址为 0x78,即 7 位地址 0x3C 左移 1 位后的写地址,常用于 SSD1306 OLED 显示屏。

c
uint8_t commands[] = {0x00, 0x8d, 0x14, 0xaf, 0xa5};
uint8_t dataRcvd;

// 发送 5 字节命令
HAL_I2C_Master_Transmit(&hi2c1, 0x78, commands,
                        sizeof(commands) / sizeof(commands[0]), HAL_MAX_DELAY);

// 读取 1 字节状态
HAL_I2C_Master_Receive(&hi2c1, 0x78, &dataRcvd, 1, HAL_MAX_DELAY);

if ((dataRcvd & (0x01 << 6)) == 0) {
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);    // 熄灭 LED
}
else {
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);  // 点亮 LED
}

I2C1 的配置如下:

c
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000;                 // 400 kHz 快速模式
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);

TIP

实际驱动 SSD1306 等 OLED 时,通常需要连续写入大量显存数据。对于大段数据写入,建议使用 HAL_I2C_Mem_Write 或 DMA 方式,减少 CPU 占用。

常见问题

通信失败,从机无应答

  • I2C 地址错误,注意 7 位地址和 8 位地址的区别。
  • 上拉电阻未接或阻值不合适,一般使用 4.7kΩ。
  • 从机未上电或复位。
  • SCL/SDA 引脚配置错误,应为开漏复用输出。

数据错位或乱码

  • 通信速率过高,超过从机支持范围。
  • 数据线受到干扰,可适当降低 I2C 速度或改善布线。

本章小结

I2C 只需要两根线就可以连接多个从机,是嵌入式系统中节省引脚的理想选择。理解起始位、寻址、应答、停止位等协议细节,掌握 HAL_I2C_Master_TransmitHAL_I2C_Master_ReceiveHAL_I2C_Mem_WriteHAL_I2C_Mem_Read 等函数,就能驱动大多数 I2C 设备。

Built with VitePress · Koyuki Palace