STM32 的 IIC 通信:IIC 接收和发送详解

1. 前言

IIC,也常写作 I2C,是单片机开发中非常常用的一种同步串行通信协议。
在 STM32 项目中,很多外设模块都会使用 IIC 通信,例如:

  • OLED 显示屏;
  • EEPROM 存储芯片;
  • MPU6050 姿态传感器;
  • SHT30 温湿度传感器;
  • PCF8574 IO 扩展芯片;
  • RTC 时钟芯片。

本文主要基于 STM32 标准外设库,讲解 STM32 的 IIC 通信发送和接收过程。
示例芯片以 STM32F103 系列 为例,使用 I2C1 外设。

I2C1 默认常用引脚如下:

功能引脚
I2C1_SCLPB6
I2C1_SDAPB7

本文重点讲解:

  • IIC 通信的基本原理;
  • STM32 标准库 I2C 初始化;
  • IIC 发送一个字节的流程;
  • IIC 读取一个字节的流程;
  • IIC 读写寄存器的典型代码;
  • ACK、NACK、START、STOP 的作用;
  • STM32 硬件 I2C 常见问题。

2. IIC 通信的基本概念

IIC 是一种双线制同步串行通信协议。
它只需要两根信号线:

信号线作用
SCL时钟线
SDA数据线

其中:

  • SCL 由主机产生时钟;
  • SDA 用于传输数据;
  • 通信双方必须共地;
  • SCL 和 SDA 都需要上拉电阻。

典型连接如下:

STM32_PB6 / SCL  --->  外设_SCL
STM32_PB7 / SDA  --->  外设_SDA
STM32_GND        --->  外设_GND

IIC 总线上可以挂多个从机设备。
每个从机设备通过不同的 设备地址 区分。


3. IIC 为什么需要上拉电阻

IIC 的 SCL 和 SDA 通常是 开漏输出 结构。
开漏输出只能主动拉低电平,不能主动输出高电平。

所以,IIC 总线要想出现高电平,就必须依靠外部上拉电阻。

简单理解:

输出低电平:设备主动把总线拉低
输出高电平:设备松开总线,由上拉电阻拉高

这也是 IIC 可以实现多设备挂在同一条总线上的原因。
多个设备都可以拉低总线,但不会主动推高总线,从而减少总线冲突。

常见上拉电阻取值:

4.7kΩ 或 10kΩ

如果通信速度较高、线路较长、总线电容较大,可以适当减小上拉电阻。


4. IIC 通信时序核心

一次完整的 IIC 通信通常包含以下几个部分:

起始信号 START
↓
发送设备地址
↓
发送读写方向位
↓
等待 ACK 应答
↓
发送或接收数据
↓
停止信号 STOP

在这里插入图片描述

4.1 START 起始信号

当 SCL 为高电平时,SDA 从高电平跳变到低电平,表示起始信号。

SCL = 1
SDA: 1 -> 0

主机发送 START 后,说明一次 IIC 通信开始。


4.2 STOP 停止信号

当 SCL 为高电平时,SDA 从低电平跳变到高电平,表示停止信号。

SCL = 1
SDA: 0 -> 1

主机发送 STOP 后,说明本次 IIC 通信结束。


4.3 ACK 应答信号

IIC 每发送 8 位数据后,接收方需要返回 1 位应答。

应答含义
ACK接收成功,继续通信
NACK不再继续接收,准备结束通信

ACK 通常表现为:

第 9 个时钟周期,SDA 被拉低

NACK 通常表现为:

第 9 个时钟周期,SDA 保持高电平

在 STM32 读取最后一个字节时,通常需要发送 NACK,然后产生 STOP 信号,告诉从机接收结束。


5. IIC 设备地址说明

IIC 从机设备通常有一个 7 位地址。
例如某些 EEPROM 的地址可能是:

0x50

但是在实际发送时,IIC 总线上发送的是:

7 位设备地址 + 1 位读写位

其中:

方向读写位
0
1

在 STM32 标准库中,I2C_Send7bitAddress() 函数会帮我们处理读写方向位。

例如:

I2C_Send7bitAddress(I2C1, 0x50 << 1, I2C_Direction_Transmitter);

