☰
OTP/EEPROM读取与数据解析:从I2C时序到掉电保护完整指南
2026/10/5 6:08:08 网站建设 项目流程

我做过一套湖泊水质监测的遥测终端,要求断电重连之后,所有校准参数和累计流量必须原样恢复。板子用的是中颖单片机,片上Flash存固件,参数就得往外部EEPROM里放,而且不同批次的产品还要区分设备地址和出厂日期。当时遇到的问题很典型:掉电丢参数、连续写入后数据错乱、I2C总线上偶尔出现死锁。后来整套逻辑重写,我才把OTP和EEPROM这两类存储器的读取和处理摸透。

最近又接到一个活儿,要从一块报废仪表的板上把配置参数捞出来。芯片丝印被磨掉了,PCB上只有四个脚连着,拿万用表一量,发现是I2C接口,大概率是AT24C系列的EEPROM。另外一个案子更麻烦,客户想知道某个传感器出厂时校准数据怎么存的,非要逆向固件里那段读取OTP的代码,折腾了好几天。这些项目串起来,我发现很多工程师其实都卡在“怎么把OTP/EEPROM的数据完整读出来,并且正确处理成可用的业务数据”这一步上。

这篇文章我就围绕OTP/EEPROM读取与处理,把硬件层面怎么接线、I2C时序怎么把握、读回来的原始二进制怎么解析、文件系统怎么设计、以及那些踩过的坑,全部写清楚。

1. 内容整体设计与思路拆解

1.1 EEPROM 是什么、能用在哪

EEPROM,全称是Electrically Erasable Programmable Read-Only Memory,电可擦除可编程只读存储器。名字里带“只读”,但实际可以擦写,只是擦写次数有限,AT24C02标称100万次,工业级可能更保守,实际用就按10万次去设计比较稳妥。

它的核心价值是掉电不丢失。MCU内部的Flash虽然也能存数据,但很多低成本的8位单片机Flash擦写次数只有1万次左右,而且按页擦除,不适合频繁修改小数据。EEPROM按字节寻址、按字节改写,有几个字节要更新就写几个字节,非常适合存设备参数、校准系数、序列号、计数值。

OTP就完全是另一回事了。OTP全称One-Time Programmable,一次性可编程。内部结构通常是熔丝或者反熔丝阵列,出厂时全部为1(或0),每次编程就是把某些位永久翻转。一旦写入,不能改也不能擦。它不存在“把0写成1”的操作空间。断开的熔丝不可能再物理接回去。

OTP最常见的用途是存根密钥、唯一ID、校准常数、安全启动的哈希值。很多MCU内部自带一小块OTP区域,比如STM32的Option Bytes里就有一块用户OTP区。

1.2 为什么要把读取和处理放在一起分析

很多开发老手都有同一个毛病:以为EEPROM读回来就是最终数据,直接强转结构体指针去用。结果数据错位、字节序不对、CRC校验失败,查了半天发现是存储布局和读取逻辑没有统一规划。

读取不只是“把字节从芯片里调出来”,还包含三件套:

  • 物理读取:正确时序拿到原始字节流
  • 逻辑校验:验证数据是否完整、未损坏
  • 语义解析:把原始字节翻译成温度值、MAC地址、计数字等

OTP也一样。虽然物理上只能烧录一次,但读出的时候要考虑读保护位是否被置位、访问地址是否需要解锁、读回来的数据是否需要经过SM3或者某种校验算法解密才能还原出真正的明文。我接触过的好几个方案都是把OTP里存的内容当做密文,MCU上电后先读出OTP,再算一遍哈希做比对,对不上就拒绝启动。

所以读取和处理必须当成一套完整流程来设计。

1.3 不同接口方案的对比与选型

读取OTP/EEPROM,接口方案常见有三种。

方案接口适用场景优点缺点
板载MCU直读I2C/SPI产品自检、正常运行无需额外硬件,成本低程序逻辑复杂,调试工具少
编程器离线读取专用引脚夹/座产线烧录、逆向分析读取稳定,安全可靠需要适配芯片型号
逻辑分析仪抓包I2C/SPI总线监听实际通信,定位软件问题低成本,无侵入需要有正常通信做参考

