前言
在嵌入式产品开发中,经常需要保存一些掉电不丢失的小容量数据------设备序列号、用户配置参数、传感器校准值、历史告警记录等。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.h、at24cxx.c、delay.h、delay.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() 必不可少。若换用其他芯片或主频,需同步修改 SystemCoreClock,delay.c 会根据该宏自动调整因子。
六、总结
AT24C 系列 EEPROM 的原理并不复杂,关键在于设备地址的复用规则 与页写入边界两条铁律。本文提供的驱动代码已完成全部细节处理,并补足了完整的微秒级延时实现,所有文件均可直接加入标准外设库工程中编译运行。只要你按照硬件连接要点接好上拉电阻和地址引脚,便能稳定读写这款经典的非易失存储芯片。
参考资料:
- AT24Cxx 系列数据手册
- STM32F10x 参考手册
- I2C 总线规范文档