简介:这份PDF文档是半导体测试数据格式STDF第4版的官方规范使用手册,面向半导体测试工程师、ATE系统开发者与测试数据分析人员,用于解决不同厂商测试设备间测试结果数据不兼容、难以统一解析与追溯的问题。文档系统梳理了STDF的记录结构、记录头、数据类型编码及时间日期表示方法,并逐一说明FAR、ATR、MIR、MRR、PCR、HBR、SBR、PMR、PGR、PLR、RDR、SDR、WIR、WRR、WCR、PIR、PRR、TSR、PTR、MPR、FTR、BPS、EPS、GDR、DTR等记录类型的字段定义与使用场景,还专门对比了V3与V4在记录类型、数据类型、文件名规则及必选记录上的差异,并给出文件排序、修复信息存储与引脚映射记录的使用建议。资源包为1个PDF文件,约550KB,结构清晰、便于检索,适合作为案头规范随时查阅。目前已有667人学习下载,可帮助读者快速掌握STDF V4的数据组织方式,为测试数据解析、格式转换与工具开发提供权威依据。
1. 半导体测试数据格式(STDF)V4规范使用手册到底解决什么问题
你拿到一份 CP 或 FT 的测试 log,几百 MB 甚至几个 GB,里面每一颗 die 的每一项测试、每一次 bin 判定、每一处坐标都躺在里面。产线催着要良率报告,客户催着要 fail bin 的原始波形,而你的第一反应往往是——这堆二进制到底怎么读。STDF(Standard Test Data Format)V4 就是干这个的:它把测试机台吐出来的原始数据,按一套公开的二进制记录结构组织起来,让不同机台、不同平台的数据能被同一套解析器读懂。这份规范使用手册的价值不在于教你写代码,而在于让你在动手解析之前,先搞清楚每条记录的类型、字段顺序、字节对齐和可选性。适合谁?适合每天跟 ATE 数据打交道、需要自己写解析脚本或排查数据异常的一线工程师。新手照着手册能搭出最小解析器,熟手能拿它当字段字典查边界条件。
2. STDF V4 的记录结构与字节级解析逻辑
STDF 文件本质上是一串连续的记录(Record),没有文件头,没有索引,从头读到尾。每条记录由 4 字节的 REC_LEN、1 字节的 REC_TYP、1 字节的 REC_SUB 和若干字节的数据体组成。REC_LEN 表示数据体的字节数,不含自身这 4 字节。这个设计意味着你只要按顺序读,永远不会迷路——但前提是你得知道每种 REC_TYP/REC_SUB 组合对应的数据体结构。
2.1 记录头与 REC_TYP/REC_SUB 的映射关系
先看记录头的解析。下面这段 Python 代码读一条记录的头,并返回类型、子类型和数据体长度:
import struct def read_record_header(f): """读取 STDF 记录头,返回 (rec_typ, rec_sub, rec_len)""" header = f.read(4) if len(header) < 4: return None # 文件结束 rec_len = struct.unpack('<I', header)[0] # 小端 4 字节无符号整数 typ_sub = f.read(2) rec_typ = typ_sub[0] rec_sub = typ_sub[1] return rec_typ, rec_sub, rec_len逻辑说明:STDF 默认小端字节序,REC_LEN 用<I解析。读到头之后,数据体长度就是 rec_len,你可以据此决定读多少字节。参数说明:f是已打开的二进制文件对象,必须用'rb'模式打开,否则换行符会被转换,导致字节错位。
常见的 REC_TYP 有:0 表示 File Attributes,1 表示 Master Information,2 表示 General Information,5 表示 Test Synopsis,10 表示 Parametric Test,15 表示 Test Results。REC_SUB 在同一 TYP 下区分具体记录。比如 TYP=10、SUB=0 是 PTR(Parametric Test Record),TYP=15、SUB=10 是 PIR(Part Information Record)。手册里有一张完整的映射表,建议打印出来贴在显示器边上。
2.2 用 Python 解析 PTR 记录的最小实现
PTR 是最常见的记录之一,每颗 die 的每个参数测试都会产生一条。它的数据体字段顺序是固定的:TEST_NUM(4 字节无符号)、HEAD_NUM(1 字节)、SITE_NUM(1 字节)、TEST_FLG(1 字节)、PARM_FLG(1 字节)、RESULT(4 字节浮点)、TEST_TXT(变长字符串)、ALARM_ID(变长字符串)等。变长字符串的编码方式是:先读 1 字节长度 N,再读 N 字节内容。
def parse_ptr(data): """解析 PTR 记录数据体,返回字典""" offset = 0 test_num = struct.unpack_from('<I', data, offset)[0]; offset += 4 head_num = data[offset]; offset += 1 site_num = data[offset]; offset += 1 test_flg = data[offset]; offset += 1 parm_flg = data[offset]; offset += 1 result = struct.unpack_from('<f', data, offset)[0]; offset += 4 # 变长字符串:先读长度字节,再读内容 test_txt_len = data[offset]; offset += 1 test_txt = data[offset:offset+test_txt_len].decode('ascii', errors='replace') offset += test_txt_len 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 }逻辑说明:struct.unpack_from从指定偏移读取,避免反复切片。变长字符串的长度字节是 1 字节无符号整数,最大 255。如果字符串实际长度超过 255,STDF 会用特殊编码,但 V4 里测试名一般不会超。参数说明:test_flg的 bit 0 表示测试是否通过,bit 1 表示结果是否有效,bit 2 表示结果是否被缩放。这些标志位在手册第 4 章有详细定义,解析时不能忽略。
2.3 处理变长字符串与可选字段的边界
STDF 里很多记录都有可选字段,比如 PTR 的 OPT_FLAG 和 RES_SCAL。如果 OPT_FLAG 的 bit 0 为 1,说明后面还有可选字段;否则数据体到此结束。这意味着你不能按固定长度读,必须动态判断。常见做法是:先读必选字段,再检查 OPT_FLAG,如果有可选字段再继续读。
def parse_ptr_with_optional(data): """带可选字段的 PTR 解析""" offset = 0 test_num = struct.unpack_from('<I', data, offset)[0]; offset += 4 head_num = data[offset]; offset += 1 site_num = data[offset]; offset += 1 test_flg = data[offset]; offset += 1 parm_flg = data[offset]; offset += 1 result = struct.unpack_from('<f', data, offset)[0]; offset += 4 test_txt_len = data[offset]; offset += 1 test_txt = data[offset:offset+test_txt_len].decode('ascii', errors='replace') offset += test_txt_len alarm_len = data[offset]; offset += 1 alarm_id = data[offset:offset+alarm_len].decode('ascii', errors='replace') offset += alarm_len opt_flag = data[offset]; offset += 1 res_scal = None if opt_flag & 0x01: res_scal = struct.unpack_from('<b', data, offset)[0]; offset += 1 return {'test_num': test_num, 'result': result, 'test_txt': test_txt, 'alarm_id': alarm_id, 'res_scal': res_scal}逻辑说明:opt_flag & 0x01判断 bit 0 是否置位。res_scal是有符号字节,表示结果的小数点缩放因子。比如 res_scal=-3,实际值要乘以 10^-3。参数说明:如果 opt_flag 还有其他 bit 置位,后面可能还有更多可选字段,手册里按 bit 顺序排列,必须逐个判断。
3. 从零搭建一个可用的 STDF 解析器
光会读单条记录不够,实际场景里你需要把整个文件跑一遍,把 PTR、PIR、PRR、TSR 等记录关联起来,才能还原出一颗 die 的完整测试结果。这一章讲怎么组织解析流程,以及怎么处理大文件。
3.1 文件级遍历与记录分发框架
核心思路是:一个主循环不断读记录头,根据 REC_TYP/REC_SUB 分发到对应的解析函数,把结果存到一个上下文对象里。PIR 记录标志着一颗新 die 的开始,PRR 记录标志着一颗 die 的结束,中间的 PTR 都属于这颗 die。
def parse_stdf(filepath): """遍历 STDF 文件,返回 die 列表""" dies = [] current_die = None with open(filepath, 'rb') as f: while True: hdr = read_record_header(f) if hdr is None: break rec_typ, rec_sub, rec_len = hdr data = f.read(rec_len) if rec_typ == 5 and rec_sub == 10: # PIR current_die = {'site': data[0], 'tests': []} elif rec_typ == 10 and rec_sub == 0: # PTR ptr = parse_ptr_with_optional(data) if current_die is not None: current_die['tests'].append(ptr) elif rec_typ == 5 and rec_sub == 20: # PRR if current_die is not None: current_die['bin'] = data[1] # 简化示例 dies.append(current_die) current_die = None return dies逻辑说明:PIR 的 REC_SUB 是 10,PRR 是 20,PTR 是 TYP=10、SUB=0。这个分发逻辑覆盖了最常见的三类记录。参数说明:data[0]取 PIR 的 HEAD_NUM,data[1]取 PRR 的 PART_FLG 或 BIN,具体偏移要看手册。实际项目中建议用字典映射(rec_typ, rec_sub)到处理函数,避免一长串 if-elif。
3.2 大文件流式处理与内存控制
一个 2 GB 的 STDF 文件,如果一次性读进内存,Python 进程可能直接 OOM。正确做法是流式读取,每次只保留当前 die 的数据,处理完就写出去或聚合。下面是一个带生成器的版本:
def iter_dies(filepath): """生成器方式逐颗 die 产出,避免全量加载""" with open(filepath, 'rb') as f: current_die = None while True: hdr = read_record_header(f) if hdr is None: break rec_typ, rec_sub, rec_len = hdr data = f.read(rec_len) if rec_typ == 5 and rec_sub == 10: current_die = {'site': data[0], 'tests': []} elif rec_typ == 10 and rec_sub == 0 and current_die is not None: current_die['tests'].append(parse_ptr_with_optional(data)) elif rec_typ == 5 and rec_sub == 20 and current_die is not None: current_die['bin'] = data[1] yield current_die current_die = None逻辑说明:yield让调用方逐颗处理,内存里最多只有一颗 die 的数据。参数说明:如果文件里有多个测试头(HEAD_NUM 不同),需要在 current_die 里按 head 分开存储,否则会串数据。手册里对多 site、多 head 的并发记录有明确说明,解析时不能假设只有一颗 die 在跑。
3.3 把解析结果落成 CSV 或 Parquet
解析完总要落地。CSV 适合小批量人工看,Parquet 适合大批量分析。下面是把 die 列表写成 CSV 的示例:
import csv def export_csv(dies, outpath): """把 die 列表导出为 CSV,每行一个测试项""" with open(outpath, 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['site', 'bin', 'test_num', 'test_txt', 'result']) for die in dies: for t in die['tests']: writer.writerow([die['site'], die.get('bin', ''), t['test_num'], t['test_txt'], t['result']])逻辑说明:每颗 die 的每个测试项占一行,方便后续用 pandas 做 pivot 或 groupby。参数说明:如果测试项很多,CSV 会很大,建议按 site 或 bin 分文件写。Parquet 可以用 pyarrow 直接写,列式存储对良率分析更友好。
4. STDF V4 解析避坑与常见问题排查
这一章是我自己踩过的坑,按「现象 → 原因 → 解决」写,每条都对应真实场景。
4.1 读出来的浮点数全是乱码
现象:RESULT 字段解析出来是 1.4e-45 或者 NaN。原因:字节序搞反了。STDF 默认小端,但有些机台配置成大端,或者文件被工具转换过。解决:先读一条已知记录,用两种字节序各试一次,看哪个结果合理。也可以在文件开头找 FAR 记录里的 CPU_TYPE 字段,手册里有说明。
4.2 变长字符串长度字节读成了 0
现象:TEST_TXT 解析出来是空字符串,但明明有测试名。原因:长度字节的偏移算错了,或者把可选字段当成了必选字段。解决:对照手册的字段顺序表,逐字段核对偏移。建议写一个 dump 函数,把每条记录的原始字节以十六进制打印出来,肉眼比对。
4.3 大文件解析到一半内存爆了
现象:进程被 OOM Killer 杀掉,或者 Python 报 MemoryError。原因:把整个文件读进内存,或者把全部 die 存在列表里。解决:用生成器逐颗处理,或者用mmap映射文件,按需读取。如果必须全量分析,考虑先落 Parquet 再读。
4.4 多 site 数据串到一起
现象:同一颗 die 的测试结果里混入了其他 site 的数据。原因:PIR 记录里的 SITE_NUM 没有正确关联到 PTR 的 SITE_NUM。解决:在 current_die 里按 site 建子字典,PTR 解析时根据 SITE_NUM 归位。手册里对 SITE_NUM 的语义有明确定义,不能想当然。
4.5 可选字段导致后续记录全部错位
现象:解析完一条 PTR 后,下一条记录头读出来是垃圾。原因:OPT_FLAG 判断漏了,少读或多读了字节。解决:在解析函数里严格按手册的 bit 定义判断,每读一个字段就更新 offset,最后校验 offset 是否等于 rec_len。如果不等,说明字段顺序或长度有问题。
5. 用 STDF 做良率分析的进阶技巧
解析只是第一步,真正有价值的是从 STDF 里挖出良率问题。我一般会先做三件事:按 bin 聚合、按 site 对比、按测试项找异常分布。下面是一个用 pandas 做 bin 汇总的示例:
import pandas as pd def bin_summary(dies): """按 bin 统计 die 数量和占比""" df = pd.DataFrame([{'site': d['site'], 'bin': d.get('bin')} for d in dies]) summary = df.groupby('bin').size().reset_index(name='count') summary['ratio'] = summary['count'] / summary['count'].sum() return summary.sort_values('count', ascending=False)逻辑说明:先构造 DataFrame,再 groupby bin 计数。参数说明:如果 bin 字段是多个(HBIN 和 SBIN),需要分别统计。手册里 PRR 记录有 HBIN 和 SBIN 两个字段,HBIN 是硬件 bin,SBIN 是软件 bin,分析时别搞混。
另一个技巧是找测试项的异常值。比如某个参数的 RESULT 分布突然偏移,可能是机台校准问题。可以用箱线图或者 3-sigma 规则筛。我习惯先算每个测试项的均值和标准差,再标记超出 3 倍标准差的 die。
def find_outliers(dies, test_num, sigma=3): """找出指定测试项的离群 die""" values = [] for d in dies: for t in d['tests']: if t['test_num'] == test_num: values.append((d['site'], t['result'])) if not values: return [] arr = [v[1] for v in values] mean = sum(arr) / len(arr) std = (sum((x - mean) ** 2 for x in arr) / len(arr)) ** 0.5 return [(site, val) for site, val in values if abs(val - mean) > sigma * std]逻辑说明:先收集指定 test_num 的所有结果,再算均值和标准差,最后筛出超出阈值的 die。参数说明:sigma 默认 3,可以根据实际分布调整。如果数据本身不是正态分布,建议改用中位数和 MAD。
最后一个技巧是时间序列分析。STDF 里没有时间戳,但你可以按 die 的顺序号当时间轴,看某个测试项是否随时间漂移。如果发现漂移,可能是机台预热不够或者探针磨损。这个在 CP 阶段特别有用。
我自己的习惯是:每次拿到新机台的 STDF,先跑一遍解析,确认记录类型和字段偏移都对,再跑 bin 汇总和离群分析。如果解析阶段就出错,后面全是白费。希望帮到你。
本文还有配套的精品资源,点击获取