STM32驱动AT24C系列I2C EEPROM详解(标准库版):零死角,直接可用

前言

在嵌入式产品开发中,经常需要保存一些掉电不丢失的小容量数据------设备序列号、用户配置参数、传感器校准值、历史告警记录等。AT24C 系列 I2C EEPROM 凭借其接口简单(仅需两根线)、擦写寿命长(100万次)、数据保持久(超100年)、成本低廉的特点,成为这类场景的标配方案。

然而很多初学者在移植 AT24C 驱动时,常常被卡在"读回来的全是 0xFF""写完读不到""多字节跨页数据错乱"等问题上。归根结底,是因为没有真正理解 AT24C 的两个核心特性:设备地址的位复用机制页写入的边界回卷

本文将以 AT24C02 为例,使用**标准外设库(Standard Peripheral Library)**编写一套完整、可直接编译运行的驱动代码,涵盖 I2C 初始化、单字节读写、页写入和跨页安全写入。文中同时给出了基于 SysTick 的微秒延时实现,确保所有代码复制到工程中即可使用,帮你彻底搞懂 AT24C 的每一个关键细节。

一、AT24C 系列 EEPROM 基础原理

1.1 什么是 AT24C 系列

AT24C 系列是 Microchip 推出的 I2C 接口 EEPROM,产品线从 AT24C01 到 AT24C1024,容量覆盖 1Kbit 到 1024Kbit。

不同型号的核心参数对比如下:

型号 bit 容量 Byte 容量 页数 页内字节数 同一总线最大挂载数
AT24C01 1Kbit 128Byte 16页 8Byte 8个
AT24C02 2Kbit 256Byte 32页 8Byte 8个
AT24C04 4Kbit 512Byte 32页 16Byte 4个
AT24C08 8Kbit 1KB 64页 16Byte 2个
AT24C16 16Kbit 2KB 128页 16Byte 1个
AT24C32 32Kbit 4KB 128页 32Byte 8个
AT24C64 64Kbit 8KB 256页 32Byte 8个
AT24C128 128Kbit 16KB 256页 64Byte 4个
AT24C256 256Kbit 32KB 512页 64Byte 4个
AT24C512 512Kbit 64KB 512页 128Byte 4个

重点 :页容量是本表最关键的一列。一次写入操作绝对不能跨越页边界,例如 AT24C02 每页仅 8 字节,从地址 5 开始写入最多只能写 3 字节,第 4 字节会回卷到该页起始地址(地址 0)造成数据覆盖。这一特性是驱动设计中最核心的约束,也是初学者出错最多的地方。

1.2 设备地址:7 位寻址与引脚复用

AT24C 使用标准 7 位 I2C 设备地址,高 4 位固定为 1010,低 3 位由 A2、A1、A0 引脚电平决定。设备地址 = 0x50 + (A2A1A0 的数值) ,范围 0x50~0x57。方向位(R/W)放在最低位形成 8 位:写操作 (addr << 1) | 0x00,读操作 (addr << 1) | 0x01

对于 AT24C01/AT24C02 :A2、A1、A0 都是纯粹的设备地址引脚,一条 I2C 总线上最多挂 8 个。

对于 AT24C04/08/16 :超出 8 位地址范围的高位地址被"借用"到了设备地址的低位中,因此可挂载数量随之减少。

到了 AT24C32 及更大容量:存储地址需要 2 个字节(16 位),设备地址重新回归 A2/A1/A0 全部用作设备地址引脚的模式。

如果你的 PCB 上 A2、A1、A0 都接 GND,那么无论是哪个型号,7 位设备地址都可以直接使用 0x50,这是工程上最简便的做法。

1.3 页写入的边界约束

页写入是最高效的写入方式,但数据不能跨越页边界。若写入长度超过当前页剩余空间,多余字节会回卷到该页起始位置,覆盖已有数据。驱动层必须处理这一约束。

1.4 写周期等待

AT24C 在收到 I2C 停止信号后,会进入内部编程周期(twr,典型值约 5ms),此期间芯片不响应任何请求。因此每次写操作后必须有 5~10ms 的延时,否则后续操作会因 NACK 而失败。

