☰
从固定故障到地址译码:SRAM/EEPROM存储器测试与March算法选型
2026/9/30 5:38:21 网站建设 项目流程

1. 故障模型到底在解决什么问题:把"坏了"翻译成可测的行为

调试一批返修板子的时候,我遇到过一个很典型的场景:设备跑内存自检全过,但连续运行几个小时之后,某几个地址位的值会莫名其妙地翻掉。换芯片没好,换板子好了,可过几天另一块板子又出同样的问题。当时如果有人告诉我"你测的是固定故障,但你遇到的是耦合故障",这个问题大概能少折腾两天。存储器故障模型这个词听起来像学术论文里的东西,实际上它是硬件工程师和底层软件工程师每天都要打交道的一把尺子——它决定了你到底能测出什么、测不出什么。

说白了,故障模型就是一套"翻译规则"。存储器芯片内部的物理缺陷有成千上万种——氧化层针孔、金属线虚焊、位线之间寄生电容偏大、驱动管阈值漂移、电荷泵电压不足——这些"物理缺陷"没法直接写进测试程序里。你得先把它们归类,抽象成有限的几种"逻辑行为异常",然后才能针对每一种行为设计激励序列。这个"逻辑行为异常"就是故障模型。

举个最直白的例子。一个存储单元本该存 0 却读出 1,你不需要知道它是位线对电源短路了还是下拉管没导通,你只需要知道"这个单元呈现固定 1 的行为"。这就是固定故障(SAF)。有了这个抽象,你就能写一个测试:往所有单元写 0,再全部读回来,读出不等于 0 的就是嫌疑单元。一条测试覆盖一大类物理缺陷,这就是故障模型的价值。

但事情没这么简单。我见过太多人(包括早期的我自己)把内存测试理解成"全写 0 读一遍,全写 1 读一遍,过了就发货"。这套流程能测出的东西非常有限,它甚至连最常见的**转换故障(TF)**都测不出来——某个单元写 0 能成功、写 1 也能成功,但从 0 翻到 1 的时候需要驱动管提供较大的瞬间电流,驱动能力退化的单元就会翻转失败,而你"先全写 0 再全写 1"的做法,恰好把它写得整整齐齐,反而掩盖了相邻两次操作之间的时序关系。

我在实际项目里踩过的一个坑值得展开说。有一批板子,用 checkerboard(棋盘格)模式测全过,用全 0/全 1 模式测全过,但一旦按地址递增顺序逐个写、然后按地址递减顺序逐个读,就必然在某个位置失败。后来用示波器抓地址线才发现,是一条地址线的走线电阻偏大,上升沿比别人慢十几纳秒,在递增访问时恰好赶在采样窗口边缘,递减访问时相位不同就暴露了。这个缺陷在物理上属于电阻性缺陷,在逻辑上表现为特定地址顺序下的地址译码故障——你只有用对故障模型,配对应地址遍历顺序的算法,才能把它揪出来。

所以判断一个存储器测试方案好不好,第一个问题永远是:它覆盖了哪些故障模型?而不是:它跑了几遍?

这里还要澄清一个概念层级,很多人会混。缺陷(defect)是物理层面的,比如某根多晶硅线断了一截;故障(fault)是逻辑层面的抽象,比如某单元固定为 1;错误(error)是数据层面的表现,比如本该是 0x5A 读出来是 0x5B。故障模型是缺陷到故障的映射,而测试算法是故障到激励序列的映射。搞清这条链,后面选算法、估时间、定判定阈值才有依据。

提示:永远不要用"测试通过"这四个字下结论。要说"在 March C- 覆盖的故障集合下未检出异常",因为任何测试方案都是有盲区的,把盲区说清楚,比拍胸脯说没问题更有价值。

2. 单端口 SRAM 的六大经典故障模型拆解

搞嵌入式的人打交道最多的就是片内 SRAM 和片外 SRAM。这块的故障模型已经非常成熟,1970 年代到 2000 年代积累了大量文献,业界共识度很高。下面这几类,是我认为做实际工作必须掌握的。

2.1 固定故障与转换故障:最基础,也最容易自欺欺人

固定故障(SAF,Stuck-At Fault)指某个单元恒为 0(SA0)或恒为 1(SA1)。它对应的物理缺陷通常是数据线对地或对电源短路、存储单元内部的负载管或驱动管彻底失效。检测方法很直接:写反值再读。所有单元写 0,读一遍,全部应该读出 0;再全部写 1,读一遍,全部应该读出 1。

关键在于第二遍读之前,你必须先写一个与期望值相反的值。很多"自检代码"犯的错误是先写 0 读 0,再写 1 读 1,中间没有强制翻转,那 SA1 的单元在第一遍写 0 时就已经暴露(读出 1),但 SA0 的单元在第二遍写 1 时才会暴露——逻辑上没错,但你没法区分这个失败是"写不进去"还是"读不出来"。区分不了就定位不了,定位不了就得整机返修。

