STM32单片机 IIC 通信协议
1、IIC概述:
- 访问传感器 这是IIC最经典的应用。许多小型、低功耗的传感器都采用IIC接口与主控制器(如MCU)通 信。
- 温度/湿度传感器: 如 SHT30, SHT40
- 加速度计/陀螺仪: 如 MPU-6050(同时集成了3轴加速度计和3轴陀螺仪)
- 磁力计: 如 QMC5883L(电子罗盘)
- 气压计: 如 BMP280
- 光强度传感器: 如 BH1750
- 控制执行器或驱动芯片
- LED驱动芯片: 如控制LED点阵屏或背光亮度的芯片(MAX7219, PCA9685舵机驱动 板)。
- IO扩展芯片: 当MCU的GPIO口不够用时,可以使用IIC接口的芯片来扩展输入输出口 (如 PCF8574)。PCA9685舵机扩展板模块 单片机I2C接口控制板 16路PWM舵机驱动 板
- 音频编码/解码芯片: 许多编解码芯片(Codec)支持IIC接口进行配置。
- 访问存储器
- 一些小容量、用于存储配置参数或数据的非易失性存储器也使用IIC接口。
- EEPROM: 如 24C02, 24C64 等系列芯片,用于存储设备参数、校准数据等。
- 一些小容量、用于存储配置参数或数据的非易失性存储器也使用IIC接口。
- 与实时时钟(RTC)通信
- 实时时钟芯片负责在系统断电时继续计时,通常通过IIC接口被主CPU读取或设置时间。 常用芯片: DS1307, PCF8563
2、IIC特点:
| 特点 | 描述 |
|---|---|
| 总线结构 | 两根信号线:SCL(时钟线)、SDA(数据线) |
| 通信方式 | 主从模式(Master-Slave) |
| 多主支持 | 允许多个主设备控制总线(但较少使用) |
| 速率 | 标准 100 kbps,快速 400 kbps,高速可达 3.4 Mbps |
| 寻址方式 | 每个从设备有唯一的 7 位或 10 位地址 |
| 数据方向 | 半双工(数据线双向,但同一时刻只有一个方向传输) |
3、IIC数据帧格式

这是 IIC 写入数据帧。
- 起始位【1位】,主机触发 I2C 数据总线工作,告知当前 I2C 对应 SDA 和 SCL 所有设备进入 就绪状态
- 设备地址【7位】,当前 I2C 设备的地址。MCU 选择当前操作的 I2C 设备是哪一个,通常情 况下, I2C 设备地址是硬件电路决定!
- 读/写标志位【1位】,明确当前 MCU 操作当前 I2C 设备是进行读操作还是写操作,需要当 前当前设备地址进行组合,一并作为一个字节数据发送给对应 I2C 设备。0 写入操作。1 位读 取操作
- 应答信号 ACK【1位】,设备之间确认数据发送是否正常的标志位,存在与设备地址 + 读写 标志位之后,寄存器地址之后,数据之后。
- 寄存器地址【8位】,按照当前图例分析,目标写入数据的 I2C 设备寄存器地址,当前寄存器 地址范围是 0x00 ~ 0xFF,仅考虑基本的字节操作,I2C 设备可以支持的最大存储器为 256 byte。可以根据后续编号进行拓展。
- 数据 Data【8位】,按照当前图例分析,MCU 写入到当前 I2C 设备的数据内容,数据具体 形式是根据 SDA 和 SCL 时序决定。
- 停止位【1位】,当前主机操作 I2C 结束信号。
IIC 的读取数据【数据帧】格式为分为两个阶段,写寄存器地址”和“读数据两个阶段。

