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 总线规范文档
相关推荐
NPE~1 天前
[嵌入式]从0到1开发环境搭建
stm32·嵌入式硬件·教程·clion·stmcubemx·stmcubeclt
项目題供诗1 天前
STM32-ADC模数转换器(十八)
stm32·单片机·嵌入式硬件
YYRAN_ZZU1 天前
Ubuntu22.04搭建QEMU嵌入式开发环境全攻略
linux·嵌入式硬件·ubuntu
_YouziTech_1 天前
【STM32】U8G2图形库应用--菜单设计与开发
stm32·单片机·嵌入式硬件·oled·开机动画·图形库
2301_805962931 天前
ESP32 使用 PlatformIO 编译点灯程序
stm32·esp32
Silicore_Emma1 天前
芯谷科技—D55126 漏电保护器专用集成电路
嵌入式硬件·新能源充电桩·芯谷科技·漏电保护器·高性能cmos漏电保护器·智能断路器/物联网配电·家用漏电保护
国科安芯1 天前
商业航天级抗辐照全双工RS-485/RS-422收发器ASM491S2Y的技术特性与应用研究
运维·网络·单片机·嵌入式硬件·安全·架构·安全性测试
国科安芯1 天前
ASP7A84AS高精度抗辐照线性稳压器技术特性与应用分析
单片机·嵌入式硬件·安全·架构
say_fall1 天前
模拟量输入输出技术超详细知识点总结
linux·开发语言·嵌入式硬件·学习·php
恶魔泡泡糖1 天前
stm32F103C8T6标准库串口发送之发送字节2
stm32·单片机·嵌入式硬件