☰
EEPROM与OTP读取实战:从I2C时序到MCU内置存储的完整指南
2026/10/5 5:47:46 网站建设 项目流程

我最早被OTP这个词坑过一次。当时拿到一块设备的存储颗粒,标称是EEPROM,结果读完发现某些地址写进去再上电又恢复成出厂值,后来手册翻到底才发现那部分是OTP区——不是动态口令那种OTP,而是One-Time Programmable,一次可编程存储区。从那以后我养成了习惯:任何一个存储器,动手读取之前先分清它到底是PROM、EEPROM还是OTP,因为三者的读取方式、编程要求和数据保护策略完全是不同逻辑。这篇东西不打算讲教科书,就把我在实际项目里读取和处理OTP/EEPROM的流程、踩过的坑和现在的标准做法记录下来。不管是拿编程器读外置存储、用单片机I2C口抓EEPROM数据,还是要处理MCU内置EEPROM和OTP配置区,应该都能找到能直接用的内容。

1. 先把OTP和EEPROM的定位搞清楚:不是所有“读出来”都一样

1.1 OTP的两种常见形态

OTP在实际产品里通常以两种形态出现。第一种是独立的一次性可编程存储芯片,这种芯片出厂时内部是空白的,通过专用编程器写一次数据后,内容就永久固定。因为结构简单、成本低,在一些量很大且数据固定不变化的场景里很常见,比如家电控制板里的参数固化、仪表里的标定系数存放。

第二种形态更常见,也更容易被忽略:MCU内部集成的OTP区域。很多国产单片机、甚至一些国际大厂的MCU,内部除了主Flash和EEPROM之外,还会有一小块OTP区域,用来放序列号、振荡器校准值、加密标识、产品版本号这类一旦出厂就不该再变的数据。还有一些安全芯片内部也有OTP区,专门存放密钥、设备证书、防抄板信息。这类OTP往往不是独立芯片,而是芯片内部存储空间的一部分,如果没仔细看手册,很容易把它当成普通EEPROM去“读和写”,结果写不进去或者写进去就后悔。

1.2 EEPROM的存储原理:浮栅晶体管与擦写寿命

EEPROM的全称是Electrically Erasable Programmable Read-Only Memory,虽然名字里带Read-Only,但实际是可擦写的。它的存储单元基于浮栅晶体管,通过向浮栅注入电荷或移除电荷来代表0和1。写入时利用隧道效应让电子穿过氧化层进入浮栅,擦除时再加反向电压把电子拉出来。整个过程不需要紫外线照射,也不需要从电路板上拆下来,所以叫“电可擦除”。

和Flash相比,EEPROM最大的特点是支持字节级擦写,不需要按扇区整块擦除。这也是很多产品用它存参数的原因:我改一个字节的配置,不用把整个64KB区域都擦一遍。但代价是速度慢、容量小、寿命有限。常规EEPROM的擦写寿命在10万次到100万次之间,数据保持时间通常在10年到100年,工业级和车规级的指标会更好一点,但也不是无限次写。

这里要强调一个经常被忽略的点:EEPROM读取的时候是没有次数限制的,但写入是消耗寿命的。有些项目里代码写得粗糙,在main循环里每隔几毫秒就向EEPROM写一次状态记录,几个月后EEPROM就“坏”了——读出来全是0xFF或者0x00,数据对不上。这不是芯片质量差,而是寿命被耗尽了。所以后面会专门讲写磨损和数据备份的问题。

1.3 读取前必须确认的电气参数

无论读OTP还是EEPROM,动手之前都必须先确认几个电气参数,省得到时候通信不上还要回头排查。第一是供电电压。老一点的芯片可能是5V,新的低功耗芯片可能是1.8V或3.3V,编程器或单片机IO口电平不匹配的话,SDA/SCL信号可能识别不了。第二是接口类型,常见的有I2C、SPI、Microwire、并行总线,每种接口的时序完全不同。第三是器件地址和引脚配置,尤其是I2C的A0/A1/A2地址线和WP写保护脚。第四是最大时钟频率,I2C EEPROM一般支持100kHz或400kHz,SPI的可以到几MHz甚至更高,超频通信容易出随机错误。