这里很多资料会把 7 位地址左移一位。
因为标准库函数内部使用的是 8 位地址格式,最低位留给读写方向位。

所以实际使用时要特别注意模块资料中的地址写法:

  • 有些资料给的是 7 位地址;
  • 有些资料给的是已经左移过的 8 位地址。

地址写错,是 IIC 通信失败最常见的原因之一。


6. STM32 I2C1 初始化流程

使用 STM32 标准库配置硬件 I2C,一般需要完成以下步骤:

  1. 开启 GPIOB 和 I2C1 外设时钟;
  2. 配置 PB6、PB7 为复用开漏输出;
  3. 配置 I2C 工作模式;
  4. 配置 I2C 时钟速率;
  5. 配置 ACK 使能;
  6. 使能 I2C1 外设。

7. I2C1 初始化代码

下面是基于 STM32F103 标准库的 I2C1 初始化代码。

#include "stm32f10x.h"

void I2C1_Init_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    I2C_InitTypeDef I2C_InitStructure;

    /* 1. 开启 GPIOB 和 I2C1 时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

    /* 2. 配置 PB6、PB7 为复用开漏输出 */
    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);

    /* 3. 配置 I2C1 参数 */
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_InitStructure.I2C_ClockSpeed = 100000;

    I2C_Init(I2C1, &I2C_InitStructure);

    /* 4. 使能 I2C1 */
    I2C_Cmd(I2C1, ENABLE);
}

这里设置的 IIC 速率为:

100kHz

这属于标准模式。
如果外设支持,也可以配置成 400kHz 快速模式。


8. IIC 发送数据的基本流程

STM32 作为主机向从机发送一个字节,基本流程如下:

产生 START
↓
等待 EV5
↓
发送从机地址 + 写方向
↓
等待 EV6
↓
发送寄存器地址
↓
等待 EV8
↓
发送数据
↓
等待 EV8_2
↓
产生 STOP

标准库中常见事件如下:

事件含义
I2C_EVENT_MASTER_MODE_SELECT起始信号已发送,主模式已选择
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED从机地址已发送,写模式已选择
I2C_EVENT_MASTER_BYTE_TRANSMITTING正在发送数据
I2C_EVENT_MASTER_BYTE_TRANSMITTED数据发送完成

这些事件本质上是对 I2C 状态寄存器标志位的组合判断。
使用标准库时,可以通过 I2C_CheckEvent() 进行检测。


9. IIC 写一个寄存器

很多 IIC 设备都是通过寄存器读写控制的。
例如向某个传感器的寄存器写入配置值。

通用写寄存器函数如下:

void I2C1_WriteReg(uint8_t dev_addr, uint8_t reg_addr, uint8_t data)
{
    /* 1. 等待 I2C 总线空闲 */
    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

    /* 2. 产生起始信号 */
    I2C_GenerateSTART(I2C1, ENABLE);

    /* 3. 等待 EV5:主模式选择 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    /* 4. 发送从机地址,写方向 */
    I2C_Send7bitAddress(I2C1, dev_addr << 1, I2C_Direction_Transmitter);

    /* 5. 等待 EV6:发送器模式选择 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    /* 6. 发送寄存器地址 */
    I2C_SendData(I2C1, reg_addr);

    /* 7. 等待数据发送中 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));

    /* 8. 发送数据 */
    I2C_SendData(I2C1, data);

    /* 9. 等待数据发送完成 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* 10. 产生停止信号 */
    I2C_GenerateSTOP(I2C1, ENABLE);
}

调用示例:

I2C1_WriteReg(0x68, 0x6B, 0x00);

这表示向设备地址 0x68 的从机中,寄存器 0x6B 写入数据 0x00


10. IIC 接收数据的基本流程

STM32 从 IIC 从机读取一个寄存器的数据,通常需要两步:

第一步:先告诉从机要读取哪个寄存器。

START
↓
设备地址 + 写方向
↓
寄存器地址

第二步:重新产生起始信号,然后切换为读方向读取数据。

Repeated START
↓
设备地址 + 读方向
↓
读取数据
↓
NACK
↓
STOP

