AT24C02 EEPROM 驱动 — Software Bit-Bang I2C on STM32F103

Babel36acl 嵌入式实战 无~ 10 次阅读 预计阅读时间: 15 分钟 发布于 1 天前 最后更新于 1 小时前 3239 字


本文记录了 GENERIC_DEVICE_CONTROLLER 项目中 AT24C02 EEPROM 驱动的完整实现。设计采用双层硬件访问架构:通用层 bsp_at24cxx 通过函数指针处理任意 AT24C01~AT24CM02 器件,具体层 bsp_eeprom 封装 AT24C02 实例化。I2C 传输完全基于 GPIO 软件 bit-bang 实现,不使用硬件 I2C 外设。

1. 为什么用软件 Bit-Bang I2C?

GENERIC_DEVICE_CONTROLLER 的 STM32F103 有硬件 I2C 外设(I2C1 在 PB6/PB7,I2C2 在 PB10/PB11),但在密集布局中这些引脚常被复用作其他功能。AT24C02 接在 PA11(SDA)和 PA12(SCL)上——这两个引脚在 F103 上不映射到任何硬件 I2C 模块,因此必须用软件 bit-bang 实现。

sequenceDiagram
    participant MCU as STM32 MCU
    participant EEP as AT24C02
    MCU->>EEP: START + 0xA0
    EEP-->>MCU: ACK
    MCU->>EEP: 寄存器地址(8bit)
    EEP-->>MCU: ACK
    MCU->>EEP: 数据字节
    EEP-->>MCU: ACK
    MCU->>EEP: STOP
    Note over MCU,EEP: 软件 Bit-Bang I2C @ ~100kHz
    MCU->>EEP: tWR 6ms 延时

Bit-bang I2C 还提供了完整的时序控制能力,避开了 STM32 I2C 广为人知的硬件缺陷(busy 标志锁死、时钟拉伸毛刺),且产生的驱动只需两个 GPIO 即可移植到任意 MCU 上运行。

2. 硬件连接

AT24C02 引脚 MCU 引脚 功能
SCL PA12 I2C 时钟(输出,开漏+上拉)
SDA PA11 I2C 数据(双向,开漏+上拉)
A0/A1/A2 GND 全部接低 → 从机地址 0xA0
WP GND 写保护关闭
VCC 3.3V 电源

SDA 和 SCL 各自外接 4.7 kΩ 上拉电阻。AT24C02 工作在 3.3 V,在其规格 1.8~5.5 V 范围内。

3. 双层架构

驱动拆分两个文件:

  • bsp_at24cxx.h / .c——通用 AT24Cxx 驱动。使用用户提供的 bsp_i2c_t 结构体(内含 bit-bang 原语的函数指针),支持从 AT24C01(128 字节 × 8 页)到 AT24CM02(256 KiB × 1024 页)全系列器件。
  • bsp_eeprom.h / .c——项目专用封装。用 AT24C02 参数和 PA11/PA12 bit-bang 实现实例化 bsp_at24cxx_t,对外暴露 eeprom_init()eeprom_read()eeprom_write()eeprom_erase() 接口。

这种分离意味着换板子时绝不修改通用驱动——只需换具体的封装层和 I2C 传输实现。

4. 软件 Bit-Bang I2C 实现

Bit-bang 层以约 100 kHz SCL 频率运行,延时循环按 72 MHz 系统时钟校准。关键原语如下:

/* 引脚控制宏(已脱敏) */
#define EEPROM_I2C_PORT       GPIOA
#define EEPROM_SCL_PIN        GPIO_Pin_12
#define EEPROM_SDA_PIN        GPIO_Pin_11