二、硬件连接

以 STM32F103C8T6 通过 软件 I2C(GPIO 模拟) 连接 AT24C02 为例(A2、A1、A0 均接 GND):

STM32F103C8T6 AT24C02
PB6 SCL
PB7 SDA
3.3V VCC
GND GND, A2, A1, A0, WP

注意:I2C 总线的 SDA 和 SCL 必须外接 4.7kΩ 上拉电阻到 VCC。WP 接地允许正常写入。使用软件 I2C 的原因在于 STM32F1 硬件 I2C 存在稳定性问题,软件模拟则完全可控、移植性强。

三、完整驱动代码(标准库,可直接编译)

本驱动由四个文件组成:at24cxx.hat24cxx.cdelay.hdelay.c。其中 delay.c 基于 SysTick 实现了微秒/毫秒延时,是 I2C 时序的基础。

3.1 at24cxx.h

c 复制代码
#ifndef __AT24CXX_H__
#define __AT24CXX_H__

#include "stm32f10x.h"

/* ========== 用户可配置参数 ========== */
#define AT24CXX_DEVICE_ADDR     0x50    /* 7位设备地址(A2=A1=A0=GND时使用0x50)*/
#define AT24CXX_PAGE_SIZE       8       /* 页大小:AT24C01/02=8, AT24C04/08/16=16,
                                           AT24C32/64=32, AT24C128/256=64, AT24C512=128 */
#define AT24CXX_WRITE_DELAY_MS  5       /* 写周期等待时间(ms),一般为5~10ms */
#define AT24CXX_ADDR_16BIT      0       /* 0=8位存储地址(≤AT24C16),1=16位(≥AT24C32) */
#define AT24CXX_CHIP_NAME       "AT24C02"

/* ========== I2C 引脚宏定义 ========== */
#define AT24CXX_I2C_PORT        GPIOB
#define AT24CXX_I2C_CLK         RCC_APB2Periph_GPIOB
#define AT24CXX_SCL_PIN         GPIO_Pin_6
#define AT24CXX_SDA_PIN         GPIO_Pin_7

/* GPIO 操作宏 */
#define AT24CXX_SCL_H()   GPIO_SetBits(AT24CXX_I2C_PORT, AT24CXX_SCL_PIN)
#define AT24CXX_SCL_L()   GPIO_ResetBits(AT24CXX_I2C_PORT, AT24CXX_SCL_PIN)
#define AT24CXX_SDA_H()   GPIO_SetBits(AT24CXX_I2C_PORT, AT24CXX_SDA_PIN)
#define AT24CXX_SDA_L()   GPIO_ResetBits(AT24CXX_I2C_PORT, AT24CXX_SDA_PIN)
#define AT24CXX_SDA_READ()  GPIO_ReadInputDataBit(AT24CXX_I2C_PORT, AT24CXX_SDA_PIN)

/* ========== 函数声明 ========== */
void     AT24CXX_Init(void);
uint8_t  AT24CXX_Check(void);
void     AT24CXX_WriteByte(uint16_t addr, uint8_t data);
uint8_t  AT24CXX_ReadByte(uint16_t addr);
void     AT24CXX_WritePage(uint16_t addr, uint8_t *pData, uint16_t len);
void     AT24CXX_Write(uint16_t addr, uint8_t *pData, uint16_t len);
void     AT24CXX_ReadBytes(uint16_t addr, uint8_t *pBuf, uint16_t len);

#endif

3.2 at24cxx.c

c 复制代码
#include "at24cxx.h"
#include "delay.h"

/* ========== 底层 I2C 时序 ========== */

static void I2C_Start(void)
{
    AT24CXX_SDA_H();
    AT24CXX_SCL_H();
    delay_us(5);
    AT24CXX_SDA_L();
    delay_us(5);
    AT24CXX_SCL_L();
}

static void I2C_Stop(void)
{
    AT24CXX_SDA_L();
    AT24CXX_SCL_L();
    delay_us(2);
    AT24CXX_SCL_H();
    delay_us(5);
    AT24CXX_SDA_H();
    delay_us(5);
}