选型原则很简单:如果是自己产品里的芯片,直接用MCU读取最方便;如果是陌生板卡上的芯片,先查PCB走线确定接口,再用逻辑分析仪监听上电阶段是否有初始化读操作,最后再看要不要动用编程器。

我用过WELLON的编程器,也用过热风枪把芯片拆下来换到测试座里读。说实话,能不拆就不拆。拆焊有风险,而且某些OTP芯片读取次数也有限制,断电重焊可能影响引脚氧化接触。能飞线读取的尽量飞线。

2. 核心细节解析与实操要点

2.1 I2C 读取EEPROM的原理与代码实现

I2C总线读取EEPROM的标准流程是:

  • 起始条件(SDA在SCL高电平期间拉低)
  • 发送设备地址+写位(例如AT24C02的设备地址是0xA0)
  • 等待ACK
  • 发送寄存器地址(EEPROM内部的存储地址)
  • 等待ACK
  • 重新发送起始条件
  • 发送设备地址+读位(0xA1)
  • 接收数据,每收完一个字节发ACK,收最后一个字节发NACK
  • 停止条件(SDA在SCL高电平期间拉高)

以AT24C02为例,它的存储容量是2Kbit,也就是256字节,地址范围0x00-0xFF。如果是AT24C512,容量64KByte,地址就需要两个字节来存放。

我用单片机读取的时候,软件I2C最怕的就是时序不对。EEPROM对时钟频率很敏感,标准模式最大100kHz,快速模式400kHz。我用GPIO模拟,干脆把时钟降到50kHz,稳定第一。

关键代码如下,这是标准的软件I2C读一字节:

uint8_t eeprom_read_byte(uint8_t dev_addr, uint16_t mem_addr) { uint8_t ret = 0xFF; i2c_start(); i2c_write_byte(dev_addr & 0xFE); // 写位 i2c_wait_ack(); if (mem_addr > 0xFF) // 16位地址 { i2c_write_byte(mem_addr >> 8); i2c_wait_ack(); } i2c_write_byte(mem_addr & 0xFF); i2c_wait_ack(); i2c_restart(); i2c_write_byte(dev_addr | 0x01); // 读位 i2c_wait_ack(); ret = i2c_read_byte(); // 读一个字节 i2c_send_nack(); i2c_stop(); return ret; }

如果希望连续读,例如读128个字节的配置块,就用顺序读模式。EEPROM支持地址自动递增,主机持续发送ACK,直到读够了再发NACK结束。这样比单字节循环读快得多,而且减少了总线切换次数,出错概率也低。

2.2 OTP 的读取方式与特殊处理

OTP的读取方式和普通EEPROM有一定差别。有些OTP芯片内部结构是独立的熔丝阵列,地址映射到固定的存储区间;有些则是挂在MCU内部总线上的特殊寄存器区。

我处理过几颗国产MCU内部的OTP区,读取方式一般是:配置选项字节开启OTP读取使能,然后像读普通Flash一样按地址读取。但要注意的是,OTP读出来的是原始物理比特,不代表真的业务数据。

我曾经接手过一个案子,客户声称OTP里存了校准温度,但读出来全是0x00。查来查去发现,写入的时候固件把校准温度先做了异或加密,再烧进OTP。我这边直接读物理地址当然全是0,因为物理地址烧录的是密文。后来找到固件里的解密函数,又用Python去模拟,才算把真实值还原出来。

所以处理OTP读取的时候,要始终问自己:我读到的这些字节,是最终数据,还是某种编码之后的中间产物?

OTP还有一个很坑的点:读保护。有些OTP区域如果被设置了读保护位,那么任何外部工具都无法再读出内容,只能读出全0或全1。这种保护一旦生效,就没有后悔药。我做逆向的时候,先用编程器读一下配置区,看看读保护位是否置位。如果置位了,就不浪费时间了,直接告诉客户:芯片内容不可读。

2.3 电平匹配与总线隔离的工程问题

I2C总线最常出现的故障就是电平不匹配。MCU供电3.3V,EEPROM供电5V,上拉电阻接到了5V,那MCU的I2C引脚就可能被钳位到5V而损坏。

我踩过一次真实的大坑:板子上主控是STM32F103,3.3V供电,AT24C256的VCC接5V,结果总线上拉电阻直接接到了5V电源。读数据时前面几十次正常,后来I2C引脚烧了,整板报废。

正确的做法是:如果电平不一致,要么用PCA9306做电平转换,要么把EEPROM的供电改成和MCU一致。EEPROM工作电压范围通常很宽,1.8V到5.5V都有,直接统一用3.3V供电,上拉电阻也接3.3V,简单又可靠。

SPI接口的EEPROM相对I2C通讯速率更高,但是引脚多一根,而且读时序上要注意读指令后先等一个dummy字节再取数。25系列SPI EEPROM读取的流程是:拉低CS,发0x03读指令,发24位地址,然后连续读数据,最后一个字节读完拉高CS。

3. 实操过程与核心环节实现

3.1 从一张陌生板卡上抓出EEPROM数据

有一次客户送来一块报废的PLC模块,说里面有组IP地址和网关配置,想读出来。我拿到板子先肉眼观察,发现一个SOIC-8封装的芯片,丝印被磨得只剩一个角,PCB丝印标着U5。旁边的走线很细,顺着走线量到主控的PB6和PB7,基本可以断定是I2C。

这时候我没急着拆芯片,而是先把逻辑分析仪的通道夹到SDA和SCL上,GND接板子的地,然后给板子上电。结果发现主控上电后大约200ms就开始发起读操作,地址是0xA8,读0x00到0x0F。这个地址看起来怪,正常EEPROM都是从0xA0开始。后来我查了主控型号,发现它把EEPROM挂在了一个带偏移的地址上。

既然主控自己会读,说明读时序是对的,芯片型号大概率也是对的。我没拆芯片,直接用编程器夹子夹住SOIC-8,选择AT24C512,全片读出64KB数据。打开Hex文件一看,0x00-0xFF区间里能找到一串ASCII码,正是客户要的IP和掩码。剩下的数据是梯形图逻辑,跟本次目的无关。

整个过程核心在于:先分析接口再决定读取手段,不要一上来就拆芯片。如果逻辑分析仪能看到通信,就按照抓到的设备地址和寄存器范围去读;如果看不到任何通信,那就只能拆下来放测试座。

3.2 用 STM32 写一个完整的 EEPROM 读取与处理函数

项目需要从一个传感器模块里把校准表读出来,校准表由300个float数组成,存在外部EEPROM中。主控是STM32L051,用硬件I2C1。

我实现的完整处理流程分三步:初始化I2C、读取原始字节到缓冲区、解析浮点数组并做有效性校验。

float calibration_table[300]; void load_calibration_from_eeprom(void) { uint8_t buf[1200]; uint16_t base_addr = 0x0800; uint32_t crc_stored, crc_calc; // 1. 连续读取1200字节 if (HAL_I2C_Mem_Read(&hi2c1, EEPROM_ADDR, base_addr, I2C_MEMADD_SIZE_16BIT, buf, 1200, 1000) != HAL_OK) { error_handler("I2C read failed"); return; } // 2. 解析float数组 for (int i = 0; i < 300; i++) { memcpy(&calibration_table[i], &buf[i * 4], 4); } // 3. 读取尾部存好的CRC32 crc_stored = (buf[1196] << 24) | (buf[1197] << 16) | (buf[1198] << 8) | buf[1199]; crc_calc = crc32_buf(buf, 1196); if (crc_stored != crc_calc) { error_handler("CRC mismatch, calibration invalid"); return; } // 4. 把解析结果通过串口输出 for (int i = 0; i < 300; i++) { printf("%.4f\n", calibration_table[i]); } }

这里有两个容易忽略的点。

第一是HAL_I2C_Mem_Read的第三个参数,地址宽度要选对。AT24C02是8位地址,AT24C512是16位地址。选错会导致读回的数据错位或者直接超时。

第二是CRC校验。校准表这种数据对完整性要求极高,少一个字节整个曲线就偏了。我的习惯是:存储区尾部预留4字节CRC32,每次软件启动时全表校验,不一致就报错并启用默认参数,而不是带病运行。

3.3 OTP 在固件烧录环节的实测记录

去年做一款网关设备,要求每个设备出厂烧录唯一的设备密钥到OTP区域。MCU选了国民技术N32G45x,它内部有一个独立的OTP区域,共128字节,地址映射到0x1FFF_F800。

烧录流程是:先用烧录器连接SWD,编写一段临时测试程序,通过芯片厂商库函数往OTP写数据。写进去之后,紧接着读回验证。OTP不能擦除,一旦写错就废了一块芯片。所以我在烧录前增加了一步:先把要写入的临时数据在RAM里算好CRC,确认无误后再执行OTP写入指令。

验证阶段还发现一个有意思的现象:OTP区读出时,某些字节始终显示的0x00,无论写入的是0x5A还是0xA5。后来查芯片手册,原来厂商把OTP区的部分地址预留给芯片ID和测试信息,这些区域硬件锁定,用户不可写。所以在设计时就把使用地址区间限定在用户区范围内,避开预留区。

OTP读取处理中还有一类特殊情况:熔丝型OTP,逻辑分析仪读回的数据是“每一位独立映射到一个熔丝”。此时如果要判断一个1字节的值,必须取出8根数据线或者通过寄存器位域拼装。有一次我做识别,读回来的字节居然不是简单的0xFF或0x00,而是像0x7F这样,说明其中一个熔丝断了而另外7个完好。这个细节提醒我:OTP处理逻辑不能简单做全字节比较,必须做位级解析。

4. 数据处理与文件系统管理

4.1 原始二进制数据的分析流程

读回来一大片原始字节怎么处理,很多人第一步就错了。他们直接盯着Hex看,试图用肉眼找规律。效率太低。

我的做法是先做统计分析。用Python把整个BIN文件读进来,看三个指标:

  • 数据分布(哪些值出现频率高)
  • ASCII可见字符串
  • 熵值分析(是否经过加密或压缩)

有段代码一直在用,就是简单扫描可读字符串:

import re import math with open('dump.bin', 'rb') as f: data = f.read() # 找出所有连续可打印ASCII字符串 strings = re.findall(rb'[\x20-\x7e]{4,}', data) for s in strings: print(s.decode()) # 计算香农熵 freq = [0] * 256 for b in data: freq[b] += 1 entropy = 0 for cnt in freq: if cnt == 0: continue p = cnt / len(data) entropy -= p * math.log2(p) print(f"Entropy: {entropy:.4f}")

如果熵值接近8,说明数据高度随机,大概率是密文或者已经压缩,此时不要试图人工解析,直接找固件里的加解密函数。如果熵值在4到6之间,通常是结构化的参数表。

数据解析的时候最核心的是确定字段边界。EEPROM里面通常不会有自动对齐,所有结构体数据都是手动排列的。我的习惯是在程序里显式定义一个打包结构体,用#pragma pack(1)强制紧凑排列,然后用memcpy整体读取。不要在单片机里用“取结构体指针指向EEPROM缓冲区”的做法,因为编译器对齐策略可能导致读出来的字段错位。

4.2 EEPROM文件系统的组织方式

EEPROM不像Flash那样有专门的磨损均衡算法。它的擦写次数虽然比Flash高,但也不能无限写。如果在固定地址反复写计数值,用不了几个月这个字节就报废了。

EEPROM文件系统的核心目标就是两件事:磨损均衡和数据掉电保护。

业界常用的一种方案是“Value + Counter”双区策略。例如记录一个电流累计值,每次更新时交替写到两个槽位,槽位0写当前值+计数,槽位1写下一个计数值。读取时比较两个槽位的计数,计数值大的就是最新数据。如果某次掉电导致写入中断,那下次上电时发现两个槽位计数相同,就取校验通过的那个。

如果管理的数据较多,可以考虑把所有数据做成一个个“记录”,每条记录包含ID、长度、数据、CRC。写入时动态寻找可用空间,而不是固定地址写。这样每次修改只会消耗当前记录所在空间,下次写到下一个空闲区,实现循环使用。

小型设备的EEPROM文件系统不需要搞复杂日志式管理。我之前做过一个简单的三层结构:

区段起始地址长度用途
Header0x000016魔数0xAA55、版本号、设备序列号
Data Area0x0010224业务参数区
Backup0x00F016关键参数备份

每次修改前先把旧值写入Backup区,成功后再写Data区,写入完成后清Backup区。上电时检测Backup区是否为空,不为空说明上次中途掉电,就用Backup区的数据回填Data区。这个机制虽然简单,但能杜绝百分之八十的掉电丢数据问题。

4.3 字节序、对齐和CRC校验的工程细节

字节序问题在EEPROM处理中特别容易被忽视。

假设MCU是Little-Endian,把一个uint32_t数值0x12345678存进EEPROM,用memcpy直接写,那么内存中低地址存放0x78,高地址存放0x12。但如果另一个设备是大端模式,同样地址读出来就会变成0x78563412,完全错误。

解决方案是:在设备间交换数据时,统一规定使用网络字节序(Big-Endian),或者自定义一个字段布局协议,明确每个多字节字段高低字节的摆放顺序。

我一般会在代码里写两个宏:

#define EEPROM_WRITE_U16(addr, val) do { \ eeprom_write_byte(addr, (uint8_t)((val) >> 8)); \ eeprom_write_byte((addr)+1, (uint8_t)(val)); \ } while(0) #define EEPROM_READ_U16(addr) ((uint16_t)((eeprom_read_byte(addr) << 8) | \ eeprom_read_byte((addr)+1)))

这样不管MCU本身是什么端序,EEPROM里的字节顺序都是固定的,程序可移植性就好多。

CRC方面,我推荐CRC16-CCITT,查表法实现,速度极快,而且检出错误能力足够。注意一定不要把CRC值为0的情况排除,因为0是合法值,只要计算和存储一致就行。

5. 常见问题与应用场景拓展

5.1 常见问题速查与排查实录

下面这些问题大概是EEPROM读取处理中最高频的,每一条我都亲身踩过。

问题现象可能原因排查方法
I2C读不到ACK设备地址错误、上拉电阻缺失用示波器看SDA/SCL波形,逐一确认地址
读回数据全0xFF芯片是空白、焊盘虚焊、选了错误型号换一片确认,万用表测供电和GND
读回数据全0x00芯片损坏或地址线被拉高量A0/A1/A2引脚电平
前几个字节正常后面全错页写边界处理错误、地址递增逻辑bug单字节读取对比
掉电后再上电参数丢失复位时序导致写入未完成增加掉电检测,延迟200ms再写
每次读取结果不一致电源纹波大、时钟线干扰加重上拉、降低I2C频率、加去耦电容

最典型的是全0xFF情况。有次同事拿了一块板子来,说读EEPROM返回全FF,我第一反应是芯片空的。但测供电发现VCC只有1.2V,明显是电压被拉低了。查了半天发现是芯片焊盘底下有残留锡珠,把VCC和相邻的WP引脚短路了。WP引脚被拉低才会允许写,被拉高了只能读不能写。VCC短路到WP导致WP为高,写操作自然失败,读操作又因为VCC电压不足而不稳定。

所以调整思路之后,看到全FF不要先怀疑芯片,先用万用表量供电和WP引脚电压。

I2C死锁也是个常见问题。SDA被从设备拉死不释放,是因为通信中途掉电或者时序异常导致从设备一直等主机给时钟。解决方法有两种:一种是给SCL额外产生9个脉冲,让从设备完成内部状态机复位;另一种是直接切换GPIO为推挽输出,手动拉高SDA几毫秒强行走完停止条件。

5.2 应用场景拓展:产线烧录和OTA升级

EEPROM读取处理不止用于开发和调试,在产线工作量也非常大。

批量生产时,设备序列号、蓝牙MAC地址、WiFi校准参数、ADC增益校准系数,所有这些都要在出厂前写入EEPROM。人工一台一台用编程器写太慢,我见过的高效率做法是:产线PC通过串口和工装板通信,工装板用I2C总线访问待烧录模块的EEPROM,PC端用Python脚本批量烧录。烧录完自动读回校验,校验通过才打PASS标签。

Python端读取逻辑大概是这样:

import serial import struct import crcmod.predefined def write_eeprom(ser, addr, data_list): payload = bytes([0xAA, 0x55, addr >> 8, addr & 0xFF, len(data_list)]) + bytes(data_list) crc = crcmod.predefined.mkCrcFun('crc16') checksum = crc(payload) ser.write(payload + struct.pack('<H', checksum)) ack = ser.read(1) return ack == b'\x06' def read_eeprom(ser, addr, length): payload = bytes([0x55, 0xAA, addr >> 8, addr & 0xFF, length]) ser.write(payload) data = ser.read(length) return data

这样产线工人只需要扫一下产品条码,脚本自动从MES系统拉取对应参数,写入EEPROM后读回比对。整个流程耗时不到两秒,出错率远低于人工按键输入。

OTA升级场景下,EEPROM通常保存的是“当前固件版本号”和“回滚标识”。升级开始时先把新版本号写入暂存区,升级完成后再把正式版本号写入EEPROM。如果升级过程中断电,下次上电读暂存区发现版本号不完整,就知道要回滚旧固件。这个设计思路我在多个物联网产品上都验证过,简单可靠。

5.3 一些值得注意的安全与可靠性细节

如果产品涉及计费、加密或者其他对数据真实性要求极高的场景,普通EEPROM的明文存储是不够的。攻击者可以通过I2C探针直接把整片内容抓走。这时需要把关键数据用密钥加密后再存入EEPROM,密钥存储在OTP区。OTP区本身不可擦除,密钥无法被静态修改,安全性就提高很多。

我还习惯在EEPROM中存放一个随机填充区,用于干扰分析。每次写入时,在数据区尾部追加一段随机数,再整体计算CRC,这样即使两次写入相同业务数据,EEPROM原始内容也不会完全一致,防止侧信道攻击时直接比对差异化数据。

写入保护脚(WP)也很关键。平时保持拉高电平,仅在真正需要写入的那一小段时间内拉低,写完再拉高。这样即使程序跑飞或者被干扰,也无法意外修改EEPROM内容。这是很多工程师忽略的可靠性设计。

另外,不要在系统掉电瞬间原地等待EEPROM写入完成。正确做法是检测到掉电先开启掉电中断,把重要的几个字节快速写入EEPROM后立刻进入低功耗休眠。AT24C系列写一个字节的时间大约5ms,如果电容只够撑10ms,就可能写一半掉电。稳妥的做法是把关键词和参数放在同一个页内,一次页写操作完成,而不是分多次。

6. 总结与实战收尾

写了这么多,最后再聊一点个人的实际体会。

OTP/EEPROM读取与处理看着是底层基本功,但真正把它做扎实的人不多。问题大多出在:只会读不会解析,只会解析不考虑掉电保护,只考虑掉电保护不关注磨损均衡。这三个层次环环相扣,跳过任何一步,长期跑下来都会出问题。

我给自己的项目立了几条规矩:所有EEPROM数据必须带CRC、关键参数必须有备份区、多字节数据必须显式固定字节序、写入之前先确认WP引脚状态。简单几条,但能挡住绝大多数诡异故障。

如果你现在正卡在“数据读不出来”或者“读出来不会用”的阶段,建议先不要着急改代码,先拿逻辑分析仪抓一次总线波形,确认硬件层没问题,再往软件层面走。很多问题其实是一根上拉电阻没焊或者地址位跳线帽插错了位置导致的,白白调试了好几天。

最后送大家一条非常实用的经验:用可编程电源给EEPROM供电,在读取过程中人为快速通断电源,反复测试,如果能稳定读出正确数据,说明你的处理逻辑在掉电场景下也站得住脚。这是我做产品可靠性测试时最常用的手段,效果明显。

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

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

立即咨询