我自己在项目里最常用的几颗料简单整理了一个表,方便快速对照:

型号容量接口供电器件地址/注意事项
AT24C022KbitI2C1.8V-5.5V地址1010 + A2A1A0
AT24C256256KbitI2C1.8V-5.5V需要发送2字节地址
AT25F20482048KbitSPI2.7V-3.6V读命令0x03
中颖MCU内置视型号而定IAP跟随MCU通过特殊功能寄存器访问

表格只是参考,实际用哪颗料一定以官方数据手册为准。但至少确认这些参数后,读取和处理的大方向不会错。

2. 读取EEPROM的硬件准备:接口、时序与最小系统

2.1 I2C与SPI两种主流接口的引脚与选型

外置EEPROM里,I2C接口和SPI接口是主流。I2C只需要SCL和SDA两根线,接线简单,最大总线电容限制了线长和器件数量,适合板内短距离通信。SPI需要CS、SCK、MOSI、MISO四根线,速率可以做得很高,适合容量大、读写频繁的场景。选型时如果引脚紧张,优先I2C;如果追求吞吐速度,优先SPI。

读取的时候,I2C和SPI的差异很大。I2C每个字节传输后都有ACK应答机制,如果从机没应答,主机能立刻发现。SPI没有应答机制,只有CS拉低后直接双向移位,主机要判断器件是否正常,只能靠回读数据是否合理。所以用单片机模拟I2C读EEPROM时,代码里必须处理ACK;读SPI EEPROM时,则要格外注意CS时序和命令码是否正确。

2.2 上拉电阻、地址引脚与硬件地址配置

I2C总线是开漏结构,SCL和SDA必须接上拉电阻才能产生高电平。上拉电阻太小,总线拉低时电流过大;上拉电阻太大,信号上升沿变缓,高速通信时容易出错。实际项目里1k到10k都能工作,我常用的默认值是4.7k。如果总线上挂了多个I2C从机,上拉电阻要综合考虑总线电容,可能换成2.2k甚至1k。

地址引脚这块是新手最容易翻车的点。以AT24C02为例,器件地址是固定的4位1010,加上A2、A1、A0三个引脚的电平,再加上最后的读写位,组成完整的8位地址字节。A0/A1/A2在芯片内部有下拉,如果悬空通常是低电平,但为了保证读取可靠,还是建议明确接地或接VCC。如果同一块板上有两颗AT24C02,必须把A0A1A2设置成不同的组合,否则会地址冲突,通信时两颗芯片抢总线。

读取板载EEPROM时还容易遇到一个问题:WP写保护脚。WP引脚在高电平时禁止写操作,但读取不受影响。所以有时候读得出来但写不进去,先不要怀疑程序,先量一下WP引脚电平。飞线读取时,如果WP悬空,部分芯片内部有下拉,默认可以写;但也有些芯片内部上拉,默认写保护,导致写入失败。稳妥做法是把WP直接接地。

2.3 我常用的读取工具组合

读取EEPROM和OTP,我会根据场景选择不同的工具组合。

第一类是用逻辑分析仪抓总线波形。这是我最推荐的起步方式,逻辑分析仪能直接看到I2C的START、STOP、地址字节、ACK、数据字节这些关键信息。采样率不需要太高,I2C 400kHz的话,25MHz采样率的8通道逻辑分析仪完全够用。关键是探头的接地线要短,夹在板子的GND上,信号线夹SCL和SDA,不要用又长又绕的杜邦线去飞,否则抓出来的波形毛刺一堆,根本没法判断。

第二类是用通用编程器,比如CH341A、RT809H、TL866这类。编程器适合直接读取板子上的存储芯片或者把芯片拆下来读。优点是不用写代码,装好软件选型号就能读整片内容,适合逆向分析、维修抄录、量产拷贝。缺点是有些编程器对板内在线读取支持不好,片选、地址线、写保护引脚状态不确定时可能读到错误数据,这时候要拆下来读,或者把相关引脚用飞线强制拉高拉低。

