ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

EEPROM原理、I2C时序、寿命管理与掉电保护实战

EEPROM原理、I2C时序、寿命管理与掉电保护实战 手上这块采集板已经是第三次返修了故障现象一次比一次一致断电放一晚第二天上电所有校准参数全部回到出厂默认值屏幕上显示的零点偏移又是那个熟悉的数字。前两次我换了电源、加了看门狗、把初始化流程从头到尾捋了一遍都没用。第三次我把示波器挂到那颗八脚小芯片的SDA和SCL上才看清问题——上电初始化时写的参数有大概三成的概率根本没落盘。那颗八脚芯片就是EEPROM一个在很多项目里被当成随手挂上去的存储颗粒、却能在关键时刻让整个产品翻车的东西。这篇就把EEPROM掰开揉碎讲一遍。从浮栅晶体管怎么把一个bit记住到选型时该盯哪几个参数到I2C时序里那五个基本操作怎么拆再到单片机和FPGA两个平台上驱动怎么写最后是寿命、掉电保护和一堆我踩过的坑。无论你是刚开始学嵌入式、第一次在原理图上画AT24C02的新手还是做FPGA高速采集、要在Verilog里手搓I2C主控的老手都能从这里找到能直接抄的部分。1. 先把EEPROM拆开看它凭什么断电还能记住数据很多人对EEPROM的认知停留在掉电不丢的小U盘但真到了调时序、算寿命、排查数据丢失的时候这个认知就不够用了。你得知道它内部到底靠什么机制保存电荷、这个机制为什么会磨损、又为什么擦写会慢到毫秒级。这些问题想明白了后面看手册里那些奇怪的参数比如5ms的写周期、100万次的擦写寿命就全都有了解释。1.1 一个浮栅管就是一个bit写入与擦除到底发生了什么EEPROM的全称是电可擦除可编程只读存储器核心存储单元是浮栅场效应管。它跟普通MOS管的结构差别集中在栅极上普通管子只有一个控制栅浮栅管在控制栅下面还夹了一层被二氧化硅完整包裹的导电层也就是浮栅。这层二氧化硅是绝缘的浮栅里的电子出不去外面也进不来这就是断电还能记住的物理基础。写1的过程本质是往浮栅里塞电子。在控制栅加一个十伏以上的高压同时在漏极加合适的电压沟道里的电子被加速到具备足够能量越过二氧化硅势垒注入浮栅这叫热电子注入。电子带负电进了浮栅之后等于在控制栅和沟道之间人为加了一层负电荷屏蔽管子的阈值电压被抬高。读的时候给控制栅加一个介于两个阈值之间的参考电压浮栅里有电子的管子不导通读出来是1没有电子的管子导通读出来是0。整个过程不需要任何外部电源维持电子就静静躺在浮栅里常温下能躺几十年。擦除就是反过来把电子从浮栅里拉出去通常靠Fowler-Nordheim隧穿在控制栅加反向高压电子在强电场作用下穿过薄氧化层回到衬底。这里有个关键点EEPROM每个存储单元通常是两管结构一个浮栅管负责存一个普通选通管负责寻址所以它可以做到按字节擦写不需要像Flash那样整块整块地来。这个差异直接决定了两者的应用边界。注意浮栅里的高压是芯片内部电荷泵升出来的外部供电只要满足芯片手册要求即可不需要你额外给一个12V。所以别看到高压就想着改原理图。1.2 EEPROM、Flash、FRAM、SRAM摆在一起怎么选同样是存储为什么有人的项目用EEPROM有人用Flash模拟有人干脆上FRAM这背后是擦写粒度、速度、寿命和价格的四方博弈。我把常见几类放在一起对比选型的时候照着看基本不会错。存储类型擦写粒度典型擦写次数写入速度掉电保持典型用途EEPROM字节100万次毫秒级页写更快是参数、校准值、配置Nor Flash扇区几KB10万次毫秒级是程序存储、大块数据FRAM字节万亿次级别纳秒级接近RAM是高频记录、仪表日志SRAM字节无限纳秒级否运行内存从表里能看清一件事EEPROM的定位是小数据量、低频改、必须要掉电保住。它的字节级擦写能力和百万次寿命是Nor Flash完全比不了的但它毫秒级的写周期和几K到几百K的容量又注定干不过Flash。FRAM性能全面占优但价格和容量是硬门槛很多项目算完成本还是回到EEPROM。实际项目里的决策逻辑通常是这样需要保存的量在几KB以内、改动频率在每天几十次以内、成本敏感选EEPROM需要记录几千上万条日志、每秒都在写那就得考虑FRAM或者带电池的SRAM需要存固件或者语音、图像这类大块数据Flash是唯一选择。中间地带比如一次要存几十KB参数、又不想加Flash可以考虑外挂SPI EEPROM或者干脆让MCU用片内Data Flash。1.3 手头常见的那几颗芯片容量、页大小、寿命对照虽然型号千千万但真正在板子上高频出现的就是那几个系列。理解它们的命名规律比死记型号有用得多。以最常见的24系列I2C EEPROM为例型号数字直接对应容量24C02是2Kbit也就是256字节24C16是16Kbit即2KB24C256是256Kbit即32KB。页大小是个特别容易忽略的参数它决定了你能一次性连写多少字节而不触发页回卷。早期小容量型号页只有8字节大容量型号能到64甚至128字节。很多人在调试时发现我明明按顺序写了16个字节结果前8个被后面的覆盖了本质就是没注意页边界。型号容量页大小字地址宽度典型擦写寿命AT24C02256B8B1字节100万次AT24C04/08/16512B/1KB/2KB16B1字节100万次AT24C32/644KB/8KB32B2字节100万次AT24C128/25616KB/32KB64B2字节100万次AT24C51264KB128B2字节100万次寿命这一栏要特别注意100万次是整片芯片的规格但早期型号是按每字节给的也就是说如果只反复写同一个地址那一个字节位置写100万次就到了极限其它位置还是全新的。这个特性后面讲磨损均衡时会重点用到。除了独立芯片很多单片机内部也集成了所谓的类EEPROM或者Data Flash中颖、STC、GD这些厂商的部分型号都有。这类片内存储的特点是省了外围器件、省了I2C走线但通常需要先擦后写、操作时要关中断、写周期也挺长具体后面单独讲。2. 选型与硬件落地从参数表到PCB上的那两颗电阻芯片选对了只是第一步真正让项目翻车的地方往往在电路上。上拉电阻选错、地址脚接反、WP脚悬空这类问题在原理图上几乎看不出来非要等到板子跑起来才暴露。这一章就把从选型到PCB的全过程讲清楚尤其是那些手册上写了一行、实际使用中却影响巨大的细节。2.1 选型时真正该盯的六个参数拿到一份EEPROM数据手册前面几十页都是时序图和封装图真正需要在选型阶段确认的其实就六个点容量、工作电压范围、接口类型、页大小、擦写寿命、工作温度范围。容量好理解但建议留出至少一倍余量。原因是产品迭代时参数只会加不会减一开始就按98%用满后面加两个校准系数就得换型号PCB要改、软件要改、认证要重做。我一般按实际需求乘以2来选比如要存128字节直接上24C04甚至24C08。工作电压范围经常被忽略。很多老型号只能到5V或者2.5V到5.5V如果你的系统是1.8V低功耗方案就得专门挑宽压型号。另外写操作期间芯片的电流会明显大于读操作如果系统对功耗敏感这个峰值电流要算进去。接口类型主要看I2C还是SPI。I2C省引脚但速度受限标准模式100kHz、快速模式400kHz、高速模式能到3.4MHzSPI速度快得多但要多占两到三根线。容量大、写入频繁的场合SPI更合适小容量参数存储I2C足够。页大小和擦写寿命前面提过这里补充一句选型时把页大小和目标写入频率一起看。如果要记录的数据长度超过页大小驱动里就必须拆分成多次页写每次之间还要等写周期整体写入时间会叠加。2.2 I2C上拉电阻怎么算为什么4.7K不是万能答案几乎每个教程都说I2C的SDA和SCL接4.7K上拉到VCC这话在100kHz、总线短、只有一两个从机的时候确实没问题。但如果你跑400kHz高速模式或者板子上挂了三四个从机、走线拉了十几厘米4.7K就很可能是你丢ACK的元凶。上拉电阻的取值是有明确边界的两头各有一个限制。上限由上升时间决定因为I2C总线的上升沿是电阻给电容充电的过程电阻越大充得越慢。公式大致是Rp(max) tr / (0.8473 × Cb) Rp(min) (VDD - VOL) / IOL其中tr是手册允许的最大上升时间标准模式100kHz是1000ns快速模式400kHz是300nsCb是整条总线的等效电容包括PCB走线、引脚和器件输入电容规范上限是400pFIOL一般是3mAVOL取0.4V。代入算一下就跑出结论了。3.3V系统、快速模式、假设总线总电容100pF上限大约是3.5kΩ下限按3mA算是约970Ω。所以4.7kΩ在400kHz下其实是超标的上限值波形会明显变圆。如果总线电容更大超得更多。反过来说如果你跑100kHz电容又小4.7K甚至10K都没问题还能省点功耗。提示手头没有精确电容值就按经验估——单根短走线按10到20pF算每个挂载器件按10pF算。算出来落在1k到4.7k之间优先选2.2k或3.3k高速场合用1.5k到2.2k别死守4.7k。还有一个坑是很多人只给SDA和SCL各接一个电阻看似没问题。I2C是开漏结构只要总线上任意一端有上拉就成立所以单组上拉是正确的做法两处都接反而等效阻值变小、功耗变大。2.3 地址引脚、WP引脚和PCB布局的三个坑地址引脚A0到A2很多人直接全部接地这本身没错但要注意两点一是有些型号的A2脚在某些容量下内部不接手册里会注明NC这时候你接什么电平都没用二是如果你用软件I2C或者多个从机共用总线地址必须唯一别两个芯片都接成0x50。WP写保护脚是重灾区。它的作用是硬件层禁止写入通常高电平保护、低电平允许写。如果你的设计里希望写保护可控就把它接到GPIO上初始化时先拉低再写参数写完拉高防止误写。如果你不需要保护直接接地。最怕的是悬空——悬空时引脚电平不确定可能出现有时候能写有时候写不进的玄学现象我前面那块板子的三成失败率后来查出来就是WP处理不干净加上虚焊。PCB布局上有三个经验。第一上拉电阻尽量靠近主控端而不是EEPROM端这样走线的分布电容影响小一些。第二SDA和SCL尽量并行走、长度接近避免和PWM、CLK这类强干扰信号平行长距离走线必要时中间加地线隔离。第三芯片的VCC脚旁边一定要放0.1uF的退耦电容距离在2mm以内写操作时电流突变的干扰靠它压住。3. 把时序吃透I2C读写EEPROM的完整流程I2C本身是很简单的协议两根线、四种状态但一旦和EEPROM的存储特性叠加起来就有了不少门道。这一章我会把常用的五种操作时序逐个拆开然后重点讲页写边界和写周期等待这两个最容易出错的地方。理解了这一章不管是写单片机驱动还是Verilog状态机都只是在翻译这些时序而已。3.1 五种基本操作时序逐个拆解先明确I2C的物理层约定SCL高电平期间SDA必须稳定SDA的变化只能发生在SCL低电平期间。起始条件是SCL为高时SDA出现下降沿停止条件是SCL为高时SDA出现上升沿。数据位是MSB先行每个字节后面跟一个ACK位从机拉低SDA表示应答主机拉低表示对最后一个字节的不应答。在EEPROM上最常打交道的操作有五种第一种是字节写。起始条件后发设备地址加写位等ACK接着发字地址等ACK再发一个数据字节等ACK然后发停止条件。芯片收到停止条件后才开始内部写通常需要5ms左右。第二种是页写。流程和字节写一样区别是数据字节可以连发多个直到主机发停止条件。这里的关键限制是所有数据必须落在同一个页内否则地址指针会回卷到本页开头把前面的数据覆盖掉。第三种是当前地址读。直接起始条件加设备地址读位芯片会把内部地址指针指向的位置返回主机读完发NACK加停止条件。这种方式的麻烦在于地址指针是上一次操作留下的如果中间发生过写操作指针位置就不是你能预期的所以生产代码里极少单独使用。第四种是随机读也是最常用的读方式。起始加设备地址写位发字地址等ACK然后重新发一个起始条件重复起始发设备地址读位等ACK接着读数据主机在最后一个字节回NACK并发停止条件。这一套伪写动作的目的是把地址指针设到指定位置。第五种是顺序读。在随机读的基础上主机连续给ACK芯片就会把地址指针自动递增并持续吐数据直到主机给NACK加停止条件。做参数批量读取时用这个最省事。3.2 页写边界回卷最容易翻车的地方页回卷是EEPROM写入里坑最深的一个特性因为它不报错、不报警数据就是安静地被写错。举个具体例子AT24C02的页大小是8字节页边界在0x00到0x07、0x08到0x0F这样划分。假设你从地址0x06开始连续写4个字节期望写到0x06、0x07、0x08、0x09。实际情况是前两个字节正常落在0x06和0x07第三个字节因为跨了页地址指针回卷到0x00第四个落到0x01。你原来存在0x00和0x01的数据就被悄悄改掉了。解决思路只有一条写之前强制对齐。驱动里加一层判断算出当前地址到本页末尾还剩几个字节按剩余空间切片每片单独发起一次页写片与片之间等待写周期。// 跨页写切片逻辑示意 uint16_t page_left PAGE_SIZE - (addr % PAGE_SIZE); uint16_t chunk (len page_left) ? len : page_left; ee_page_write(addr, buf, chunk); ee_wait_ready(); // 等待本次写周期结束 addr chunk; buf chunk; len - chunk;这段逻辑看着简单但如果你不做后期出现随机数据错乱时排查起来会非常痛苦因为现象没有规律跟写入的数据内容有关。注意不同型号的页大小差别很大从8字节到128字节都有。移植驱动时第一件事就是改这个宏别直接复制别人代码里的8。3.3 tWR写周期与ACK轮询EEPROM收到停止条件后进入内部写周期这段时间里芯片对外部请求基本不响应tWR典型值5ms、最大可以到10ms。如果你在这期间继续发起新的读写芯片不会给ACK从机地址发出后没有应答很多主控会直接报总线错误。处理办法有两条。粗暴的是固定延时5到10毫秒简单但浪费时间写一个字节等10ms写100个字节就等1秒系统响应明显卡顿。更优雅的方式叫ACK轮询反复发起起始条件加设备地址这个空动作如果芯片内部还在忙就不给ACK一旦写完就给ACK此时立刻进入下一步操作。这种方式的效率提升很明显。实际测量下来页写8字节的内部写周期大约3到4ms固定延时是5ms轮询通常能省下1到2ms如果芯片更快或者数据量更大节省的时间相当可观。void ee_wait_ready(void) { do { i2c_start(); } while (!i2c_write_byte(EE_ADDR_WR)); // 无ACK就继续轮询 i2c_stop(); }轮询要有超时保护一般设成30到50ms超过就报错退出。否则芯片虚焊或者损坏时这个循环会直接把主程序卡死。4. 两种平台的代码落地单片机C与FPGA Verilog时序搞明白之后剩下的就是把它翻译成代码。很多人卡在道理都懂但写不出来问题通常出在没有把协议层和硬件层分开。我在两个平台上都写过EEPROM驱动思路其实是共通的底层只管收发一个字节上层负责组织时序。这一章分别给出单片机和FPGA的实现要点重点是那些教程里不讲的细节。4.1 中颖单片机类EEPROMIAP操作要点中颖的8位单片机在很多家电和小家电控制板上非常常见其中不少型号内置了类EEPROM区域可以通过IAP方式读写。它跟外挂I2C芯片的用法完全不同本质是操作片内Flash的特定区域所以要遵守Flash的规矩先擦后写、按扇区擦除、写周期不能被打断。典型流程是这样的操作前先关闭总中断原因是IAP的整个序列必须连续执行中间被中断打断会导致操作失败甚至数据异常。接着按数据手册给出的顺序往一组控制寄存器里写入解锁序列这个序列的顺序和数值是固定的写错一位就进不去。然后设置目标地址和数据启动写入轮询忙标志位直到操作完成最后恢复中断使能。这里有几个实测出来的经验。第一解锁序列不能省也不要用宏去优化老老实实按手册写。第二地址要落在数据手册明确标注的Data Flash范围内写到程序区会直接损坏固件。第三擦除是按扇区进行的如果只改一个字节得先把整个扇区读到RAM改完再擦再写回去这个读改写流程一定要做掉电保护否则擦完断电数据就全没了。第四写周期内芯片通常不能取指所以这类操作一般放在RAM中执行或者用厂商提供的库函数自己写的代码放在Flash里跑可能会挂。4.2 STM32/通用单片机的I2C EEPROM驱动骨架外挂I2C EEPROM的驱动可以分成三层GPIO模拟层、字节收发层、操作时序层。用硬件I2C外设也行但硬件I2C在某些主控上有状态机卡死的经典问题参数存储这种低频操作其实用软件模拟更省心。GPIO模拟层负责起始、停止、SDA方向切换。这里最容易错的是时序延时延时太长速度慢太短芯片认不出。100kHz的标准模式下半周期大约5微秒用空循环或者定时器都能实现。字节收发层的核心是拉低SCL准备SDA电平拉高SCL读回SDA。发送时读回SDA判断是否有ACK接收时直接采样。uint8_t i2c_write_byte(uint8_t dat) { for (uint8_t i 0; i 8; i) { sda_out((dat 0x80) ? 1 : 0); dat 1; scl_high(); i2c_delay(); scl_low(); i2c_delay(); } sda_in(); // 释放SDA交还给从机 scl_high(); i2c_delay(); uint8_t ack sda_read(); // 0表示从机应答 scl_low(); i2c_delay(); sda_out(1); return ack; }操作时序层把前面讲的那五种操作封装成函数对外只暴露读N字节和写N字节两个接口内部自动处理页对齐和写周期等待。这样上层业务代码完全不用关心页大小是多少、tWR是几毫秒。4.3 FPGA上用Verilog写I2C主控状态机FPGA上的做法完全不同因为没有现成的I2C外设所有东西都要自己搭。好消息是FPGA时序精确做出来的波形比软件模拟好看得多。第一个要解决的是时钟分频。假设系统时钟50MHz要跑400kHz的SCL分频系数是50MHz除以400kHz再除以2也就是约62.5取125个系统时钟为一个SCL周期。用一个计数器循环计数计到一半翻转SCL同时产生一个相位点信号用来标记数据应该在什么时刻稳定。第二个是三态处理。I2C的SDA是双向的Verilog里通常写成三个信号sda_out要输出的值、sda_oe输出使能、sda_in读回的值顶层再用assign做拼接assign sda sda_oe ? sda_out : 1bz; assign sda_in sda;方向切换的时机很关键。发送ACK位之前要把sda_oe拉低释放总线读完之后再拉高。这个切换必须发生在SCL低电平期间因为SCL高电平期间SDA变化会被当成起始或停止条件。我见过不少FPGA实现的I2C通信不稳定问题就出在这里方向切换晚了一个时钟产生了一个假的起始条件。第三个是状态机设计。我一般用三段式IDLE、START、SEND_BYTE、WAIT_ACK、READ_BYTE、SEND_ACK、STOP再加一个字节计数器和位计数器。位计数器0到7发数据位第8个周期读ACK第9个周期准备下一个字节。// 状态机核心片段简化 SEND_BIT: begin scl_low_phase(); sda_out tx_byte[7 - bit_cnt]; sda_oe 1b1; if (phase_done) bit_cnt bit_cnt 1b1; end READ_ACK: begin sda_oe 1b0; // 释放总线给从机 if (phase_done) begin ack_r sda_in; // 采样0为应答 ... end end调试Verilog I2C最有效的手段是做仿真。写一个简单的EEPROM行为模型用always块响应起始和停止条件把测试激励跑一遍用波形窗口看SDA和SCL的相对关系。等仿真完全干净了再上板能省掉大量在示波器前发呆的时间。4.4 跨平台通用的驱动分层思路不管是C还是Verilog抽象层次都是一样的最底层是物理时序中间层是字节传输上层是业务语义。区别只在于C语言用函数调用Verilog用状态机。想清楚这个结构换平台就只是换个语法的事。我个人的习惯是把页大小设备地址写周期时长这些参数全部抽成宏或parameter集中放在文件开头。这样换型号时改三行就完事不用在几百行代码里找哪里写了8哪里写了64。还有一点写驱动的时候顺手把超时保护加上。嵌入式项目里最怕死循环I2C总线一旦有从机拉死SDA没有超时的驱动会把整个系统拖垮。我的做法是每个可能无限等待的循环都配一个计数器超限就返回错误码上层可以选择重试或者重启总线。5. 寿命、可靠性与掉电保护的那些硬功夫驱动能跑通只是及格线产品要出货还得解决三个问题擦写次数够不够用、突然断电会不会丢数据、读写速度会不会拖慢系统。这三件事在实验室里基本不会被发现非要等到客户投诉才暴露。这一章讲的就是这些防患于未然的功夫。5.1 磨损均衡把100万次摊到更多地址上EEPROM标称100万次擦写寿命听起来很多但如果你的设备每秒写一次状态一天就是八万多次十几天就写爆了。所以关键不是寿命有多长而是你怎么把这100万次摊开。最简单的做法是双区轮换。把要保存的数据在存储区里分两个副本每次写之前读一下标记写另一个副本然后更新标记。这样每个物理位置的写入次数直接减半。再进一步是加一个写入计数器并使用多个槽位。比如把参数区划分成8个槽每个槽存一份完整参数加一个递增的序号。写的时候找序号最大的槽往它的下一个槽写。读的时候找序号最大的槽。这样每个槽的写入次数就变成总写入次数的八分之一寿命直接乘8。typedef struct { uint32_t seq; // 写入序号最大的那份是有效数据 uint16_t crc; // 数据校验 uint8_t data[PARAM_LEN]; } ee_slot_t;这套机制的核心是序号最大的那份有效。上电时遍历所有槽找序号最大且CRC正确的那一份。写入时先计算下一个槽的位置写新数据、更新CRC最后校验一遍再返回。整个过程不依赖任何额外的存储纯靠数据结构本身实现。提示序号要考虑溢出用32位基本不可能溢出但如果槽数量不是2的幂取模运算会增加开销建议槽数取2、4、8、16。槽位数量也不是越多越好。每增加一个槽就要多占一份存储空间而且上电扫描时间会增加。参数区不大的话4到8个槽是比较舒服的平衡点。5.2 掉电保护三件套双备份、CRC、魔术字掉电保护的核心问题是写操作进行到一半断电怎么保证下次上电能识别出数据不完整答案是永远把新数据写在别的地方确认写完再切换指针加上校验。双备份的思路是两份数据互为主备。写的时候永远写备份区写完校验通过再把主备标记翻转。读的时候先看标记再选有效的那份。这样任何时刻至少有一份是完整的。CRC校验负责识别数据是否被破坏。常用的CRC16实现简单、开销小对几十字节的参数区足够了。计算范围要覆盖除了CRC字段以外的全部内容包括序号和标记否则可能出现数据改了但CRC没更新的漏网情况。魔术字是一个固定的十六进制常量比如0xA5A5。它的作用是在芯片第一次使用或者被擦除后能快速判断出这里还没有有效数据该加载默认值了。如果不加魔术字全0xFF的存储区也可能碰巧通过CRC校验概率极低但不是零导致加载出一堆乱码参数。三件套组合起来的读流程是读槽位检查魔术字检查CRC通过则使用不通过则换下个槽全部失败就加载默认值并重新初始化。这套逻辑写下来不到一百行但能挡掉绝大多数掉电导致的参数异常。5.3 读写速度与缓存设计EEPROM的写入慢是物理特性决定的改不了但可以把它藏在系统启动阶段或者空闲时段。最常见的优化是加RAM缓存运行时所有参数读写都在RAM里进行只在参数真正改变并且稳定一段时间后才异步落盘到EEPROM。这个稳定一段时间的机制可以用一个脏标记加计数器实现。参数被修改时把脏标记置位主循环里每隔一段时间检查一次如果脏标记置位且已经稳定超过1秒就启动一次写操作。这样连续修改十个参数只会触发一次实际写入寿命和响应速度都受益。读取方向的优化更简单上电时把所有参数一次性读进RAM运行期间不再访问EEPROM。一次顺序读32字节的时间和一次随机读差不多但省掉了后面成百上千次的读操作。如果参数特别多、超过RAM负担可以按模块分块缓存常用的常驻RAM不常用的按需读。这个策略在那些参数动辄几百字节的工业控制器上很常见。6. 翻车现场实录常见问题与排查清单前面讲的都是应该怎么做这一章讲的是做错了会怎样。我把这些年遇到过的EEPROM问题整理成了一张速查表加上几个典型的排查场景希望能帮你少走点弯路。6.1 故障速查表现象可能原因排查动作写完立即读数据不对没等写周期就发起读加ACK轮询或延时某些地址的数据被覆盖页写回卷检查页对齐逻辑概率性写入失败WP脚电平不稳、上拉电阻过大量WP脚电平看波形上升沿全部读回0xFF从机地址错、器件未供电测VCC核对地址配置通信偶尔卡死总线电容过大、缺少超时示波器看波形加超时退出用一段时间后数据丢失反复写同一地址寿命耗尽统计写入次数加磨损均衡上电参数变默认值掉电时写了一半加双备份和CRC板子温度一高就不认器件温漂、虚焊换温度等级更高的型号补焊6.2 ACK丢失与总线锁死怎么救ACK丢失是最常见的异常表现是从机地址发出去之后第八个时钟拉高期间SDA还是高电平。原因无非三种地址写错、从机没供电、总线时序不满足。排查顺序建议从地址开始。很多芯片的地址位里有可以配置的引脚原理图上接的是高还是低要实测确认别照着别人的代码抄。然后量VCC和WP脚供电正常才会有响应。最后才看波形重点看SCL周期是否均匀、上升沿是否太慢、SDA在SCL高电平期间是否有毛刺。总线锁死是更麻烦的情况表现为SDA一直被拉低发起始条件也没反应。这通常发生在一次通信被中断打断从机正在输出一个0的时候主机复位了导致从机一直等着时钟。解救办法是主机切换SDA为输入、连续发9个SCL脉冲让从机把剩下的位吐完然后发一个停止条件。如果还不行就只能重启总线或者掉电重来。void i2c_bus_recover(void) { sda_in(); for (uint8_t i 0; i 9; i) { scl_low(); i2c_delay(); scl_high(); i2c_delay(); } i2c_stop(); }我把这个函数放在初始化最开始每次上电先执行一遍能解决相当一部分开不了机的玄学问题。6.3 数据错乱的排查顺序数据错乱比通信失败更难查因为通信完全正常只是内容不对。我的排查顺序固定是四步。第一步确认写入路径。把写入函数改成写完之后立刻读回并打印看是不是写进去的时候就已经错了。如果读回就错问题在读之前也就是页对齐或写周期。第二步确认读取路径。如果写进去正确、读出来错问题在地址指针。特别是混合使用随机读和顺序读的场景指针位置很容易乱。最稳妥的写法是每次读都重新发一次完整的随机读时序用一点效率换稳定。第三步检查是否被其它代码覆盖。有时候数据本身没问题是别的地方在初始化时把整个参数区清零了。这时候用断点或者日志在参数区上加内存监视看是哪个时刻变的。第四步才怀疑硬件。芯片本身坏掉的概率其实很低但如果前面三步都排除了用同型号的新芯片替换试试同时检查焊接和退耦电容。这个顺序的价值在于从高概率往低概率走。我见过太多人一上来就换芯片换了几轮问题还在最后发现是页对齐的锅。我在实际使用中最深的一个体会是EEPROM这东西的可靠性不取决于你选了什么品牌而取决于你有没有认真对待它的两个物理特性页边界和写周期。把这两件事处理干净再加一层带CRC的双备份基本就不会再遇到参数丢失的问题了。剩下那些玄学故障九成都能靠上电时的总线恢复函数和足够的超时保护挡掉。
返回列表