转换故障(TF)比 SAF 更隐蔽。它分两种:上升转换故障(↑)指单元无法从 0 翻到 1,下降转换故障(↓)指无法从 1 翻到 0。注意,一个单元可能同时"能写入但翻转慢"——你在写之后立刻读,读到的是旧值,因为写驱动还没把节点拉到位。这类缺陷在高频访问下暴露得特别明显。

我个人的经验:凡是内存相关的偶发问题,第一步永远是把访问频率降到原来的十分之一再看。如果降频之后问题消失,八成是转换故障或者时序余量不足,而不是隔离性的硬缺陷。这个判断能帮你快速把问题从"芯片坏了"这个死胡同里拽出来。

检测 TF 的标准做法是"写后立即读反值":写 0、读 0、写 1、读 1,或者更严格地,写 0、读 0、写 1、读 1、再写 0、读 0。March 算法里的每一个r0,w1元素本质上就是在做这件事。

2.2 耦合故障:两个单元之间的"串扰"

耦合故障(Coupling Fault)是实际工程中最容易被漏掉的一类,因为它的触发条件是"另一个单元发生了某种操作"。它分三个子类,区别很有意思。

幂等耦合故障(CFid):攻击单元发生一次跳变,把受害单元强制拉成某个确定值(0 或 1)。注意是"强制",不管受害单元原来是什么。物理上通常对应两个相邻单元之间有短路通路,一个节点翻转时把另一个节点也拽过去了。

反相耦合故障(CFin):攻击单元跳变导致受害单元取反。物理上常见于位线之间的耦合电容——一条位线放电时通过寄生电容把相邻位线的电压往下拉,再被灵敏放大器放大成翻转。

动态耦合故障(CFdyn):攻击单元上的一次读或写操作(不一定发生跳变),就会改变受害单元的内容。这类故障和位线预充电结构关系很大,在低电压、慢工艺角下特别容易出现。

三者的区别总结成一句话:CFid 是"强制定值",CFin 是"取反",CFdyn 是"操作即影响,不看跳变"。它们对应的测试序列也不同——CFin 需要一个翻转激励加一次读回,CFid 需要两个方向的翻转都要试,CFdyn 则需要考虑读写操作的时序窗口,往往要用到带延迟控制的专用算法。

再说一类相关但独立的:状态耦合故障(SCF)。它不需要攻击单元跳变,只要攻击单元处于某个状态(比如保持 1),受害单元就被迫变成某个值。测试方法是先把攻击单元设成某种状态,保持住,然后去读受害单元。

2.3 桥接、开路与电阻性缺陷:能读写不等于没坏

**桥接故障(Bridging Fault)**分两种:同一条字线上相邻两列的桥接(AND 型/OR 型),以及不同信号线之间的桥接。两个单元被短路之后,写入可能出现"与"或者"或"的效果。这类故障用棋盘格模式最容易被抓出来,因为棋盘格让相邻单元的期望值总是相反,一旦发生桥接,读回来两个都是相同值。

**开路故障(Open Fault)**比较讨厌。传统上认为断路就是固定值,但在深亚微米工艺下,浮空节点的电压可能由周围耦合决定,可能读出来是"上一拍的值",也可能是随机值。这类缺陷在功能测试里表现得像"偶尔出错",最容易被打上"软件 bug"的标签然后搁置。

电阻性缺陷是我最想强调的一类。它是说短路或者断路不是彻底的,而是带了一个电阻值。表现就是:在常温常压下读写完全正常,一升温、一降压、一提高频率,就崩了。这类缺陷的功能测试覆盖率天然很低,必须靠参数测试——改变供电电压和温度,在极限条件下跑功能测试。

故障模型触发条件典型物理诱因有效激励
固定故障 SAF访问即暴露彻底短路/开路写反值后读
转换故障 TF发生跳变驱动能力退化、时序余量不足写 0 读 0 写 1 读 1
幂等耦合 CFid攻击单元跳变相邻单元短路两方向跳变 + 读受害单元
反相耦合 CFin攻击单元跳变位线间寄生电容跳变 + 立即读
动态耦合 CFdyn攻击单元读写预充电结构、低电压带时序约束的读写序列
状态耦合 SCF攻击单元停留某状态弱短路保持状态再读受害单元

2.4 邻域图形敏感故障与数据保持故障

**邻域图形敏感故障(NPSF)**是耦合故障的推广:一个单元的内容受它周围若干单元构成的"图形"影响。按邻域单元数量分,有 5 单元邻域、9 单元邻域、13 单元邻域等。按行为再分:

  • 静态 NPSF(SNPSF):邻域处于某个特定图形时,受害单元被强制成某值。
  • 被动 NPSF(PNPSF):邻域处于某个图形时,受害单元写不进去。
  • 主动 NPSF(ANPSF):对邻域某个单元的写操作改变受害单元。

NPSF 的完整测试复杂度是随邻域大小指数增长的,实际项目里几乎不可能穷举。通常的做法是用准确定向的图形(比如在受害单元周围铺特定的 0/1 组合)去覆盖那些最可能出现的邻域关系,而不是全枚举。这也是为什么很多高可靠存储器的内建自测会带一整套"图形库"。