第三类是用单片机当I2C主机,比如STM32、Arduino、ESP32。这种方法适合做联调,读出来的数据可以直接参与逻辑判断,也可以加校验算法,比编程器灵活得多。缺点是要写代码、调试,上手门槛高一些。但如果项目本身就用单片机控制这个EEPROM,那么直接基于现有代码加一个读取功能是最自然的选择。

3. I2C EEPROM读取实操:从器件地址到连续读的完整流程

3.1 器件地址的构成:A0/A1/A2引脚与读写位

先说清楚I2C EEPROM的地址字节怎么算。拿AT24C02举例,它是2Kbit容量,也就是256字节,只需要一个字节的存储地址。它的从机地址格式是“1010 A2 A1 A0 R/W”。高四位1010是厂商定义好的器件标识,中间三位对应硬件的A2A1A0引脚电平,最低位是读写控制位:写为0,读为1。于是当A2=A1=A0=0时,写地址是0xA0,读地址是0xA1。如果A0接高电平,写地址变成0xA2,读地址变成0xA3。理解了这一点,就知道网上那些代码里为什么会有0xA0和0xA1两个宏定义,它们不是同一个地址,而是同一颗芯片的写和读两种访问地址。

容量超过16Kbit的I2C EEPROM,比如AT24C256,存储地址需要两个字节,主机发送数据时,地址字也是两字节,但器件地址里的P0、P1位可能参与选择页地址。实际读取时先看手册确认“Device Address”里哪几位是固定值,哪几位是页选择,避免出现地址错位。

3.2 当前地址读、随机读、顺序读的区别与适用场景

I2C EEPROM的读操作有三种。第一种是Current Address Read,当前地址读。芯片内部有一个地址计数器,上电后默认指向0x00,或者指向最后一次读/写操作的下一个地址。主机发送从机地址+读位后,从机直接把当前地址的数据放到SDA上。这种读法不用先传存储地址,速度最快,但只适合临时读取或连续扫描。

第二种是Random Read随机读,流程是主机先发送“写地址”,再发送要读取的存储地址,然后发送一个重复起始位(Restart),再发送“读地址”,最后把该地址的数据读出来。这种读法可以跳到任意位置,适合读取特定参数。几乎所有场景都在用这种方式,缺点是多了一次重复起始,时序上要注意。

第三种是Sequential Read顺序读。从本质上说,只要主机在读取一个字节后不回ACK、而是继续回ACK,从机就会自动把地址计数器加1,并把下一个地址的数据继续送出来。主机想读多少字节就连续发ACK,读完最后想要的字节后再发NACK,然后发STOP。这样可以用一条命令读整片EEPROM,效率非常高,也是固件备份时最常用的方式。

3.3 一个真实的读取用例:AT24C02读取固件参数

用一个很典型的场景举例:设备上有一颗AT24C02,里面存了产品序列号、校准系数和几个运行状态参数,我现在要通过STM32的I2C接口把它们读出来。假设STM32已经完成了I2C底层初始化,下面这部分就是随机读取的核心代码:

#define EEPROM_ADDR_W 0xA0 // A2A1A0均接地时的写地址 #define EEPROM_ADDR_R 0xA1 // 读地址 uint8_t EEPROM_ReadByte(uint16_t MemAddr) { uint8_t val = 0xFF; I2C_Start(); I2C_SendByte(EEPROM_ADDR_W); // 发送器件写地址 I2C_WaitAck(); I2C_SendByte(MemAddr & 0xFF); // 发送片内存储地址 I2C_WaitAck(); I2C_Start(); // 重复起始位 I2C_SendByte(EEPROM_ADDR_R); // 发送器件读地址 I2C_WaitAck(); val = I2C_RecvByte(); I2C_SendNack(); // 只读一个字节,所以给NACK I2C_Stop(); return val; }

