stm32内部集成电路(I2C)接口
嵌入式老杨 2026-01-14


本文介绍 STM32F4xx 的 I2C 接口,含简介、协议、功能及程序设计。I2C 为双线制串行协议,支持多设备通信,程序实现与 EEPROM 读写,写入和读出数据一致,验证了其可靠性。


01


I2C简介


I2C(Inter-Integrated Circuit)是由飞利浦公司开发的一种串行、多主机、多从机通信协议,旨在实现芯片间的低速数据传输。STM32F4xx 系列微控制器集成了 I2C 接口,支持标准模式(100kbps)、快速模式(400kbps)和快速模式 Plus(1Mbps),具备主从模式切换、7 位 / 10 位地址寻址、中断驱动和 DMA 传输等功能。


1.电气特性:

参数

标准模式

快速模式

快速模式 Plus

高速模式(部分型号)

最高时钟频率

100kbps

400kbps

1Mbps

3.4Mbps

逻辑高电平(Vcc=3.3V)

≥2V

≥2V

≥2V

≥2V

逻辑低电平

≤0.8V

≤0.8V

≤0.8V

≤0.8V

最大总线负载

400pF(含线缆和器件)

400pF

200pF

200pF

上拉电阻推荐值

4.7kΩ~10kΩ

2.2kΩ~4.7kΩ

1kΩ~2.2kΩ

1kΩ~2.2kΩ

总线仲裁机制:当多个主机同时尝试控制总线时,通过 SDA 线的电平竞争实现仲裁,避免数据冲突。


超时机制:支持时钟延长功能,从机可通过拉低 SCL 线延长时钟周期,以适应低速设备。


2.物理层简介

I2C 物理层采用双线制结构,由以下两根信号线组成:

SDA(Serial Data):串行数据线,用于传输数据。

SCL(Serial Clock):串行时钟线,用于同步数据传输。

两根线均需通过上拉电阻(通常 4.7kΩ~10kΩ) 连接到正电源,以确保总

线在空闲状态下保持高电平。总线支持多设备共线连接,各设备通过地址区分身份,主机负责发起通信并控制时钟,从机响应主机命令。


若I²C使用7位地址寻址,地址范围0x00~0x7F(共128个地址),其中0x00为广播地址(不能分配给具体从机),剩余0x01~0x7F共127个可用地址,因此理论最大从机数为127个。但受限于STM32F4的GPIO驱动能力、总线电容和信号完整性,实际设计中建议单条总线挂载不超过 8-16个从机。

3.不同型号的 STM32F4xx 芯片集成的 I2C 接口数量不同,常见型号如下:

STM32F401/405/411:2 个 I2C 接口(I2C1、I2C2)。

STM32F407/417/427/437:3 个 I2C 接口(I2C1、I2C2、I2C3)。

STM32F429/439/446/469:通常集成 3~4 个 I2C 接口,具体以芯片手册为准。


4.应用场景

传感器通信:连接温湿度传感器(如 SHT30)、气压传感器(如 BMP280)、加速度计(如 MPU6050)等。

存储器扩展:访问 EEPROM(如 AT24C 系列)、FRAM(铁电存储器)等。

显示模块:驱动 OLED(如 SSD1306)、LCD(如 PCF8574 控制的 I2C 接口 LCD)等。

音频芯片:配置 CODEC(如 WM8960)、数字麦克风(如 MP34DT05)等。

电源管理:控制电源监控芯片(如 TPS3803)、电池管理 IC(如 BQ24780)。

多芯片协作:在复杂系统中连接 ADC、DAC、时钟芯片(如 DS3231)等外设

02


I2C协议



1.数据帧格式

起始位 → 从机地址 + R/W位 → ACK → 数据字节 → ACK → 停止位


2. 关键信号定义

起始位(S):SCL 为高电平时,SDA 由高变低。

停止位(P):SCL 为高电平时,SDA 由低变高。

数据有效性:SCL 高电平期间,SDA 必须保持稳定(数据传输)。

应答位(ACK):接收器在第 9 个时钟周期拉低 SDA 表示确认。