**数据保持故障(Data Retention Fault)**单独拿出来说,因为它的测试方式和别的都不一样。它是指单元写进去之后,过一段时间自己丢数据。SRAM 的物理机制通常是单元内部节点漏电流偏大,DRAM 是刷新周期不够,EEPROM/Flash 是浮栅电荷泄漏。

测试方式很特别:写数据 → 等待(等待时间往往要几十毫秒到几分钟)→ 读回验证。而且等待期间芯片最好处于"压力状态",比如处于低功耗模式或者持续被其他地址访问,这样更容易把漏电问题逼出来。我在一个项目里遇到过整批芯片在常温下数据保持没问题,但一到 70℃ 就大面积丢数据,最后发现是单元漏电随温度指数增长——这类问题不做温度循环老化根本发现不了。

2.5 地址译码故障:那条最容易被忽略的地雷

地址译码故障(Address Decoder Fault,AF)是唯一一类"故障不在存储阵列里,而在外围电路"的模型。它分四种表现形式:

  1. 某个地址无法访问任何单元(地址"打不开门")。
  2. 某个地址能访问到多个单元(地址混叠,aliasing)。
  3. 某个单元没有任何地址能访问到(永久隐身)。
  4. 某个单元能被多个地址访问到(一个单元挂多把钥匙)。

第二种和第四种在工程里最常见,表现就是"内存变小了"或者"写入 A 地址却影响了 B 地址"。检测 AF 的经典手段是Walking 1 / Walking 0:先把所有单元写成 0,然后对地址 i 写 1,再把其他所有地址读一遍,必须全是 0;然后对地址 i 写 0,把其他所有地址读一遍,必须全是 0。逐个地址做一遍。复杂度是 O(N²),在 64KB 上就是 40 多亿次访问,实际项目里跑不动。

所以我实际用的妥协方案是地址特征值法:每个地址写入由该地址派生出的数据(比如低字节用地址低位、高字节用地址取反),然后全阵列读回逐字比对。这个方法不能证明不存在 AF,但能抓住绝大多数由地址线粘连、片选译码错误引起的混叠。真正的全覆盖 AF 测试只在量产测试机上用,用专门的算法(比如带特定地址序的 March 变体)来完成。

3. March 系列算法的选型逻辑:从 MATS+ 到 March SS

知道故障模型之后,下一步就是选算法。March 算法是存储器测试领域的通用语言,搞懂它,你跟测试工程师或者 IC 验证同事沟通会顺畅很多。

3.1 先把 March 记号看懂

March 记号看着像天书,其实规则很简单。一个 March 元素由若干个操作组成,元素前面带有地址遍历方向的符号:

  • ⇑表示地址从低到高遍历。
  • ⇓表示地址从高到低遍历。
  • ⇕表示两个方向都可以,效果等价。

元素内部的操作按从左到右的顺序执行,r0是读出并期望是 0,w1是写入 1,依次类推。比如⇑(r0,w1)的意思是:地址从低到高,每个单元先读出并检查是 0,然后写入 1。

整个算法由若干个 March 元素顺序执行,元素之间用分号隔开。写地址遍历方向的意义在于:有些故障模型(比如特定方向的耦合)只在某个遍历方向上才暴露,所以你需要在两个方向上都跑一遍。

3.2 常见算法覆盖能力与复杂度对照

下面这张表是我自己整理的,实际选型时经常拿出来看。

算法复杂度覆盖的故障模型适用场景
MATS+5NSAF、部分 AF快速冒烟测试
March C-10NSAF、TF、CFin、CFid、AF通用首选,性价比最高
March C+14N在 C- 基础上加强耦合覆盖汽车电子等可靠性要求高
March SS22NSAF、TF、CFin、CFid、CFdyn、SOF、NPSF高可靠 SRAM / SoC 内建自测
Checkerboard4N(多次)桥接、部分耦合板级快速筛查
Walking 1/04N²AF、桥接小容量 RAM、地址线专项诊断
GALPAT4N²几乎全部简单故障小容量、极慢,只用于抽样验证

其中 N 是存储单元数量,系数代表每个单元平均被访问的次数。MATS+ 的意思是:⇕(w0);⇑(r0,w1);⇓(r1,w0)——总共 5 次操作每单元。

March C- 是我最推荐的默认选择,它的六个元素是:

M0: ⇕(w0) M1: ⇑(r0,w1) M2: ⇑(r1,w0) M3: ⇓(r0,w1) M4: ⇓(r1,w0) M5: ⇕(r0)

复杂度 10N,能覆盖 SAF、TF、CFin、CFid 和一部分 AF,性价比在所有经典算法里是最高的。我在大多数量产自检里用的都是它。

3.3 怎么根据容量和时间预算选算法

选算法的核心约束只有一个字:时间。测试时间等于复杂度系数 × 单元数 ÷ 有效访问频率。这个估算必须动手算,不能拍脑袋。

举个实际的例子。一颗 1Mbit(128KB)的片内 SRAM,工作频率 100MHz,一个时钟周期完成一次访问。用 March C-(10N):

1,048,576 单元 × 10 ÷ 100,000,000 次/秒 ≈ 0.105 秒