如果想读取连续多个字节,建议直接用顺序读。配置好接收缓冲区后,循环读取,前N-1个字节回ACK,最后一个字节回NACK。下面这段函数可以连续读取任意长度的数据:

void EEPROM_ReadBytes(uint16_t MemAddr, uint8_t *buf, uint16_t len) { uint16_t i; I2C_Start(); I2C_SendByte(EEPROM_ADDR_W); I2C_WaitAck(); I2C_SendByte(MemAddr & 0xFF); I2C_WaitAck(); I2C_Start(); I2C_SendByte(EEPROM_ADDR_R); I2C_WaitAck(); for (i = 0; i < len; i++) { buf[i] = I2C_RecvByte(); if (i == len - 1) { I2C_SendNack(); } else { I2C_SendAck(); } } I2C_Stop(); }

读完得到的原始字节数组,可以先用串口打印成hex格式。在调试阶段千万别偷懒只看十进制数,把串口输出格式化成“偏移地址: 数据”,再配合EEPROM的地址分布表,一眼就能看出参数边界在哪。如果读出来全是0xFF,大概率是空片或引脚问题了。

3.4 波形上的常见异常:ACK丢失、SCL毛刺、地址错误

读I2C EEPROM遇到通信问题的时候,波形比代码更能说明问题。我在项目里抓过不少异常波形,最常见的有三种。

第一种是ACK丢失。现象是主机发完地址字节后,SDA在第9个时钟周期保持高电平,从机没有把SDA拉低回应答。原因可能是器件地址写错了,比如A0/A1/A2引脚状态和代码里的地址不一致;也可能是I2C总线上根本没有这个器件,或者器件供电异常。排查方法很简单:用万用表量一下芯片VCC引脚是否有电压,再用逻辑分析仪看主机发出的地址字节是否正确。

第二种是SDA一直被拉低。现象是整个总线都处于忙状态,主机无法发起新的通信。常见原因是从机锁死了I2C总线,或者SDA线上有器件拉低不放。遇到这种情况可以先把SCL翻转几个时钟脉冲让从机释放总线,或者干脆把整个板子断电重新上电。如果是飞线临时搭的电路,还要检查SDA和SCL有没有和别的引脚短接。

第三种是SCL毛刺严重。现象是逻辑分析仪抓到的时钟边沿特别乱,主机和从机对不上。通常是杜邦线太长、没有接地回路或者上拉电阻太大导致的。解决方法很简单:把飞线剪短、加粗地线、降低I2C速率试试看。有时候把400kHz降成100kHz问题就没了,这也能反推是信号质量问题。

如果你是在FPGA里用Verilog写I2C读取EEPROM的控制逻辑,思路一样,只是用状态机实现。状态机建议按这个顺序设计:IDLE(空闲)→ START(发起始位)→ SEND_DEV_ADDR_W(发器件写地址)→ WAIT_ACK1(等待应答)→ SEND_MEM_ADDR(发存储地址)→ WAIT_ACK2(等待应答)→ RESTART(发重复起始)→ SEND_DEV_ADDR_R(发器件读地址)→ WAIT_ACK3(等待应答)→ READ_DATA(读数据字节)→ SEND_NACK(发非应答)→ STOP(发停止位)。每进入一个状态都加一个超时保护,万一从机不应答,不至于整个状态机卡死。

4. 单片机内置EEPROM的读取与处理:以中颖/51内核为例

4.1 中颖单片机EEPROM的映射方式与时钟顾虑

很多单片机把EEPROM做进了MCU内部,中颖的SH79F、SH89F这些51内核系列就是典型代表。内置EEPROM的好处是省一颗外部芯片,成本低、布线简单,可靠性也比外部飞线高。但它和外置I2C EEPROM的访问方式完全不同,不是通过I2C地址去读,而是要通过一组特殊功能寄存器去操作IAP(In-Application Programming,在应用编程)模块。

