ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

OTP与EEPROM从原理到实操:选型、读写时序与数据可靠性设计

2026/10/5 1:23:54 拓冰建站 浏览量
OTP与EEPROM从原理到实操:选型、读写时序与数据可靠性设计 做嵌入式这几年几乎每个项目都躲不开 OTP 和 EEPROM 这两个词。有人拿它们存校准数据有人存设备序列号还有人当掉电保存区用。听起来简单真正把读取和处理做扎实踩的坑可不少。这篇文章我打算把 OTP 和 EEPROM 从原理、选型、读写时序、数据可靠性设计到问题排查一次讲透给正在做相关开发的同学一份可以直接参考的实操手册。如果你正准备在 MCU 方案里挂一块 EEPROM 存配置参数或者要在芯片内部 OTP 区烧录序列号和出厂校准数据再或者已经在 I2C 读写上遇到奇奇怪怪的问题这篇内容都值得你读到最后。1. 先把概念理清楚OTP 和 EEPROM 到底是怎么回事1.1 从存储单元的原理说起EEPROM 的中文全称叫“电可擦除可编程只读存储器”名字有点绕但核心就几个字电可擦除、可编程、只读。这里的“只读”不是指不能写而是指数据在掉电后依然能保存平时程序只去读它只有在需要更新参数时才执行写操作。原理层面EEPROM 用的是浮栅晶体管。每个存储单元里有一个悬浮在绝缘层中间的浮栅外部通过控制栅来操作它。写入数据时控制栅加高电压电荷穿过隧穿氧化层注入浮栅擦除时再加反向高电压把电荷从浮栅里抽出来。因为浮栅被绝缘层包围电荷一旦放进去即使断电也能保持很长时间典型标称值是数据保存 100 年。关键区别在于EEPROM 每个存储单元都有独立的选择晶体管所以可以做到字节级擦写。这一点和 Flash 很不一样Flash 为了做高密度单元之间共享字线和位线只能按扇区或块擦除。这就是为什么你在用 Nor Flash 存小参数时总觉得别扭写一个字节要先擦掉一整块而 EEPROM 直接改写对应字节就行这也是它在小数据存储场景里至今没被 Flash 完全替代的原因。OTP 的全称是 One-Time Programmable一次性可编程存储器。它在出厂时内容为空用户只能写一次写完就固化物理上无法再修改。有些 OTP 采用熔丝结构编程时通过大电流把熔丝烧断来记录信息也有不少 OTP 本质上就是电荷存储结构但厂商把擦除功能在逻辑和物理上屏蔽了。不管哪种实现OTP 的设计目标都一样保证信息一旦写入就不能被意外改动或篡改。1.2 OTP 不只是“一次性存储器”这里要特别说一个容易混淆的点。OTP 在信息安全领域经常代表 One-Time Password也就是动态口令银行 U 盾、数盾令牌、登录短信验证码都属于这一类。而在半导体和嵌入式开发语境里OTP 指的是一次性可编程存储器是存储器件两者缩写相同但完全是两个世界的东西。搜索引擎里搜“OTP 技术”会同时蹦出这两类内容刚接触的同学容易被带偏。回到存储领域OTP 和 EEPROM 还有一个中间态叫 MTPMulti-Time Programmable允许写有限次数比如几十次常见于 MCU 内部做模拟 EEPROM 的 Flash 区域。很多 MCU 没有真正意义上的独立 EEPROM而是用一小块 Flash 配合软件模拟出 EEPROM 的效果这本质上就是一种 MTP 方案后面我在数据处理部分会专门讲。1.3 产品里 OTP 和 EEPROM 怎么搭配使用在实际产品中OTP 和 EEPROM 往往是组合出现的。举一个我做过的手持设备例子主控芯片内部有一个 OTP 区域用来烧录设备 MAC 地址、产品序列号、出厂校准的射频功率值。这些信息在生产测试流程中一次性写入之后终身不变。为什么要用 OTP因为 MAC 地址和序列号是产品的唯一身份标识如果用 EEPROM 存理论上任何人都能通过 I2C 或者 SPI 把它改掉轻则造成设备身份冲突重则带来安全隐患。OTP 写入后物理锁定想改只能换芯片这在安全角度是非常理想的设计。而同设备外挂的一颗 AT24C02 EEPROM则用来存运行参数比如告警阈值、设备编号、用户配置、里程计数。这些数据会随着设备运行而变化可能需要写上百次、上千次甚至上万次显然不能用 OTP 来解决。所以选型逻辑很清楚不可变且需要防篡改的数据优先放 OTP需要动态更新、反复擦写的数据放 EEPROM。把两者的角色搞反后面出问题基本是必然的。2. 硬件接口、芯片选型与连接细节2.1 接口怎么选I2C、SPI、还是 MicrowireEEPROM 的通信接口主要有三种I2C、SPI 和 Microwire。工程里最常见的是 I2C 和 SPIMicrowire 在汽车和部分老产品里还能看到这里就不展开。I2C 接口的 EEPROM 占用引脚最少只需要 SCL 和 SDA 两根线多个设备还能挂在同一条总线上靠器件地址区分。缺点是通信速率相对低标准模式 100kHz快速模式 400kHz遇到大量数据搬运时吞吐量不太够。另一个隐患是总线如果没接上拉电阻或者多个设备地址冲突调试起来会很痛苦。SPI EEPROM 需要 CS、SCK、MOSI、MISO 四根线速率可以跑到几十 MHz适合需要频繁读写较大数据的场景。代价是引脚占用多而且每个 SPI 设备要独占一个 CS 引脚多个设备挂一条 SPI 总线时要额外占用主控 GPIO。我的选型习惯是只存少量配置参数主控引脚紧张优先 I2C EEPROM数据量大、读写频繁、对速度有要求用 SPI EEPROM对可靠性要求极高的工业场景优先选 SPI 接口的汽车级 EEPROM时序更简单抗干扰能力普遍好一些。2.2 常见芯片型号与参数对比I2C 阵营里最经典的就是 Microchip 的 24C 系列和 ST 的 M24C 系列后面还有国内厂商的一堆兼容型号。容量从 2Kbit 到 2Mbit 都有价格从几毛到几块不等。以 24C 系列为例我整理了一份常用型号的参数对照型号容量页大小地址引脚字地址长度AT24C022Kbit / 256B8 字节A0-A21 字节AT24C044Kbit / 512B16 字节A2 块选择位1 字节AT24C088Kbit / 1KB16 字节A2 块选择位1 字节AT24C1616Kbit / 2KB16 字节全部块选择位1 字节AT24C3232Kbit / 4KB32 字节A0-A22 字节AT24C256256Kbit / 32KB64 字节A0-A22 字节注意 24C02 和 24C32 虽然都带 A0-A2 地址引脚但字地址长度不同代码不能直接复用。24C04 以上容量器件地址里的低位变成了块选择位比如 24C16 的地址字节是 1010 P2 P1 P0 R/WP2P1P0 共同决定寻址哪个块这时候 A0-A2 引脚实际上不接外部地址只能悬空或接地多个同型号芯片就不能靠 A2 引脚区分地址了这是一处非常容易踩坑的地方。SPI 阵营常见的是 25AA 系列和 25LC 系列比如 Microchip 的 25AA256容量 256Kbit工作电压可以低到 1.8V。SPI EEPROM 的指令集比较标准WREN、WRITE、READ 等指令在各大厂之间基本通用。2.3 硬件连接和电路设计要点I2C EEPROM 的硬件连接看似简单其实有一堆细节。SCL 和 SDA 必须接上拉电阻阻值一般在 2.2kΩ 到 10kΩ 之间。上拉电阻太小总线低电平驱动电流过大太大会导致上升沿变缓高速通信时波形畸变。我自己的经验是总线设备少、长度短、标准 100kHz用 4.7kΩ总线设备多、长度长、要用 400kHz用 2.2kΩ低功耗电池供电系统为了省静态电流可以用 10kΩ但前提是通信速率不能太高。电源去耦也不能省。EEPROM 的电源引脚旁边加一个 0.1μF 陶瓷电容尽量靠近芯片引脚放置。量产批次中偶发的数据写坏问题有不少就是电源纹波过大导致的。WP 写保护引脚是很多人忽略的重点。24C02 的 WP 引脚拉高时整个器件禁止写入拉低或悬空时允许写入。如果你的产品量产时希望固件运行阶段能正常修改配置WP 必须拉到地或者通过 GPIO 动态控制。但要注意有些型号的 WP 保护范围并不是全片比如 AT24C32 的 WP 可以只保护上半区具体要看芯片手册。地址引脚 A0-A2 的接法决定器件地址。24C02 挂在同一条 I2C 总线上的时候三颗芯片可以把 A0-A2 分别接 000、001、010实现三颗扩展。但同一总线上挂两颗晶振地址接近的芯片比如把 A2 都接地导致地址相同通信必然冲突读取的数据会错乱这类问题我在最后一章再细说。3. 读取与写入的实操细节3.1 I2C 时序基础起始、停止、应答所有 I2C EEPROM 的读写都是围绕时序展开的。别看通信协议简单时序错了数据就全乱了。I2C 总线上的每次通信由起始条件和停止条件包起来。SCL 保持高电平时SDA 产生一个下降沿就是起始条件SDA 产生一个上升沿就是停止条件。起始条件之后主设备发送 8 位数据其中高 7 位是器件地址最低位是读写的方向位0 表示写1 表示读。For 24C02器件地址的格式是 1010 A2 A1 A0 R/W默认 A0-A2 全接地时写地址是 0xA0读地址是 0xA1。每个字节传输完成后接收方需要回一个应答位 ACK也就是把 SDA 拉低一拍。主设备写模式下从机每收到一个字节都会 ACK主设备读模式下从机每发完一个字节会等待主设备回 ACK主设备回 ACK 表示“继续发”回 NAK 表示“我不想再读了结束吧”。这里有一个新手最常见的错误连续读的时候读最后一个字节之前没有回 NAK后面直接发 STOP从机在总线上检测到的时序就会变得很奇怪有些芯片会多输出一个字节导致本次读取的数据漂移到下一个字节位置。正确的做法一定是读完倒数第二个字节后回 ACK读到最后一个字节时回 NAK然后再发 STOP。3.2 写 EEPROM字节写、页写、跨页问题24C02 的字节写流程比较简单发送起始条件发送器件写地址 0xA0等待从机 ACK发送片内字地址比如要写 0x10 地址等待从机 ACK发送要写入的 1 字节数据等待从机 ACK发送停止条件。停止条件发出后EEPROM 内部开始执行写入动作这个过程叫内部写周期典型时间是 5ms。在写周期结束之前芯片不会响应任何新的 I2C 指令如果你立刻发送下一个写操作从机不会回 ACK程序就会卡死等待。用一个简单的 C 语言伪代码表示就是uint8_t i2c_write_byte(uint8_t dev_addr, uint8_t mem_addr, uint8_t data) { i2c_start(); if (i2c_send_byte(dev_addr 0xFE) ! ACK) return ERROR; // 写地址 if (i2c_send_byte(mem_addr) ! ACK) return ERROR; if (i2c_send_byte(data) ! ACK) return ERROR; i2c_stop(); delay_ms(5); // 等待内部写周期结束 return OK; }页写是一口气写多个字节24C02 一页是 8 字节24C32 是一页 32 字节。页写的地址格式和字节写一样只是发完首个数据字节后继续发后续字节不用重新发地址。从机内部会自动把低地址递增。但这里有个非常容易踩的坑页写不能跨页。比如 24C02 页大小是 8 字节你在页地址 0x06 处开始写 4 个字节写第 3 个字节时内部地址溢出会回卷到同一页的开头 0x00把之前已经写好的数据覆盖掉。最简单的规避办法是每次页写前先计算当前地址到页末尾还剩多少字节不足就分批写或者干脆拆成单个字节写。我自己的习惯是写一个通用的跨页写函数逻辑是循环拆分每次最多写当前页剩余字节数。这样无论应用层传来多长的缓冲区底层都不会触发回卷覆盖。3.3 读 EEPROM当前地址读、随机读、连续读读操作比写操作稍微复杂一点但总共也就三种模式。当前地址读最简单。发送起始条件后直接发送器件读地址比如 0xA1从机就把内部地址计数器指向的那个字节输出到总线上。所谓“当前地址”其实是上次操作结束后停留的地址。这种模式适合用来判断数据是否写成功比如写完一个字节后读回来比对但一般不会单独依赖它做数据上半场随机读。随机读是更常用的模式。它和写操作的头半段一样先发送起始条件发送器件写地址 片内字地址这一步相当于“告诉芯片我要读哪个地址”但是不发数据直接再发一个起始条件重复起始随后发送器件读地址。从机收到读地址后把刚指定的那个地址的数据发出来。连续读则是随机读的扩展。主设备读走第一个字节后继续回 ACK从机就会自动把地址递增继续发下一个字节的数据直到主设备回 NAK 并发送停止条件内部地址计数器才会停下来。连续读的典型应用是读取一整块结构体配置。比如配置参数总共 32 字节就做一次随机读定位到起始地址然后连续读 32 字节。一个常见的读写组合是把 32 字节的配置结构体一次性写入写入时底层自动拆成 4 次页写24C02 页 8 字节正好 4 页读取时用连续读一次性读回。这样吞吐量高代码也简洁。3.4 OTP 烧录的注意事项OTP 的烧录和 EEPROM 完全不是一个套路。EEPROM 你写错了还能重写OTP 写错了就是废片所以 OTP 操作的铁律是先校验后烧录烧录后再回读验证。很多 MCU 内部 OTP 区域的写入不是单纯靠 I2C 或 SPI而是通过芯片厂商提供的标准库函数或烧录工具。一般流程是读取 OTP 目标区域当前值确认是全 0xFF 或者全 0x00也就是空片状态在内存里组好要写入的数据包比如序列号加 CRC 校验调用 OTP 写入接口单次烧录回读整个区域逐字节比对任意不一致立即报错。不要小看回读这一步。我在一个量产项目里遇到过 OTP 部分位烧录不成功的现象表面看起来是软件流程问题实际排查发现是烧录电压不稳导致某些位没有彻底编程。如果不做回读校验这些有问题的设备流入市场等客户发现序列号读出来是乱码返工成本就完全失控了。另一个经验是 OTP 烧录前要在程序里加入防呆逻辑。比如序列号必须以固定前缀开头或者 MAC 地址的第一个字节必须符合厂商 OUI 范围烧录前先校验数据合法性而不是盲目把缓冲区内容烧进去。在批量产线环境中操作员手误选错烧录文件的事故是真实发生过的。4. 数据处理、可靠性设计与简易文件系统4.1 数据校验和结构标识EEPROM 本身只管存字节不关心你存的内容是配置、日志还是键值表所以应用层的数据结构设计直接决定可靠性。我写 EEPROM 数据时每个配置区块都会加上三类字段魔数Magic Number、校验值、版本号。魔数是一个固定值比如 0xA55A用于判断这个区域内是否已经写入过有效数据。新出厂的 EEPROM 全片都是 0xFF如果首字节不是魔数程序就知道这个区块尚未初始化需要按默认配置写入。校验值通常用 CRC。CRC8 适合校验 16 字节以内的小结构体CRC16-CCITT 适合校验几十到几百字节的配置。不要把校验值简单设成所有字节的和累加和检查在数据字节顺序交换时完全失效CRC 在工程可靠性上要扎实得多。版本号的作用也很关键。配置格式升级后旧设备里存的是旧版结构体代码不知道字段布局已经变化轻则读出错误配置重则越界访问。有版本号程序可以先判断版本再决定怎么解析数据或者主动做一次迁移重写。我在工程里常用的结构是这样设计的typedef struct { uint16_t magic; // 固定 0xA55A判断有效性 uint8_t version; // 结构体版本 uint8_t reserved; uint16_t crc; // CRC16-CCITT覆盖 data 区域 uint8_t data[32]; // 真正的业务配置 } config_block_t;读取的时候先读整个块检查 magic 和 crc都通过了才认为数据可用。任何一步校验失败直接按默认配置运行并把默认配置回写进去这样既能自愈初始状态也能避免一块数据坏掉导致整个设备无法启动。4.2 磨损均衡别让同一块被反复写EEPROM 虽然寿命比 Flash 好得多工业级芯片的擦写寿命普遍标称 100 万次但依然经不住高频写入的长期消耗。典型场景是设备运行计数器每小时写一次一天 24 次一年就是 8760 次100 万次寿命看似够用但如果设备每秒钟都要记录一次运行时长一个月就是 260 万次很快就到寿命上限数据就守不住了。磨损均衡的核心思想是让写入均匀分布在多个地址上而不是每次都戳同一个位置。最简单的实现是分槽轮流写比如把 8 页空间作为计数器的 8 个槽位每次写入顺序递增到下一个槽位等到所有槽位都写满后再做一次整体迁移把最新值写到第一个槽位后面槽位清零备用。这样寿命直接扩展 N 倍N 就是槽位数。具体算法可以是读取全部槽位找到序列号最大的那个槽它就是最新计数写新计数时写入下一个空槽如果所有槽位都满了将最新计数写入第一个槽并把其余槽清零开机初始化时扫描所有槽用最大序列号决定当前有效值。这种方案在网络设备、电表、水表表计的计量数据存储里非常普遍。实现不复杂但可靠性提升非常明显。4.3 模拟键值存储在 EEPROM 上做简易文件系统I2C 和 SPI 的 EEPROM 没有地址线访问操作系统通常直接按地址操作但遇到需要频繁删改配置项的时候固定地址布局的方式可用性不高。比较新的做法是在 EEPROM 上实现一个简易键值存储KV Store思路和日志式文件系统类似。典型的实现是把存储区划成多个定长记录槽每个记录包含键、长度、数据、序列号、CRC写入新值时不覆盖旧记录而是追加到下一个空槽并递增序列号读取时扫描所有槽找到相同键中序列号最大的那条记录返回当空槽不足时触发一次垃圾回收把有效记录搬到临时区再擦除整片并重写。这样一个方案即使数据每次只修改一个字节物理上的写入开销也是固定的同时天然具备一定的写保护能力。因为它带有 CRC断电导致半截写入时CRC 校验不过就会被丢弃只有完整写入的记录才会被识别。微博正文里有人提到过一个“一种 EEPROM 的文件管理系统”说的基本就是这个思路。如果你不想自己造轮子可以参考一些开源 KV 库的实现比如 LittleFS 的思路把它移植到 EEPROM 上或者使用一些面向小容量存储的键值库。对大多数 MCU 项目来说几百行代码量就能跑起来一个非常可靠的 KV 区。4.4 双备份和掉电保护工业设备里还有一个经典设计就是双备份。也就是把关键配置同时存在 A 区和 B 区写入时先写 A 区再写 B 区读取时先读 A 区校验失败再读 B 区两个都失败就用默认配置。这样做的目的是对付掉电。假如写入 A 区进行到一半突然断电A 区数据损坏由于 B 区还是旧的有效数据设备上电后依然能正常工作只是这次的配置可能不是最新值但至少不会进入不可用状态。更严格的方案是三区冗余类似飞机上的余度设计两个区一致才认为数据可靠当三个区不一致时以多数一致为准。针对掉电瞬间的数据完整性我一般会在写入首个字节之前先把“正在更新”标志写到另一个专门区域更新完成后擦掉标志。上电时如果发现标志还在说明上次更新没有完成程序可以主动进入恢复流程。4.5 写保护和防盗版的最小实现OTP 数据读出来是公开的谁拿逻辑分析仪都能抓到字节流所以不要把密钥明文存到 OTP 里。一些安全敏感的产品会把密钥分成若干段经过对称加密后再写入 OTP读取后在 RAM 中解密使用。虽然这不是最安全的方案但至少提高了作恶成本。量产序列号同样不建议明文裸存可以在序列号基础上附加一个 HMAC 或 CRC读取时校验能有效拦截简单的手工篡改。EEPROM 的 WP 引脚硬件上做了写保护之后数据不能简单通过工业化工具修改需要同时控制软件和硬件两种手段才能实现相对完整的防篡改。5. 常见问题与排查思路实录5.1 I2C 通信类问题问题一发送器件地址后一直收不到 ACK。 这是最经典的 I2C EEPROM 问题。优先检查三处器件地址是否正确24C02 的写地址是不是发送成了 0xA2 而不是 0xA0特别是 A0-A2 引脚接地后地址只有 0xA0SCL/SDA 是否接上拉电阻漏接了上拉总线电平会一直处于低电平从机根本无法识别起始条件总线上的设备地址是否冲突用逻辑分析仪抓一下事件看看有没有其他设备占了相同地址。问题二通信一段时间后总线被拉死。 总线卡死常见原因是 SDA 线被某个设备始终拉低。排查方式是断开主控单测从机或者检查代码里是否在接收中断尚未完成时又启动了新的 I2C 传输。时序上尤其注意重复起始条件的正确性如果代码里在发完地址后没有正确产生重复起始从机可能认为传输未结束一直保持总线占用。问题三使用 400kHz 快速模式时读写不稳定。 很多便宜的国产 EEPROM 宣称支持 400kHz但实测在上升沿比较陡的上拉条件下还是容易出错。遇到这种情况第一步把通信速率降到 100kHz 试试只要能稳定工作就证明是时序余量不足。其次检查上拉电阻换小一点的阻值或者使用带斜率控制的 I2C 总线缓冲器。5.2 数据丢失和写入异常问题问题一写入后立刻读取数据是错的但断电重启后是对的。 这是内部写周期没有等待导致的。EEPROM 收到停止条件后才开始内部编程编程期间不响应外部指令。如果你的代码没有等 5ms 就发读指令从机可能不回应或者读到的是写入前的旧值。解决方法是写完后加延时或者采用 ACK 轮询方式——一直发读指令直到从机 ACK 响应再做后续操作。问题二页写入后数据出现部分正确、部分错乱。 基本可以判定是跨页写。24C02 页写超过页结尾会回卷覆盖页首导致数据被随机覆盖。遇到这种情况不要急着改芯片先算一下起始地址是否能整除页大小。问题三设备运行几天后配置偶发丢失。 这种偶发性问题通常和电源、噪声、时序余量有关。我用示波器抓到过一例系统电源在电机启动瞬间掉落到 2.8VEEPROM 在低电压下写入时序异常导致数据整块写坏。后级加了一个储能电容并且在上电和下电过程中禁止写 EEPROM问题就不再复现。经验是写 EEPROM 之前一定要检查电源是否稳定在上电或掉电过程中不要做写操作。5.3 OTP 相关的典型事故OTP 最惨痛的事故就是烧错数据。量产线上操作员把 A 版本的序列号范围烧到了 B 版本设备上等到产品到客户端才发现整批产品只能报废。后来我在 OT P 烧录流程里增加了三重确认操作员手动确认、产线系统自动解析烧录文件中的型号字段、上位机在写入前回读空片状态并校验序列号合法性。虽然不能完全杜绝人为失误但至少把事故概率大幅降低。另一个 OTP 问题是编程电压不足导致部分位没有完全写入。回读时虽然大多数位看起来是对的但个别位会有随机波动一旦到了高温环境下就会出现漂移。OTP 写入后做一次 100% 的回读和边界比对是非常有必要的。5.4 排查工具和流程排查 EEPROM 问题我常用的工具优先级是这样的逻辑分析仪抓 I2C 时序直接看 ACK、地址、数据字节解决 90% 的通信问题示波器看波形质量和时序余量检查上升沿、毛刺、电源纹波回读打印把读出来的原始十六进制打印到串口肉眼比对数据写一个测试命令通过串口输入起始地址和长度读一片区域出来直接验证芯片映射关系。如果怀疑芯片本身故障用编程器把 EEPROM 拆下来离线读写能快速区分是主控软件问题还是芯片硬件问题。写在最后的一点经验我自己的习惯是EEPROM 读写代码从第一天就加入严格的状态返回值处理任何 ACK 异常、CRC 校验失败都要有明确的错误码而不是静默忽略。很多看起来莫名其妙的“数据偶发丢失”最后都是被某个吞掉的错误状态掩盖掉导致的。OTP 烧录则永远坚持“先校验、后烧录、再回读”三件事缺一不可。这些习惯的养成往往需要在量产现场熬好几个通宵才能刻进骨子里。如果你正在规划一个新项目的存储方案建议从数据生命周期入手哪些数据写一次就不变哪些数据天天变哪些数据掉了无所谓哪些数据丢了会出大问题。把这些梳理清楚再回头看用 OTP 还是 EEPROM方案基本就呼之欲出了。