static uint8_t I2C_WaitAck(void)
{
    uint16_t timeout = 5000;

    AT24CXX_SDA_H();
    delay_us(2);
    AT24CXX_SCL_H();
    delay_us(2);

    while(AT24CXX_SDA_READ()) {
        if(--timeout == 0) {
            I2C_Stop();
            return 1;   /* NACK */
        }
    }

    AT24CXX_SCL_L();
    return 0;           /* ACK */
}

static void I2C_SendAck(void)
{
    AT24CXX_SDA_L();
    delay_us(2);
    AT24CXX_SCL_H();
    delay_us(5);
    AT24CXX_SCL_L();
}

static void I2C_SendNack(void)
{
    AT24CXX_SDA_H();
    delay_us(2);
    AT24CXX_SCL_H();
    delay_us(5);
    AT24CXX_SCL_L();
}

static void I2C_SendByte(uint8_t data)
{
    uint8_t i;
    for(i = 0; i < 8; i++) {
        if(data & 0x80)
            AT24CXX_SDA_H();
        else
            AT24CXX_SDA_L();
        delay_us(2);
        AT24CXX_SCL_H();
        delay_us(5);
        AT24CXX_SCL_L();
        data <<= 1;
    }
}

static uint8_t I2C_ReadByte(void)
{
    uint8_t i, data = 0;
    AT24CXX_SDA_H();
    for(i = 0; i < 8; i++) {
        delay_us(2);
        AT24CXX_SCL_H();
        data <<= 1;
        if(AT24CXX_SDA_READ())
            data |= 0x01;
        delay_us(2);
        AT24CXX_SCL_L();
    }
    return data;
}

/* ========== AT24CXX 应用层 ========== */

void AT24CXX_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(AT24CXX_I2C_CLK, ENABLE);

    GPIO_InitStructure.GPIO_Pin   = AT24CXX_SCL_PIN | AT24CXX_SDA_PIN;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_Out_OD;      /* 开漏输出 */
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(AT24CXX_I2C_PORT, &GPIO_InitStructure);

    AT24CXX_SCL_H();
    AT24CXX_SDA_H();
}

uint8_t AT24CXX_Check(void)
{
    uint8_t ack;
    I2C_Start();
    I2C_SendByte(AT24CXX_DEVICE_ADDR << 1);
    ack = I2C_WaitAck();
    I2C_Stop();
    return ack;   /* 0=设备存在,1=不存在 */
}

void AT24CXX_WriteByte(uint16_t addr, uint8_t data)
{
    I2C_Start();

#if AT24CXX_ADDR_16BIT
    I2C_SendByte(AT24CXX_DEVICE_ADDR << 1);
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr >> 8));
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr & 0xFF));
#else
    I2C_SendByte((AT24CXX_DEVICE_ADDR << 1) | ((addr >> 8) & 0x07));
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr & 0xFF));
#endif

    I2C_WaitAck();
    I2C_SendByte(data);
    I2C_WaitAck();
    I2C_Stop();

    delay_ms(AT24CXX_WRITE_DELAY_MS);
}

uint8_t AT24CXX_ReadByte(uint16_t addr)
{
    uint8_t data;

    I2C_Start();

#if AT24CXX_ADDR_16BIT
    I2C_SendByte(AT24CXX_DEVICE_ADDR << 1);
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr >> 8));
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr & 0xFF));
#else
    I2C_SendByte((AT24CXX_DEVICE_ADDR << 1) | ((addr >> 8) & 0x07));
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr & 0xFF));
#endif

    I2C_WaitAck();

    I2C_Start();
    I2C_SendByte((AT24CXX_DEVICE_ADDR << 1) | 0x01);
    I2C_WaitAck();
    data = I2C_ReadByte();
    I2C_SendNack();
    I2C_Stop();

    return data;
}

