07-I2C通信与EEPROM 24C02.md 12 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 是传的纸条(数据线)。老师每拍一次手(时钟周期),大家就传递一位。每个同学有唯一的学号(设备地址 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 从高电平切换到低电平

代码实现

SCL_HIGH;       // SCL = 1
SDA_HIGH;       // SDA = 1
I2C_DELAY;      // 等待(数据建立时间)
SDA_LOW;        // SDA = 0 → 起始条件(在 SCL 高电平时 SDA 变低)
I2C_DELAY;      // 等待(保持时间)

停止条件(Stop Condition)

SCL 为高电平时,SDA 从低电平切换到高电平

代码实现

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 硬件电路设计

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_base/13_i2c_software_register

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

#ifndef __I2C_H
#define __I2C_H

#include "stm32f10x.h"

// GPIO 引脚: PB10=SCL, PB11=SDA
// 使用宏定义简化位操作,提升代码可读性
#define SCL_HIGH  (GPIOB->ODR |=  GPIO_ODR_ODR10)   // |= 将ODR10置1
#define SCL_LOW   (GPIOB->ODR &= ~GPIO_ODR_ODR10)   // &=~ 将ODR10清0
#define SDA_HIGH  (GPIOB->ODR |=  GPIO_ODR_ODR11)   // |= 将ODR11置1
#define SDA_LOW   (GPIOB->ODR &= ~GPIO_ODR_ODR11)   // &=~ 将ODR11清0
#define READ_SDA  (GPIOB->IDR &   GPIO_IDR_IDR11)   // & 读取IDR11位

#define ACK  0
#define NACK 1

// I2C 延时: 100KHz 模式下 10us ≈ 半个时钟周期
// 调整此值可改变 I2C 通信速率
#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_base/13_i2c_software_register/Hardware/I2C/i2c.c

#include "i2c.h"

// I2C 初始化: PB10(SCL) + PB11(SDA) 开漏输出
void I2C_Init(void)
{
    // 1. 开启 GPIOB 时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;       // |=: IOPBEN位置1

    // 2. PB10, PB11 开漏输出 50MHz
    // CNF=01(开漏输出), MODE=11(50MHz)
    // MODE10[1:0] 在 CRH 第 8~9 位, CNF10[1:0] 在第 10~11 位
    // MODE11[1:0] 在 CRH 第 12~13 位, CNF11[1:0] 在第 14~15 位
    GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11);       // MODE=11
    GPIOB->CRH &= ~(GPIO_CRH_CNF10_1 | GPIO_CRH_CNF11_1);    // CNFx[1]=0
    GPIOB->CRH |= (GPIO_CRH_CNF10_0 | GPIO_CRH_CNF11_0);     // CNFx[0]=1
    // CNF[1:0]=01 → 开漏输出
}

// 起始条件: SCL=1 时 SDA 下降沿
void I2C_Start(void)
{
    SCL_HIGH;   // 先确保 SCL 高电平(准备总线)
    SDA_HIGH;   // SDA 先高
    I2C_DELAY;
    SDA_LOW;    // SDA 从高变低 → 起始条件
    I2C_DELAY;
    // 起始后 SCL 可拉低准备传输数据
    SCL_LOW;
}

// 停止条件: SCL=1 时 SDA 上升沿
void I2C_Stop(void)
{
    SCL_LOW;    // 先确保 SCL 低
    SDA_LOW;    // SDA 拉低
    I2C_DELAY;
    SCL_HIGH;   // SCL 先高
    I2C_DELAY;
    SDA_HIGH;   // SDA 从低变高 → 停止条件
    I2C_DELAY;
}

// 发送应答: 在第 9 个时钟拉低 SDA
void I2C_Ack(void)
{
    SDA_LOW;    // SDA 拉低 = 应答
    I2C_DELAY;
    SCL_HIGH;   // SCL 脉冲(从机采样)
    I2C_DELAY;
    SCL_LOW;
    I2C_DELAY;
    SDA_HIGH;   // 释放 SDA
}

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

// 等待从机应答: 返回 0=ACK, 1=NACK
uint8_t I2C_Wait4Ack(void)
{
    SDA_HIGH;   // 释放 SDA(让从机控制)
    I2C_DELAY;
    SCL_HIGH;   // SCL 高 = 从机可应答
    I2C_DELAY;

    uint8_t ack = READ_SDA;   // & 测试: 读取 SDA 电平
    // SDA=0 → 从机应答(ACK); SDA=1 → 从机非应答(NACK)

    SCL_LOW;    // 完成应答位
    I2C_DELAY;

    return ack ? NACK : ACK;
}

// 发送 1 字节(MSB first)
void I2C_SendByte(uint8_t byte)
{
    for (uint8_t i = 0; i < 8; i++)
    {
        // SCL 低电平时改变 SDA
        if (byte & 0x80)     // &测试: 最高位=1?
            SDA_HIGH;
        else
            SDA_LOW;

        byte <<= 1;           // 左移, 准备下一位

        // SCL 高电平期间 SDA 保持稳定(从机采样)
        SCL_HIGH;
        I2C_DELAY;
        SCL_LOW;
        I2C_DELAY;
    }
}

// 读取 1 字节(MSB first)
uint8_t I2C_ReadByte(void)
{
    uint8_t byte = 0;

    SDA_HIGH;   // 释放 SDA(让从机控制)

    for (uint8_t i = 0; i < 8; i++)
    {
        byte <<= 1;           // 左移留出低位

        SCL_HIGH;             // SCL 高 → 从机将数据放到 SDA
        I2C_DELAY;

        if (READ_SDA)         // & 测试: 读取 SDA 电平
            byte |= 1;        // 如果 SDA=1, 置当前位为 1

        SCL_LOW;              // SCL 低 → 从机准备下一位
        I2C_DELAY;
    }

    return byte;
}

文件: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 = 1010 0000)
    I2C_Wait4Ack();          // 等待 24C02 应答

    I2C_SendByte(0x00);      // 要写入的 24C02 内部字节地址
    I2C_Wait4Ack();

    I2C_SendByte(0x55);      // 要写入的数据
    I2C_Wait4Ack();
    I2C_Stop();

    // 等待 24C02 内部写周期完成(约 5ms)
    Delay_ms(10);

    /* ========== 读操作: 从 24C02 地址 0x00 读取数据 ========== */
    // 组合格式: START + 写地址 + 寄存器地址 + RESTART + 读地址 + 数据
    I2C_Start();
    I2C_SendByte(0xA0);      // 写地址(先写寄存器地址)
    I2C_Wait4Ack();
    I2C_SendByte(0x00);      // 要读取的字节地址
    I2C_Wait4Ack();

    I2C_Start();              // RESTART: 不发送 STOP 直接发第二个 START
    I2C_SendByte(0xA1);      // 读地址
    I2C_Wait4Ack();

    uint8_t data = I2C_ReadByte();  // 读取数据
    I2C_NAck();               // 最后一字节主机发 NACK
    I2C_Stop();

    printf("写入 0x55, 读出 0x%02X\r\n", data);
    // 预期输出: "写入 0x55, 读出 0x55"

    while (1);
}

核心函数速查表

I2C 操作 函数 说明
初始化 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) 或 NACK(1)
发应答 I2C_Ack() 拉低 SDA 表示继续接收
发非应答 I2C_NAck() 释放 SDA 表示停止接收

常见问题与避坑

  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 半周期