3. 数据传输方向

写操作(主机发送数据到从机):

S → 从机地址(0) → ACK → 数据字节1 → ACK → 数据字节2 → ACK → P

读操作(主机从从机接收数据):

S → 从机地址(1) → ACK → 数据字节1 → 主机ACK → 数据字节2 → 主机NACK → P。


4.STM32的 I2C 收发模式

STM32标准的I2C共有4中模式:主机发送模式、主机接收模式、从机接收模式、从机发送模式。常用的是主机发送模式、主机接收模式以及由这两种演变而成的复合模式。


5 主机发送模式(Master Transmitter)

整个通信过程如下图:

其中阴影部分为主机负责,无阴影部分为从机负责。

详细数据帧格式如下图所示:


生成起始位(I2C_GenerateSTART());

发送从机地址(最低位为 0);

等待从机应答;

循环发送数据字节(I2C_SendData());

生成停止位(I2C_GenerateSTOP()。

// 初始化I2CI2C_Init(...); // 生成起始位I2C_GenerateSTART(I2C1, ENABLE); // 发送从机地址(写操作)I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Transmitter); // 发送数据for (i = 0; i < data_length; i++) { I2C_SendData(I2C1, data[i]); // 等待发送完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));} // 生成停止位I2C_GenerateSTOP(I2C1, ENABLE);}


6. 主机接收模式(Master Receiver)

整个通信过程如下图:


其中阴影部分为主机负责,无阴影部分为从机负责。

详细数据帧格式如下图所示:


生成起始位;

发送从机地址(最低位为 1);

等待从机应答;

循环接收数据字节:

最后一个字节之前的数据接收后发送 ACK(I2C_AcknowledgeConfig(I2C1, ENABLE));

最后一个字节发送 NACK 并生成停止位。

// 生成起始位I2C_GenerateSTART(I2C1, ENABLE); // 发送从机地址(读操作)I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Receiver); // 接收多个字节if (num_bytes > 1) { // 启用ACK I2C_AcknowledgeConfig(I2C1, ENABLE); while (num_bytes-- > 2) { // 等待接收完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data[i++] = I2C_ReceiveData(I2C1); }} // 最后一个字节前禁用ACKI2C_AcknowledgeConfig(I2C1, DISABLE);I2C_GenerateSTOP(I2C1, ENABLE); // 接收最后一个字节while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));data[i] = I2C_ReceiveData(I2C1);

7. 从机接收模式(Slave Receiver)

等待主机发送起始位和匹配的从机地址;

接收主机发送的数据;

每字节后返回 ACK;

主机发送停止位后结束通信。


8. 从机发送模式(Slave Transmitter)

等待主机发送起始位和匹配的从机地址(读位);

向主机发送数据;

等待主机 ACK/NACK;

主机发送停止位后结束通信。


9.复合模式

9.1 连续读写数据(以连续读为例)

整个通信过程如下图:

  • 生成起始位;

  • 发送从机地址(最低位为 1);

  • 等待从机应答;

  • 循环接收数据字节;

  • 最后一个字节之前的数据接收后发送 ACK(I2C_AcknowledgeConfig(I2C1, ENABLE));

  • 最后一个字节发送 NACK 不生成停止位;

  • 重新生成起始位;

  • 发送从机地址(读位 1);

  • 等待从机应答;

  • 最后一个字节之前的数据接收后发送 ACK(I2C_AcknowledgeConfig(I2C1, ENABLE));

  • 最后一个字节发送 NACK 并生成停止位。

// 生成起始位I2C_GenerateSTART(I2C1, ENABLE); // 发送从机地址(读操作)I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Receiver); // 接收多个字节if (num_bytes > 1) { // 启用ACK I2C_AcknowledgeConfig(I2C1, ENABLE); while (num_bytes-- > 2) { // 等待接收完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data[i++] = I2C_ReceiveData(I2C1); }} // 最后一个字节前禁用ACKI2C_AcknowledgeConfig(I2C1, DISABLE); (等待I2C_EVENT_MASTER_MODE_SELECT事件,确保总线已释放) // 重新生成起始位I2C_GenerateSTART(I2C1, ENABLE); // 发送从机地址(读操作)I2C_Send7bitAddress(I2C1, SLAVE_ADDRESS, I2C_Direction_Receiver); // 接收多个字节if (num_bytes > 1) { // 启用ACK I2C_AcknowledgeConfig(I2C1, ENABLE); while (num_bytes-- > 2) { // 等待接收完成 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data[i++] = I2C_ReceiveData(I2C1); }} // 最后一个字节前禁用ACKI2C_AcknowledgeConfig(I2C1, DISABLE);I2C_GenerateSTOP(I2C1, ENABLE);// 接收最后一个字节while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));data[i] = I2C_ReceiveData(I2C1);


9.2 读写从机特定存储单元或功能寄存器(如读写EEPROM)

向从机寄存器写数据过程:

  • 生成起始位(I2C_GenerateSTART());

  • 发送从机地址(最低位为 0);

  • 等待从机应答;

  • 发送从机寄存器地址;

  • 等待从机应答;

  • 循环写入数据字节(I2C_SendData());

  • 生成停止位(I2C_GenerateSTOP()。

  • 读从机寄存器数据过程:

  • 生成起始位;

  • 发送从机地址(最低位为 0);

  • 等待从机应答;

  • 发送从机寄存器地址;

  • 等待从机应答;

  • 生成起始位;

  • 发送从机地址(最低位为 1);

  • 等待从机应答;

  • 循环接收数据字节:

  • 最后一个字节之前的数据接收后发送 ACK(I2C_AcknowledgeConfig(I2C1, ENABLE));

  • 最后一个字节发送 NACK 并生成停止位。


03


I2C功能说明


以stm32f40x为例,框图显示:数据移位寄存器是 SDA 线与 DR 寄存器之间的桥梁,所有数据收发均通过移位寄存器完成。

1.I²C 外设包含以下关键模块:

模块名称

功能描述

数据寄存器 (DR)

存放待发送或已接收的数据字节

数据移位寄存器

逐位移入/移出数据,与 SDA 线交互

地址寄存器 (OAR1/OAR2)

存储本机从机地址(7 位或 10 位)

PEC 寄存器

存放硬件 CRC 校验结果(SMBus 模式)

控制逻辑

产生起始/停止条件、时钟同步、仲裁检测

状态寄存器 (SR1/SR2)

反映当前通信状态(如 ADDR、TXE、RXNE 等)

时钟控制

根据 I²C_CR2 寄存器中的 FREQ 字段生成 SCL 时钟


2.数据发送流程(主发送器模式)

2.1 软件触发起始条件

  • CPU 置位 I2C_CR1.START = 1,硬件自动:

  • 拉低 SDA(SCL 为高)→ 产生起始信号 (S);

  • 置位 SR1.SB = 1(起始位标志)。


2.2 发送从机地址 + 写方向位

  • CPU 写入地址到 DR → 移位寄存器逐位移出到 SDA,同时硬件:

  • 等待从机应答 (ACK);

  • 收到 ACK 后置位 SR1.ADDR = 1(地址已发送);

  • 置位 SR1.TXE = 1(数据寄存器空,可写入新数据)。


2.3 连续发送数据字节

  • CPU 循环写入数据到 DR:

  • 每写入一次,TXE 自动清 0;

  • 移位寄存器移出 8 位数据 → 等待 ACK;

  • 收到 ACK 后 TXE 再次置 1,循环继续。

2.4 终止传输
CPU 置位 I2C_CR1.STOP = 1 → 硬件产生停止条件 (P),释放总线。

3.数据接收流程(主接收器模式)

3.1 起始条件 + 发送地址 + 读方向位
同发送流程步骤 1~2,但方向位为“读”。


3.2 接收数据字节

  • 从机逐位移出数据到 SDA → 移位寄存器逐位移入:

  • 每完整接收 1 字节,硬件自动:

  • 存入 DR;

  • 置位 SR1.RXNE = 1(数据寄存器非空);

  • 触发中断(若使能)。

3.3 CPU 读取数据

  • CPU 读取 DR → RXNE 自动清 0;

  • 同时硬件自动发送 ACK(继续接收)或 NACK(终止接收)。

3.4 终止传输

CPU 在最后一个字节前发送 NACK,随后置位 I2C_CR1.STOP = 1 → 产生停止条件 (P)。

4.关键寄存器与标志位

寄存器

标志位

含义

SR1

SB

起始条件已发送

ADDR

地址已发送/匹配

TXE

发送数据寄存器空

RXNE

接收数据寄存器非空

BTF

字节传输完成(发送/接收)

CR1

START

产生起始条件

STOP

产生停止条件

ACK

使能/禁止应答


04


硬件介绍


STM32的I2C1与EEPROM芯片直连,EEPROM的SDA和SCL接4.7KΩ上拉电阻至3.3V,上拉电阻建议连接到 3.3V,且走线长度不超过 10cm 以减少寄生电容,STM32与EEPROM芯片共地。

05


程序设计案例


stm32的I2C配置为主模式,通过I2C总线向EEPROM指定存储器地址写入数据,再通过I2C向EEPROM指定存储器地址读出数据,比较写入和读出的数据是否不一致,将结果通过串口打印出来

main.c

int main(void){ HAL_Init();  /* 配置系统时钟为168 MHz */  SystemClock_Config(); /* 初始化RGB彩灯 */  LED_GPIO_Config();  /*初始化USART 配置模式为 115200 8-N-1,中断接收*/ DEBUG_USART_Config();  printf("\r\n 这是一个I2C外设(AT24C02)读写测试例程 \r\n");  /* I2C 外设初(AT24C02)始化 */ I2C_EE_Init();  if(I2C_Test() ==1) { LED_GREEN; } else { LED_RED; }  while (1) {  } }


bsp_i2c_ee.c

void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c){  GPIO_InitTypeDef  GPIO_InitStruct;  /*##-1- Enable peripherals and GPIO Clocks #################################*/ /* Enable GPIO TX/RX clock */ I2Cx_SCL_GPIO_CLK_ENABLE(); I2Cx_SDA_GPIO_CLK_ENABLE(); /* Enable I2C1 clock */ I2Cx_CLK_ENABLE();   /*##-2- Configure peripheral GPIO ##########################################*/  /* I2C TX GPIO pin configuration  */ GPIO_InitStruct.Pin = I2Cx_SCL_PIN; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FAST; GPIO_InitStruct.Alternate = I2Cx_SCL_AF;  HAL_GPIO_Init(I2Cx_SCL_GPIO_PORT, &GPIO_InitStruct);  /* I2C RX GPIO pin configuration  */ GPIO_InitStruct.Pin = I2Cx_SDA_PIN; GPIO_InitStruct.Alternate = I2Cx_SDA_AF;  HAL_GPIO_Init(I2Cx_SDA_GPIO_PORT, &GPIO_InitStruct);  /* Force the I2C peripheral clock reset */  I2Cx_FORCE_RESET() ;   /* Release the I2C peripheral clock reset */  I2Cx_RELEASE_RESET(); }  /** * @brief I2C 工作模式配置 * @param * @retval */static void I2C_Mode_Config(void){  I2C_Handle.Instance = I2Cx;  I2C_Handle.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; I2C_Handle.Init.ClockSpeed = 400000; I2C_Handle.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; I2C_Handle.Init.DutyCycle = I2C_DUTYCYCLE_2; I2C_Handle.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; I2C_Handle.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; I2C_Handle.Init.OwnAddress1 = I2C_OWN_ADDRESS7 ; I2C_Handle.Init.OwnAddress2 = 0;  /* Init the I2C */ HAL_I2C_Init(&I2C_Handle);  //  HAL_I2CEx_AnalogFilter_Config(&I2C_Handle, ENABLE); } /** * @brief I2C 外设(EEPROM)初始化 * @param * @retval */void I2C_EE_Init(void){ I2C_Mode_Config(); } /** * @brief 将缓冲区中的数据写到I2C EEPROM中 * @param  * @arg pBuffer:缓冲区指针 * @arg WriteAddr:写地址 * @arg NumByteToWrite:写的字节数 * @retval */void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite){ uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;  Addr = WriteAddr % EEPROM_PAGESIZE; count = EEPROM_PAGESIZE - Addr; NumOfPage = NumByteToWrite / EEPROM_PAGESIZE; NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;  /* If WriteAddr is I2C_PageSize aligned  */ if(Addr == 0)  { /* If NumByteToWrite < I2C_PageSize */ if(NumOfPage == 0)  { I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); } /* If NumByteToWrite > I2C_PageSize */ else  { while(NumOfPage--) { I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);  WriteAddr += EEPROM_PAGESIZE; pBuffer += EEPROM_PAGESIZE; }  if(NumOfSingle!=0) { I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); } } } /* If WriteAddr is not I2C_PageSize aligned  */ else  { /* If NumByteToWrite < I2C_PageSize */ if(NumOfPage== 0)  { I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); } /* If NumByteToWrite > I2C_PageSize */ else { NumByteToWrite -= count; NumOfPage = NumByteToWrite / EEPROM_PAGESIZE; NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;   if(count != 0) {  I2C_EE_PageWrite(pBuffer, WriteAddr, count); WriteAddr += count; pBuffer += count; }   while(NumOfPage--) { I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE); WriteAddr += EEPROM_PAGESIZE; pBuffer += EEPROM_PAGESIZE;  } if(NumOfSingle != 0) { I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);  } } } } /** * @brief 写一个字节到I2C EEPROM中 * @param  * @arg pBuffer:缓冲区指针 * @arg WriteAddr:写地址  * @retval */uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr){ HAL_StatusTypeDef status = HAL_OK;  status = HAL_I2C_Mem_Write(&I2C_Handle, EEPROM_ADDRESS, (uint16_t)WriteAddr, I2C_MEMADD_SIZE_8BIT, pBuffer, 1, 100);   /* Check the communication status */ if(status != HAL_OK) { /* Execute user timeout callback */ //I2Cx_Error(Addr); } while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY) {  }  /* Check if the EEPROM is ready for a new operation */ while (HAL_I2C_IsDeviceReady(&I2C_Handle, EEPROM_ADDRESS, EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);  /* Wait for the end of the transfer */ while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY) {  } return status;} /** * @brief 在EEPROM的一个写循环中可以写多个字节,但一次写入的字节数 *          不能超过EEPROM页的大小,AT24C02每页有8个字节 * @param  * @arg pBuffer:缓冲区指针 * @arg WriteAddr:写地址 * @arg NumByteToWrite:写的字节数 * @retval */uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite){ HAL_StatusTypeDef status = HAL_OK; /* Write EEPROM_PAGESIZE */ status=HAL_I2C_Mem_Write(&I2C_Handle, EEPROM_ADDRESS,WriteAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t*)(pBuffer),NumByteToWrite, 100);  while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY) {  }  /* Check if the EEPROM is ready for a new operation */ while (HAL_I2C_IsDeviceReady(&I2C_Handle, EEPROM_ADDRESS, EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);  /* Wait for the end of the transfer */ while (HAL_I2C_GetState(&I2C_Handle) != HAL_I2C_STATE_READY) {  } return status;} /** * @brief 从EEPROM里面读取一块数据  * @param  * @arg pBuffer:存放从EEPROM读取的数据的缓冲区指针 * @arg WriteAddr:接收数据的EEPROM的地址 * @arg NumByteToWrite:要从EEPROM读取的字节数 * @retval */uint32_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead){ HAL_StatusTypeDef status = HAL_OK;  status=HAL_I2C_Mem_Read(&I2C_Handle,EEPROM_ADDRESS,ReadAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t *)pBuffer, NumByteToRead,1000);  return status;}


06


实验结果


对0~255存储器地址地址写入数据0~255,写入和读出数据完全一致。



声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。 微信联系小助理
0
评论
  • 相关技术文库
  • 单片机
  • 嵌入式
  • MCU
  • STM
下载排行榜
更多
评测报告
更多
广告