#define SCL_H()   GPIO_SetBits(EEPROM_I2C_PORT, EEPROM_SCL_PIN)
#define SCL_L()   GPIO_ResetBits(EEPROM_I2C_PORT, EEPROM_SCL_PIN)
#define SDA_H()   GPIO_SetBits(EEPROM_I2C_PORT, EEPROM_SDA_PIN)
#define SDA_L()   GPIO_ResetBits(EEPROM_I2C_PORT, EEPROM_SDA_PIN)
#define SDA_IN()  GPIO_ReadInputDataBit(EEPROM_I2C_PORT, EEPROM_SDA_PIN)

/* ~360 CPU ticks at 72 MHz → ~100 kHz SCL */
#define I2C_DELAY()  { volatile uint32_t _d = 90; while (_d--); }

4.1 START 和 STOP 条件

static void i2c_start(void)
{
    SDA_H();
    SCL_H();
    I2C_DELAY();
    SDA_L();          /* SCL 高电平时 SDA 下降 → START */
    I2C_DELAY();
    SCL_L();
}

static void i2c_stop(void)
{
    SDA_L();
    SCL_H();
    I2C_DELAY();
    SDA_H();          /* SCL 高电平时 SDA 上升 → STOP */
    I2C_DELAY();
}

4.2 字节写入与 ACK 检测

static uint8_t i2c_write_byte(uint8_t data)
{
    uint8_t ack;
    for (int i = 0; i < 8; i++)
    {
        if (data & 0x80)
            SDA_H();
        else
            SDA_L();
        data <<= 1;
        SCL_H();
        I2C_DELAY();
        SCL_L();
        I2C_DELAY();
    }
    /* 释放 SDA,等待从机 ACK */
    SDA_H();
    SCL_H();
    I2C_DELAY();
    ack = SDA_IN();   /* 0 = ACK, 1 = NACK */
    SCL_L();
    I2C_DELAY();
    return ack;
}

4.3 字节读取与 ACK/NACK 生成

static uint8_t i2c_read_byte(uint8_t ack)
{
    uint8_t data = 0;
    SDA_H();          /* 释放总线 */
    for (int i = 0; i < 8; i++)
    {
        data <<= 1;
        SCL_H();
        I2C_DELAY();
        if (SDA_IN()) data |= 0x01;
        SCL_L();
        I2C_DELAY();
    }
    /* 产生 ACK (0) 或 NACK (1) */
    if (ack)
        SDA_H();
    else
        SDA_L();
    SCL_H();
    I2C_DELAY();
    SCL_L();
    I2C_DELAY();
    SDA_H();          /* 释放 */
    return data;
}

5. 通用驱动:bsp_at24cxx

通用驱动在运行时通过描述符配置:

typedef struct {
    uint16_t total_size;      /* EEPROM 总容量(字节) */
    uint16_t page_size;       /* 页面大小(字节) */
    uint8_t  dev_addr;        /* 7 位器件地址(如 0xA0 >> 1 = 0x50) */
    uint8_t  use_16bit_addr;  /* AT24C16 以上为 1(16 位寄存器地址),否则 0 */
    void     (*scl_h)(void);
    void     (*scl_l)(void);
    void     (*sda_h)(void);
    void     (*sda_l)(void);
    uint8_t  (*sda_in)(void);
    void     (*delay)(void);
} bsp_at24cxx_t;

5.1 页写入算法

页写入尊重页边界。如果写入跨越边界,自动拆分为多个页大小(或更小)的写入。每次页写入后加 6 ms 延时(tWR 最大值 5 ms,留 1 ms 余量)。

int bsp_at24cxx_write(bsp_at24cxx_t *eep, uint16_t addr,
                      const uint8_t *data, uint16_t len)
{
    uint16_t page_size = eep->page_size;
    uint16_t remain;
    int      ret = 0;

    if (addr + len > eep->total_size)
        return -1;  /* 边界检查 */

    while (len > 0)
    {
        /* 当前页从该偏移量起还剩多少字节 */
        remain = page_size - (addr % page_size);
        if (remain > len) remain = len;

        if (eep->use_16bit_addr)
            ret = write_page_16bit(eep, addr, data, remain);
        else
            ret = write_page_8bit(eep, addr, data, remain);

        if (ret != 0) return ret;

        addr += remain;
        data += remain;
        len  -= remain;

        /* tWR max = 5ms(AT24C02),用 6ms 余量 */
        eep->delay();
        eep->delay();
        eep->delay();
        eep->delay();
        eep->delay();
        eep->delay();  /* 总计 ~6 ms */
    }
    return 0;
}