void AT24CXX_WritePage(uint16_t addr, uint8_t *pData, uint16_t len)
{
    uint16_t i;

    I2C_Start();

#if AT24CXX_ADDR_16BIT
    I2C_SendByte(AT24CXX_DEVICE_ADDR << 1);
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr >> 8));
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr & 0xFF));
#else
    I2C_SendByte((AT24CXX_DEVICE_ADDR << 1) | ((addr >> 8) & 0x07));
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr & 0xFF));
#endif

    I2C_WaitAck();

    for(i = 0; i < len; i++) {
        I2C_SendByte(pData[i]);
        I2C_WaitAck();
    }

    I2C_Stop();
    delay_ms(AT24CXX_WRITE_DELAY_MS);
}

void AT24CXX_Write(uint16_t addr, uint8_t *pData, uint16_t len)
{
    uint16_t page_remain;

    while(len > 0) {
        page_remain = AT24CXX_PAGE_SIZE - (addr % AT24CXX_PAGE_SIZE);
        if(len > page_remain) {
            AT24CXX_WritePage(addr, pData, page_remain);
            addr   += page_remain;
            pData  += page_remain;
            len    -= page_remain;
        } else {
            AT24CXX_WritePage(addr, pData, len);
            break;
        }
    }
}

void AT24CXX_ReadBytes(uint16_t addr, uint8_t *pBuf, uint16_t len)
{
    uint16_t i;

    I2C_Start();

#if AT24CXX_ADDR_16BIT
    I2C_SendByte(AT24CXX_DEVICE_ADDR << 1);
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr >> 8));
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr & 0xFF));
#else
    I2C_SendByte((AT24CXX_DEVICE_ADDR << 1) | ((addr >> 8) & 0x07));
    I2C_WaitAck();
    I2C_SendByte((uint8_t)(addr & 0xFF));
#endif

    I2C_WaitAck();

    I2C_Start();
    I2C_SendByte((AT24CXX_DEVICE_ADDR << 1) | 0x01);
    I2C_WaitAck();

    for(i = 0; i < len; i++) {
        pBuf[i] = I2C_ReadByte();
        if(i < (len - 1))
            I2C_SendAck();
        else
            I2C_SendNack();
    }

    I2C_Stop();
}

3.3 delay.h(基础延时模块)

c 复制代码
#ifndef __DELAY_H__
#define __DELAY_H__

#include "stm32f10x.h"

void delay_init(void);
void delay_us(uint32_t nus);
void delay_ms(uint32_t nms);

#endif

3.4 delay.c(基于 SysTick,必须添加到工程)

c 复制代码
#include "delay.h"

static uint32_t fac_us = 0;   /* 微秒倍乘因子 */

void delay_init(void)
{
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);  /* 系统时钟/8 = 9MHz */
    fac_us = SystemCoreClock / 8000000;                    /* 微秒倍乘因子 = 72/8 = 9 */
}

void delay_us(uint32_t nus)
{
    uint32_t temp;
    SysTick->LOAD = nus * fac_us;         /* 装载计数初值 */
    SysTick->VAL  = 0x00;                /* 清空计数器 */
    SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* 使能计数 */

    do {
        temp = SysTick->CTRL;
    } while((temp & 0x01) && !(temp & (1 << 16))); /* 等待计数完成 */

    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* 关闭计数器 */
    SysTick->VAL = 0x00;
}

void delay_ms(uint32_t nms)
{
    while(nms--) {
        delay_us(1000);
    }
}

说明 :使用本延时时,需在 main 函数开始处调用 delay_init()SystemCoreClock 由库文件定义,默认为 72MHz。

四、使用示例(main.c)

c 复制代码
#include "at24cxx.h"
#include "delay.h"
#include <stdio.h>   /* 若使用 printf 需要重定向 fputc */

