07-I2C通信与EEPROM 24C02.md 7.6 KB


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 是传的纸条(数据)。每次老师拍一下手(时钟跳变),纸条就传递一位信息。每个同学有唯一的学号(设备地址),只有学号对得上的人才能接收纸条。


I2C协议基础

I2C(Inter-Integrated Circuit)是一种同步、半双工、多主从的串行通信总线,仅需 2 根线:SCL(时钟线)和 SDA(数据线)。

特性 说明
线数 SCL + SDA 两根
模式 半双工
速率 标准 100Kbps,快速 400Kbps
主从 多主多从(通过仲裁解决冲突)
地址 7位地址(最多 127 个设备)或 10位地址
上拉 两根线都需要外部上拉电阻(4.7KΩ 典型)

I2C时序协议详解

I2C 协议由几个基本操作组成:

起始条件(Start Condition)

SCL 在高电平时,SDA 从高电平切换到低电平
SCL  ────┐  ┌────
          │  │
SDA  ────┘──┘────

终止条件(Stop Condition)

SCL 在高电平时,SDA 从低电平切换到高电平
SCL  ────┐  ┌────
          │  │
SDA  ────┘──┘────

数据字节传输

每个字节 8 位,MSB 先发。 SCL 低电平时 SDA 改变数据,SCL 高电平时 SDA 保持稳定。

应答(ACK/NACK)

每个字节后,接收方在第 9 个 SCL 周期拉低 SDA 表示应答(ACK),释放 SDA 表示非应答(NACK)。

I2C完整通信流程

主机写数据到从机

START + 从机地址(W) + ACK + 数据1 + ACK + 数据2 + ACK + ... + STOP

主机从从机读数据

START + 从机地址(R) + ACK + 数据1 + ACK + 数据2 + NACK + STOP

I2C硬件电路设计

I2C 总线需要外部上拉电阻(RPU):

VCC(3.3V)
  ├── RPU(4.7KΩ)
  ├──── SCL
  ├── RPU(4.7KΩ)  
  └──── SDA

软件模拟 I2C 使用 GPIO 的开漏输出模式(CNF=01, MODE=11):

  • 开漏输出允许"线与"(多个设备可同时拉低总线)
  • 高电平靠外部上拉电阻实现

参考:原理图 I2C 接口部分(24C02 电路),参考手册 §16


24C02 EEPROM

参数
容量 256 字节(2Kbit)
页大小 8 字节(页写最多 8 字节)
地址 0xA0(写)/ 0xA1(读)
接口 I2C
工作电压 1.8V~5.5V

页写限制:24C02 每页 8 字节,跨页写时需要手动分页(超过页边界会自动卷回页开头)。

写周期:每个写操作后需要等待 5ms(内部擦写时间),在此期间不响应任何 I2C 命令。


实验:软件模拟 I2C 读写 24C02

软件设计(寄存器版 — 软件模拟 I2C)

项目路径stm32_base/13_i2c_software_register

文件:stm32_base/13_i2c_software_register/Hardware/I2C/i2c.h

#ifndef __I2C_H
#define __I2C_H

#include "stm32f10x.h"

// 引脚宏定义:PB10=SCL, PB11=SDA
#define SCL_HIGH  (GPIOB->ODR |= GPIO_ODR_ODR10)   // |= SCL置1
#define SCL_LOW   (GPIOB->ODR &= ~GPIO_ODR_ODR10)  // &=~ SCL清0
#define SDA_HIGH  (GPIOB->ODR |= GPIO_ODR_ODR11)   // |= SDA置1
#define SDA_LOW   (GPIOB->ODR &= ~GPIO_ODR_ODR11)  // &=~ SDA清0

#define READ_SDA  (GPIOB->IDR & GPIO_IDR_IDR11)    // & 读取SDA电平

#define ACK  0
#define NACK 1

#define I2C_DELAY  Delay_us(10)  // 100KHz:半周期延时10us

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_base/13_i2c_software_register/Hardware/I2C/i2c.c(核心函数)

#include "i2c.h"

void I2C_Init(void)
{
    // 开启GPIOB时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    // PB10(PB11) 开漏输出:MODE=11(50MHz), CNF=01(开漏)
    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);   // MODE位置1
    GPIOB->CRH &= ~(GPIO_CRH_CNF10_1 | GPIO_CRH_CNF11_1); // CNF[1]清0
    GPIOB->CRH |= (GPIO_CRH_CNF10_0 | GPIO_CRH_CNF11_0);  // CNF[0]置1
}