一百毫秒,完全可以接受。换成 March SS(22N):大约 0.23 秒,稍长但还能忍。如果换成 GALPAT(4N²):4 × 10¹² 次访问,按 100MHz 算要 11 个小时。这就是为什么 GALPAT 只能用在几 KB 的小存储器上做抽样验证。

再看一个典型的 51 单片机场景。外扩 64KB 数据存储器,用MOVX @DPTR访问,12MHz 晶振、12 分频模式下一条MOVX指令是 2 个机器周期,也就是 2 微秒:

65,536 单元 × 10 × 2µs ≈ 1.31 秒

一秒多。听起来还行,但如果你是上电自检,用户会明显感觉到开机的延迟。这时候要么改用 MATS+ 把时间砍一半,要么只对前若干 KB 做完整测试,剩下的做抽样。

外挂 I2C EEPROM 的情况更极端。以常见的 AT24C08 为例,1KB 容量,I2C 时钟 400kHz,每个字节的随机读大约需要几十微秒(含起始、地址、应答、数据、停止的开销),而每次写操作之后必须等待内部写周期完成,典型值 5 毫秒。假设按 16 字节页写来优化:

整片写一次 = 1024 ÷ 16 = 64 个页写周期 × 5ms ≈ 320ms March C- 有 5 个写入元素(M0 到 M4),即 5 × 320ms ≈ 1.6 秒 再加上 5N 次读操作,按 40µs 每次算 = 5 × 1024 × 40µs ≈ 0.2 秒 总计约 1.8 秒

接近两秒,而且这里还藏着一个更重要的问题:每跑一遍 March C-,每个存储单元就被擦写了 5 次。EEPROM 的耐久性通常是 100 万次,单次测试可以忽略,但如果你把它放在生产线的循环测试里,一块板子测 1000 遍就是 5000 次擦写。所以对 EEPROM 这类非易失存储器,测试次数必须计入耐久预算,这是和 SRAM 测试完全不同的思路。

这是我给过很多人的一条提醒:设计 EEPROM 自检流程时,先在文档里写清楚"每次上电自检消耗 5 个擦写周期,设计寿命内上电次数上限 X 次",然后算总消耗。这个数字不算清楚,产品后期出现批次性数据保持失效,你连原因都找不到。

3.4 用 C 语言手写一个 March C- 实现

理论讲完,来点能直接抄的代码。下面这段是针对一段连续内存区域实现的 March C-,设计上考虑了裸机和用户态两种环境。要点有三个:用volatile防止编译器优化掉访问;用uint8_t*逐字节访问避免对齐问题;记录首次失败的位置和期望值、实际值,方便定位。

#include <stdint.h> #include <stddef.h> typedef struct { volatile uint8_t *base; size_t len; size_t op_count; /* 已执行的基本操作数 */ size_t err_count; /* 错误计数 */ size_t first_addr; /* 首次失败地址 */ uint8_t first_expect; uint8_t first_actual; } march_ctx_t; static void march_fail(march_ctx_t *c, size_t idx, uint8_t exp, uint8_t got) { if (c->err_count == 0) { c->first_addr = idx; c->first_expect = exp; c->first_actual = got; } c->err_count++; } static void march_write(march_ctx_t *c, size_t idx, uint8_t v) { c->base[idx] = v; c->op_count++; } static void march_read_check(march_ctx_t *c, size_t idx, uint8_t exp) { uint8_t got = c->base[idx]; c->op_count++; if (got != exp) march_fail(c, idx, exp, got); } /* March C- : 10N */ int march_c_minus(march_ctx_t *c) { size_t i, n = c->len; /* M0: ⇕(w0) */ for (i = 0; i < n; i++) march_write(c, i, 0x00); /* M1: ⇑(r0,w1) */ for (i = 0; i < n; i++) { march_read_check(c, i, 0x00); march_write(c, i, 0xFF); } /* M2: ⇑(r1,w0) */ for (i = 0; i < n; i++) { march_read_check(c, i, 0xFF); march_write(c, i, 0x00); } /* M3: ⇓(r0,w1) */ for (i = n; i-- > 0; ) { march_read_check(c, i, 0x00); march_write(c, i, 0xFF); } /* M4: ⇓(r1,w0) */ for (i = n; i-- > 0; ) { march_read_check(c, i, 0xFF); march_write(c, i, 0x00); } /* M5: ⇕(r0) */ for (i = 0; i < n; i++) march_read_check(c, i, 0x00); return (c->err_count == 0) ? 0 : -1; }

几个使用上的细节值得单独说。c->len必须是字节数,不是字数,因为基址是uint8_t*。如果目标平台是 32 位,逐字节访问会比按字访问慢三到四倍——March 算法本身不要求按字节访问,你完全可以改成按字访问,把uint8_t换成uint32_t,期望值从0x00/0xFF换成0x00000000/0xFFFFFFFF,效率提升很明显。但在地址混叠诊断场景下,逐字节访问更容易定位到具体是哪条地址线出问题,所以调试阶段用字节、量产阶段用字,这个切换很值得做。