| 阶段 | 组成部分(按传输顺序) | 说明 |
|---|---|---|
| 写地址阶段 | 起始位(S)→ 设备地址(7 位)+ 写位(0)→ 应答(ACK)→ 寄存器地址(8 位)→ 应答(ACK)→ 重复起始位(Sr) | 先向从机指定要读取的寄存器地址 |
| 读数据阶段 | 设备地址(7 位)+ 读位(1)→ 应答(ACK)→ 数据(8 位)→ 非应答(NACK)→ 停止位(P) | 从机向主机返回目标寄存器的数据 |
各个阶段的电平情况:
IIC空闲信号:空闲状态代表总线未被占用,所有设备处于监听状态。在多主机或复杂 IIC 总线系统中,主机(或控制器)常常需要确认 IIC 总线处于空闲状态,才开始发送数据,否则会引发协议冲突或通信错误。

IIC起始信号和停止信号:

起始信号是 SCL 处于高电平状态,在 SCL 处于高电平期间,SDA 实现一次从高到低的跳变。
停止信号是 SCL 处于高电平状态,在 SCL 处于高电平期间,SDA 实现一次从低到高的跳变。
IIC传递数据 0 和 1 :

逻辑 1: SCL 时钟线处于高电平状态,SDA在整个 【SCL高电平周期内】,保持高电平状态,一般情况下,SDA 数据线拉高早于时钟线 SCL 时钟线,后续拉低晚于SCL时钟线。
逻辑 0:SCL 时钟线处于高电平状态,SDA 在整个【SCL高电平周期内】,保持低电平状态,SDA数据线拉低早于SCL时钟线,后续拉高晚于SCL时钟线。

IIC读写标志位:
IIC主设备发送给从设备,需要明确告知当前 IIC从设备是读操作还是写操作。【主机行为】
- 0 : 表示主设备向从设备进行数据写入操作【写操作】
- 1 : 表示主设备需要读取从设备目标寄存器的数据内容【读操作】

IIC应答信号:
| 名称 | 缩写 | 电平状态 | 含义 |
|---|---|---|---|
| 应答信号 | ACK | SDA 拉低(0) | 表示“数据已成功接收,请继续” |
| 非应答信号 | NACK | SDA 保持高(1) | 表示“数据接收完毕或出错,请停止” |
应答信号的时序细节
每传输 1 字节(8 位) 数据后,都会产生 第 9 个时钟周期(每发送 1 个比特位(bit),占用 1 个时钟周期)
在 第 9 个时钟周期:
发送方释放 SDA(变为输入)
接收方在该周期内拉低 SDA(表示 ACK),或保持高电平(表示 NACK)
SCL 由主机控制,产生第 9 个脉冲读取 SDA 状态
在主机需要连续读取从机数据,在接收一个字节后还要继续接收,给予从机的应答信号为 ACK。
停止读取给予从机应答信号 NACK,表示主机读完最后一个数据。
4、IIC操作 EEPROM 存储设备。
先看原理图:


在 STM32中 IIC的SCL时钟线对应的引脚为PB6,SDA数据线对应的引脚是PB7,结合EEPROM原理图来看
在IIC输出的时候,将PB6和PB7设置为开漏输出模式,IIC输入的时候需要将SDA 对应的PB7引脚配置为浮空输入模式。
在 软件模拟 I²C(bit-bang I2C)实现中:
| 引脚 | 作用 | 是否需要输入模式 | 配置方式 |
|---|---|---|---|
| SCL (PB6) | 时钟线,由主机驱动 | 不需要切换为输入 | 始终保持 开漏输出 + 上拉 |
| SDA (PB7) | 数据线,可读可写 | 需要在发送/接收之间切换 | 在发送数据时 → 开漏输出 在接收数据时 → 浮空输入或上拉输入 |
4、IIC发送字节数据:
IIC 发送数据内容都是 8位组成 【MSB高位先发】
设备地址:

寄存器地址:

发送数据:


代码:
#include "my_i2c.h"void I2C_GPIO_Init(void)
{// 1. 时钟使能只需要使能 GPIOB 组RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;/*2. 配置 GPIOB 中的 PB6 和 PB7 都为开漏输出模式*/GPIOB->CRL &= ~(0xFF << 24);GPIOB->CRL |= (0x77 << 24);/*3. 当前配置完成之后,软件 I2C 处于空闲状态。要求 SCL 和 SDA 都保持【高电平状态】*/GPIOB->BSRR |= (0x03 << 6);
}void SCL_Set(u8 statu)
{if (statu){// SCL 时钟线输出高电平GPIOB->BSRR |= I2C_SCL_PIN;}else{// SCL 时钟线输出低电平GPIOB->BRR |= I2C_SCL_PIN;}
}void SDA_Set(u8 statu)
{if (statu){// SDA 数据线输出高电平GPIOB->BSRR |= I2C_SDA_PIN;}else{// SDA 数据线输出低电平GPIOB->BRR |= I2C_SDA_PIN;}
}void I2C_Start(void)
{// 【TODO 未完待续】明确当前 SDA 数据线处于输出工作模式SDA_Output_Mode();/*【必须保证】SDA 的变化过程是在 SCL 高电平周期以内完成,不可以出现SCL 时钟周期早于 SDA 变化过程。*//*首先 SDA 数据线拉高,拉高之后,SCL 时钟线拉高,可以保证 时钟周期高电平范围以内,SDA 数据线状态控制当前 I2C 传递内容 */SDA_Set(1);SCL_Set(1);I2C_Delay();/*通过延时操作之后,此时 SCL 高电平状态,可以任务是时钟高电平周期范围SDA 设置低电平,可以认为是在 SCL 高电平周期内,SDA 完成了高电平到低电平跳变,对应【I2C 起始信号时序要求】*/SDA_Set(0);I2C_Delay();/*时钟线拉低,进入下一个时钟周期低电平状态。*/SCL_Set(0);I2C_Delay();
}void I2C_Stop(void)
{// 【TODO 未完待续】明确当前 SDA 数据线处于输出工作模式SDA_Output_Mode();/*【必须保证】SDA 的变化过程是在 SCL 高电平周期以内完成,不可以出现SCL 时钟周期早于 SDA 变化过程。*//*保证 SDA 数据线低电平状态,之后进入到 SCL 时钟线高电平周期内*/SDA_Set(0);SCL_Set(1);I2C_Delay();/*I2C 停止信号是在 SCL 高电平周期内容,SDA 实现低电平到高电平跳变*/SDA_Set(1);I2C_Delay();/*时钟线拉低,进入下一个时钟周期低电平状态。*/SCL_Set(0);I2C_Delay();
}void SDA_Output_Mode(void)
{// SDA 数据线设置为输出模式 --> 开漏输出// MCU 主设备进行相关操作,例如 【起始信号,终止信号,数据传递】GPIOB->CRL &= ~(0x0F << 28);GPIOB->CRL |= (0x07 << 28);
}void SDA_Input_Mode(void)
{// SDA 数据线设置为输入模式 --> 浮空输入// MCU 主设备接受从设备数据,例如 【应答位信息,从设备寄存器数据】GPIOB->CRL &= ~(0x0F << 28);GPIOB->CRL |= (0x04 << 28);
}void I2C_Delay(void)
{/*控制延时时间为 5 us */SysTick_Delay_us(5);
}u8 SDA_Read(void)
{return (GPIOB->IDR & I2C_SDA_PIN) ? 1 : 0;
}u8 I2C_SendByte(u8 data)
{// 控制循环的临时变量u8 i = 0;// 数据发送完成 I2C 从设备应答内容u8 ack = 0;/*【重点】保证 MCU 对应 SDA 引脚处于输出状态*/SDA_Output_Mode();/*每一次发送数据都是 8 位/ 1 字节内容*/for (i = 0; i < 8; i++){// 【发送操作核心内容】// 1. SCL 时钟线拉低SCL_Set(0);I2C_Delay();// 2. 根据发送数据情况,调整 SDA 电平/*I2C 数据发送是高位 (MSB) 先出。利用 data & 0x80 进行 0 1 判断如果 data & 1000 0000 结果不为 0,表示当前需要发送的数据位为 1如果 data & 1000 0000 结果为 0,表示当前需要发送的数据位为 0发送完成一次,将 data 进行左移一位操作*/if (data & 0x80){// 发送数据 1,SDA 数据线处于高电平状态SDA_Set(1);}else{// 发送数据 0,SDA 数据线处于低电平状态SDA_Set(0);}// 延时控制,此时 SCL(0) SDA(1)I2C_Delay();// 当前 SCL 时钟线拉高,进入 I2C 设备 SDA 数据采样阶段SCL_Set(1);I2C_Delay();// 数据 data 左移 1 位data <<= 1;}/*以上操作将 8 位数据发送给 I2C 设备, 主设备需要读取 I2C 从设备对应应答信号在应答周期内 SDA 低电平对应 0 。表示 I2C 从设备接收数据正常在应答周期内 SDA 高电平对应 1 。表示 I2C 从设备接收数据异常,设备异常*//*【I2C 设备应答信号处理】*/// 以上操作数据发送完成之后,SCL 处于高电平状态,需要拉低操作,进入时钟低电平SCL_Set(0);I2C_Delay();/*修改当前 SDA 工作模式输入模式,准备读取 SDA 电平情况分析应答数据。*/SDA_Input_Mode();/*利用 SDA ODR 将电平拉高,如果 I2C 设备提供明确的低电平信号,表示 ACK 应答成功。*/SDA_Set(1);I2C_Delay();/*电平情况分析SCL 处于低电平,无效时钟阶段SDA 处于高电平状态,【输入模式】,非 MCU 主动模式。*/// 进入时钟的有效周期内容,此时 I2C 从设备会按照时钟高电平有效周期内容// 进行数据反馈,如果应答成功,SDA 数据线电平拉低,如果响应未成功,SDA // 依然保持高电平SCL_Set(1);I2C_Delay();// 读取 SDA 电平情况ack = SDA_Read();// 时钟线拉低,进入无效时钟阶段SCL_Set(0);// 将 SDA 设置为输出模式状态。SDA_Output_Mode();return ack;
}u8 I2C_ReadByte(u8 ack)
{u8 i = 0;u8 data = 0;/*SDA 工作模式调整为输入状态。同时将 SDA 设置为高电平。*/SDA_Input_Mode();SDA_Set(1);for (i = 0; i < 8; i++){/*SCL 时钟处于低电平,无效时钟阶段*/SCL_Set(0);I2C_Delay();/*SCL 明确低电平状态之后,直接拉高,进入有效时钟周期此时 I2C 从设备,会发送数据内容到 SDA 数据线,从而改变 SDA 数据线电平情况*/SCL_Set(1);I2C_Delay();data <<= 1;// 在 SCL 时钟线拉高之后,延时 5 us ,此时 I2C 提供数据// 对于 SDA 电平影响已经稳定。读取 SDA 数据,判断当前数据内容if (SDA_Read()){// 如果读取到的 SDA 高电平,数据为 1 进行 |= 赋值操作。data |= 0x01;}}/*当前数据接收完毕字后,SCL 时钟线处于高电平状态。首先拉低 SCL 进入无效时钟阶段。*/SCL_Set(0);I2C_Delay();/*当前接收数据的是 MCU ,需要给予 I2C 从设备对应的应答信息,当前应答位有特殊功能ACK(0) 表示数据接收成功,可以继续连续读取数据假设第一次 MCU 读取设备地址为 101 0000 设备,寄存器 0000 0001 数据。ACK(0), 此时 I2C 从设备,继续发送寄存器 0000 0010 位置数据ACK(0), 此时 I2C 从设备,继续发送寄存器 0000 0011 位置数据NACK(1) 表示数据接收完成,终止后续数据读取*/// 调整当前 SDA 工作模式以为输出模式SDA_Output_Mode();// 此时 SDA 是输出工作模式,根据用户参数 ACK 设置 SDA 电平if (ack){SDA_Set(1);}else{ SDA_Set(0);}I2C_Delay();// SCL 时钟线进入高电平模式,有效时钟周期SCL_Set(1);I2C_Delay();// SCL 时钟线进入低电平模式,无效时钟周期SCL_Set(0);return data;
}
5、EEPROM介绍和驱动配置:
在 STM32 微控制器中,EEPROM(Electrically Erasable Programmable Read-Only Memory,电可擦除可编程只读存储器) 是一种非易失性存储元件,用于保存需要长期掉电不丢失的数据(如配置参数、校准值、用户设置等)。
STM32 中的 EEPROM 特点
- 非易失性:断电后数据不会丢失,与 RAM(易失性)不同,无需持续供电维持数据。
- 可擦写性:支持电擦除和重写(次数通常为 10 万次以上,具体取决于型号),但擦写速度比 RAM 慢,且有寿命限制。
- 容量较小:STM32 芯片集成的 EEPROM 容量通常在几 KB 到几十 KB(例如 STM32F103 系列部分型号约 2KB,STM32L 系列因低功耗特性可能稍大),远小于 Flash(通常几十 KB 到几 MB)。
- 独立于 Flash:虽然部分 STM32 型号(如早期 F1 系列)没有物理上独立的 EEPROM,而是通过Flash 模拟 EEPROM 功能(利用 Flash 的部分扇区实现类似 EEPROM 的擦写特性),但逻辑上仍作为专门的非易失性数据存储区使用。
代码:
#include "eeprom_at24c02.h"u8 EEPROM_WriteByte(u8 addr, u8 data)
{u8 retry = 3;u8 timeout = I2C_TIMEOUT;// 利用 retry 进行多次发送尝试,在 I2C 传递过程中,// 任何一个环节出现问题,直接重开!!!【I2C起始位】while (retry--){// 1. I2C 起始信号I2C_Start();/*2. 发送设备地址 + 写入标志位 【8位数据】同时利用 ACK 应答信号进行判断数据是否发送完成。*/if (I2C_SendByte(EEPROM_AT24C02_ADDR)){/*以上判断,如果返回 ACK[0] 表示一切正常,if 大括号内容不执行如果返回 NACK[1] 表示 I2C 无应答/未应答起始信号。【重新开始 I2C 发送,首先终止当前 I2C 流程】*/I2C_Stop(); // I2C 总线进入【空闲状态,所有设备停止通信。】continue;}/*3. 发送设备地址 + 写入标志位成功,开始进行目标寄存器地址发送*/if (I2C_SendByte(addr)){// 同理地址发送判断规则I2C_Stop();continue;}/*4. 明确告知当前 I2C EERPOM 存储设备,存储数据地址。进入数据发送阶段*/if (I2C_SendByte(data)){// 同理地址发送判断规则I2C_Stop();continue;}// 5. 数据发送完成,当前操作是单字节写入操作,MCU 发送 STOPI2C_Stop();/*数据发送完成之后 EERPOM 会进入到数据处理阶段,需要一定的时间,根据文档分析,当前周期时间一个字节需要 几 ms ,可以认为是 EERPOM 设备的【冥想模式】。此时EEPROM 不会应答外部的任何请求。数据处理完成之后,当前 EEPROM 会应答外部的数据内容。*/while (timeout--){// I2C 总线开始工作I2C_Start();/*发送设备地址 + 写入标志到之前接收数据的 EERPOM 芯片,等等当前 EEPROM 芯片应答,如果有 ACK(0) 表示设备数据处理完成,【MCU 数据发送成功,EEPROM 数据存储成功】如果始终位 NACK(1) 表示数据处理过程出现异常。*/if (!I2C_SendByte(EEPROM_AT24C02_ADDR)){I2C_Stop();return 0; // 表示数据存储成功}I2C_Stop();}}return 1; // 表示数据存储失败
}u8 EEPROM_ReadByte(u8 addr)
{// 读取操作同样设置尝试次数 3 次u8 retry = 3;u8 data = 0;while (retry--){// 1. 发送 I2C 起始形式,当前 I2C 所有设备 StandbyI2C_Start();/*2. 发送当前目标 I2C 设备地址 + 写入数据标志位*/if (I2C_SendByte(EEPROM_AT24C02_ADDR)){// 如果有问题 I2C 总线空闲 --> StopI2C_Stop();continue;}/*3. 目标读取寄存器地址*/if (I2C_SendByte(addr)){// 如果有问题 I2C 总线空闲 --> StopI2C_Stop();continue;}// 4. 根据 I2C 时序要求,MCU 再次发送起始信号I2C_Start();/*5. 发送当前目标 I2C 设备地址 + 读取数据标志位需要在 7 位设备地址最低位补充 1 读取数据标志位*/if (I2C_SendByte(EEPROM_AT24C02_ADDR | 0x01)){// 如果有问题 I2C 总线空闲 --> StopI2C_Stop();continue;}/*6. 直接利用 I2C_ReadByte 读取数据,同时 MCU 应答信息为 1 表示当前读取数据操作结束。*/data = I2C_ReadByte(1);/*7. I2C 停止信号*/I2C_Stop();return data;}return 0;
}u8 EEPROM_WritePage(u8 addr, u8 *buffer, u8 len)
{u8 i = 0;u8 timeout = I2C_TIMEOUT;// 如果用户要求写入的数据大于 8 个字节,恕难从命!!!if (len > 8){return 1;}// 1. I2C 起始信号I2C_Start();// 2. 目标设备地址 + 写入标志位if (I2C_SendByte(EEPROM_AT24C02_ADDR)){I2C_Stop();return 1;}// 3. 目标写入数据的起始位置。if (I2C_SendByte(addr)){I2C_Stop();return 1;}/*4. 重点【循环写入数据内容】 EEPROM 设备会自动根据开始位置和数据数量逐步累加地址,进行数据存取。*/for (i = 0; i < len; i++){if (I2C_SendByte(buffer[i])){I2C_Stop();return 1;}}// 5. I2C 停止位I2C_Stop();// 等待当前 EERPOM 数据存储完成,利用设备地址进行 I2C 设备唤醒// 如果 EEPROM 没有任何应答,表示当前设备在进行数据处理,有应答// 数据存储完毕while (timeout--){I2C_Start();if (!I2C_SendByte(EEPROM_AT24C02_ADDR)){I2C_Stop();return 0;}I2C_Stop();SysTick_Delay_ms(1);}return 1;
}u8 EEPROM_ReadPage(u8 addr, u8 *buffer, u8 len)
{u8 i = 0;// 最多一次处理 8 字节数据if (len > 8){return 1;}// 1. I2C 起始信号I2C_Start();// 2. 利用设备地址 + 写入数据标志位,找到目标设备if (I2C_SendByte(EEPROM_AT24C02_ADDR)){I2C_Stop();return 1;}// 3. 明确告知 EEPROM 读取数据的开始寄存器地址if (I2C_SendByte(addr)){I2C_Stop();return 1;}// 4. 重启起始位I2C_Start();// 5. 利用设备地址 + 读入数据标志位,找到目标设备if (I2C_SendByte(EEPROM_AT24C02_ADDR | 0x01)){I2C_Stop();return 1;}// 6,利用 for 循环接收当前设备发送到 MCU 的数据内容for (i = 0; i < len; i++){/*读取多个数据内容,需要明确当前操作 MCU 应答位情况MCU 发送应答信息 0 表示继续读取,EEPROM 会继续返回数据MCU 发送应答信息 1 表示停止读取,EEPROM 终止发送buffer 使用一个缓冲数组,假设需要读取数据长度为 4, 当前读取到下标为 3 的数据时,为最后一个数据内容,需要告知 EEPROM 停止发送 */if (i == len - 1){buffer[i] = I2C_ReadByte(1); // 告知 EEPROM 停止数据发送}else {buffer[i] = I2C_ReadByte(0);}}// 7. I2C 停止位I2C_Stop();return 0;
}