5.2 顺序读取(当前地址读)

顺序读取复用同样的页拆分逻辑,但读操作不需要页间延时——器件在读操作中会自动跨页递增内部地址计数器。

int bsp_at24cxx_read(bsp_at24cxx_t *eep, uint16_t addr,
                     uint8_t *data, uint16_t len)
{
    if (addr + len > eep->total_size)
        return -1;

    /* 通过伪写入序列设置寄存器地址 */
    eep->scl_h(); eep->sda_h();   /* 总线空闲 */
    i2c_start_g(eep);
    i2c_write_byte_g(eep, eep->dev_addr << 1);  /* 写位 = 0 */

    if (eep->use_16bit_addr)
    {
        i2c_write_byte_g(eep, (addr >> 8) & 0xFF);
        i2c_write_byte_g(eep, addr & 0xFF);
    }
    else
    {
        i2c_write_byte_g(eep, addr & 0xFF);
    }

    /* 重复 START,转为读 */
    i2c_start_g(eep);
    i2c_write_byte_g(eep, (eep->dev_addr << 1) | 0x01);  /* 读位 = 1 */

    while (len--)
    {
        *data++ = i2c_read_byte_g(eep, (len == 0) ? 1 : 0);
    }

    i2c_stop_g(eep);
    return 0;
}

5.3 器件地址分辨率

驱动根据 use_16bit_addr 标志自动选择 8 位或 16 位寄存器寻址。≤ AT24C16(2 KiB)用 8 位寄存器地址,≥ AT24C32(4 KiB)用 16 位。阈值在静态配置表中设定:

static const bsp_at24cxx_cfg_t at24cxx_config[] = {
    /* 名称         容量      页大小  地址   16bit */
    {"AT24C01",     128,      8,      0x50,  0},
    {"AT24C02",     256,      8,      0x50,  0},
    {"AT24C04",     512,      16,     0x50,  0},
    {"AT24C08",     1024,     16,     0x50,  0},
    {"AT24C16",     2048,     16,     0x50,  0},
    {"AT24C32",     4096,     32,     0x50,  1},
    {"AT24C64",     8192,     32,     0x50,  1},
    {"AT24C128",    16384,    64,     0x50,  1},
    {"AT24C256",    32768,    64,     0x50,  1},
    {"AT24CM01",    131072,   256,    0x50,  1},
    {"AT24CM02",    262144,   256,    0x50,  1},
};

6. 具体封装:bsp_eeprom

封装层为 AT24C02 实例化通用驱动:

/* bsp_eeprom.c — AT24C02 具体实例化 */
#include "bsp_at24cxx.h"
#include "bsp_eeprom.h"

static bsp_at24cxx_t eeprom_dev = {
    .total_size     = 256,
    .page_size      = 8,
    .dev_addr       = 0x50,       /* 0xA0 >> 1 */
    .use_16bit_addr = 0,
    .scl_h          = pa12_scl_h,
    .scl_l          = pa12_scl_l,
    .sda_h          = pa11_sda_h,
    .sda_l          = pa11_sda_l,
    .sda_in         = pa11_sda_in,
    .delay          = eeprom_delay_6ms,
};