另外要注意,上面这段代码本身可能被放在正被测的那段内存里执行。裸机环境下,如果你把测试代码和栈放在被测试的 RAM 区域,测试过程中的写入会把栈冲掉,程序直接跑飞。解决办法有三种:把测试代码放到 Flash 或 ROM 里执行;把栈搬到不被测试的 RAM 区域;或者干脆分段测试,每次只测一段,测试代码和执行栈都在段外。第三种最稳妥,也是我在资源紧张的 MCU 上常用的做法。

还有一个细节:volatile只保证编译器不会优化掉这一次访问,但不保证 CPU 的写缓冲和乱序执行。在有写缓冲的处理器上,写完立即读可能读到写缓冲里还没落地的值。这种情况需要插入内存屏障或者对存储区域配置为强序访问。在 51 这种简单架构上不存在这个问题,但在 ARM Cortex-M 系列上,如果存储器区域被配置为 Normal memory 且允许写缓冲,就必须留意。

4. EEPROM 与 Flash 的故障模型:和 SRAM 完全不是一回事

把 SRAM 那一套直接搬到 EEPROM 或者 Flash 上,是新手最容易犯的错误。非易失存储器的物理机制完全不同,故障模型也必须重新建立。

4.1 AT24C08 这类 I2C EEPROM 的失效排查思路

AT24C08 是 8Kbit(1KB)的 I2C 接口 EEPROM,结构上是 1024 × 8 位,分 4 页,每页 256 字节,通过器件地址的低两位选择页。它的常见失效模式可以分几类:

读写通道失效:I2C 从机地址不对、上拉电阻取值不当导致电平上升太慢、总线电容过大导致波形畸变。这类问题在示波器上一眼能看出来,不是存储器本身的故障。

内部写周期超时:每次写操作之后,芯片内部要花时间把电荷注入浮栅,这段时间(典型 5 毫秒,最坏 10 毫秒)内芯片不响应任何请求。如果主机不等就继续发命令,会直接被 NACK。这个问题在实践中极其常见,表现为"偶尔写失败"。

单元级别的固定故障和耦合故障:EEPROM 的存储单元是两个浮栅晶体管构成,读通路和写通路分开,所以它的固定故障往往表现为"某个字节永远是 0xFF"(擦除态)或者"永远是某个值"(编程之后卡住)。

数据保持故障:这是 EEPROM 最本质的失效模式——浮栅上的电荷会慢慢泄漏,通常以 10 年为量级。高温会大幅加速泄漏。

我见过不少人想用手持数字万用表来判断一颗 EEPROM 的好坏,比如拿常见的胜利 890C 系列去量引脚的通断或者对地电阻。这里必须说清楚:万用表能判断的只是"引脚有没有物理断路、电源脚有没有对地短路"这类最基本的电气连通性,对存储器功能本身完全无能为力。一颗电荷已经漏光、数据全丢的 EEPROM,在万用表上和在读卡器里都一样"正常"。正确的判断方法只有一条:实际读写测试。写一组已知数据,读回比对,再做数据保持测试(写完后高温放置,再读回)。这个结论听上去很朴素,但确实是我在返修现场见过最多的误判来源。

4.2 电荷泵、擦除与编程干扰带来的特有故障

Flash 和 EEPROM 都依赖内部电荷泵产生高压来完成擦除和编程,这引入了一类 SRAM 完全没有的故障。

过擦除(Over-Erase):擦除操作把浮栅上的电子抽得太多,导致阈值电压偏低,单元在未选中时就轻微导通。后果是同一列上的其他单元读取时被拖累,表现为整列读数据出错。这是 Flash 里非常经典的失效模式,修复手段通常是擦除后加一个"软编程"(soft program)把阈值拉回目标区间。

编程干扰(Program Disturb):对某个单元编程时,同一字线上的其他单元会被加上较高的栅压,虽然不足以改变它们的电荷量,但多次重复之后会累积偏移。这是 Flash 与生俱来的可靠性问题,靠编程次数和刷新策略来缓解。

读干扰(Read Disturb):反复读同一个块,会在未选中的字线上积累电荷,最终改变其阈值。所以 Flash 控制器通常要记录每块的读取次数,超过阈值就触发数据搬移。

耐久性退化:每次擦写都会在氧化层上留下少量陷阱电荷,写擦次数越多,氧化层退化越严重,最终导致单元卡在某个值或者数据保持能力大幅下降。这就是为什么很多 Flash 规格书上写"10 万次擦写",而不是"无限次"。

这些故障有一个共同的检测难点:它们往往要累积很多次操作才显现。跑一遍 March 算法是测不出来的,必须做加速老化——高温下反复擦写、反复读取。这也是为什么非易失存储器的验证周期比 SRAM 长得多。

4.3 数据保持与耐久性:怎么设计一个可信的验证实验

如果让我给一个可执行的方案,大概是这样的。假设你手上有一批新到的 AT24C08,需要评估批次质量。

第一步,全片写入特征图形。往每个地址写入由地址派生的值,比如addr ^ 0x5A,然后整体读回比对。这一步能抓住读写通道问题和明显的固定故障。

