☰
STDF V4解析避坑:记录头偏移、类型码与字节序
2026/11/3 14:05:06 网站建设 项目流程

简介:这份《半导体测试数据格式(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 + n

rstrip是为了处理部分写入方在变长字符串尾部补零或补空格的情况,解码失败时用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 out

buf[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 的可选字段有一个必须记住的前提:可选字段仍然必须存在于记录中,只是可以用特定值表示“数据缺失或无效”。规范给了三种标记方式:

  1. 变长字符串:长度字节置 0;
  2. 定长字符串:整个字段填空格;
  3. 定长二进制和数值字段:使用记录定义里的 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,再用几条已知结果的记录做对照。这三步走完,后面写字段解析基本不会再出大的意外。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询