int main(void)
{
    uint8_t write_buf[12] = "Hello-AT24C";
    uint8_t read_buf[12]  = {0};
    uint8_t single_write, single_read;

    /* 初始化延时模块(必须先于一切使用延时的函数) */
    delay_init();

    /* 初始化 AT24CXX */
    AT24CXX_Init();

    /* 检测设备 */
    if(AT24CXX_Check() == 0) {
        // 设备存在,可加入指示灯或打印信息
    } else {
        // 设备未找到,进入错误处理
        while(1);
    }

    /* 单字节写入并读回 */
    AT24CXX_WriteByte(0x10, 0xAB);
    single_read = AT24CXX_ReadByte(0x10);   /* 应为 0xAB */

    /* 多字节跨页安全写入 */
    AT24CXX_Write(0x00, write_buf, 12);

    /* 连续读取 */
    AT24CXX_ReadBytes(0x00, read_buf, 12);  /* read_buf 应为 "Hello-AT24C" */

    while(1);
}

五、常见问题与避坑指南

5.1 读回全是 0xFF

  • 检查 SCL、SDA 外部上拉电阻(4.7kΩ 接 VCC),没有上拉总线无法产生高电平。
  • GPIO 必须配置为开漏输出
  • 确认芯片 WP 引脚接地,A2/A1/A0 电平与代码地址一致。
  • 用示波器或逻辑分析仪抓取 I2C 波形,确认从机是否回复 ACK。

5.2 写完立即读,数据不对

写操作后等待时间太短,芯片仍在内部编程。确保 AT24CXX_WRITE_DELAY_MS 大于等于 5ms,或改为轮询 ACK 方式更健壮。

5.3 多字节写入时某部分数据丢失

大多由跨页引起。必须使用本文提供的 AT24CXX_Write() 函数,它会自动拆分跨页数据,而不是直接调用 WritePage

5.4 不同型号的页大小和地址长度配置

更换芯片时,只需修改 at24cxx.h 中的三个宏:

c 复制代码
#define AT24CXX_PAGE_SIZE       32    /* 例如 AT24C32 的页大小 */
#define AT24CXX_ADDR_16BIT      1     /* ≥AT24C32 改为 1 */
#define AT24CXX_WRITE_DELAY_MS  10    /* 部分型号写周期略长 */

5.5 延时不准导致 I2C 时序错乱

delay_us 基于 SysTick 9MHz 时钟,delay_init() 必不可少。若换用其他芯片或主频,需同步修改 SystemCoreClockdelay.c 会根据该宏自动调整因子。

六、总结

AT24C 系列 EEPROM 的原理并不复杂,关键在于设备地址的复用规则页写入边界两条铁律。本文提供的驱动代码已完成全部细节处理,并补足了完整的微秒级延时实现,所有文件均可直接加入标准外设库工程中编译运行。只要你按照硬件连接要点接好上拉电阻和地址引脚,便能稳定读写这款经典的非易失存储芯片。

参考资料:

  • AT24Cxx 系列数据手册
  • STM32F10x 参考手册
  • I2C 总线规范文档
相关推荐
一枝小雨2 小时前
RISC-V架构的中断与异常处理机制学习笔记
单片机·架构·嵌入式·risc-v·内核原理·中断与异常
国产芯片设计2 小时前
小家电单段码屏项目实战|YL1621 LCD驱动开发与调试心得
驱动开发·stm32·单片机·mcu·51单片机
czhaii3 小时前
STM32G系列单片机产品说明
stm32·单片机·嵌入式硬件
都在酒里3 小时前
STM32标准库驱动TB6612FNG双H桥电机驱动模块(PWM调速/正反转/制动/多模式实战,附完整工程代码)
stm32·单片机·嵌入式硬件
Tech_D3 小时前
AVA系列音圈电机技术拆解:直驱无间隙+高响应,适配精密自动化场景
单片机·自动化·制造
小+不通文墨4 小时前
树莓派接温湿度传感器显示温度湿度
经验分享·笔记·单片机·嵌入式硬件·学习
我要成为嵌入式大佬4 小时前
正点原子MP157问题详解--烧录出错在ssb1(ox6)
单片机·嵌入式硬件
嵌入式Q4 小时前
FreeRTOS源码解析(10)软件定时器
单片机·mcu·freertos
都在酒里4 小时前
STM32 ADC采样详解(标准库版):普通模式与DMA模式,附完整可用代码
stm32·单片机·嵌入式硬件