第二步,交叉耦合测试。按棋盘格模式写入 0x55 和 0xAA 交替,读回比对;再换成 0xAA/0x55 反相模式,读回比对。这一步能抓住相邻单元之间的耦合缺陷。

第三步,数据保持加速测试。全片写入特征图形之后,把芯片放进恒温箱,设定 85℃ 或 125℃,放置 168 小时(七天)或者 1000 小时,取出后常温回读比对。这一步是评估数据保持能力的关键。行业里的加速模型通常用阿伦尼乌斯方程描述:温度每升高 10℃ 左右,退化速率大约翻一倍。所以 125℃ 下保持 1000 小时,大致对应常温下相当长的时间——具体换算系数要看器件的激活能参数,不同工艺差别不小,不能随便套用。

第四步,耐久性抽样。随机抽若干个字节,做反复擦写,比如 10 万次、50 万次、100 万次各一组,每组在完成后做一次完整读回验证,看有没有单元卡死。这一步会消耗芯片寿命,所以一定要用抽样而不是全片。

四步下来,一批芯片的底细基本就摸清了。这里有个容易忽略的点:第三步和第四步需要记录每一颗样品的编号和位置,因为如果只统计"10 颗里坏了 2 颗",你没法知道失效是不是集中在晶圆的某个位置。批量评估的价值一半在数据,一半在溯源。

5. 接线与译码层面的故障:存储器与 CPU 连接时的真实坑

前面讲的都是芯片内部的故障模型。但实际工程里,尤其是用 51、STM32 这类单片机外扩存储器的场景,问题往往出在芯片和 CPU 之间的连接上。这类故障有自己的规律,也需要自己的一套诊断手法。

5.1 地址线数据线开路短路的表现与定位方法

先建立一个直觉:数据线出问题,症状是"数据错了但位置对";地址线出问题,症状是"位置错了但数据对"。这个判断能让你在十分钟内把问题范围砍掉一半。

数据线粘连的诊断很简单:写一组"每条线上只有一位为 1"的图案,也就是 0x01、0x02、0x04、0x08…… 逐条测试。如果数据线 D3 和 D4 短路了,那么写入 0x08 读回会变成 0x18。

地址线粘连的诊断用 Walking 1。但在大容量存储器上 Walking 1 是 O(N²),跑不动。我的实用替代方案是地址特征值法:往每个地址写入(addr & 0xFF)作为低字节、((addr >> 8) & 0xFF)作为高字节,然后全片读回逐字比对。这个方法假设"地址本身能唯一标识数据",能抓住绝大多数地址线粘连。它的局限是检测不出"两条地址线互换"这种对称故障——A3 和 A4 交换之后,0x08 和 0x10 互换位置,用特征值法写入的就是交换后的位置,读回时也是交换后的位置,看起来完全正常。要抓这类故障,得用"写入固定值再逐地址改写"的方法。

我在一个项目里遇到的真实案例值得详细记录,因为它完整展示了排查链路。

症状:一批 50 块板子,其中 6 块在连续运行 2 到 6 小时后出现数据错乱,复位后恢复,再跑几个小时又出问题。

第一步,先排除软件。把出问题的板子固件换成"只做内存读写 + 串口打印结果"的最小程序,问题依旧。软件被排除。

第二步,换芯片。把疑似板子的 SRAM 芯片和一块正常板子互换。结果:正常板子装上疑似芯片后仍然正常,疑似板子换上确认良好的芯片后仍然出错。结论——问题在板子,不在芯片。这一步很关键,很多人跳过它直接怀疑芯片,会浪费大量时间。

第三步,压力测试定位。写一个专门的测试程序,只做一件事:把整个 32KB 空间按 0x5A 写一遍,再按 0xA5 写一遍,然后校验,循环执行,同时通过串口打印出错地址。跑了两小时之后,出错地址统计出来了——全部集中在地址的高 4KB 区间,而且错误地址的高位总是 0xF 变成 0x7,或者反过来。

第四步,锁定量级。地址高位 0xF 和 0x7 的差别在第 12 位(A12)。用示波器抓 A12 的波形,发现上升沿明显比别的地址线慢,测量上升时间大约 40 纳秒,而其他线只有 5 纳秒左右。

第五步,找根因。顺着 A12 的走线找过去,发现那颗限流电阻的焊点看起来正常,但用四线法测量阻值时是 470 欧姆——而设计值应该是 33 欧姆。同一个料盘里的电阻混料了。这种级别的阻值偏差,在常温下大部分时间还能工作(因为地址线的上升时间留有裕量),一旦芯片温度升高、驱动能力下降,就顶不住了。

第六步,验证。换回正确阻值之后,跑 48 小时无错。

这个案例里,物理缺陷是"错料电阻",逻辑故障模型是"地址译码故障"结合"电阻性缺陷"。如果一开始就用 Walking 1 做地址线专项测试,问题能在第一天就收敛到 A12 上。

这里有个经验:地址线专项测试必须放在常温下也跑一遍,因为它的目的是"验证连通性和时序余量",不是"验证当前能不能工作"。只看功能是否通过,是抓不到这类余量不足的问题的。