// 起始:SCL高电平时,SDA高→低
void I2C_Start(void)
{
    SCL_HIGH;  // 先确保SCL高
    SDA_HIGH;  // SDA先高
    I2C_DELAY;
    SDA_LOW;   // SDA从高变低 → 起始条件
    I2C_DELAY;
}

// 停止:SCL高电平时,SDA低→高
void I2C_Stop(void)
{
    SCL_HIGH;  // SCL拉高
    SDA_LOW;   // SDA先低
    I2C_DELAY;
    SDA_HIGH;  // SDA从低变高 → 停止条件
    I2C_DELAY;
}

// 发送ACK:在第9个时钟将SDA拉低
void I2C_Ack(void)
{
    SDA_LOW;   // SDA拉低表示应答
    SCL_HIGH;  // SCL产生脉冲
    I2C_DELAY;
    SCL_LOW;
    I2C_DELAY;
    SDA_HIGH;  // 释放SDA
}

// 发送NACK:在第9个时钟释放SDA
void I2C_NAck(void)
{
    SDA_HIGH;  // SDA高表示非应答
    SCL_HIGH;  // SCL产生脉冲
    I2C_DELAY;
    SCL_LOW;
}

// 等待从机ACK:返回0=有ACK, 1=无ACK
uint8_t I2C_Wait4Ack(void)
{
    SDA_HIGH;  // 释放SDA(让从机控制)
    SCL_HIGH;  // SCL高电平采样
    I2C_DELAY;
    uint8_t ack = READ_SDA;  // &测试: 读取SDA电平
    SCL_LOW;
    I2C_DELAY;
    return ack ? NACK : ACK;  // SDA=0: 从机应答; SDA=1: 无应答
}

文件:stm32_base/13_i2c_software_register/User/main.c

#include "i2c.h"

int main(void)
{
    usart1_init();         // 串口初始化(用于打印调试)
    I2C_Init();            // I2C GPIO初始化

    // 测试:向24C02地址0x00写入0x55
    I2C_Start();
    I2C_SendByte(0xA0);    // 从机地址+写 (0xA0)
    I2C_Wait4Ack();        // 等待24C02应答
    I2C_SendByte(0x00);    // 写入地址(24C02的字节地址)
    I2C_Wait4Ack();
    I2C_SendByte(0x55);    // 写入数据
    I2C_Wait4Ack();
    I2C_Stop();

    Delay_ms(10);          // 等待内部写周期完成

    // 读回验证:从地址0x00读取
    I2C_Start();
    I2C_SendByte(0xA0);
    I2C_Wait4Ack();
    I2C_SendByte(0x00);    // 再次发出地址
    I2C_Wait4Ack();
    I2C_Start();           // 重启(组合格式)
    I2C_SendByte(0xA1);    // 从机地址+读 (0xA1)
    I2C_Wait4Ack();
    uint8_t data = I2C_ReadByte();  // 读取数据
    I2C_NAck();            // 主机发NACK表示读取结束
    I2C_Stop();

    printf("Read data: 0x%02X\r\n", data);  // 应输出0x55
    while (1);
}

核心函数速查表

函数 说明
I2C_Init() PB10(SCL)+PB11(SDA) 开漏输出初始化
I2C_Start() SCL高→SDA高→低切换,产生起始条件
I2C_Stop() SCL高→SDA低→高切换,产生停止条件
I2C_SendByte(byte) 逐位发送8位数据(MSB first)
I2C_ReadByte() 逐位读取8位数据
I2C_Wait4Ack() 等待从机ACK(返回0=应答)
I2C_Ack() 主机发送ACK
I2C_NAck() 主机发送NACK(读最后一个字节后)

常见问题与避坑

  1. I2C 总线卡死(SDA 一直为低) → 从机异常,发 9 个以上的 SCL 脉冲让从机释放 SDA
  2. 24C02 写入后读回不对 → 页写溢出(跨页写会自动卷回),需分页写入
  3. NACK 持续 → 设备地址错误、上拉电阻缺失、SCL 频率太高
  4. Start 条件后无 ACK → 检查从机电源、I2C 地址(7位+读写位 = 8位)
  5. 软件 I2C 有时钟干扰 → I2C_DELAY 时间不够,增加至 20~50us