编制日期 2026-09-23 | 版本号 V1.0
面向XTX 串行 EEPROM(I²C 24Cxx / SPI 25xx 系列)的固件驱动设计约定 —— 覆盖ACK 轮询 / WIP 轮询、页写边界回绕、写保护体系、1M 次写 endurance与掉电原子提交,逐条给出可落地的命令序列、轮询判据与超时预算。
| 文档编号 | EEPROM-SW-SPEC-001 |
|---|---|
| 版本 / 状态 | V1.0 · 正式发布 |
| 适用器件 | XTX 串行 EEPROM 24 系列(I²C)/ 25 系列(SPI),容量 1 Kbit ~ 2 Mbit 各档 |
| 参考标准 | JEDEC / 厂商 I²C-Compatible Serial EEPROM 与 SPI Serial EEPROM 协议;I²C-bus Specification |
| 配套文档 | EEPROM_电路设计指南.html(硬件 / 原理图 / 上拉 / 布线约束) |
0. 目的与范围
本规范约束 EEPROM 器件在嵌入式固件中的软件行为:命令序列、写完成检测(ACK / WIP 轮询)、页写边界、写保护、endurance 预算与掉电保护。凡本文与器件 datasheet 冲突,以 datasheet 为准。
- 统一写完成检测:I²C 用ACK 轮询,SPI 用RDSR 轮询 WIP;绝不靠固定延时替代轮询。
- 统一页写边界规则:一次页写不得跨页边界,超出部分会回绕到本页首地址覆盖。
- 统一写保护策略:WP# 引脚 + BP/WPEN 位 + 块保护组合,上电默认状态必须明确。
- 统一掉电保护:给出写窗口、提交协议与写入频次预算,明确哪些操作是原子的。
不在本规范范围:硬件原理图与 PCB 布线(见《EEPROM 电路设计指南》);FRAM / NVSRAM / Microwire / UNI-O / 并行 28Cxx 等特殊接口的算法差异仅作提示; 文件系统与磨损均衡算法选型(EEPROM 的 endurance 已达百万级,通常无需 FTL)。
一句话结论:EEPROM 是「字节可改、页内聚合」的器件 —— 所有写都必须先WREN(SPI)或起始条件(I²C)使能,再以内部写周期 tWR(典型 5 ms)落到阵列;写完之后器件进入busy状态(I²C 不响应 ACK,SPI 的 WIP=1),不轮询完成就继续发命令,数据会被静默吞掉。没有坏块、没有 ECC,但页边界回绕与写 endurance 累积是它独有的两个坑。
文档使用说明
- 本文是《EEPROM 电路设计指南》的软件配套篇:硬件篇解决「怎么接、怎么供电、怎么拉上拉」,本文解决「驱动怎么写、写完怎么等、错误怎么救」。
- 命令码、寄存器位、时序值均取自典型 I²C 24Cxx 与 SPI 25xx 协议;其它厂商/其它密度型号存在差异,移植前必须逐条核对。
- 文中 C 代码为协议骨架伪代码,演示流程与判断条件,非特定平台可直接编译的完整实现;
i2c_*()、spi_*()、now_ms()等为平台相关底层。
贯穿全文的三条铁律
①绝不用固定延时替代写完成轮询——tWR 随温度、电压波动,且器件 busy 时直接拒绝新命令; ②一次页写绝不跨页边界——超出的字节会回绕覆盖本页开头,不会报错; ③超时门限一律取 datasheet max 以上——typ 值只用来安排轮询节奏,不能当失败判据。
1 · 器件定位与地址组织
EEPROM 不是「能改几个字节的 Flash」。它保留了字节可寻址、就地改写的能力,但写是以一个内部页为单位聚合提交的——软件设计的第一件事,是把「地址空间」「页粒度」和「写完成窗口」搞清楚。
1.1 器件定位与关键参数
8~256 B
页写粒度
同一内部写周期最多写入的字节数
2 Mbit
单片最大容量
1 Kbit ~ 2 Mbit 多档;24 系列 2 字节地址可达 512 Kbit
1,000,000
写 endurance
每字节/每页 100 万次(典型)
100 年
数据保持
常温典型;高温下缩短
5 ms
写周期 tWR
最大 5 ms(部分型号 10 ms),期间器件 busy
无
坏块 / ECC
EEPROM 无出厂坏块、无片内 ECC
100/400 kHz
I²C 时钟
标准模式 / 快速模式(部分 1 MHz)
10/20 MHz
SPI 时钟
25 系列典型最大时钟
数据取自典型 I²C 24Cxx 与 SPI 25xx datasheet 的 Features 与 DC/AC 特性章节;具体数值随器件型号与厂商不同,移植时以对应 datasheet 为准。
| 项目 | I²C 24Cxx 系列 | SPI 25xx 系列 | 软件影响 |
|---|---|---|---|
| 接口 | 2 线(SDA / SCL) | 4 线(CS / SO / SI / SCK) | 底层总线抽象必须分离 |
| 寻址 | 器件地址 1010 + A2A1A0(或块地址)+ R/W;字地址 1/2 字节 | 命令字节内嵌 16/24 位字地址 | I²C 有独立器件地址,SPI 靠 CS# 选片 |
| 写完成检测 | ACK 轮询(busy 时 NACK) | 05h读 SR 的 WIP 位 | 两类驱动路径不同 |
| 写保护 | WP# 引脚(高=整片保护)+ BP 位(部分型号) | WP# 引脚 + SR 的 BP0/1/2 + WPEN | SPI 保护更细;I²C 经典器件靠 WP 引脚 |
| 页大小 | 8 / 16 / 32 / 64 / 128 / 256 B(随容量) | 同左 | 页写长度上限必须按型号配置 |
| 状态寄存器 | 无(仅靠 ACK 行为反映 busy) | 有(RDSR / WRSR) | I²C 无法读 SR,保护位靠 WP 引脚 |
1.2 容量、页与地址映射
EEPROM 的地址是一段连续的字节线性空间,软件最容易在这里出错的是「页边界」:
| 器件密度 | 总字节数 | 页大小(典型) | 字地址宽度 | 页数量 |
|---|---|---|---|---|
| 1 Kbit(128 B) | 128 | 8 B 或 16 B | 1 字节 | 8 ~ 16 |
| 2 Kbit(256 B) | 256 | 8 / 16 B | 1 字节 | 16 ~ 32 |
| 4 Kbit(512 B) | 512 | 16 / 32 B | 1 字节 | 16 ~ 32 |
| 8 Kbit(1 KB) | 1024 | 16 / 32 B | 1 字节 | 32 ~ 64 |
| 16 Kbit(2 KB) | 2048 | 16 / 32 B | 1 字节 | 64 ~ 128 |
| 32 Kbit(4 KB) | 4096 | 32 / 64 B | 2 字节 | 64 ~ 128 |
| 64 Kbit(8 KB) | 8192 | 32 / 64 / 128 B | 2 字节 | 64 ~ 256 |
| 128 Kbit(16 KB)~ 2 Mbit | 16384 ~ 262144 | 64 / 128 / 256 B | 2 字节(大容量 3 字节) | 128 ~ 1024 |
1 Device = N × 256 B(线性字节空间)容量从 128 B 到 256 KB(2 Mbit);无块/页坏块概念1 Page = 8 ~ 256 B一次内部写周期最多写入的粒度;边界回绕的边界1 Byte = 最小读写单位字节可寻址、可随机改写;无需擦除即可写写完成窗口 tWR ≈ 5 ms窗口内器件 busy:I²C 不 ACK,SPI 的 WIP=1
图 1 · EEPROM 阵列组织:字节可寻址、以「页」为单位聚合写入、以 tWR 为单位提交
页大小随容量变化,绝不能硬编码一个值
很多驱动把页大小写死成 32 B,移植到 64 B 或 128 B 页的器件后,会发生:要么每次只写 32 B(浪费 tWR、写得很慢),要么超过真实页边界造成回绕覆盖。
正确做法:页大小作为器件表的配置项,启动阶段按型号查表或读标识页得到。
1.3 与 SPI NOR / NAND 的关键差异(移植必读)
| 维度 | SPI NOR / NAND | EEPROM | 移植后果 |
|---|---|---|---|
| 擦除 | 必须先擦除再写(NOR 按扇区,NAND 按块) | 无需擦除,字节可直接改写 | 照搬「先擦后写」会多花无意义操作 |
| 写完成检测 | 轮询 OIP / 状态位 | I²C 用 ACK 轮询;SPI 用 WIP 轮询 | NOR 的 05h 轮询在 EEPROM 上含义不同 |
| 页写边界 | NOR 按页/扇区,NAND 按 cache | 严格页边界,超出回绕覆盖 | 跨边界写被静默吞掉且不报错 |
| 坏块 | NOR 无;NAND 必有坏块 + BBT | 无坏块 | EEPROM 不需要坏块管理 |
| ECC | NAND 必有片内 ECC | 通常无 ECC | EEPROM 读到的就是实际存储值 |
| endurance | NOR 10⁵、NAND 6×10⁴ 次 | 10⁶ 次(百万级) | EEPROM 可直接当小参数存储,不必上 FTL |
| 写保护 | BP 位 + WP# | BP 位 + WP# + WPEN(SPI);WP#(I²C 经典) | I²C 经典器件仅靠 WP 引脚整片保护 |
最容易踩的一条
把 NOR 驱动改两行就拿去驱动 EEPROM,最常见的死法是写完后用「延时 1 ms」代替轮询:EEPROM 的 tWR 是 5 ms 量级,1 ms 后器件仍在内部写,此时发下一条命令会被忽略或无视——表现为「偶尔丢数据、测多了又正常」。
2 · 术语与缩写
本章统一术语口径,避免「页/块/sector/word address」在文档与代码里指的不是同一件事。
2.1 结构与命令术语
| 缩写 / 术语 | 全称 | 本文中的含义 |
|---|---|---|
| Word Address | 字地址 | EEPROM 内部字节的线性地址(1/2/3 字节),即数据在阵列中的位置 |
| Page | 页 | 一次内部写周期(tWR)所能写入的最大字节数,是边界回绕的边界 |
| tWR | Write Cycle Time | 内部写周期时间,典型 5 ms / 最大 5~10 ms;窗口内器件 busy |
| ACK / NACK | Acknowledge / Not Acknowledge | I²C 应答位;EEPROM busy 时在第 9 个时钟返回 NACK |
| WREN / WRDI | Write Enable / Disable | SPI:06h / 04h,置位/清零 WEL 锁存 |
| RDSR / WRSR | Read / Write Status Register | SPI:05h / 01h |
| WIP | Write In Progress | SPI 状态位 bit0;1 = 内部写进行中,0 = 就绪 |
| WEL | Write Enable Latch | SPI 状态位 bit1;写操作前必须先置位 |
| BP0/1/2 | Block Protect bits | 保护阵列的上 1/4、1/2、全部等区段的组合位 |
| WPEN | Write Protect Enable | SPI 状态位 bit7;为 1 时 WP# 引脚才生效 |
| WP# | Write Protect pin | 硬件写保护引脚;拉高(I²C 经典器件)或配合 WPEN(SPI)锁保护 |
| Endurance | 耐久度 | 同一字节/页可承受的擦写次数,典型 1,000,000 次 |
| Retention | 数据保持 | 断电后数据可保存年限,典型 100 年(常温) |
2.2 本文书写约定
- 命令码用小写十六进制带后缀
h书写(如06h、05h),与 datasheet 保持一致。 - 状态位写作
SR[0]形式,方括号内为 bit 位序(bit0 为最低位)。 - 时序符号沿用 datasheet:tWR(写周期)、tSU:WP(WP 建立)、tBUF(停止到起始空闲)、fSCL / fCLK(总线时钟)。「典型值 / 最大值」成对给出时,软件超时必须按最大值预算。
- I²C 字节序列用
S <…> P简记:S=Start,P=Stop,Sr=Repeated Start,括号内为字节(含 R/W 位);[ACK]/[NACK]表示第 9 时钟的应答。 - 代码示例为 C 伪代码,省略总线传输底层实现,只表达命令序列与判据。
3 · 驱动架构与初始化
EEPROM 的驱动比 Flash 简单:没有坏块映射、没有 ECC 决策层。但 I²C 与 SPI 两套总线要抽象干净,否则上层会写满分支。
3.1 分层职责
| 层 | 职责 | 禁止做的事 |
|---|---|---|
| 应用层 | 业务调用 read/write/ioctl;不感知总线类型 | 直接拼 I²C/SPI 命令、直接算字地址边界 |
| 存储管理层 | 地址越界检查、页写拆分、提交协议、写频次预算 | 绕过保护检查直接下发写 |
| 命令协议层 | WREN + WRITE + 轮询(SPI);START + 字地址 + 数据 + 轮询(I²C) | 省略写使能、省略写完成轮询 |
| 硬件抽象层 | I²C / SPI 传输、CS# 时序、上拉与时钟 | 在传输中途切换总线频率或改 GPIO 配置 |
3.2 上电初始化流程
上电后 EEPROM 处于就绪状态(I²C 立即可响应;SPI 的 SR 默认 WEL=0)。但 I²C 总线可能处于「中间字节」状态,SPI 的 CS# 需要正确建立。初始化必须按下列顺序执行:
1. 上电:等待 VCC 稳定I²C 需先满足 tBUF;SPI 需满足 tPU(上电到 CS# 低)2. 总线恢复(I²C):发 9 个时钟脉冲清 SDA若之前总线卡死(SDA 被拉低),用 GPIO 模拟 9 个 SCL 释放3. 探测器件:I²C ACK / SPI RDSRI²C:发 S + 器件地址[W] 看是否 ACK;SPI:06h 无关 + 05h 读 SR4. 读标识 / 容量:确定页大小与地址宽度I²C 按器件型号查表;SPI 可 RDID(0x9F,若支持)或查表5. 回读保护状态:确认 BP / WPEN / WP#SPI:RDSR 看保护位;I²C:确认 WP# 当前电平与期望一致6. 就绪:返回设备句柄(含页大小 / 容量 / 总线类型)此后所有读写都经管理层做边界与保护检查7. 自检(可选):回读已知区 / CRC 校验镜像验证上次提交标记是否完整,发现半写则回滚
图 2 · 上电初始化流程(I²C 与 SPI 共用骨架,差异在步骤 2/3/4)
/* SPI 25xx 上电初始化:返回 0 表示成功 */ int eeprom25_init(struct eeprom_dev *dev) { uint8_t sr; delay_ms(1); /* tPU:上电到 CS# 低至少数百 µs,留余量 */ spi_cs_low(); spi_cs_high(); /* 确保 CS# 有高→低跳变,SPI 从机进入可寻址态 */ if (rdsr(&sr) != 0) return -E_NODEV; /* 05h 应能读到字节 */ dev->page_size = dev->cfg->page_size; /* 来自器件表 */ dev->addr_bytes = dev->cfg->addr_bytes; /* 回读保护状态:确认 BP / WPEN 符合设计(I²C 此步检查 WP# 电平) */ if (sr & (SR_WPEN | SR_BP_MASK)) dev->protect_state = eeprom_read_protect(); return 0; }I²C 上电初始化里最容易漏的两件事
① 总线恢复(清 SDA)。若上次通信在「主机发完地址但 EEPROM 正要回 ACK」时被复位,SDA 可能被 EEPROM 拉低,主机再发 Start 会被自己踩住——表现为「器件永远 NACK,I²C 总线锁死」。必须在初始化时先发 9 个 SCL 脉冲尝试释放 SDA。
② 不探测就直写。EEPROM 没有「写不进去就返回错误」的强约束(尤其 I²C 仅靠 ACK),地址接错或器件没贴都会表现为「写函数返回成功、读出来是 FFh」。初始化阶段务必做一次探测 + 回读校准。
3.3 器件识别与总线探测
- I²C 地址探测:发
S + 1010 A2A1A0 0 + P,若收到 ACK 说明该地址上有器件;经典 24Cxx 用 A2/A1/A0 引脚区分同总线多片。 - SPI RDSR 探测:对每片拉 CS# 后发
05h,应能读回一个字节(WEL=0 时通常为 0x00 或含保护位);读不到则器件不在位。 - 容量/页大小确认:I²C 按器件型号查表(24C02→256 B / 页 8 或 16 B,随料号);SPI 可试用
9FhRDID(部分 25xx 支持)或直接查器件表。 - 写保护现状读取:SPI 读 SR 拿 BP/WPEN;I²C 经典器件读 WP# 引脚电平,确认与「期望的保护策略」一致。
什么时候用器件表而不是探测
BOM 固定的产品,页大小、地址宽度、保护位定义都应写死在器件表里,探测只用于「确认在位」。若固件要兼容同一总线上的多种 EEPROM,应以器件表为主键、探测为辅,避免把「4 KB 器件按 2 KB 地址访问」这类错误。
4 · 状态寄存器与锁存体系
SPI 25xx 有一张状态寄存器(RDSR/WRSR),I²C 24Cxx 则没有状态寄存器——它的「busy」完全靠 ACK 行为体现。理解这两套机制,就理解了 EEPROM 的写完成检测。
4.1 状态寄存器总览(SPI 25xx)
RDSR (05h) / WRSR (01h) —— 8 位状态寄存器WPENb7—b6BP2b5BP1b4BP0b3—b2WELb1WIPb0WIP = 1:内部写进行中(busy);= 0:就绪
图 3 · SPI 25xx 状态寄存器位图(不同密度 BP 位数量可能为 2~3 个)
| 位 | 名称 | 读写 | 上电默认值 | 软件约定 |
|---|---|---|---|---|
| bit7 | WPEN | R/W(经 WRSR) | 0 | 为 1 时,WP# 引脚高电平才使能写保护 |
| bit5~3 | BP2 / BP1 / BP0 | R/W(经 WRSR) | 0 | 块保护组合(见第 8 章);默认 000 = 无保护 |
| bit1 | WEL | 只读(由 WREN/WRDI 翻转) | 0 | 写操作前必须为 1;写完自动清零 |
| bit0 | WIP | 只读 | 0 | 写完成轮询的唯一判据 |
4.2 WIP / WEL 判读
写操作(WRITE 后)器件进入 busy:WIP=1;内部写完成后 WIP 自动清零。WEL 在每次写入结束后自动清零,下次写前必须重新 WREN。
/* SPI 25xx:读状态寄存器 */ uint8_t eeprom25_rdsr(void) { uint8_t tx = 0x05, rx = 0; spi_cs_low(); spi_transfer(&tx, 1); spi_read(&rx, 1); spi_cs_high(); return rx; } /* 写完成轮询:WIP 清零即完成 */ int eeprom25_wait_ready(uint32_t timeout_ms) { uint32_t t0 = now_ms(); while (eeprom25_rdsr() & SR_WIP) { if (now_ms() - t0 > timeout_ms) /* 建议 ≥ 10 ms(tWR max 5~10 ms) */ return -E_TIMEOUT; } return 0; }WIP 只说忙不忙,写成功与否另看返回
SPI 25xx 的写失败(被保护)表现为:写被直接忽略,WIP 仍然正常清零,不会置任何错误位。也就是说「轮询到 WIP=0」只代表「器件闲了」,不代表「写进去了」。被保护的写要靠保护位检查提前拦截,而不是事后看状态。
4.3 写保护位与 WPEN
| 条件 | 受保护区域 | 说明 |
|---|---|---|
| WPEN=0(默认) | BP 位仍可通过 WRSR 配置,但 WP# 引脚不生效 | 软件可自由改 BP,硬件锁不工作 |
| WPEN=1 且 WP# 高 | 阵列保护由 BP 位决定;SR 解锁,可改 WPEN/BP | 允许写且配置可改 |
| WPEN=1 且 WP# 低 | 阵列保护仍由 BP 位决定;但状态寄存器被锁(WRSR 被忽略) | 仅锁 SR(硬件写保护生效) |
WPEN 是「是否允许 WP# 生效」的总开关,不是「是否保护」
很多工程师误以为 WPEN=1 就直接保护了阵列——其实阵列保护只由 BP 位决定(与 WP# 电平无关);WP# 低 + WPEN=1 仅锁状态寄存器(防改 BP/WPEN),WP# 拉高则 SR 解锁。只置 WPEN 不置 BP,阵列仍然可写。相反,I²C 经典 24Cxx 没有 WPEN/BP,WP# 一拉高就是整片保护,无需任何寄存器配置。
4.4 I²C 24Cxx 的无状态寄存器
I²C 24Cxx没有状态寄存器,也不提供读状态命令。它的「写入进行中」完全通过 ACK 行为体现:
- 写周期(tWR)内,器件把 SDA 拉低,对
S + 器件地址[W]返回NACK(第 9 时钟 SDA=1); - 写完成后,器件释放 SDA,再次探测时返回ACK(SDA=0);
- 因此 I²C 的「写完成轮询」就是反复发起始 + 地址、看 ACK(详见第 6.2 节)。
I²C 写保护极简:只有 WP# 引脚
经典 24Cxx(含绝大多数 24C02/04/08/16/32/64/128/256)整片写保护只有一个开关:WP# 引脚为高→ 整片只读;为低 → 可写。没有 BP 区段、没有 WPEN。软件要保护某段数据,要么在硬件上把 WP# 接固定高电平,要么在固件里用「数据本身 + 校验 + 幂等提交」来兜底。
后续章节请下载「芯参谋」查看
……(篇幅原因, 还有17章· 详见芯参谋→方案设计)
当前分享仅开放前5章正文。详细资料请在芯参谋客户端中打开完整文档。
打开路径:🔧解决方案&应用市场分析 -> 原理方案设计 -> EEPROM_软件设计规范
📥 下载芯参谋客户端