5.2 51 单片机扩展存储器的地址译码与片选冲突

用 8051 外扩存储器,架构上有几个固定的坑,值得单独列出来。

8051 的 P0 口是地址/数据复用总线,访问外部数据存储器时,先从 P0 送出地址低 8 位,由 ALE 信号锁存到外部的锁存器(常见的是 74HC373),然后 P0 变成数据总线。P2 口送出地址高 8 位。所以外部存储器的连接是这样的:A0 到 A7 来自锁存器输出,A8 到 A15 来自 P2 口,D0 到 D7 接 P0 口,写信号是 WR,读信号是 RD。

如果 ALE 锁存器没接或者接错,会出现什么症状?A0 到 A7 会变成总线上的残留值,表现为"低地址位的随机翻转"。典型现象是:写入地址 0x0000 和 0x0100,读回来都是同一个位置的内容。这类症状和地址混叠极其相似,但根因完全不同——前者是时序问题,后者是译码问题。

片选冲突是另一个高频问题。如果你用了 74LS138 做地址译码,要注意它的输出是低有效的,且同一时刻只能有一个输出有效。如果不小心把两个片选同时拉低,或者 138 的使能脚没接好一直有效,两条存储器的数据线就会同时驱动总线,产生总线冲突。表现是读回的数据是两个存储内容的"线与"结果,而且电流消耗会明显偏大。

我的建议是:外扩存储器调试的第一件事,不是写测试程序,而是用示波器确认 ALE、WR、RD 和片选四条信号的时序关系正确。确认时序之后再上功能测试,能省掉大量返工。

5.3 地址混叠是怎么被误判成"内存变小了"的

地址混叠是 AF 的一种表现形式:一个物理单元被多个地址访问到。症状是"明明有 64KB,但写满数据之后只有一部分生效"。

举个具体例子。假设地址线 A15 没有连上,一直是被上拉为高,那么地址 0x0000 和 0x8000 实际上访问的是同一个单元。你往 0x0000 写 0x11,再往 0x8000 写 0x22,然后读 0x0000 会得到 0x22。这类问题的诊断方法就是前面提到的地址特征值法:如果每个地址存的是与地址相关的特征值,出现混叠时读回的特征值会和地址对不上,一眼就能看出来。

这类故障还有一个隐蔽的变种:片选译码错误导致的高位混叠。比如存储器实际只有 32KB,但译码逻辑让 32KB 到 64KB 这段地址也响应,结果就是这段地址映射到同一个 32KB 空间。测试程序如果只测前 32KB,完全正常;一旦往高地址写数据,就会把低地址的内容冲掉。这种问题在程序运行一段时间之后才会暴露,因为堆栈或者堆正好用到了高地址区域。

5.4 Walking 1/0 在小容量 RAM 上的实操

如果容量不大(比如几 KB 的片内 RAM),Walking 1 是完全跑得动的。以 2KB 为例,复杂度 4N² = 4 × 2,097,152 ≈ 840 万次访问,在 24MHz 的 MCU 上大概零点几秒,可以接受。

实现思路是:先把整个区域清零;然后对每个地址 i,写入 1(或者该地址对应的位模式),接着遍历所有其他地址,验证它们仍然是 0;然后把地址 i 恢复为 0。整个过程重复一遍,把 1 换成 0、0 换成 1,就完成了 Walking 1 和 Walking 0 两轮。

这个测试的收益特别高,因为它直接给出了"地址 i 有没有和其他地址串起来"的确定答案。我在做小容量片内 RAM 的可靠性验证时,基本都会加上这一项,因为它抓出来的问题往往是其他算法根本碰不到的。

6. 软件侧的自测落地:在没有 MBIST 的平台上做内存测试

芯片厂商给的高可靠 SRAM 通常带 MBIST(内建自测试)硬件模块,一键启动、硬件遍历、自动报错。但绝大多数消费类和工业类 MCU 没有这个待遇,你得自己在软件里实现。

6.1 裸机环境的测试代码怎么写才不会自杀

前面提过,测试代码和栈不能位于被测区域。除此之外还有几个坑。

第一,编译器可能把循环优化掉。如果你的读写没有volatile修饰,编译器发现"写进去的值没被使用",会直接删掉整个循环。加了volatile之后,每次访问都是真实的读写,优化开关失效。

第二,缓存会掩盖问题。如果被测区域被配置为可缓存的,那么你写进去之后读出来的是缓存里的副本,根本没有真正访问到存储器。做内存测试前必须把被测区域配置为非缓存,或者使用专门的缓存维护指令在测试前后做无效化。这一点在带 MMU 的处理器上尤其重要。

第三,中断会打断测试。如果测试过程中发生中断,中断服务程序可能访问了被测区域,破坏测试的前置条件。稳妥的做法是测试期间关中断,或者用一个独立的内存段给中断服务程序专用。

第四,测试失败之后要能报告。最简单的方式是通过串口打印失败地址和期望值、实际值,或者用 GPIO 输出一个错误码。有些团队会预留几 KB 的保留内存专门存放测试日志,测试结束后再从保留区读出来上传。