这里的第二个 START 叫 重复起始信号,也叫 Repeated START。
它的作用是不释放总线,直接从写寄存器地址切换到读数据。


11. IIC 读取一个寄存器

下面是读取单个寄存器的通用函数。

uint8_t I2C1_ReadReg(uint8_t dev_addr, uint8_t reg_addr)
{
    uint8_t data;

    /* 1. 等待 I2C 总线空闲 */
    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

    /* 2. 产生起始信号 */
    I2C_GenerateSTART(I2C1, ENABLE);

    /* 3. 等待 EV5 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    /* 4. 发送从机地址,写方向 */
    I2C_Send7bitAddress(I2C1, dev_addr << 1, I2C_Direction_Transmitter);

    /* 5. 等待 EV6 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    /* 6. 发送要读取的寄存器地址 */
    I2C_SendData(I2C1, reg_addr);

    /* 7. 等待寄存器地址发送完成 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* 8. 再次产生起始信号,也就是重复起始 */
    I2C_GenerateSTART(I2C1, ENABLE);

    /* 9. 等待 EV5 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    /* 10. 发送从机地址,读方向 */
    I2C_Send7bitAddress(I2C1, dev_addr << 1, I2C_Direction_Receiver);

    /* 11. 等待 EV6 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    /* 12. 读取最后一个字节前关闭 ACK */
    I2C_AcknowledgeConfig(I2C1, DISABLE);

    /* 13. 产生停止信号 */
    I2C_GenerateSTOP(I2C1, ENABLE);

    /* 14. 等待接收数据 */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));

    /* 15. 读取数据 */
    data = I2C_ReceiveData(I2C1);

    /* 16. 重新开启 ACK,方便下一次通信 */
    I2C_AcknowledgeConfig(I2C1, ENABLE);

    return data;
}

调用示例:

uint8_t id;

id = I2C1_ReadReg(0x68, 0x75);

这表示读取设备地址 0x68 的寄存器 0x75

如果是 MPU6050,0x75 是 WHO_AM_I 寄存器,正常情况下可以读到设备 ID。


12. 多字节连续读取

很多传感器数据不是一个字节,而是多个字节。
例如 MPU6050 的加速度、陀螺仪数据通常需要连续读取多个寄存器。

多字节读取的基本流程如下:

START
↓
设备地址 + 写
↓
起始寄存器地址
↓
Repeated START
↓
设备地址 + 读
↓
连续读取多个字节
↓
最后一个字节发送 NACK
↓
STOP

示例函数如下:

void I2C1_ReadMultiReg(uint8_t dev_addr, uint8_t reg_addr, uint8_t *buf, uint8_t len)
{
    uint8_t i;

    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

    I2C_GenerateSTART(I2C1, ENABLE);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    I2C_Send7bitAddress(I2C1, dev_addr << 1, I2C_Direction_Transmitter);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    I2C_SendData(I2C1, reg_addr);
    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, dev_addr << 1, I2C_Direction_Receiver);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    for (i = 0; i < len; i++)
    {
        if (i == len - 1)
        {
            I2C_AcknowledgeConfig(I2C1, DISABLE);
            I2C_GenerateSTOP(I2C1, ENABLE);
        }

        while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
        buf[i] = I2C_ReceiveData(I2C1);
    }

    I2C_AcknowledgeConfig(I2C1, ENABLE);
}

使用示例:

uint8_t buf[6];

I2C1_ReadMultiReg(0x68, 0x3B, buf, 6);

这表示从设备 0x68 的寄存器 0x3B 开始,连续读取 6 个字节。


13. IIC 发送和接收的技术总结

13.1 写数据流程

IIC 写数据的核心是:

START
↓
设备地址 + 写
↓
寄存器地址
↓
数据
↓
STOP

对应标准库函数流程:

I2C_GenerateSTART()
I2C_Send7bitAddress(..., I2C_Direction_Transmitter)
I2C_SendData(reg_addr)
I2C_SendData(data)
I2C_GenerateSTOP()

13.2 读数据流程

IIC 读数据的核心是:

START
↓
设备地址 + 写
↓
寄存器地址
↓
Repeated START
↓
设备地址 + 读
↓
读取数据
↓
NACK
↓
STOP

对应标准库函数流程:

I2C_GenerateSTART()
I2C_Send7bitAddress(..., I2C_Direction_Transmitter)
I2C_SendData(reg_addr)
I2C_GenerateSTART()
I2C_Send7bitAddress(..., I2C_Direction_Receiver)
I2C_ReceiveData()
I2C_GenerateSTOP()

读操作看起来比写操作复杂,主要原因是:

读取寄存器前,需要先告诉从机要读哪个寄存器。

所以读寄存器通常不是直接读,而是先写寄存器地址,再切换成读方向。


14. STM32 硬件 I2C 常见问题

14.1 IIC 总线一直忙

常见原因:

  • SDA 被从机拉低;
  • 上一次通信异常,没有正确产生 STOP;
  • 外设复位后状态不一致;
  • 总线上某个设备异常;
  • 上拉电阻不合适。

解决思路:

  • 检查 SDA 和 SCL 空闲时是否为高电平;
  • 重新初始化 I2C 外设;
  • 必要时手动模拟 SCL 释放总线;
  • 确认从机供电正常。

14.2 IIC 没有 ACK

常见原因:

  • 从机地址写错;
  • 7 位地址和 8 位地址混淆;
  • SCL / SDA 接反;
  • 没有共地;
  • 从机模块没有供电;
  • 上拉电阻缺失;
  • 从机还没有完成内部初始化。

其中最常见的是:

设备地址写错

如果数据手册给的是 7 位地址,使用标准库时通常需要左移一位。


14.3 读取数据一直为 0xFF

常见原因:

  • SDA 没有被从机正确拉低;
  • 从机没有响应;
  • 上拉电阻过大;
  • 读写方向设置错误;
  • 读取的寄存器地址错误;
  • ACK / NACK 处理不正确。

14.4 程序卡在 while 等待事件

例如程序卡在:

while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

常见原因:

  • 没有收到从机 ACK;
  • 从机地址错误;
  • 总线时序异常;
  • I2C 外设状态异常;
  • 硬件连接问题。

实际项目中建议给 while 等待增加超时机制,避免程序永久卡死。

例如:

uint32_t timeout = 10000;

while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
    if (timeout-- == 0)
    {
        return;
    }
}

15. 为什么很多项目更喜欢软件 IIC

STM32F1 系列的硬件 I2C 在一些项目中比较容易遇到总线忙、状态卡死等问题。
所以很多教程和开源项目会使用 软件 IIC

软件 IIC 的优点:

  • 时序可控;
  • 移植方便;
  • 出问题更容易调试;
  • 不依赖具体硬件 I2C 外设;
  • 适合 OLED、MPU6050、EEPROM 等低速设备。

硬件 IIC 的优点:

  • 占用 CPU 更少;
  • 速度更稳定;
  • 适合中高速通信;
  • 更适合规范的工程项目。

简单来说:

新手学习、低速模块:软件 IIC 更好调
工程项目、数据量较大:硬件 IIC 更规范

本文使用的是 STM32 标准库硬件 I2C,重点是帮助理解 IIC 外设发送和接收的完整流程。


16. 小结

IIC 通信的核心并不复杂。

写数据时,主机发送:

START + 设备地址写 + 寄存器地址 + 数据 + STOP

读数据时,主机发送:

START + 设备地址写 + 寄存器地址 + Repeated START + 设备地址读 + 数据 + STOP

在 STM32 标准库中,需要重点掌握以下函数:

函数作用
I2C_GenerateSTART()产生起始信号
I2C_GenerateSTOP()产生停止信号
I2C_Send7bitAddress()发送从机地址
I2C_SendData()发送数据
I2C_ReceiveData()接收数据
I2C_CheckEvent()检查 I2C 事件
I2C_AcknowledgeConfig()配置 ACK 应答

对于初学者来说,建议按照下面的顺序学习:

IIC 基本时序
↓
START / STOP / ACK
↓
设备地址
↓
写一个寄存器
↓
读一个寄存器
↓
连续读取多个寄存器
↓
加入超时机制和错误处理

只要把 IIC 的写寄存器和读寄存器流程搞清楚,后面使用 OLED、MPU6050、EEPROM、温湿度传感器都会更加容易。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