不同中颖型号的寄存器名称会略有差异,但思路是一样的:IAP_DATA是数据寄存器,IAP_ADDRH/IAP_ADDRL是地址寄存器,IAP_CMD是命令寄存器,IAP_TRIG是触发寄存器,还有IAP_CONTR控制寄存器。访问地址空间也有讲究,有些型号的程序Flash和EEPROM共用一块地址空间,有些则是独立编址。所以拿到一颗不熟的单片机,第一件事是去查数据手册里的“Data Flash/EEPROM”章节,看地址范围和寄存器位定义,不要想当然套别的型号代码。

中颖的EEPROM访问还有一个很容易踩的坑:时钟。IAP操作需要Flash/EEPROM的时钟达到一定频率,有些型号在初始化时必须先配置IAP时钟分频,否则触发写操作后芯片会一直卡在忙状态,甚至在读操作时也返回错误数据。更隐蔽的是,个别型号在低功耗模式下IAP时钟会停止,这时执行EEPROM读取会直接跑飞或读出0xFF。所以低功耗唤醒后要重新初始化IAP模块,不能只初始化一次就认为万事大吉。

4.2 读、写、擦除操作的代码骨架

中颖内置EEPROM的读操作和外部I2C EEPROM完全不是一个套路。它不需要发送从机地址,只需要把目标地址写入地址寄存器,然后在控制寄存器里设置读命令,再按规定的触发序列写入0x5A和0xA5,就能从数据寄存器里拿回这个地址的内容。触发序列是51内核系列常见的软触发保护机制,目的是防止程序跑飞时误操作IAP寄存器。

下面这段代码是一个典型的中颖内置EEPROM读字节函数:

uint8_t EEPROM_ReadByte(uint16_t addr) { uint8_t val = 0x00; IE_EA = 0; // 部分型号要求在IAP操作时关中断 IAP_ADDRH = (uint8_t)(addr >> 8); IAP_ADDRL = (uint8_t)(addr & 0xFF); IAP_CMD = 0x01; // 读命令,具体值看手册 IAP_TRIG = 0x5A; IAP_TRIG = 0xA5; val = IAP_DATA; IE_EA = 1; return val; }

写操作和擦除操作相对复杂。许多中颖内置EEPROM在写入之前要求目标字节处于擦除状态(通常是0xFF),如果之前写过数据,必须先执行擦除命令,把整个扇区或整行清成0xFF,然后再写入。这也是内置EEPROM和AT24C02这类外置EEPROM不一样的地方,后者很多型号支持字节擦写,前者更接近Flash逻辑,按扇区/按行擦除。实际使用时,如果只改一个字节,而这个字节所在扇区里还有别的关键数据,就得先把这个扇区的内容读到RAM,再修改指定位置,最后整扇区擦除后重新写回去。这个过程如果中间掉电,整个扇区的数据都会丢,所以备份策略必须跟上。

4.3 掉电保护与数据备份策略

讲到掉电,这是EEPROM写入时最隐蔽的风险。外部I2C EEPROM在写入过程中如果掉电,芯片可能处于半写状态,这个字节内容既不是旧值也不是新值,极难恢复。中颖内置EEPROM更是如此,因为写/擦除操作的脉冲电压和时序都由内部电荷泵产生,掉电时电荷泵电压不足,写了一半的浮栅状态可能处于临界区,读出来会出现随机值。

所以我在项目里处理关键数据时,基本都采用“双备份+写入完成标志”的方案。思路很简单:数据区域划分成A区和B区,每次写入时先写A区,A区写完后写入一个特定的完成标志;再写B区,写完B区后也写完成标志。读取时,先看A区标志是否有效,再看B区标志是否有效。如果A区正常就用A区;如果A区标志损坏,就用B区;两个标志都异常,就认为这是第一次上电或者数据丢失,需要恢复默认值。更进一步可以减少写次数:先在RAM中累加,等收到掉电检测信号或者系统休眠前才写入一次EEPROM。毕竟EEPROM寿命有限,写一次就少一次,能少写就少写。

4.4 读取时遇到的典型问题:复位引起的数据错乱

内置EEPROM读取过程中,最让人觉得离谱的问题之一是复位导致的“数据错乱”。现象是这样的:系统工作一段时间后,程序突然复位,复位后读取EEPROM,发现某几个地址的数据变成0x00或者0xFF。一开始怀疑是EEPROM坏了,后来查下来才发现是读操作没处理好的问题。某些型号在IAP读操作的过程中如果发生复位,IAP寄存器可能处于中间态,下一次读操作读到的是寄存器残值而不是真正的存储内容。

解决办法有几个方向。第一,在IAP读操作之前关闭总中断,避免读写过程中被中断打断,导致IAP_TRIG触发序列分成了两截。某些型号对IAP_TRIG的5A/A5写入要求是连续两个机器周期内完成,中断一来,时序就断了。第二,在复位初始化流程中,对IAP相关的寄存器进行强制复位,把地址、命令、触发寄存器清零后再开始正常读取。第三,读取结果不直接用,加一层CRC校验或多次读取一致判断,发现数据可疑时重新读一遍。

5. OTP的读取、编程与保护:一次写入背后的处理细节

5.1 OTP单元的工作原理:熔丝/反熔丝与电荷俘获

OTP这个名字直译就是“一次性可编程”,从原理上和EEPROM有本质区别。传统OTP单元的物理结构是熔丝或者反熔丝。熔丝型OTP出厂时是导通的,编程时通入大电流把熔丝烧断,形成开路;反熔丝型OTP出厂时是断开的,编程时加高压让介质击穿,形成导通。无论哪种方式,都是不可逆的物理变化,所以写入后不能擦除,也不能重写。

现在不少现代OTP其实是用类似EEPROM的浮栅单元做的,物理上可以反复编程,但芯片在设计上去掉了擦除功能,或者擦除条件极为苛刻,实际上只能写一次。对使用者来说,不需要纠结内部到底是不是熔丝,只需要记住一个结论:OTP区域一旦编程,就不能再用常规方式改回去。所以OTP区最适合放“出厂后到报废都不应该变”的数据,比如设备唯一ID、密钥、硬件版本、校准参数。而那些运行时会被修改的用户配置,必须放EEPROM或Flash。

5.2 读取OTP内容:直读与加密读取的区别

读取OTP区域内部分内容和读取EEPROM在指令层面可能很接近,很多芯片对OTP区使用相同的读命令。区别在芯片的访问控制机制上。一些MCU在OTP区旁边还配置了加密位/读保护位,如果这个保护位被编程熔断,那么外部调试器、编程器或者应用程序就无法再直接读出OTP区的明文数据。表现出来的现象就是“OTP区读出来全是FF”,但程序实际还能使用,这种时候不要急着认为OTP是空的,先检查芯片的加密位状态。

实际维修场景中,拿到一个已经锁死的芯片想读OTP内容,经常会发现要么全FF、要么全00,要么读出来的是经过加密的乱码。这不是OTP物理损坏,而是保护机制生效了。除非有厂商的解锁通道,否则常规手段基本无法绕过。从工程角度,这也是安全设计的一部分:密钥写在OTP里,出厂前熔断读保护,就算芯片被拆下来也无法通过编程器把密钥读走。

安全芯片里的OTP区域也是类似的逻辑,但管理更严格。外部只能通过内部固件提供的接口去读写一定范围的数据区,OTP区往往只能由生产工具在特定安全环境下写入,之后连固件自身都无法改写。所以如果产品里用到这类芯片,生产流程一定要规划好:什么时候烧OTP、谁负责烧、烧错了怎么报废。

5.3 编程OTP的注意事项:高压、时序、校验

OTP编程比EEPROM编程对时序更敏感,因为写错了无法重来。使用编程器烧录独立OTP芯片时,建议严格按照“查空→编程→校验→保护”的流程来。查空是为了确认芯片没有残留内容,避免在已有数据上重复编程。编程时先把整片内容缓存到编程器缓冲区,执行烧写之后立即做校验,如果校验失败,这颗芯片基本就废了,需要更换。

现代OTP芯片有些已经不需要外部高压,内部自带电荷泵,编程电压由内部产生。但老式的EPROM/OTP芯片,比如经典的27系列,编程需要12.5V甚至21V的高压脉冲。用通用编程器选错型号时,编程电压可能给错,轻则写入失败,重则直接把芯片烧穿。现在很多编程器软件会自动处理,但遇到非知名型号或者测试座接触不良的时候,一定要多做几次空片检测再编程。

时序方面,OTP编程器一般会严格按照芯片手册的编程脉冲宽度和周期来控制。有些OTP支持“重复脉冲式写入”,即对同一地址打多个脉冲,直到该位被编程到位。编程器会在校验失败后自动补打脉冲,但如果芯片本身质量有问题,反复补打也可能某个位始终写不进去。量产时遇到这种情况,直接换芯片,不要纠结。

5.4 OTP区域擦除的“不可逆”问题与工程规避

OTP不可逆这个特性,在工程上既是优点也是风险。优点是出厂数据无法被篡改,用户或者外部攻击者改不了设备的唯一标识;风险是研发阶段如果把临时数据写进OTP,想改回来就只能换芯片。我的规避原则非常简单:产品设计时把存储空间按“可变”和“不可变”严格分开。可变的放EEPROM/Flash,不可变的才放OTP。

具体到代码实现上,驱动程序要做一个抽象层,向上层提供读参数、写参数的接口;底层则明确区分参数存储区和OTP区。OTP区只提供读接口,不提供写/擦除接口,甚至不把OTP的写时序编译进最终固件。这样做还有一个好处:即使程序被攻击者拿到,也无法利用软件漏洞去改写OTP区。量产产测时,OTP写入要放在整个生产链路的最后一步,先跑完所有功能测试、老化测试、校准流程,最终确认设备即将装箱前再写OTP。否则OTP写完了才发现某个硬件缺陷,整片芯片就跟着报废了。

6. 数据读取后的校验与容错:比“读出来”更重要的处理

6.1 校验和、CRC在EEPROM读取中的应用

从EEPROM读出数据之后,如果不做任何校验,直接信任读出来的字节,那迟早要出问题。EEPROM数据损坏的原因很多:写入中途掉电、芯片老化、位翻转、PCB焊接不良、电磁干扰等等。比如设备上电瞬间电源不稳,读取到的数据可能会偶发一个字节错误。此时如果没有校验机制,设备可能把错误参数当成正常参数运行,轻则功能异常,重则引发安全事故。

最简单的校验是累加校验和,把所有数据字节逐个相加,取低8位或16位,存入最后一个字节或最后两个字节。读取时重新计算累加和,和存储的校验值比对,相等则说明数据基本可靠。这个方法代码量小、速度极快,适合数据量不大的场景。但对可靠性要求稍高的场合,我更建议用CRC16。CRC对数据错误、位翻转的检测能力远强于简单累加,特别是它能检测出连续多字节错误和数据位移这类累加和发现不了的问题。下面是一个查表法CRC16的参考实现片段:

uint16_t crc16_update(uint16_t crc, uint8_t data) { crc ^= (uint16_t)data << 8; for (int i = 0; i < 8; i++) { if (crc & 0x8000) { crc = (crc << 1) ^ 0x8005; // 多项式0x8005 } else { crc <<= 1; } } return crc; }

实际项目里不用每次都从零写,可以直接移植成熟的开源CRC算法,但要注意多项式初值、输入输出是否反转、输出是否异或这些参数,封装成统一函数后,在写入和读取时都用同一个计算逻辑,保证一致性。

6.2 多次读取一致性判断

CRC能解决数据是否损坏的问题,但解决不了“这一次读出来是错、下一次读出来又对”的瞬态错误。尤其在电源纹波大、总线干扰强的环境中,I2C读取偶尔会拿到一个错误字节。如果你读关键参数时只用一次随机读就提交给业务逻辑,那就有概率读到错误值。

我在实际项目里常用的方案是“三取二”:连续读取同一个数据块三次,对每一次做CRC校验,然后比较三次结果。如果至少两次CRC通过且内容一致,就采用多数一致的那份。如果三次结果都不一样,就认为EEPROM数据不可信,返回默认值或进入错误处理流程。缺点很明显,速度慢了,但数据可靠性大幅提高。对启动时读取配置这类场景,多花几百毫秒完全值得。

还有一种更细粒度的方法:对单个关键字节,连续读三次,如果三次值一样才采用;三次不一样就再读。这种方法适合单个校准系数这种数据的读取,成本比整块三取二低一些,但这个方案在极端情况下可能陷入死循环,所以一定要加最大重试次数,比如读5次都不一致就返回错误。

6.3 空片与全FF数据的辨识

整片EEPROM读出来全是0xFF,这是维修和开发时最常见的现象。很多人第一反应是“芯片是空片”,其实未必。有几种情况也会导致全FF:芯片电源没接上、I2C的SDA线虚焊导致主机读到的都是高电平、WP或者地址线被拉到了错误状态、逻辑分析仪探头接错了线。判断空片之前,先做几件事情:量一下VCC和GND电压,确认芯片有供电;检查SDA/SCL是否和芯片引脚可靠连接;找一个已知正常的EEPROM作为对照测试。

如果确认通信链路正常,再用写入测试来判断。对一个非关键地址写入一个测试字节,再读回,看看是否能读到刚才写入的值。如果写入后读回正确,说明芯片可写;如果读回全是0xFF,可能是写保护生效或芯片写寿命耗尽;如果写0xAA读回0x55这种错位,就要怀疑地址线接错了。OTP空片通常也是全FF或者全00,由芯片结构和出厂状态决定,具体要看手册。

还有一个经验:芯片读出来全是0x00也不能简单认为“全零片”。有些芯片未编程状态是0x00,有些芯片损坏后读出全0x00。结合供电、时钟、引脚状态一起判断,才能下结论。实在判断不了,换一颗新片对比是最快的方法。

6.4 数据布局与版本管理

EEPROM里存的往往是产品参数,如果设计得乱七八糟,时间长了没人能维护。我的习惯是在EEPROM中定义一个带头部信息的结构,把魔数、版本号、数据长度、CRC都放在一起。魔数用来识别这块区域是否被初始化过,版本号用来兼容不同软件版本的数据格式,长度字段规定数据区实际有效字节数,CRC则覆盖整个数据区。

一个典型的布局可以是这样:

地址偏移内容字节数说明
0x00魔数2固定0x5A 0xA5
0x02版本号1从0x01开始
0x03数据长度2有效数据区总长度
0x05数据区N业务参数
0x05+NCRC162覆盖0x05到0x05+N-1

读取时先检查魔数,如果魔数不对,说明该区域可能从未初始化或者数据已经损坏。版本号的作用是应对软件升级后数据结构变化:旧版本固件读旧结构,新版本固件读新结构,如果版本不支持就迁移数据。不要小看这个设计,项目升级时如果没有版本号,新旧数据格式一冲突,往往会出现“升级后所有参数都丢失”的问题。

更进一步,把关键参数数据做双份存储甚至多份存储。主区存放当前生效的参数,备份区存放上一次成功写入的参数。每次正常写入时同时更新主区和备份区;读取时主区CRC通过则使用主区,主区损坏则自动启用备份区,同时把备份区内容写回主区。这套机制虽然增加了一点存储空间和读写时间,但对于重要数据来说,非常值得。

最后再分享一个我自己的习惯:每次用逻辑分析仪或者单片机读取EEPROM数据后,我都会把前32个字节的十六进制内容打印出来扫一眼。不是说每次都靠眼睛去核对,而是这个动作能快速发现地址错位、全FF、全00、数据边界不对这些低级问题。等确认数据整体结构没问题之后,再去做CRC校验和业务解析。OTP区也是一样,读取前先看手册里“Security/Protection”章节,很多芯片读不出来其实不是坏了,而是保护熔丝已经被熔断。这个坑,希望你看完这篇文章之后不要再踩一次。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询