简介:面向半导体测试工程师、ATE 系统开发者与测试数据分析人员,这份 PDF 手册系统整理了 STDF 第4版规范,重点解决不同厂商测试设备之间测试数据格式不统一、交换与追溯困难的问题。内容覆盖文件属性记录、主信息记录、最终结果记录等记录类型,并纳入审计追踪记录、重测数据记录、晶圆与零件测试信息、参数测试结果等模块,同时说明记录头结构、数据类型表示、可选字段与缺失值处理,以及 V3 到 V4 在记录类型、字段定义和并行测试支持方面的变化,便于读者理解规范全貌并用于数据解析、接口开发和兼容性核对。包内为 1 个 PDF 文件,约 550KB,轻量便于随查随用。已有 669 人学习,适合需要查阅 STDF 记录定义、规划数据字段映射或完善测试数据审计与重测流程的从业者作为案头规范参考。
1. STDF V4 手册到手:文件读不通时,先查规范再改代码
一个 .stdf 文件读出来 PRR 分箱号对不上,PTR 测试项又少了十几条,第一反应是改解析器,改到第三个小时才发现根因是文件用了 V4 新增的记录类型,而解析器按 V3 字段偏移硬编码。半导体测试数据分析里这种翻车太常见:STDF(Standard Test Data Format)是面向 ATE 的二进制数据记录格式,V4 在记录类型、可选字段判定和并行测试支持上都做了扩展。手里这份是 V4 规范 PDF,主体是记录结构、记录类型目录与字段定义三块。适合要把 STDF 写入数据库的数据工程师、给测试机写转换程序的上位机开发者,以及核对分箱与参数良率的测试工程师。下面按“结构 → 记录链 → 缺失值 → 排错 → 自校验”的顺序走一遍。
2. 四字节记录头与数据类型码:把顺序读文件的基础打牢
STDF 是纯二进制、无索引、只能顺序扫的格式。这决定了第一件事不是理解每条记录里有什么字段,而是怎么在文件里“走”。记录头加数据类型码就是这一层的全部内容,很多人急着写字段解析,跳过这一段,后面所有错误都是从这里开始积累的。
2.1 REC_LEN / REC_TYP / REC_SUB 的寻址与跳过逻辑
每条记录开头固定是 4 字节:REC_LEN 为 2 字节无符号整数,表示记录头之后的负载字节数,注意它不包含这 4 字节头本身;REC_TYP 为 1 字节,标识一组相关记录;REC_SUB 为 1 字节,标识组内具体记录。REC_TYP 与 REC_SUB 的组合唯一确定一种记录类型,这个设计让解析器可以按“族”做批处理,也方便按族快速过滤。
规范里 REC_TYP 小于 200 的编码由标准制定方保留,大于 200 的开放给自定义应用。这一条在真实文件里是必须处理的:遇到不认识的 (REC_TYP, REC_SUB),正确动作是按 REC_LEN 读出并跳过负载,同时记一条日志,而不是直接抛异常。一条带厂商私有扩展的记录,如果让它把整条数据处理管道打断,代价极大。
常用记录类型分组整理成表更直观,写解析器时可以直接当映射字典用:
| REC_TYP | 含义 | 组内 REC_SUB 与记录 |
|---|---|---|
| 0 | 文件信息 | FAR=10、ATR=20 |
| 1 | Lot 级数据 | MIR=10、MRR=20、PCR=30、HBR=40、SBR=50、PMR=60、PGR=62、PLR=63、RDR=70、SDR=80 |
| 2 | Wafer 级数据 | WIR=10、WRR=20、WCR=30 |
| 5 | Part 级数据 | PIR=10、PRR=20 |
| 10 | 按测试程序 | TSR=30 |
| 15 | 按测试执行 | PTR=10、MPR=15、FTR=20 |
| 20 | 程序段 | BPS=10、EPS=20 |
| 50 | 通用数据 | GDR=10、DTR=30 |
这张表还有一个容易被忽视的用途:如果你要做“只读某几类记录”的轻量扫描器,比如只抽 PRR 和 PTR 做良率统计,那么在读到表外的记录时直接按长度跳过,能省掉大量字段解析开销。
2.2 数据类型码的坑:定长串、变长串和位编码
STDF 的字段类型用类似C*12、R*4、U*2这样的码表示。粗看很直观,真正写代码时,容易翻车的正好是那些带*n、*f后缀的类型。
固定长度字符串C*12:不足部分必须左对齐、用空格补齐,读出来要rstrip掉空格,否则不同文件里同一个工序名长度不一样,数据库里会变成多个“名字”。变长字符串C*n首字节是跟随的无符号字节数,最大 255,长度字节为 0 就是“这个字段缺失”。C*f则是字符串长度存在别的字段里,解析时不能自己猜。
位编码有三类,很容易混:B*6是固定 6 字节位数组;B*n首字节是字节数,数据从第二字节的低位开始;D*n前两字节是位计数,最大 65535 位,未使用的高位补零。三者长度字段的单位一个是定长、一个是字节、一个是位,写通用读取函数时必须分开实现,不能复用同一个read_bytes。
还有V*n,首字节先给一个数据类型码,然后才是数据——这是一种“字段类型本身可变”的编码。N*1是半字节打包,低 4 位是第一个数,高 4 位是第二个数,读到奇数个半字节时高 4 位补零。kxTYPE是数组,元素个数k来自记录中更早的一个字段,解析顺序错了就会越界。
时间字段另外单独说:规范用 4 字节无符号整数存储自 1970 年 1 月 1 日午夜起、按本地时区调整过的秒数。它不是一个普通整数,做跨机房汇总时如果直接当 UTC 用,时间会整体偏离几个小时,这在按小时做良率趋势图时非常明显。
2.3 一个可运行的记录扫描骨架
把上面的规则落成一个最小扫描器,是所有后续解析的公共底座:
import struct from typing import Iterator # 只登记常用记录,未登记的类型按长度跳过,保证扫描不中断 REC_NAME = { (0, 10): "FAR", (0, 20): "ATR", (1, 10): "MIR", (1, 20): "MRR", (1, 30): "PCR", (1, 40): "HBR", (1, 50): "SBR", (1, 60): "PMR", (1, 62): "PGR", (1, 63): "PLR", (1, 70): "RDR", (1, 80): "SDR", (2, 10): "WIR", (2, 20): "WRR", (2, 30): "WCR", (5, 10): "PIR", (5, 20): "PRR", (10, 30): "TSR", (15, 10): "PTR", (15, 15): "MPR", (15, 20): "FTR", (20, 10): "BPS", (20, 20): "EPS", (50, 10): "GDR", (50, 30): "DTR", } def scan(path: str) -> Iterator[tuple]: with open(path, "rb") as f: offset = 0 while True: hdr = f.read(4) if not hdr: break # 正常到达文件尾 if len(hdr) < 4: raise ValueError(f"偏移 {offset} 处记录头不完整,文件可能被截断") # 小端读记录头:2 字节长度 + 1 字节类型 + 1 字节子类型 rec_len, rec_typ, rec_sub = struct.unpack("<HBB", hdr) payload = f.read(rec_len) if len(payload) < rec_len: raise ValueError( f"偏移 {offset} 处记录 ({rec_typ},{rec_sub}) 负载长度不足" ) name = REC_NAME.get((rec_typ, rec_sub), "UNKNOWN") yield name, rec_typ, rec_sub, payload offset += 4 + rec_len代码里三个位置值得说清楚。struct.unpack("<HBB", hdr)里的<决定了按小端解释,这是绝大多数现有文件的实际写法;如果 FAR 里的 CPU_TYPE 显示写入方属于异种字节序平台,就需要在读完 FAR 之后切换到对应的转换分支,而不是在这里一刀切。f.read(rec_len)直接读负载而不逐字段解析,是因为这一层只负责“走”,不负责“懂”。最后的offset累加是留给报错定位的——真实文件动辄几百兆,出错时没有字节偏移,排查基本靠猜。
提示:REC_LEN 不含 4 字节头,这是最容易多算或少算一位的地方。每写一个新解析器,先用一个已知记录的十六进制串对一遍长度,比事后 debug 便宜得多。
3. Lot / Wafer / Part 三级记录链:从 FAR 顺到 MRR
单条记录会解了,接下来是记录之间的关系。STDF 用一套文件级、Lot 级、Wafer 级、Part 级、测试级的层次来组织数据,顺序本身就是语义的一部分。打乱这个顺序,或者把“先出现 MIR 再出现 PTR”当成可选项,分析结果就会出现某些测点全部丢失却没有任何报错的情况。
3.1 文件级与 Lot 级记录的合法顺序
文件第一条记录按规范必须是 FAR,它承载文件属性,其中 CPU_TYPE 决定了后续所有多字节字段要不要做字节序转换,所以读任何字段之前都应该先把 FAR 解出来。ATR 是审计追踪,V4 新增,用于记录文件被谁改过、改了什么,做数据可追溯时很有用。
Lot 级以 MIR 开始,里面是批次号、测试程序名、测试机标识等;MIR 之后是规范里说的“初始序列”,常见排布是 PCR(零件计数)、HBR(硬件分箱定义)、SBR(软件分箱定义)、PMR(引脚图)、PGR(引脚组,可选)、PLR(引脚列表)、SDR(站点描述)。这一段的顺序不是随便排的:HBR 和 SBR 必须在任何 PRR 之前出现,因为 PRR 里的分箱号是拿这两张定义表去查的,先有结果再有定义就没法反查。
Lot 结束时写 MRR,里面是最终良率、总测试数、总通过数。写 MRR 之前必须有 PCR,否则计数对不上。做数据校验时,一个很实用的检查是:MRR 里的总数是否等于 PCR 累计数,以及是否等于 PRR 条数。这三个数不一致,基本可以确定文件在写入过程中被中断过,或者解析器漏读了中间某一类记录。
3.2 单个 Die 的记录链与初始序列
Wafer 测试场景下,先有 WIR(晶圆信息)与 WCR(晶圆配置),每个 Die 测试完写 WRR(晶圆结果),整片结束后汇总。Part 测试场景下,每个器件一对 PIR / PRR:PIR 标识“开始测第几个”,PRR 收结果,包括硬件分箱、软件分箱、重测次数、坐标等。
测试级记录夹在 PIR 和 PRR 之间,按测试程序顺序写:PTR 是参数测试,一条记录对应一个测试项;MPR 是多结果参数测试,一条记录里带一组结果;FTR 是功能测试。BPS 和 EPS 用来标记测试程序的段落,做分段良率分析时,它们能告诉你某个失败项属于哪一段。TSR 在 V4 里是测试概要,记录测试名称与序号映射,如果没有 TSR,PTR 里的 TEST_NUM 就只是一个编号,需要靠程序文件对照。
还有几个 V4 或 V3 后期新增、容易被旧解析器忽略的记录:RDR 存重测数据,同一个器件连续测多次时,没有它就只能看到最后一次结果;SDR 定义站点,并行测试下 SITE_NUM 的语义靠它确定;PMR/PGR/PLR 共同描述引脚映射,做失效分析时要把测试项映射回物理引脚,靠的就是这三类记录。
3.3 从 PTR / MPR 抽参数测试结果
从这一层开始就能写“可抄作业”的抽取脚本了。下面的代码不追求覆盖 PTR 的全字段,而是把分析最常用的几项拉出来:
def parse_ptr(payload: bytes): """按手册 PTR 一节的字段表顺序读取前置必需字段。 字段顺序与偏移以 PDF 中 ParametricTestRecord 的字段表为准, 不同版本若有字段增删,以手册为准同步调整。""" off = 0 def rd_u4(b, o): return int.from_bytes(b[o:o + 4], "little", signed=False), o + 4 def rd_u1(b, o): return b[o], o + 1 def rd_r4(b, o): return struct.unpack("<f", b[o:o + 4])[0], o + 4 def rd_cn(b, o): n = b[o] # 长度字节为 0 表示该字符串缺失 if n == 0: return "", o + 1 return b[o + 1:o + 1 + n].decode("ascii", "replace").rstrip(), o + 1 + n test_num, off = rd_u4(payload, off) head_num, off = rd_u1(payload, off) site_num, off = rd_u1(payload, off) test_flg, off = rd_u1(payload, off) # 位标记,含结果是否有效 parm_flg, off = rd_u1(payload, off) result, off = rd_r4(payload, off) test_txt, off = rd_cn(payload, off) # 测试项名称 return { "test_num": test_num, "head_num": head_num, "site_num": site_num, "test_flg": test_flg, "parm_flg": parm_flg, "result": result, "test_txt": test_txt, }几个参数要说明。test_flg不是普通数值,它是位域,其中若干位用来声明结果是否有效、是否告警,具体位定义要看手册该字段的说明,不能当布尔值直接用。head_num和site_num在做多工位并行数据分析时必须分开聚合,混在一起算良率会把不同工位的偏移抹平。test_txt用长度字节判定缺失,长度字节为 0 就表示这个测试项没有名称,此时应回退到test_num加 TSR 映射去取名,而不是塞空字符串。
如果一个文件里 PTR 条数远小于程序中的测试项数,先别怀疑解析器,去看看test_flg:规范允许把一批测试项合并成 MPR 写入,也可能因为采样策略没有逐项落 PTR。这两种情况在字段层面完全合法,只按 PTR 计数会漏。
4. 可选字段与缺失数据:-1、空串和尾部截断怎么判
做过一阵 STDF 解析的人都会遇到同一个问题:字段读出来了,但不知道它是“真的等于 0”还是“没有测”。规范对这件事有专门设计,理解不到位,良率统计里就会混进大量伪值。
4.1 Optional Data bit 与两种缺失值标记
规范规定可选字段必须“存在”,但可以用两种方式声明它无效。第一种是预定义值:变长字符串的长度字节置 0,固定长度字符串填满空格,时间日期字段用二进制 0,有符号数值用 -1。第二种是记录里带一个 Optional Data 位域,每一位对应一个可选字段,置位表示该字段数据无效,应忽略。
这两种方式不能混用。有的字段所有取值(包括 -1)都合法,那它只能用 Optional Data 位来标缺失;有的字段规范直接指定了 -1 或空串为缺失标记,那就不要去查位域。写代码时每读一个可选字段,先回手册确认它属于哪一类,这是最省事的做法。
有一个很隐蔽的坑:位域置位表示“无效”,而不是“有效”。有些实现按“置位表示有值”写,结果所有正常数据的字段被丢弃,而缺失数据反而被保留,表现出来的现象是“大部分测点值为空”,很容易被误判成文件损坏。
4.2 尾部省略的合法边界
规范允许省略记录末尾的可选字段以节省空间,但有严格条件:被省略的字段必须是缺失或无效的,并且它后面所有字段也必须是缺失或无效的可选字段。从记录中间省略任何一个可选字段都是非法的。
这条规则对解析器的直接影响是:不能假设负载长度一定覆盖到最后一个字段。常见做法是先按字段表算出“必需字段最小长度”,读负载时以len(payload)为上限逐字段推进,读不到就把该字段标记为缺失并停止,而不是抛下标越界。这样即使文件由不同版本的写入器生成,字段数略有差异,解析也能跑完。
4.3 解析器里的缺失值归一化
真实的 STDF 文件里,同一种缺失语义可能来自三种途径:字段被截断、字段值等于预定义缺失值、Optional Data 位被置位。如果不在入口处统一,后面每张报表都要写一遍三种判断。
比较稳的做法是在解析层产出一个带标记的值对象,或者用None统一表示缺失,绝不用 0 或空串代替。用 0 代替缺失是这类项目里最常见的血泪经验:平均值被拉低、良率被算高,而且从结果上完全看不出问题,因为 0 本身是一个看起来非常正常的值。
def normalize(value, opt_bit, truncated, predefined_missing): """缺失值归一:三种来源统一成 None,避免 0 和空串冒充缺失。""" if truncated: # 尾部省略导致字段未出现 return None if opt_bit: # Optional Data 位声明无效 return None if predefined_missing: # 规范预定义缺失值(空串、-1、时间 0 等) return None return value参数含义按顺序是:字段是否因尾部省略而未读到、对应可选位是否置位、值是否等于手册规定的预定义缺失标记。三个条件任一成立就返回None。这个函数看着简单,但把它放在读取字段的第一现场,比事后在业务代码里补判断可靠得多。
注意:时间日期字段的缺失标记是二进制 0,但 0 本身也对应 1970-01-01,如果直接转成时间戳再格式化,会得到一串看似合法的远早期时间。转换前先判缺失。
5. 避坑与排查:STDF 解析里最常见的五类翻车
这一节按“现象 → 原因 → 解决”记录我实际踩过或帮人查过的五类问题。它们覆盖了大部分“解析器能跑但结果不对”的场景,遇到对不上的数据,可以按这个顺序排查。
5.1 现象:整条记录字段整体错位一格
现象是 PRR 里的分箱号读出来像测试项编号,PTR 的site_num读出来像标志位。几乎每次都是字段表的读取起点错了。
原因是把 REC_LEN 当成了包含 4 字节头的总长度,或者在跳过记录时多减了 4 字节,导致下一条记录的起始位置整体偏移。也有一种是把C*n当成C*12读,多读了 11 字节的固定长度。
解决方式是给扫描器加一个偏移断言:在读到 MIR、PIR 这类结构稳定的记录时,检查负载首字段是否符合预期取值范围(比如 TEST_NUM 一般不会大到异常)。更彻底的方案是用一个已知正确的文件建一条十六进制转储,对着字段表手工走一遍,把每个字段的偏移写进单元测试。
5.2 现象:浮点数读出天文数字
现象是R*4字段解出来是接近 10 的几十次方,或者一个极小的次正规数。
原因是字节序判断错了。STDF 允许写入方用本机表示,由 FAR 的 CPU_TYPE 声明,读方需要据此决定是否转换。很多解析器默认小端,遇到由异种字节序平台写出的文件就直接错。
解决办法是在打开文件后立刻解 FAR 的 CPU_TYPE,把它作为全局开关。如果 CPU_TYPE 指示的表示与运行机器不一致,对所有U*、I*、R*字段套一层转换。注意B*、C*这类字节序列字段不受影响,不要一起转。
5.3 现象:PTR 数量对不上测试项
现象是按 PTR 条数统计的测试项数,明显少于测试程序里列出的项目数,而且缺的往往是同一批。
原因有两个。一是这些测试项被合并成 MPR 写入,一条 MPR 带多个结果,只数 PTR 自然不够。二是写入方启用了采样,只记录部分测试项,这在字段层面完全合法。
解决办法是统计时把 PTR 与 MPR 一起计入,并对 MPR 里的结果数组按元素展开。同时用 TSR 建立测试项名称与编号的映射,在没有 PTR 的编号上做一致性检查,确认是采样还是真的丢失。
5.4 现象:读到某个记录后抛异常
现象是文件前面几十万条记录都正常,到某个位置开始报长度不足或解析越界。
原因通常是文件本身被截断,比如传输中断、磁盘写满,也可能是遇到了不认识的记录类型后没有按长度跳过,而是继续按已知类型解析负载。前者是数据问题,后者是解析器问题。
解决办法是两层:扫描层对每条记录先校验len(payload) == rec_len,不等就记录偏移并停止,把已解析的部分先落盘;解析层对不认识的(REC_TYP, REC_SUB)一律跳过。两条都加上之后,一个残缺文件至少能拿到残缺之前的数据,而不是整批报废。
5.5 现象:同一个记录两种解析器结果不同
现象是用两套工具打开同一个文件,test_txt一个有值一个是空,或者可选字段一边有值一边没有。
原因多半出在缺失值判定上:一边用长度字节判空,一边用 Optional Data 位判断,还有一种是把截断当成了空值,但没有区分“字段不存在”和“字段存在但无效”。
解决办法是先统一语义再比结果。用一份字段表把每个可选字段归到“预定义值类”或“位域类”中的某一类,两边都按同一套规则归一,再比对。这一步做完之后,差异通常只剩时间字段的时区处理,而这也能通过固定时区转换消掉。
6. 拿规范当测试用例:给解析器补上自校验
解析器能跑通只是及格线,能自己证明“我读对了”才算能用。PDF 里那些定义性条款,其实每一条都可以翻译成一个断言,下面是我现在固定会做的一轮自校验。
6.1 用 V3 / V4 差异做静默降级检测
V4 相对 V3 在记录类型、数据类型、文件名字符集、必需记录以及部分具体记录的字段上都做了调整。这意味着同一个解析器如果只按 V3 实现,碰到 V4 文件不一定会报错,而是安静地少读几类记录。
一个很实用的自检是:读文件头部的版本线索与记录集合,如果出现 V4 才有的记录类型(比如 ATR、RDR、SDR),而解析器里对应的处理器是空的,就打印一条显式警告,而不是默默跳过。文件名一侧也有可校验项:规范对文件名字符集有要求,遇到超出字符集的路径名,先怀疑文件被非规范工具重命名过。
6.2 端到端校验清单与一条字段级断言
我一般会跑这么一组检查,任何一条不过就不往下游送数据:
| 检查项 | 判定方式 | 不通过的典型含义 |
|---|---|---|
| 首记录类型 | 第一条必须是 FAR | 文件不是 STDF 或头部损坏 |
| 记录长度守恒 | 所有len(payload) == rec_len | 文件被截断 |
| Lot 级顺序 | MIR 在 MRR 之前,HBR/SBR 在首个 PRR 之前 | 写入器实现不规范或解析漏读 |
| 计数一致性 | MRR 总数 = PCR 累计 = PRR 条数 | 写入中断或漏读记录 |
| 引脚映射完备 | 出现 PMR 时 PLR 不应缺失 | V3 遗留文件或解析器只实现一半 |
字段级再补一条最能兜底的断言:对每类记录,解析完必需字段后,检查已消费的偏移量是否不超过REC_LEN,若恰好等于则说明无尾部可选字段,若小于则把剩余字节数记下来,与字段表中可选字段的最小长度之和做比较,差值为负就说明字段表用错了版本。这个断言在我这里抓到过不止一次“字段表照抄旧版”的问题。
def check_record(rec_len: int, consumed: int, min_optional: int) -> None: """字段级兜底:已消费偏移 + 剩余可选字段最小长度 不应超过 REC_LEN。""" remain = rec_len - consumed if remain < 0: raise ValueError("必需字段读超记录长度,字段表版本可能不匹配") if remain and remain < min_optional: raise ValueError(f"剩余 {remain} 字节不足以容纳可选字段,疑似字段表错版")min_optional从当前记录类型的字段表里事先算出来,一旦在实际文件上稳定通过,就可以把它固化成回归测试用例,后续换版本时先跑这一组。
把那以后我每次接手新的 STDF 数据源,都强制先跑一遍“首记录 + 长度守恒 + 计数一致”三件套再谈业务逻辑,因为它能在几分钟内把“文件有问题”和“代码有问题”分开,省下的时间远比写这三个检查多。希望帮到你。
本文还有配套的精品资源,点击获取