int eeprom_init(void)
{
    GPIO_InitTypeDef gpio;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    /* SCL — 开漏输出 */
    gpio.GPIO_Pin   = EEPROM_SCL_PIN;
    gpio.GPIO_Mode  = GPIO_Mode_Out_OD;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(EEPROM_I2C_PORT, &gpio);

    /* SDA — 开漏输出(bit-bang 中动态切换为输入) */
    gpio.GPIO_Pin   = EEPROM_SDA_PIN;
    GPIO_Init(EEPROM_I2C_PORT, &gpio);

    /* 总线空闲:SCL 高,SDA 高 */
    SCL_H();
    SDA_H();

    return 0;
}

int eeprom_write(uint16_t addr, const uint8_t *data, uint16_t len)
{
    return bsp_at24cxx_write(&eeprom_dev, addr, data, len);
}

int eeprom_read(uint16_t addr, uint8_t *data, uint16_t len)
{
    return bsp_at24cxx_read(&eeprom_dev, addr, data, len);
}

int eeprom_erase(uint16_t addr, uint16_t len)
{
    /* 将区域填充为 0xFF(EEPROM 擦除默认状态) */
    uint8_t fill = 0xFF;
    int ret;
    while (len > 0)
    {
        ret = bsp_at24cxx_write(&eeprom_dev, addr, &fill, 1);
        if (ret != 0) return ret;
        addr++;
        len--;
    }
    return 0;
}

7. AT24C02 关键参数

参数
总容量 256 字节(2 Kbit)
页大小 8 字节
从机地址 0xA0(A0/A1/A2 = GND)
寄存器地址宽度 8 位
最大时钟频率 400 kHz(快速模式);实际运行 ~100 kHz
写入周期时间(tWR) 最大 5 ms;实际用 6 ms 余量
擦写寿命 1,000,000 次
数据保持 100 年

8. 设计决策与权衡

  • 不用硬件 I2C。PA11/PA12 不映射到 I2C 外设。Bit-bang 也消除了 STM32 I2C 的 busy 标志异常。
  • 不加 CRC。这只是个薄硬件访问层。CRC、磨损均衡和页面管理属于上层(FATFS、EEPROM 抽象层等)。
  • 不做磨损均衡。AT24C02 有 100 万次擦写寿命。GENERIC_DEVICE_CONTROLLER 的保存频率(启动时写配置、偶尔存校准值)下,裸访问足够了。
  • 函数指针 I2C。便于用 mock GPIO 做测试、移植到其他 MCU,以及在不同板子上复用同一套通用驱动而无需重新编译。
  • 6 ms 页写入延时。保守值。tWR 最大为 5 ms;6 ms 在温度和电压波动下留了 1 ms 余量。更激进的方案可以用 ACK 轮询技巧(重复 START 直到收到 ACK)提前检测写入完成。
  • 擦除逐字节写。AT24Cxx 没有硬件页擦除命令。eeprom_erase() 循环将每个字节写为 0xFF。大批量擦除较慢但语义正确。

9. 使用示例

#include "bsp_eeprom.h"

void save_config(void)
{
    uint8_t cfg[8] = {0xAA, 0x55, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};

    eeprom_init();
    eeprom_write(0x00, cfg, 8);   /* 向页 0 写入 8 字节 */
}

void load_config(void)
{
    uint8_t buf[8];

    eeprom_init();
    eeprom_read(0x00, buf, 8);    /* 从页 0 读回 */
}

10. 总结

GENERIC_DEVICE_CONTROLLER 的 AT24C02 驱动提供了一套干净的双层抽象:基于函数指针 bit-bang I2C 的可复用通用 AT24Cxx 核心,以及针对 AT24C02 的薄封装层。软件 I2C 在 PA11/PA12 上以 ~100 kHz 运行,实现了标准的 START/STOP/字节写入/字节读取原语,正确处理了读写操作中的页边界拆分。驱动故意保持最小化——无 CRC、无缓存、无磨损均衡——作为嵌入式系统配置存储的可靠硬件访问层。

完整源码维护在 GENERIC_DEVICE_CONTROLLER 固件仓库的 hw/eeprom/ 目录下。

此作者没有提供个人介绍。
最后更新于 2026-05-30