--- tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(基础篇)V1.0.2 — 第9章:I2C通信 + 配套代码13~15" author: "尚硅谷研究院" date: 2026-07-15 created: 2026-07-15 --- # I2C通信与EEPROM 24C02 > **用生活理解**:I2C 就像教室里的传纸条游戏——SCL 是老师拍手的节奏(时钟线),SDA 是传的纸条(数据线)。老师每拍一次手(时钟周期),大家就传递一位。每个同学有唯一的学号(设备地址 7 位),老师喊学号时对应同学才回答。STOP 条件就像下课铃——老师拍手节奏变了,表示传纸条结束。 --- ## I2C 协议基础 I2C(Inter-Integrated Circuit,读作 I-squared-C)是一种**同步、半双工、多主从**的串行通信总线,由 Philips 公司(现 NXP)发明。 | 特性 | 说明 | |------|------| | **线数** | SCL(时钟)+ SDA(数据)两根线 | | **模式** | 半双工(同一时刻只能单向传输) | | **速率** | 标准 100Kbps / 快速 400Kbps / 高速 3.4MHz | | **主从** | 多主多从(通过仲裁解决冲突) | | **地址** | 7 位地址(最多 127 个设备)或 10 位地址 | | **上拉电阻** | 两根线都需要外部上拉电阻(4.7KΩ 典型) | > **参考**:参考手册 §16(I2C 寄存器描述) > **参考**:NXP I2C 总线规范(UM10204) ### I2C 时序协议详解 #### 空闲状态 SCL 和 SDA 都是高电平(由上拉电阻拉高)。 #### 起始条件(Start Condition) ``` SCL 为高电平时,SDA 从高电平切换到低电平 ``` **代码实现**: ```c SCL_HIGH; // SCL = 1 SDA_HIGH; // SDA = 1 I2C_DELAY; // 等待(数据建立时间) SDA_LOW; // SDA = 0 → 起始条件(在 SCL 高电平时 SDA 变低) I2C_DELAY; // 等待(保持时间) ``` #### 停止条件(Stop Condition) ``` SCL 为高电平时,SDA 从低电平切换到高电平 ``` **代码实现**: ```c SCL_LOW; // SCL = 0(准备) SDA_LOW; // SDA = 0 SCL_HIGH; // SCL = 1 I2C_DELAY; SDA_HIGH; // SDA = 0 → 1 → 终止条件(在 SCL 高电平时 SDA 变高) I2C_DELAY; ``` #### 数据字节传输 每个字节 8 位,MSB(最高位)先发。SCL 低电平时允许 SDA 变化,SCL 高电平时 SDA 必须**稳定**(接收方采集数据)。 ``` SCL: ████░░░░████░░░░████░░░░....████░░░░██ SDA: ██░░██░░██░░██░░ ██░░██░░ ↑MSB ↑LSB SCL低时SDA可以变 SCL高时采集 ``` #### 应答(ACK/NACK) 每个字节后,接收方在第 9 个 SCL 周期控制 SDA: - **ACK**:SDA 拉低(表示"收到,继续") - **NACK**:SDA 释放(高电平,表示"收完"或"不再接收") ``` 主机发送8位数据后: 主机释放SDA → 从机在第9个SCL脉冲拉低SDA → 应答(ACK) ``` ### I2C 完整通信流程 **主机写数据到从机**: ``` START + 7位从机地址 + R/W(0=写) + ACK + 数据1 + ACK + 数据2 + ACK + ... + STOP ``` **主机从从机读数据**: ``` START + 7位从机地址 + R/W(1=读) + ACK + 从机发数据1 + 主机ACK + ... + 从机发数据N + 主机NACK + STOP ``` **组合格式(先写地址再读)**: ``` START + 从机地址(W) + ACK + 寄存器地址 + ACK + RESTART + 从机地址(R) + ACK + 数据 + NACK + STOP ``` --- ## I2C 硬件电路设计 ```ascii VCC(3.3V) VCC(3.3V) ├── Rpu(4.7KΩ) ├── Rpu(4.7KΩ) │ │ ├──── SCL ─────────┤ │ PB10 │ 24C02 SCL ├──── SDA ─────────┤ │ PB11 │ 24C02 SDA │ │ GND ────────────── GND ``` I2C 总线需要外部上拉电阻(典型值 4.7KΩ),因为 I2C 引脚使用**开漏输出**: - 输出低电平:N-MOS 导通,拉低总线 - 输出高电平:N-MOS 截止,靠上拉电阻拉高(开漏原理) - 这种设计允许多个设备"线与"(任意设备可拉低总线) > **参考**:开发板原理图 I2C 接口部分(24C02 电路) > **参考**:参考手册 §16(I2C 寄存器) --- ## 24C02 EEPROM 详解 24C02 是一个 **2Kbit(256 字节)** 的 I2C 串行 EEPROM。 | 参数 | 值 | |------|-----| | 容量 | 256 字节(2Kbit) | | 页大小 | **8 字节**(一次页写最多 8 字节) | | 写周期 | 5ms(内部擦写时间,期间不响应) | | I2C 地址 | 0xA0(写)/ 0xA1(读) | | 地址引脚 | A0/A1/A2(可配置,最多 8 个同型号设备共用总线) | | 工作电压 | 1.8V~5.5V | | 接口 | I2C(标准/快速模式) | **地址计算**: ``` 设备地址 = 1010 + A2 + A1 + A0 + R/W [7:4] [3] [2] [1] [0] 1010 0 0 0 0/1 写地址 = 1010 0000 = 0xA0 读地址 = 1010 0001 = 0xA1 ``` **页写注意事项**:24C02 的页大小为 8 字节,一次页写操作只能在**同一页内**。如果尝试跨页写入(如从地址 0x07 开始写 3 字节),地址会**自动卷回**到页开头(地址 0x00),覆盖已有数据。跨页时必须**手动分页写入**。 --- ## 实验:软件模拟 I2C 读写 24C02 ### 软件设计(寄存器版 — 软件模拟 I2C) **项目路径**:`stm32/13_i2c_software_register` **文件:`stm32/13_i2c_software_register/Hardware/I2C/i2c.h`** ```c #ifndef __I2C_H #define __I2C_H #include "stm32f10x.h" #include "usart.h" #include "delay.h" // PB10=SCL, PB11=SDA #define SCL_HIGH (GPIOB->ODR |= GPIO_ODR_ODR10) #define SCL_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR10) #define SDA_HIGH (GPIOB->ODR |= GPIO_ODR_ODR11) #define SDA_LOW (GPIOB->ODR &= ~GPIO_ODR_ODR11) #define READ_SDA (GPIOB->IDR & GPIO_IDR_IDR11) #define ACK 0 #define NACK 1 #define I2C_DELAY Delay_us(10) void I2C_Init(void); void I2C_Start(void); void I2C_Stop(void); void I2C_Ack(void); void I2C_Nack(void); uint8_t I2C_Wait4Ack(void); void I2C_SendByte(uint8_t byte); uint8_t I2C_ReadByte(void); #endif ``` **文件:`stm32/13_i2c_software_register/Hardware/I2C/i2c.c`** ```c #include "i2c.h" void I2C_Init(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11); GPIOB->CRH &= ~(GPIO_CRH_CNF10_1 | GPIO_CRH_CNF11_1); GPIOB->CRH |= (GPIO_CRH_CNF10_0 | GPIO_CRH_CNF11_0); } void I2C_Start(void) { SCL_HIGH; SDA_HIGH; I2C_DELAY; SDA_LOW; I2C_DELAY; } void I2C_Stop(void) { SCL_HIGH; SDA_LOW; I2C_DELAY; SDA_HIGH; I2C_DELAY; } void I2C_Ack(void) { SCL_LOW; SDA_HIGH; I2C_DELAY; SDA_LOW; I2C_DELAY; SCL_HIGH; I2C_DELAY; SCL_LOW; I2C_DELAY; SDA_HIGH; I2C_DELAY; } void I2C_Nack(void) { SCL_LOW; SDA_HIGH; I2C_DELAY; SCL_HIGH; I2C_DELAY; SCL_LOW; I2C_DELAY; } uint8_t I2C_Wait4Ack(void) { SDA_HIGH; SCL_LOW; I2C_DELAY; SCL_HIGH; I2C_DELAY; uint8_t ack = READ_SDA; SCL_LOW; I2C_DELAY; return ack ? NACK : ACK; } void I2C_SendByte(uint8_t byte) { for (uint8_t i = 0; i < 8; i++) { SCL_LOW; I2C_DELAY; if (byte & 0x80) SDA_HIGH; else SDA_LOW; I2C_DELAY; SCL_HIGH; I2C_DELAY; SCL_LOW; I2C_DELAY; byte <<= 1; } } uint8_t I2C_ReadByte(void) { uint8_t data = 0; for (uint8_t i = 0; i < 8; i++) { SDA_HIGH; SCL_LOW; I2C_DELAY; SCL_HIGH; I2C_DELAY; data <<= 1; if (READ_SDA) data |= 0x01; SCL_LOW; I2C_DELAY; } return data; } ``` **文件:`stm32/13_i2c_software_register/User/main.c`** ```c #include "usart.h" #include "m24c02.h" #include int main(void) { USART_Init(); M24C02_Init(); printf("尚硅谷 I2C 软件模拟实验...\n"); M24C02_WriteByte(0x00, 'a'); M24C02_WriteByte(0x01, 'b'); M24C02_WriteByte(0x02, 'c'); uint8_t byte1 = M24C02_ReadByte(0x00); uint8_t byte2 = M24C02_ReadByte(0x01); uint8_t byte3 = M24C02_ReadByte(0x02); printf("%c\n%c\n%c\n", byte1, byte2, byte3); M24C02_WriteBytes(0x00, "123456", 6); uint8_t buff[50] = {0}; M24C02_ReadBytes(0x00, buff, 6); printf("%s\n", buff); memset(buff, 0, sizeof(buff)); M24C02_WriteBytes(0x05, "1234567890abcdefghijk", 21); M24C02_ReadBytes(0x00, buff, 21); printf("%s\n", buff); while (1) {} } ``` --- ## 实验:硬件 I2C 读写 24C02 ### 软件设计(寄存器版 — 硬件 I2C) **项目路径**:`stm32/14_i2c_hardware_register` **文件:`stm32/14_i2c_hardware_register/Hardware/I2C/i2c.h`** ```c #ifndef __I2C_H #define __I2C_H #include "stm32f10x.h" #include "delay.h" #define OK 0 #define FAIL 1 void I2C_Init(void); uint8_t I2C_Start(void); void I2C_Stop(void); void I2C_Ack(void); void I2C_Nack(void); uint8_t I2C_SendAddr(uint8_t addr); uint8_t I2C_SendByte(uint8_t byte); uint8_t I2C_ReadByte(void); #endif ``` **文件:`stm32/14_i2c_hardware_register/Hardware/I2C/i2c.c`** ```c #include "i2c.h" void I2C_Init(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; RCC->APB1ENR |= RCC_APB1ENR_I2C2EN; GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11 | GPIO_CRH_CNF10 | GPIO_CRH_CNF11); I2C2->CR1 &= ~I2C_CR1_SMBUS; I2C2->CCR &= ~I2C_CCR_FS; I2C2->CR2 |= 36; I2C2->CCR |= 180; I2C2->TRISE |= 37; I2C2->CR1 |= I2C_CR1_PE; } uint8_t I2C_Start(void) { I2C2->CR1 |= I2C_CR1_START; uint16_t timeout = 0xffff; while ((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout) { timeout--; } return timeout ? OK : FAIL; } void I2C_Stop(void) { I2C2->CR1 |= I2C_CR1_STOP; } void I2C_Ack(void) { I2C2->CR1 |= I2C_CR1_ACK; } void I2C_Nack(void) { I2C2->CR1 &= ~I2C_CR1_ACK; } uint8_t I2C_SendAddr(uint8_t addr) { I2C2->DR = addr; uint16_t timeout = 0xffff; while ((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout) { timeout--; } if (timeout > 0) { I2C2->SR2; } return timeout ? OK : FAIL; } uint8_t I2C_SendByte(uint8_t byte) { uint16_t timeout = 0xffff; while ((I2C2->SR1 & I2C_SR1_TXE) == 0 && timeout) { timeout--; } I2C2->DR = byte; timeout = 0xffff; while ((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout) { timeout--; } return timeout ? OK : FAIL; } uint8_t I2C_ReadByte(void) { uint16_t timeout = 0xffff; while ((I2C2->SR1 & I2C_SR1_RXNE) == 0 && timeout) { timeout--; } return timeout ? I2C2->DR : FAIL; } ``` **文件:`stm32/14_i2c_hardware_register/User/main.c`** ```c #include "usart.h" #include "m24c02.h" #include int main(void) { USART_Init(); M24C02_Init(); printf("尚硅谷 I2C 硬件寄存器实验...\n"); M24C02_WriteByte(0x00, 'a'); M24C02_WriteByte(0x01, 'b'); M24C02_WriteByte(0x02, 'c'); uint8_t byte1 = M24C02_ReadByte(0x00); uint8_t byte2 = M24C02_ReadByte(0x01); uint8_t byte3 = M24C02_ReadByte(0x02); printf("byte1 = %c\t byte2 = %c\t byte3 = %c\n", byte1, byte2, byte3); M24C02_WriteBytes(0x00, "123456", 6); uint8_t buffer[100] = {0}; M24C02_ReadBytes(0x00, buffer, 6); printf("buffer = %s\n", buffer); memset(buffer, 0, sizeof(buffer)); M24C02_WriteBytes(0x00, "1234567890abcdefghijk", 21); M24C02_ReadBytes(0x00, buffer, 21); printf("buffer = %s\n", buffer); while (1) { } } ``` --- ## 实验:HAL 库硬件 I2C 读写 24C02 ### 软件设计(HAL 库版 — 硬件 I2C) **项目路径**:`stm32/15_i2c_hardware_hal` **文件:`stm32/15_i2c_hardware_hal/Core/Src/i2c.c`** (CubeMX generated I2C init) ```c #include "i2c.h" I2C_HandleTypeDef hi2c2; void MX_I2C2_Init(void) { hi2c2.Instance = I2C2; hi2c2.Init.ClockSpeed = 100000; hi2c2.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c2.Init.OwnAddress1 = 0; hi2c2.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c2.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c2.Init.OwnAddress2 = 0; hi2c2.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c2.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c2) != HAL_OK) { Error_Handler(); } } void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle) { GPIO_InitTypeDef GPIO_InitStruct = {0}; if(i2cHandle->Instance==I2C2) { __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); __HAL_RCC_I2C2_CLK_ENABLE(); } } ``` **文件:`stm32/15_i2c_hardware_hal/Core/Src/m24c02.c`** ```c #include "m24c02.h" void M24C02_Init(void) { MX_I2C2_Init(); } void M24C02_WriteByte(uint8_t innerAddr, uint8_t byte) { HAL_I2C_Mem_Write(&hi2c2, W_ADDR, innerAddr, I2C_MEMADD_SIZE_8BIT, &byte, 1, 1000); HAL_Delay(5); } uint8_t M24C02_ReadByte(uint8_t innerAddr) { uint8_t byte; HAL_I2C_Mem_Read(&hi2c2, R_ADDR, innerAddr, I2C_MEMADD_SIZE_8BIT, &byte, 1, 1000); return byte; } void M24C02_WriteBytes(uint8_t innerAddr, uint8_t *bytes, uint8_t size) { HAL_I2C_Mem_Write(&hi2c2, W_ADDR, innerAddr, I2C_MEMADD_SIZE_8BIT, bytes, size, 1000); HAL_Delay(5); } void M24C02_ReadBytes(uint8_t innerAddr, uint8_t *buffer, uint8_t size) { HAL_I2C_Mem_Read(&hi2c2, R_ADDR, innerAddr, I2C_MEMADD_SIZE_8BIT, buffer, size, 1000); } ``` **文件:`stm32/15_i2c_hardware_hal/Core/Src/main.c`** ```c #include "main.h" #include "i2c.h" #include "usart.h" #include "gpio.h" #include "m24c02.h" #include int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C2_Init(); MX_USART1_UART_Init(); printf("尚硅谷 I2C HAL 库实验...\n"); M24C02_WriteByte(0x00, 'a'); M24C02_WriteByte(0x01, 'b'); M24C02_WriteByte(0x02, 'c'); uint8_t byte1 = M24C02_ReadByte(0x00); uint8_t byte2 = M24C02_ReadByte(0x01); uint8_t byte3 = M24C02_ReadByte(0x02); printf("byte1 = %c\t byte2 = %c\t byte3 = %c\n", byte1, byte2, byte3); M24C02_WriteBytes(0x00, "123456", 6); uint8_t buffer[100] = {0}; M24C02_ReadBytes(0x00, buffer, 6); printf("buffer = %s\n", buffer); memset(buffer, 0, sizeof(buffer)); M24C02_WriteBytes(0x00, "1234567890abcdefghijk", 21); M24C02_ReadBytes(0x00, buffer, 21); printf("buffer = %s\n", buffer); while (1) { } } ``` ## 核心函数速查表 | I2C 操作 | 软件模拟 | 硬件 I2C(寄存器) | 硬件 I2C(HAL) | |----------|---------|-------------------|----------------| | 初始化 | `I2C_Init()` | `I2C_Init()` | `HAL_I2C_Init()` / `MX_I2C2_Init()` | | 写 EEPROM | `I2C_Start/SendByte/Stop` 组合 | `I2C_SendAddr/SendByte/Stop` 组合 | `HAL_I2C_Mem_Write(addr, mem_addr, data, len, timeout)` | | 读 EEPROM | `Start+写地址+寄存器地址+RESTART+读地址+数据+NACK+Stop` | `I2C_SendAddr/SendByte/Restart/ReadByte` 组合 | `HAL_I2C_Mem_Read(addr, mem_addr, data, len, timeout)` | | 发送字节 | `I2C_SendByte(byte)` | `I2C_SendByte(byte)` | —(底层封装在 HAL 中) | | 读取字节 | `I2C_ReadByte()` | `I2C_ReadByte()` | —(底层封装在 HAL 中) | | 等待应答 | `I2C_Wait4Ack()` | —(硬件自动处理) | —(硬件自动处理) | | 发应答/非应答 | `I2C_Ack()` / `I2C_Nack()` | `I2C_Ack()` / `I2C_Nack()` | —(硬件自动处理) | | 启动传输 | `I2C_Start()` | `I2C_Start()` | —(封装在 Mem_Write/Read 中) | | 停止传输 | `I2C_Stop()` | `I2C_Stop()` | —(封装在 Mem_Write/Read 中) | ## 常见问题与避坑 1. **I2C 总线卡死(SDA 一直被拉低)** → 从机异常,连续发 9~18 个 SCL 脉冲让从机释放 SDA,然后发 STOP 2. **NACK 持续(从机不应答)** → 检查设备地址(7 位+读写位是否正确)、上拉电阻是否焊接、SCL 频率是否太高 3. **24C02 写入后读回不对** → 页写跨页溢出(每页 8 字节,跨页自动卷回!),需分页写入或逐字节写 4. **写操作后需等待** → 24C02 需要约 5ms 内部写周期,在此期间不响应任何 I2C 操作 5. **组合格式(先写后读)** → 中间的 RESTART 不是 STOP+START,必须用 `I2C_Start()` 但不先发 `I2C_Stop()` 6. **延时偏短导致时序异常** → I2C_DELAY 不够时增加(10us→20us),标准模式至少 5us 半周期