STM32 的IIC通信接收和发送详解
STM32 的 IIC 通信:IIC 接收和发送详解
1. 前言
IIC,也常写作 I2C,是单片机开发中非常常用的一种同步串行通信协议。
在 STM32 项目中,很多外设模块都会使用 IIC 通信,例如:
- OLED 显示屏;
- EEPROM 存储芯片;
- MPU6050 姿态传感器;
- SHT30 温湿度传感器;
- PCF8574 IO 扩展芯片;
- RTC 时钟芯片。
本文主要基于 STM32 标准外设库,讲解 STM32 的 IIC 通信发送和接收过程。
示例芯片以 STM32F103 系列 为例,使用 I2C1 外设。
I2C1 默认常用引脚如下:
| 功能 | 引脚 |
|---|---|
| I2C1_SCL | PB6 |
| I2C1_SDA | PB7 |
本文重点讲解:
- 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,一般需要完成以下步骤:
- 开启 GPIOB 和 I2C1 外设时钟;
- 配置 PB6、PB7 为复用开漏输出;
- 配置 I2C 工作模式;
- 配置 I2C 时钟速率;
- 配置 ACK 使能;
- 使能 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、温湿度传感器都会更加容易。
更多推荐
所有评论(0)