简介:面向ATE自动测试设备开发场景的完整工程源码包,适用于半导体测试工程师和自动化测试开发者搭建标准化、多工位并行的测试程序;框架以STDF标准数据格式为核心,支持类似传统测试机的用例开发模式,可整合多种仪器设备并自动生成标准测试总结数据。压缩包共68个文件、约12.91MB,包含30个Python脚本、20张PNG图形文件、3个INI配置、2个CSV数据文件,以及UI界面、开发记录文档与开源许可;源码中提供了虚拟ATE、示波器、单片机、四象限DPS电源、简单开关芯片等模拟设备,并附PPMU、IO操作演示。已有423人学习下载,借助项目内的pytest测试用例、示例CSV数据、界面文件与开发记录,读者可快速理解STDF数据结构、BenchCore核心交互流程和多工位测试程序组织方式,直接复制扩展用于自建ATE测试框架。
1. 基于STDF架构的现代标准化ATE测试框架设计源码:先讲清楚它在解决什么问题
做ATE测试设备数据这块的工程师,几乎天天都要跟STDF打交道——量产线上的良率系统、MES和SPC,默认认的就是这种二进制的标准测试数据格式。所谓「基于STDF架构的现代标准化ATE测试框架设计源码」,落点其实是一件事:把测试机吐出来的记录流,收口成一套可维护、可扩展、能对接产线的标准化框架,而不是让每个项目各写各的解析脚本。这个方向适合测试程序开发、量产数据工程师,以及想统一测试数据管道的软件工程师。它要解决的核心问题,是让PIR/PTR/PRR这类记录在框架里有单一出口、统一编解码和校验。下面按实战顺序拆:先看STDF格式本身,再定架构和源码目录,然后写一个最小可跑的Codec,最后讲产线避坑和验证方法。
2. STDF的数据骨架拆开看:记录头、MIR/PTR/PRR与「为什么不用自造格式」
STDF(Standard Test Data Format)不是一种「文件格式」那么简单,它本质是一条接一条的二进制记录流。测试机每完成一个动作,就往流里追加一条记录;分析工具只要顺着流往下读,就能完整还原一颗芯片从进站、测参数、到最终bin分类的全过程。这套格式由设备厂商定义后成了行业事实标准,后来几乎成了所有ATE设备数据交换的默认语言。要设计一个现代的ATE测试框架,第一步不是写代码,而是先把这条记录流的骨架吃透。
2.1 4字节记录头:STDF最该先锁定的二进制约定
每条STDF记录以固定4字节头开头:前2字节是record length,表示后面body的字节数,这个长度不包含4字节头本身;第3字节是Record Type,第4字节是Record Subtype。整份文件就是一系列record顺序堆叠,多字节数值统一按小端(Little Endian)存放。读取时先拿4字节头,按长度读body,再读下一条,如此反复。
这4字节头是整个框架最先要锁死的地方。我的习惯是,在框架里单独拆一个codec.py,只做「4字节头拆分 + body截取」这一件事,不掺任何业务逻辑。因为所有基于STDF的上层功能——良率统计、bin分布、测试项回溯——都建立在能稳定读出每一条record的前提上。如果头解析做不干净,后面全白搭。
还需要留意一个约定:标准STDF文件通常以FAR(File Attributes Record)开头,FAR里携带格式版本和生成平台信息。开发解析器时,先读FAR做一次「格式体检」是个好习惯,因为不同测试机厂商导出STDF时,个别字段的填法真的会不一样,提前确认总比中途翻车强。
2.2 MIR/PTR/PRR的分工:一个part从进站到bin结果的数据流
在STDF的记录类型里,有几条是高频出现的,设计框架时基本绕不开:
- MIR(Master Information Record):记录测试程序名、测试时间、测试机类型、Handler/Prober信息,相当于整份文件的「header元信息」。
- PIR(Part Information Record):一颗待测芯片开始测试的标记,相当于「进站打卡」。
- PTR(Parametric Test Record):存放参数测试项的结果,包括测试项号、测量值、上下限、单位、测试文本等,是最细粒度的数据。
- PRR(Part Result Record):一颗芯片测试结束后的最终结论,包括hard bin、soft bin、pass/fail标志、测试耗时,相当于「出站小结」。
一次完整的测试闭环是:PIR进站 → 若干个PTR记录参数结果 → PRR出站。产线上多site并行时,多个part的PIR/PTR/PRR会交错出现在同一条记录流里,靠site信息来区分。框架设计时,不要只盯着PTR里的数值,MIR和PRR同样要建模——没有bin结论的STDF,良率系统根本没法消费。
2.3 与pytest这类软件测试框架的分界:ATE框架的第一职责是数据规范
不少同事问过我:能不能直接用pytest来组织ATE测试项?能,但得先分清楚边界。pytest解决的是用例调度与断言——怎么定义测试步骤、怎么收集失败、怎么生成报告;而基于STDF的ATE框架解决的是数据规范与产线对接——测试机吐出的测量结果,怎么变成MES/良率系统认的STDF记录流。
真正落地的做法是两层叠着用:上层用pytest管用例组织和执行逻辑,下层把每次测量结果交给STDF Codec层,统一编码成PIR/PTR/PRR记录。如果把两层混在一起,写进一个巨型类里,短期爽,换测试机或换产线系统时就要重写。现代标准化的含义就在这:格式层认STDF,语义层收口成统一Record对象,流程层给产线一个单一出口。
3. 现代标准化ATE测试框架的架构分层与源码目录设计
标题里的「架构」两个字,落到底层设计上就是分层。ATE测试框架和普通软件框架最大的区别是它贴着硬件走:数据源是一台真实的测试机,数据出口是产线的良率系统。中间任何一层没有捋清楚,都会出现「解析结果对不上」「换台测试机就挂」这类问题。
3.1 从测试机事件到STDF文件:四层架构的分界
我一般会把框架切成四层,层与层之间只通过接口通信,不允许跨层调用:
第一层是设备适配层,负责对接测试机/Handler/Prober的数据出口,可能是监听某个目录下的datalog文件,也可能是从socket或共享内存里收流。这一层只做一件事:把外部数据源变成统一Record事件,不管对面是泰瑞达还是别的机台,进了框架后都长一个样子。第二层是执行调度层,负责测试项编排和limit判断,这一层不是每个项目都需要,但如果你想把pytest接进来,它是接入点。第三层是STDF Codec与数据模型层,负责record的编解码、字段映射、校验,是这套框架的心脏。第四层是归档输出层,负责分卷、压缩、上传MES。
分层的理由是换测试机时只换设备适配层,Codec和数据模型保持稳定。产线上的现实是:同一个工厂里往往有多种品牌的测试机,如果每台机器都单独写一套解析逻辑,数据口径一定会乱。四层分界后,所有机台的数据都进同一个模型,后续良率分析才可比。
3.2 统一Record对象:把各厂商记录流收口成单一事件
跨设备之后,第二个要统一的是「记录」的抽象。我的习惯是定义一个最小Record对象,只包含rec_type、rec_subtype、body三个字段;body在初始阶段先保留原始bytes,由注册的Handler按需解析。这样设计的好处是:遇到厂商自定义记录时,框架不会因为不认识就崩溃,而是先当作opaque数据原样保留,等后续需要时再补解析逻辑。
「不认识也要保住」这一点非常重要。产线解析STDF时,一条待测芯片的数据流里偶尔会出现测试机厂商扩展的自定义记录(例如DTR/GDR类记录),如果解析器遇到不认识的就抛异常,整个批次的回流都会中断。统一Record对象 + 兜底opaque保存,是标准化框架和一次性脚本之间最明显的分水岭:前者对未知记录有容错,后者只能处理「写脚本时见过」的记录。
3.3 源码目录怎么摆:模块职责与依赖方向
源码结构我倾向按层建包,而不是按「解析、写入、统计」这样按动作建包。按层建包,依赖方向是单向的:设备层可以依赖Codec层,但Codec层绝不能反向依赖设备层。一个最小可扩展的目录可以参考:
ate_framework/ device/ # 设备适配:文件监听、socket接收 scheduler/ # 测试执行调度(可选接pytest) stdf/ codec.py # 4字节头编解码 registry.py # (type, subtype) -> handler specs/ # 各记录字段表 model/ record.py # 统一Record对象 result.py # part结果模型 io/ writer.py # 写文件、分卷、缓冲 tests/ golden_samples/ # 黄金样本,见最后一章依赖方向的核心约束是:model不依赖任何机台厂商,stdf不依赖device,io只负责写字节。每个模块的输入输出尽量是纯bytes或纯数据对象,这样测试时不用接真实测试机,直接喂文件就能验证。
这个目录不是拍脑袋定的,它对应一个具体工程诉求:代码的可测试性。当Codec和Model都变成无状态纯函数后,你可以不看测试机一眼,就把整套框架在本地拿假数据跑通。后面会专门讲这个验证方法。
4. 写一个最小可跑的STDF Codec:读取、注册表、写入与参数设置
前两章讲清了「为什么」,这一章给「怎么做」。下面这套Codec是框架的地基,用Python标准库实现,不依赖任何第三方包。它能完整跑通「写入一份记录流 → 再读回来 → 分发到处理函数」的闭环。真实项目的字段解析会比这复杂,但骨架就是这个样子。
4.1 最小STDF读取器:头分解与截断保护
读取器的核心逻辑很简单:循环读4字节头,解析长度,读body,返回一条record。但其中有一个细节必须处理——截断保护。产线上拿到的STDF文件经常是「半成品」,测试机可能还没写完,或者传输中断了,这种文件在解析时很容易在尾部出错。所以代码里要把「正常读到EOF」和「数据被截断」区分开。
import struct HEADER_STRUCT = struct.Struct("<HBB") def iter_records(fileobj, max_body_len=65535): while True: head = fileobj.read(4) if not head: return # 正常EOF if len(head) < 4: raise EOFError("truncated header") body_len, rec_type, rec_sub = HEADER_STRUCT.unpack(head) if body_len > max_body_len: raise ValueError(f"record body too large: {body_len}") body = fileobj.read(body_len) if len(body) < body_len: raise EOFError("truncated record body") yield rec_type, rec_sub, body逻辑说明:HEADER_STRUCT固定按小端解析2字节长度 + 1字节类型 + 1字节子类型;body_len不含4字节头;读到文件尾且头部为空时正常结束,头部不完整或body不完整时抛明确异常。参数说明:max_body_len默认设为65535,这是2字节长度字段的理论上限;如果你们的测试机会产出更大的自定义记录,按需调大,但要小心那些异常文件里出现的超大假长度,上限本身就是一道防御。
4.2 记录注册表:用(type, subtype)映射处理函数
读取器只负责把字节切分成record,真正理解每条记录的是注册表。注册表的key是(rec_type, rec_subtype),value是处理函数。这样做的好处是:新增一种记录类型时,不需要改读取器,只需要往注册表里多注册一个函数,开闭原则在这里体现得很直接。
RECORD_HANDLERS = {} def handler(rec_type, rec_sub): def decorate(func): RECORD_HANDLERS[(rec_type, rec_sub)] = func return func return decorate @handler(0, 10) def on_far(body): # FAR: File Attributes Record,文件属性,通常出现在文件开头 return {"kind": "FAR", "body": body} def dispatch(rec_type, rec_sub, body): func = RECORD_HANDLERS.get((rec_type, rec_sub)) if func is None: return {"kind": "opaque", "type": rec_type, "sub": rec_sub, "body": body} return func(body)逻辑说明:handler是装饰器,负责把函数注册进RECORD_HANDLERS;dispatch是分发入口,查不到对应Handler时返回opaque结构,而不是抛异常。参数说明:注册表的key用整数元组,不要用字符串拼接,因为二进制头里本身就是两个整数,直接映射省一次转换;opaque分支保留原始body字段,为后续补充解析留后悔药。
4.3 写入器与全链路回环:编码、落盘与自检
读取之外,框架还要能写STDF。写入端的核心函数是把一条记录的body编码成完整二进制块。注意body的字段拼装需要按测试机厂商的字段表来,这里只演示Codec层本身的写法。
def encode_record(rec_type, rec_sub, body): return struct.pack("<HBB", len(body), rec_type, rec_sub) + body def write_demo_file(path): # body用占位字节演示;真实项目里由specs层的字段表生成 with open(path, "wb") as f: f.write(encode_record(0, 10, b"fake-far-body")) f.write(encode_record(1, 0, b"fake-pir-body")) f.write(encode_record(1, 10, b"fake-ptr-body")) f.write(encode_record(1, 20, b"fake-prr-body")) def roundtrip_check(path): rows = list(iter_records(open(path, "rb"))) assert [(t, s) for t, s, _ in rows] == [(0, 10), (1, 0), (1, 10), (1, 20)]逻辑说明:encode_record先把body长度、类型、子类型打包成4字节头,再接body原始字节,这与读取器严格对称;write_demo_file写出一个最小但结构完整的记录流;roundtrip_check把它读回来断言类型序列完全一致。参数说明:这里的(0,10)是FAR的常见类型/子类型,(1,0)/(1,10)/(1,20)按PIR/PTR/PRR的常见编号填写。不同测试机厂商如果编号不同,以你们的STDF版本为准——这也是我把字段解析单独留在specs层的原因,Codec层不需要关心具体字段,只需要关心「哪条记录」。
4.4 参数设置与边界:max_body_len、分卷、缓冲与压缩
Codec跑通后,还有几个工程参数要在框架启动时配好,它们直接决定产线上的稳定性。
| 参数 | 建议值 | 说明 |
|---|---|---|
| max_body_len | 65535,特殊大记录可调大 | 防御坏文件里异常长度字段 |
| 文件读取缓冲 | 64KB起 | 产线文件动辄几百MB,缓冲太小IO压力大 |
| 分卷阈值 | 1GB或按MES要求 | 单卷过大时上传失败要重传,成本高 |
| 压缩时机 | 归档/上传前 | 压缩后不能流式追加,正在写入的文件不压缩 |
| 尾部残缺容忍 | 开启 | 等文件完整后再回流,见下章 |
参数设置没有统一答案,因为不同产线的MES规格不一样。但有一条原则是通用的:把「正在写入的文件」和「已写完整的文件」区分对待。最简单做法是测试机写完数据后再生成一个.done标记文件,解析侧看到标记才开始消费。这套手法能消掉后面一半的踩坑问题。
5. STDF落地的避坑记录:从字节序到大文件半截
这一章是我在产线上攒下的真实教训。每个问题都按「现象 → 原因 → 解决」来写,方便你排查时直接对号入座。
5.1 现象:长度字段异常导致解析器截断或死循环
最开始写读取器时,我遇到过解析到某条记录时body_len读出65535,但f.read(65535)根本拿不到这么多字节,程序直接崩。查下来有两种原因:一是测试机写入时缓冲未刷满,文件尾部有残缺;二是某些厂商自定义的DTR/GDR记录体量很大,被拆成了多条,但长度字段没按常规填。解决方式就是读取器里的防御:设max_body_len上限,超限就报错并跳过该条;body读不够就明确抛EOFError,由外层决定是等待还是终止。
提示:产线解析STDF的正确姿势是「坏一条记录,记下来继续读」,而不是让一个坏记录把整批数据卡死。
5.2 现象:读出来的数值像被字节对调了
有段时间我们从一批历史文件里解析MIR,时间字段读出来完全不对,像是每两个字节被交换过。原因是有台测试机中间经过了一层转换软件,把STDF从小端转成了大端输出。STDF标准规定多字节数值按小端存放,但中间链路不一定会守规矩。解决方式是框架里固定按小端解析,同时引入FAR自检:FAR里通常有可读的版本字符字段,前几条记录解析后先做一次「字符合理性检查」,发现不对就立刻报警,而不是带着错误数据往下跑。
5.3 现象:多site数据交错,统计结果「串台」
并行测试产线上,多个site的PIR/PTR/PRR是交错写入同一条流的。如果解析时只按先后顺序更新统计,A site的PTR可能被记到B site的part头上,bin分布就全乱了。原因就是没有按site维度做聚合。解决方式是在框架里以site_num为键暂存中间结果,等同一个part的PRR到齐后再落库,不要拿到一条PTR就立刻更新全局统计。涉及多head多site时,组合键用head_num + site_num + part_id最稳。
5.4 现象:用测试名定位PTR结果,数据莫名丢失
早期有个项目用PTR里的测试名字符串去关联测量结果,线上跑一段时间后经常发现某些测试项关联不上。原因一是测试程序迭代后测试名会变;二是中文或特殊字符在编码转换中被吞掉;三是同一测试名可能重复出现。解决方式是用test_num + head_num + site_num做逻辑主键,测试文本只用于展示。测试号里记录的是「测试项在程序里的位置」,比名称稳定得多。
5.5 现象:文件还没写完就开始解析,总在末尾报截断
产线数据回流最常见的一个坑:测试机还在写文件,解析程序已经开跑,每次都在最后一条记录上报读不够body。原因很简单,文件没有close,尾部数据还在缓冲里。解决方式是我在4.4提到过的「.done标记文件」机制——解析侧轮询等待标记文件出现,出现后才开始消费STDF文件。如果没有标记文件机制,就在解析逻辑里把「读到EOF但head不为空、或body不完整」当作「等待更多数据」处理,定时重试而不是报错退出。
6. 用「黄金样本」验证框架:回放、差分与回归基线
Codec写完、避坑踩完,最后一步是验证。这个验证方法帮我在框架迭代中少走了很多弯路,养成习惯后每次改动都有底气。
6.1 自造一份已知记录流做回归
在tests/golden_samples/下固定放一份最小但结构完整的STDF文件——就是第4章write_demo_file生成的那类文件,字段值固定、记录顺序固定。每次改Codec代码后,跑一次回归测试,断言类型序列和body长度完全一致。注意不要做字节级diff,因为真实文件里时间戳等字段每次生成都不同,做语义级断言才稳定。
GOLDEN = "tests/golden_samples/minimal.stdf" GOLDEN_TYPES = [(0, 10), (1, 0), (1, 10), (1, 20)] def test_regression(): rows = list(iter_records(open(GOLDEN, "rb"))) assert [(t, s) for t, s, _ in rows] == GOLDEN_TYPES for (_, _, body), (_, _, glen) in zip(rows, GOLDEN_TYPES): assert len(body) == glen这段测试的核心价值是:任何一次Codec改动,只要破坏了记录流的长度或类型序列,立刻就能被发现,不用等到产线回流时炸。
6.2 真实文件回放做语义差分
黄金样本只能验证「结构没坏」,验证不了「解析结果对不对」。我的做法是留一份测试机产出的真实标准文件作为回放样本,统计它的记录总数、各类型记录数量、PRR的bin分布。框架升级后,用新旧两版解析器各跑一次,两个统计结果必须一致。这个技巧在我把解析逻辑改成注册表分发时,帮我抓到了一个漏注册的subtype——结构对,但真实文件里有种不常见的记录没被处理,统计数量对不上。
早年我吃过亏:只盯着PTR数值对不上,最后查出来是测试名编码把字符吞了。从那以后,我的习惯是先做黄金样本回归,再拿真实文件对拍,改代码心里才有底。这套「自造样本 + 真实回放」的双层验证,是投入产出比最高的防线,希望帮到你。
本文还有配套的精品资源,点击获取