简介:这份《半导体测试数据格式(STDF)V4规范使用手册》面向半导体测试工程师、ATE系统开发者与测试数据分析人员,系统说明STDF第4版如何以统一记录结构存储和传输不同厂商测试设备产生的数据,帮助读者理解跨平台数据兼容、晶圆与零件测试信息组织以及参数结果表达等核心问题。包内为1个PDF文件,约550KB,篇幅紧凑,便于随查随用;内容从FAR、ATR、MIR、MRR等文件级记录,延伸到WIR、WRR、PIR、PRR等晶圆与零件记录,并覆盖PTR、MPR、FTR等测试结果记录,同时讨论记录头、数据类型、时间日期表示、可选字段与缺失值处理。手册还梳理STDF文件命名、记录顺序、引脚映射记录用法及V3与V4差异,对重测数据、审计追踪和并行测试支持也有交代。已有669人学习,适合需要核对规范字段、设计解析程序或排查数据兼容问题的读者作为案头参考。
1. 偏移四个字节就全盘崩:STDF 记录流的第一个反直觉设计
拿到 STDF V4 手册,多数人第一件事是搜一个解析库,结果第一次跑起来就明白了一个容易被忽略的细节:从第二条记录开始,读出来的 REC_TYP 全是明显不符合预期的值。原因常常只有一行:STDF 记录头里的 REC_LEN 只统计它自己之后的数据字节数,四个字节的头不计入在内。多算或少算这四个字节,后面每一条记录都会错位。
STDF(Standard Test Data Format,半导体测试数据格式)是行业给 ATE(自动测试设备)产出数据定的一份数据记录格式约定:不规定数据库怎么建,也不规定网络怎么传,只规定一条条记录长什么样。V4 在这套约定上补充了审计追踪记录与重测数据记录,把测试数据分析绕不开的追溯链条补完了。搞懂它的头结构、类型码和数据类型表示,比会调一个库重要得多。
2. 记录头与层级语义:一条 STDF 流怎么自己讲清结构
STDF 文件不是一段带自描述标签的文本,而是一串首尾相接的二进制记录。它之所以能被流式读取、能边读边写入文件、能直接交给数据库入库,全靠头部的固定结构和类型码的语义分层。这一章把“记录怎么被切出来”和“记录之间怎么组成层次”拆开讲。
2.1 REC_LEN 为什么不含自己这四字节
每条记录以四个字节开头,字段依次是 REC_LEN、REC_TYP、REC_SUB。REC_LEN 是该记录在头部之后的数据字节数,也就是记录体长度;REC_TYP 是记录大类;REC_SUB 是大类下的具体记录编号。三个字段的分工非常明确:长度负责“切到哪”,类型负责“这是什么”。
这个设计最直接的好处是读取逻辑可以极其简单:读取 4 个字节,解析出长度,跳到下一条,循环。解析器不需要先认识任何记录定义,就能把整个文件切成一条条独立的记录块。对于动辄几百 MB 的晶圆测试数据来说,这种“先切块、后解释”的两段式处理是能边读边处理的前提。
用 Python 写一个不依赖任何记录定义的切分器,逻辑如下:
import struct def iter_stdf_records(fp): """按记录粒度流式切分 STDF,不预设任何具体记录类型。""" while True: header = fp.read(4) if not header: return # 干净的结尾 if len(header) < 4: raise EOFError("记录头被截断") # REC_LEN 为 2 字节无符号,REC_TYP / REC_SUB 各 1 字节 rec_len, rec_typ, rec_sub = struct.unpack("<HBB", header) body = fp.read(rec_len) # 头部的 4 字节不重复计入 if len(body) != rec_len: raise EOFError( f"记录 {rec_typ:02X}/{rec_sub:02X} 声明 {rec_len} 字节,实读 {len(body)}" ) yield rec_typ, rec_sub, body<HBB里的<表示小端,H是 2 字节无符号整数,两个B是各 1 字节无符号整数。REC_LEN 是 2 字节,所以单条记录的记录体上限是 65535 字节;超出这个长度的数据只能拆成多条记录,这也是为什么 FTR 这类可能携带大量失败信息的记录在设计上要留出可扩展字段。struct.unpack只解头部,不解记录体,正是为了让切分和解释彻底解耦。
提示:文件尾读到 0 字节是正常结束,读到 1~3 字节才是文件损坏。这两种情况必须区分,否则一个不完整的落盘文件会被当成“解析成功”。
2.2 REC_TYP 与 REC_SUB 的两级编码,以及 200 这条分界线
REC_TYP 标识一组相关记录,REC_SUB 标识组内的具体记录。两者拼接起来才唯一确定一种记录类型。这种两级编号比单一编号多花一个字节,换来的是“按大类批量跳过”的能力:一个只想抽取参数测试结果的分析程序,看到 REC_TYP 不是 15 就可以整条跳过,连 REC_SUB 都不用细看。
V4 中已定义的记录大类如下:
| REC_TYP | 采集粒度 | 主要 REC_SUB 与记录 |
|---|---|---|
| 0 | 文件级 | 10 FAR(文件属性)、20 ATR(审计追踪) |
| 1 | 每个批次(lot) | 10 MIR、20 MRR、30 PCR、40 HBR、50 SBR、60 PMR、62 PGR、63 PLR、70 RDR、80 SDR |
| 2 | 每片晶圆 | 10 WIR、20 WRR、30 WCR |
| 5 | 每个器件(part) | 10 PIR、20 PRR |
| 10 | 每个测试项摘要 | 30 TSR |
| 15 | 每次测试执行 | 10 PTR、15 MPR、20 FTR |
| 20 | 程序段 | 10 BPS、20 EPS |
| 50 | 通用数据 | 10 GDR、30 DTR |
规范里有一条常被忽略的约定:所有小于 200 的 REC_TYP / REC_SUB 编码保留给标准未来使用,大于 200 的编码开放给厂商自定义。这条分界线是整个格式最重要的兼容性设计,意味着设备厂要增加私有的诊断信息或校准数据,不需要修改标准、不需要发起版本协商,只要占一个大于 200 的编号,老解析器看到不认识的类型会按 REC_LEN 直接跳过,文件依然可以完整走完。没有这条线,每一次私有扩展都会变成一次格式分裂。
2.3 per-lot 到 per-test 的隐式嵌套
STDF 没有显式的树状结构,也没在文件里写“下一条记录是上一条的子节点”。层次关系完全靠记录类型的语义和出现顺序表达:
- MIR 出现表示一个新批次开始,MRR 出现表示这个批次结束;
- MIR 与 MRR 之间,可以出现多组 PIR / PRR,每组包围一个器件;
- 每组 PIR / PRR 之间,是若干条 PTR、MPR、FTR,一条对应一次测试执行;
- PMR / PGR / PLR 描述引脚映射,PLR 的引脚列表被 PTR / FTR 按索引引用,避免每条测试记录都重复引脚名;
- 晶圆测试另有一层:WIR / WRR 包围一批 PIR / PRR,WCR 描述晶圆的坐标与 bin 定义。
这种“靠顺序表达嵌套”的做法在解析上有一个硬性要求:必须按顺序单遍处理,不能随机访问某条 PTR 然后反推它属于哪个器件。因为一条 PTR 里根本没有指回 PIR 的字段,上下文只能靠“当前正在处理的 PIR”这个状态来维护。写解析器时把这个状态机建对,比记住任何一条记录的字段顺序都关键。
V4 相对 V3 的增量也集中在这一层:ATR 记录文件的修改与传输历史,RDR 记录重测关系,SDR 描述并行测试的各个 site,TSR 给测试项做一次汇总。这些记录的共同动机是让“这条数据是怎么来的、可不可信”也能被记录下来,而不是只留下结果本身。
3. 数据类型码:C*n、B*n、N*1 为什么把位都拆开存储
记录体内部不再有分隔符,字段边界完全由数据类型码和字段顺序决定。一个 12 字节的字段到底是 12 个字符还是 3 个 4 字节整数,解析器只能靠事先知道字段定义。这就是 STDF 的“半自描述”特性:记录边界自描述,字段语义不自描述。理解这套类型码,等于理解了整个格式的编码基础。
3.1 五个基本类型族与 C 语言类型对照
规范给出的类型码刻意做得很短,两个字符就能表达“类别 + 宽度”。完整映射关系如下:
| 类型码 | 含义 | C 类型 |
|---|---|---|
| C*12 | 定长字符串,不足左对齐补空格 | char[12] |
| C*n | 变长字符串,首字节为后续字节数 | char[] |
| C*f | 变长字符串,长度存在记录内另一字段 | char[] |
| U*1 / U*2 / U*4 | 无符号整数 | unsigned char / short / long |
| I*1 / I*2 / I*4 | 有符号整数 | char / short / long |
| R*4 / R*8 | 浮点数 | float / double |
| B*6 | 定长 6 字节位编码数据 | char[6] |
| V*n | 首字节为类型码,后跟该类型的数据 | 变体 |
| B*n | 位编码,首字节为字节数 | char[] |
| D*n | 位编码,首两字节为位数 | char[] |
| N*1 | 半字节整数,低半字节在前 | 无对应 |
这套编码有一个共同取向:能用一个字节表达长度就不用两个,能用整数就不用浮点。测试数据里绝大多数字段是 bin 号、计数、坐标、索引,都是小整数,把它们统一塞进 4 字节浮点会浪费四分之三的空间。一份包含几十万器件的批次文件,这种节省是数量级的差别。
3.2 C*n、C*f、V*n:三种变长字符串各管一段
三种变长字符串并存,看起来是重复设计,实际是三种不同的长度来源:
- C*n:长度就在字符串自己前面,自包含,适合独立字段;
- C*f:长度在记录里另一个字段,适合“长度已经算过一次、不想再存一遍”的场景,比如一组名称共享一个总数;
- V*n:首字节是类型码,后面才是数据,适合“同一个字段可能存整数也可能存字符串”的通用容器,比如 GDR 里的自定义数据。
解析 C*n 时有一个语义陷阱:长度字节为 0 不代表空字符串,而代表这个字段的数据缺失。写代码时必须把这个区分做出来,否则下游会拿到一堆“看起来是空串、实际是无数据”的值。
def read_cn(buf, pos): """读取一个 C*n 字段,返回 (值, 新偏移)。""" n = buf[pos] # 首字节 = 后续字节数 pos += 1 if n == 0: return None, pos # 长度为 0 表示缺失,不是空串 raw = buf[pos:pos + n] return raw.rstrip(b"\x00 ").decode("ascii", "replace"), pos + nrstrip是为了处理部分写入方在变长字符串尾部补零或补空格的情况,解码失败时用replace兜底,保证一条脏数据不会让整个文件解析中断。pos的推进必须是1 + n,把长度字节本身算进去,漏算这一字节就是典型的偏移漂移。
3.3 B*n、D*n、N*1:位级打包的三套规则
这三种类型是 STDF 里最容易写错的部分,因为它们都以“位”为计数单位,但前缀的含义各不相同。
- B*n:第一个字节是后续的字节数,数据从第二字节的最低位开始排列;
- D*n:前两个字节是后续的位数(最大 65535),数据从第三字节的最低位开始,最后一个字节里未用到的高位必须置 0;
- N*1:半字节流,第一个数据项在低 4 位,第二个在高 4 位;若半字节总数为奇数,最后一个字节的高半字节为 0。
三者的共同点是“低位在前”(LSB first),也就是第一个数据项占的是字节的最低位,而不是最高位。这一点与大多数人的直觉相反,是位域解析失效的首要原因。按位取值的通用写法:
def read_bits(buf, nbits): """从 buf 中取 nbits 位,低位在前。""" out = 0 for i in range(nbits): bit = (buf[i // 8] >> (i % 8)) & 1 out |= bit << i return outbuf[i // 8]定位字节,>> (i % 8)定位字节内的位,out |= bit << i按同样的低位在前的顺序拼回整数。如果按高位在前写,读出来的 bin 号会变成完全不同的值,而且通常仍然落在合法范围内,不会报错——这是最难排查的一类错误。
注意:D*n 的计数单位是位,B*n 的计数单位是字节,两者只差一个前缀的宽度,但混用会让后续所有字段的偏移全部错位。
3.4 kxTYPE:数组长度由前面的字段决定
STDF 的数组写法是kxTYPE,其中k本身不是字面量,而是由记录中前面某个字段给出的元素个数。比如一个引脚列表字段可能写成kxU\*2,前面的k字段先给出引脚数量,后面才是这么多 2 字节无符号整数。
这意味着字段的解析顺序不能乱,也不能先跳到某字段再回头补。写解析器时的常规做法是按记录定义顺序线性读,每读一个字段就维护一个偏移游标,遇到kx前缀时从上下文里取出计数。把记录定义写成表驱动结构(字段名、类型、是否可选、缺失标记),解析器只负责按表执行,会让后面增加新记录类型的成本降到最低。
4. 可移植性与缺失数据:CPU_TYPE 和时间戳留下的两个容易忽略的问题
一份测试数据从设备产出,可能先落到本地工作站,再转存到中央数据库,最后被另一套分析软件读取。这三台机器完全可能是不同的处理器架构、不同的字节序、不同的时区。STDF 对这个问题给出的答案集中在一个字段和一套约定上,理解它们才能解释很多“数据看着对、值却不对”的现象。
4.1 CPU_TYPE:写的时候用原生表示,读的时候再转
规范明确规定:写文件时,系统使用自己的原生数据表示,把自己处理器的类型写进文件属性记录 FAR 的 CPU_TYPE 字段;读文件时,系统检查这个字段,如果发现与自己的表示不兼容,就在读取过程中逐条记录转换成本机表示。
这个方案有三层考量。第一,设备端不需要在测试过程中做任何转换,测试节拍不受影响,这对每小时要跑几千个器件的产线是硬指标。第二,本地数据分析读本地数据时零转换开销。第三,转换逻辑集中在读取路径上,对上层分析软件透明,软件看到的永远是本机原生表示。
代价是把“字节序正确”这个责任完全压在了读取端的实现上。写入端只要如实填写 CPU_TYPE 就算尽责,读取端一旦忽略这个字段,就会把 4 字节浮点解成完全不同的数值。这类错误不会抛异常,只会让参数测试的上下限判断悄悄失效,属于典型的难以察觉的问题。
4.2 为什么 FAR 必须是文件第一条记录
FAR 是 REC_TYP 0 / REC_SUB 10,规范要求它出现在文件最前面。原因很直接:不知道写入方的处理器类型,就无法正确解释后续任何一条含多字节数值的记录。这是整个“半自描述”体系中最薄弱的一环——自描述信息只写了一次,而且写在最前面。
这里有一个先有鸡还是先有蛋的细节:要读 FAR,得先知道 REC_LEN 的字节序;而 REC_LEN 的字节序又由 CPU_TYPE 决定。常见的实现思路是利用 FAR 头的结构特征做探测:REC_TYP 和 REC_SUB 都是单字节,不受字节序影响,FAR 的头一定是[?, ?, 0x00, 0x0A];而 REC_LEN 通常在几百字节以内,高字节为 0。于是看前两个字节里哪一个是 0,就能判断出小端还是大端。这个启发式判断成立的前提是 REC_LEN 小于 256,对 FAR 这种小记录总是成立的,所以足够安全。
def sniff_endian(fp): """用 FAR 的头两字节探测字节序,之后回退文件指针。""" pos = fp.tell() head = fp.read(4) fp.seek(pos) if len(head) < 4: raise EOFError("文件不足一条记录") # 小端:[len_lo, len_hi, 0x00, 0x0A],len_hi 通常为 0 # 大端:[len_hi, len_lo, 0x00, 0x0A],len_hi 同样为 0 return "<" if head[1] == 0 else ">"这段代码只处理“长度小于 256”的常见情形,遇到超长 FAR 会判断失败,所以实际实现里还会结合 CPU_TYPE 交叉校验,探测只用来决定第一次unpack的字节序。把探测结果和 FAR 里读到的 CPU_TYPE 对比,两者不一致就应当直接报错,而不是硬着头皮往下读。
4.3 32 位时间戳:本地时区这个坑
STDF 的日期时间字段是 4 字节无符号整数,表示从 1970 年 1 月 1 日零时起算的秒数,且按本地时区计算。规范里对 Setup time、Start time、Finish time 有各自定义,用来区分设备准备、测试开始和测试结束三个时刻。
“本地时区”这四个字是跨厂区比对数据时的主要麻烦。同一物理时刻,在不同时区的设备上写出来的数值不同,直接相减会得到错误的时间差。做跨站点分析前必须先把时间戳归一化到同一时区,或者至少记录下产出地的时区偏移。另一个隐患是宽度:4 字节无符号理论上能撑到 2106 年,但如果下游实现按有符号整数解释,2038 年之后的值会直接变成负数。这类问题在解析器里加一行注释、写一个单元测试,比事后排查要划算得多。
4.4 缺失数据的三种标记,和一张 OptionalData 位图
STDF 的可选字段有一个必须记住的前提:可选字段仍然必须存在于记录中,只是可以用特定值表示“数据缺失或无效”。规范给了三种标记方式:
- 变长字符串:长度字节置 0;
- 定长字符串:整个字段填空格;
- 定长二进制和数值字段:使用记录定义里的 OptionalData 位字段,某一位被置位表示该字段数据无效;或者在字段定义中约定 -1 作为缺失值。
第三种方式里,OptionalData 位图的语义是“位被置位 = 数据无效”,而不是“位被置位 = 数据有效”。这个反向约定在实现时极易写反,写反的结果是所有有效数据被丢弃、所有无效数据被当成真值,而且程序不会报错。数值类型的 -1 也有同样的陷阱:如果某个字段的合法取值范围本身包含 -1,就不能用 -1 表示缺失,必须改用位图,这也是规范为什么对一部分字段专门定义 OptionalData 的原因。
还有一条节省空间的规则:位于记录末尾的可选字段可以整个省略不写,前提是这个字段本身及其后所有字段都是缺失或无效。记录中间的可选字段绝对不能省略,因为省略会让后面的字段全部前移,偏移彻底错乱。读的时候要按“能读多少读多少、读不到就置缺失”的方式处理,而不是假设记录体长度恒定。
5. 避坑清单:解析 STDF 时最容易出错的五处
这五条都是从“文件能打开、程序不报错、结果就是不对”这一类现象里总结出来的。它们共同的特点是失败时没有明显异常提示,只会在下游以数据异常的形式暴露。
5.1 记录边界类:长度和单位
现象一:第一条记录正常,从第二条开始 REC_TYP 全是乱七八糟的值。
原因:REC_LEN 被当成了包含 4 字节头的总长度,导致每次读记录体时多读或少读了 4 字节。第一次读时因为头部刚好被对齐,FAR 还能正常解析出来,从第二条开始就整体偏移了。
解决:明确 REC_LEN 只算记录体。在切分器里加一个自检——把文件里所有 REC_LEN 累加,再加上 4 乘以记录条数,结果必须等于文件实际字节数。这个校验只需要几行代码,能在解析任何字段之前就把偏移错误拦下来。
现象二:解析到某条含位编码字段的记录时偏移错开几个字节,之后的记录全部错位。
原因:把 B*n 的首字节当成了位数,或者把 D*n 的前两字节当成了字节数。B 是按字节计数、D 是按位计数,两者只差一个前缀宽度,但读错之后游标推进量完全不同。
解决:给每种类型码写独立的读取函数,函数内部自己负责推进游标,调用方只拿返回值和新偏移,不允许在调用方手工计算长度。同时用“记录读完后游标必须正好等于记录体长度”作为断言,任何一条记录读完后长度对不上就立刻抛出,把错误定位到具体记录而不是整个文件。
现象三:某些记录读出来的字段值看着像合理的数字,但和实际测试结果对不上。
原因:kxTYPE数组的元素个数没从上下文字段里取,而是按固定值或者剩余长度反推。数组的实际长度由记录内前面的字段给出,取错之后后续所有字段的起点都会偏移,但因为偏移后读到的仍然是合法范围内的整数,程序不会报错。
解决:把每种记录的字段定义写成有序的表,解析时严格按表执行,kx的计数从表中已解析的字段里取。字段表同时也是文档,新接手的人看表就知道每条记录长什么样,不用去翻几百页手册。
5.2 数据解释类:字节序、缺失值与时区
现象四:同一份文件在设备自带的软件里看参数值正常,自己写的解析器读出来是天文数字或者 0。
原因:忽略了 FAR 里的 CPU_TYPE,按固定的字节序解析所有多字节数值。浮点字段尤其明显,字节序反了之后指数位和尾数位全部错位,得到的值往往是一个巨大或极小的数。
解决:解析前先读 FAR 拿 CPU_TYPE,建立“文件字节序 → 本机字节序”的转换规则,所有多字节字段读取时统一走一个带字节序参数的读取函数。不要在图省事的地方直接用struct.unpack的默认字节序。
现象五:某些测试项的结果显示为 0,但同一批次在别的工具里能看到实际值;或者跨厂区比对同一批数据时时间对不上。
原因有两个。一是可选字段的 OptionalData 位没检查,把“无效”的 0 当成了真实测量值;二是时间戳按本地时区存储,跨时区比对时没有归一化。
解决:读可选字段时先看位图再看值,位图被置位就直接返回缺失,不要走进数值分支。时间字段在读入时立刻转换成带时区信息的绝对时间,内部统一用 UTC 存储,只在展示层转回本地时间。这两条都属于“读入时多做一步、后续全流程省心”的类型,值得在解析层一次性做掉。
提示:把上述五条做成一组固定的回归用例——一段包含大端小端各一份、含可选字段缺失、含位编码字段的小样本文件,每次改动解析器都跑一遍。STDF 的错误大多不会当场报错,只能靠这种对照样本兜住。
6. 设计取舍:这套格式为什么宁可不够灵活也不自描述
回头看 STDF 的整套设计,会发现它在一个问题上反复做了同一个选择:把复杂度推给格式定义,把简单留给运行时。记录不做自描述,字段名不写进文件,每条记录只留三字段的头;类型码短到两个字符;能用整数就不用浮点;可选字段不写就表示缺失。这些决定单独看都像是为了省空间,合起来看其实是在为一个非常具体的场景服务:产线上每天产生海量数据,设备端不能因为写文件而拖慢节拍,分析端要能用最简单的循环把文件扫完。
和文本格式对比一下就很清楚:
| 维度 | 二进制记录流(STDF 这类) | 自描述文本格式 |
|---|---|---|
| 单条记录体积 | 紧凑,字段宽度固定 | 含字段名,通常大数倍 |
| 写入开销 | 低,可直接追加写 | 需序列化与格式化 |
| 读取前提 | 必须先知道字段定义 | 可边读边推断结构 |
| 扩展方式 | 预留编号段 + 尾部可选字段 | 增加新字段名 |
| 出错表现 | 静默错位,难以察觉 | 通常直接解析失败 |
这张表解释了 STDF 为什么至今仍在用:它的优势全部集中在写入侧和体积上,劣势全部集中在读取侧。而读取侧的成本是可以一次性摊掉的——写一次正确的解析器,之后所有文件都受益;写入侧的成本则是每天都要付的。
200 这条分界线是这套取舍里最聪明的部分。它承认了标准不可能覆盖所有厂商的所有需求,于是直接划出一块自留地:小于 200 的编号由标准维护,大于 200 的编号厂商随便用。新记录类型不需要走标准流程,老解析器不认识就按长度跳过,双方都不受损。V4 新增的 ATR、RDR、WCR、TSR、SDR 都是在这个框架内加进来的,没有破坏任何已有的读取逻辑。
代价也很真实。半自描述意味着格式定义和解析代码必须同步演进,任何一方落后都会产生静默错误;字节序靠一个字段集中声明,意味着读取端一旦忽略它就全盘皆输;本地时区的时间戳意味着跨地域分析必须先做归一化。这些都是当年为了极致紧凑做出的让步,后来者只能在解析层把它们一一补回来。
我自己的习惯是:拿到任何一份新的测试数据,先不碰字段,只做三件事——把所有 REC_LEN 加起来和文件大小对一遍,把 FAR 读出来确认字节序和 CPU_TYPE,再用几条已知结果的记录做对照。这三步走完,后面写字段解析基本不会再出大的意外。希望帮到你。
本文还有配套的精品资源,点击获取