6.2 用户态程序做内存测试的额外陷阱

如果是 Linux 用户态,情况又不一样。malloc拿到的内存是虚拟地址,映射到哪个物理页由内核决定,而且可能被换出。这意味着你测的是"虚拟地址映射关系加存储器的组合表现",不是纯粹的存储器本身。

我把这类测试的坑分成三层。

第一层,编译器和操作系统优化。malloc之后如果只写不读,内核可能根本不会给你分配物理页(延迟分配,写时分配)。正确的做法是先memset一遍把物理页落实下来,再做测试。另外像calloc拿到的内存可能映射到内核的全零页面(COW),你写的时候会触发页复制,这个过程中断会引入额外的时序扰动。要测底层存储器,最好用mmap加MAP_ANONYMOUS | MAP_POPULATE先把物理页落实,再用mlock防止被换出。

第二层,测试过程的干扰。用户态程序运行在多任务系统上,随时可能被调度出去,导致读写之间的时间间隔不可控。对于测试耦合故障和动态故障这类依赖精确时序的模型,用户态测试基本无能为力。用户态能可靠测的只有固定故障、转换故障和部分静态耦合故障。

第三层,结果判读。用户态的单比特翻转很可能被 ECC 修正掉,你根本看不到错误。Linux 系统的 EDAC 子系统会在/sys/devices/system/edac/mc/下暴露可纠正错误和不可纠正错误的计数器,这些计数器才是判断"内存是否真的出问题"的关键依据。只看应用程序有没有崩,会严重低估实际的错误率——因为大部分错误都被 ECC 悄悄修掉了,只有累积到一定程度才会演变成不可纠正错误。

我个人的做法是:用户态压力测试和 EDAC 计数器一起看。如果压力测试全过但可纠正错误计数持续增长,说明存储器已经在退化了,这时候就该考虑更换硬件,而不是等它崩。

6.3 结果判定与误报抑制:怎么区分真故障和测试环境问题

判定逻辑设计得好不好,直接决定测试的可信度。我的原则是:首次失败必须记录完整现场,重复失败才判定为真故障。

原因很简单。单次失败可能是由电源毛刺、DMA 干扰、外部噪声耦合引起的,属于环境问题。但如果同一个地址在多次运行中反复失败,那就是单元本身的问题。实现上,测试程序应该维护一个失败地址的频次表,跑多轮之后统计。对于容量不大的存储器,可以用一块固定的保留内存来存这个表;容量大或者要跑很多轮,就得用哈希或者分段计数的方式压缩存储。

另一个误报来源是测试程序自身的内存需求。如果测试程序为了记录日志而动态分配内存,而这个分配动作本身又触发了存储器的某些问题,就会出现"测试程序自己把自己搞崩"的情况。所以内存测试程序的内存使用必须是静态的、预先确定的,不能动态分配。

还有一个容易忽略的点:测试的顺序会互相影响。如果一个不良单元在前一轮测试中影响到了它旁边的单元,可能导致后一轮测试出现连锁误报。解决办法是每轮测试开始前都做一次完整的初始化和复位,不依赖上一轮的结束状态。

6.4 我踩过的几个坑

最后分享几条具体的经验,都是实打实踩出来的。

第一条,不要在现场用相对地址做测试。早期我做过一个自检程序,用数组下标访问被测区域,编译器生成的代码依赖基址寄存器。结果基址寄存器被污染之后,测试写到了错误的位置,反而把好数据冲掉了。后来改成绝对地址访问,问题消失。

第二条,测试参数要能配置。不要硬编码测试算法和范围。我现在的做法是把算法类型、起始地址、长度、遍历次数、是否开启延迟都做成配置项,通过串口或者编译宏控制。调试一个偶发问题时,能动态切换算法和范围,效率差别是数量级的。

第三条,在常温下通过不代表没问题。前面那个电阻混料的案例就是典型。如果条件允许,内存相关的验证一定要做至少一轮高低温循环。高温逼漏电和时序余量,低温逼驱动能力和启动特性,两者不可偏废。

第四条,记录不该只记录错误。把测试的执行时间、访问次数、最终的状态字都记录下来,哪怕是正常的。这些数据在后期做故障率分析的时候价值极高。有一次客户投诉某批次"内存容易坏",我们调出历史测试日志,发现出问题的板子在出厂测试时访问耗时比正常板子长 12%——虽然当时全部通过,但这个异常早就露头了。有了完整日志,责任判定和根因定位都快了很多。

第五条,也是最想说的:把故障模型写在测试设计文档里。不要只写"做了内存自检",要写"覆盖 SAF、TF、CFin、CFid、AF,使用 March C-,复杂度 10N,预计耗时 X 毫秒,未覆盖 CFdyn 和 NPSF,原因是时间预算不足,风险等级中等"。这份文档在你离职之后,是接手的人唯一能依靠的东西。我在接手别人项目的时候,最怕看到的就是一句"内存测试通过"——它什么都说明了,也什么都没说明。

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

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

立即咨询