做IC测试的人多少都碰到过这种尴尬:一套测试程序在Chroma平台上跑得好好的,良率、稳定性都验证过了,结果客户或封测厂一句“换到爱德万平台”,整个测试程序就得推倒重来。最让人头疼的还不是那些measurement条件,而是pattern——成千上万行的数字向量,靠手工根本没法搬。这篇就从我去年做过的一个实际项目说起:写一个脚本,把Chroma测试机的pattern文件转换成爱德万V93000能识别的pattern格式。整个过程涉及格式解析、时序映射、电平映射、校验回归,踩了不少坑,也积累了一些方法论,分享给同样在跟pattern迁移较劲的测试工程师。
1. 为什么必须写这个脚本:Chroma转爱德万的现实困境
1.1 测试平台切换不是少数派需求
很多人以为测试程序写好了就能在一个平台上用到老,实际根本不是这么回事。芯片量产过程中,封测厂产能调配、客户指定第二供应商、多site并行扩产,任何一个环节变动都可能导致测试平台切换。以我遇到的这个项目为例,产品原本在Chroma的机型上做量产测试,程序、硬件、良率数据都齐了。结果客户那边要求增加一个备用封测厂,而那个厂的主力机型是爱德万V93000。于是整个测试程序——包括pattern、timing、levels、测试flow——全部要迁移过去。
这种跨平台迁移里,pattern是最底层的硬骨头。它能直接决定一颗芯片能不能被正确激励和判断,动一个bit都可能影响测试结果。所以转换脚本不是“能转就行”,而是要保证转换后的pattern在逻辑行为、时序关系、电平定义上和原来完全一致。
1.2 手动转换为什么不可行
有人可能觉得,pattern不就是一堆0和1吗,复制粘贴不就行了。真的上手做一次就知道太天真了。一个中等规模的数字IC测试pattern,动辄几万行向量,还夹杂着循环、子程序、macro调用、时序组切换。Chroma导出的格式和爱德万用的格式,在引脚顺序、波形定义、数据类型标识上都对不上。手工转的话,眼神再好也扛不住几万行的扫描,转完也不敢保证没抄错。
而且这个工作量不是一次性投入。产品版本更新,pattern跟着改;同一个产品多个测试方案,pattern又要有不同版本。每来一版都靠手转,等于把自己变成人肉转换器。所以写一个脚本,把转换过程自动化、可重复、可校验,是唯一靠谱的路子。这也是我把整个方案定位成“脚本工具”而不是“一次性的转换作业”的原因。
2. Pattern文件拆解:两种格式到底差在哪
2.1 Chroma pattern导出格式的典型结构
要写转换脚本,第一步是吃透两边格式。先说Chroma这边。不同型号、不同软件版本导出的pattern格式不完全一样,但我接触到的主流导出文件,核心结构基本可以拆成四块:
- 文件头和注释区:记录产品名、生成时间、软件版本、测试项目名称等信息,通常是
//或者;开头。 - 引脚定义区:声明pattern里用到的所有引脚名称,以及它们在向量中的排列顺序。这跟后面向量每一列对应谁直接相关。
- 时序定义区:包括测试周期(period)、各信号波形的边沿位置、采样点(strobe)位置等。
- 向量数据区:真正的主干部分,每一行代表一个周期的激励和期望值。
以一个简化过的Chroma导出文本为例,大致长这样:
// Chroma Pattern Export PIN: A0 A1 A2 A3 DIN CLK OUT1 OUT2 PERIOD: 100ns LEVEL: VIH=3.3 VIL=0 VOH=1.5 VOL=0 VECTOR: 0000 000 1 L 0001 001 1 H 0010 010 0 Z 0011 011 0 L这里的向量行,前四位是地址信号,接着是数据输入,再往后是时钟和输出期望值。L和H一般表示期望输出为低或高,Z表示高阻,X表示不关心。当然真实产品的pattern要复杂得多,还有repeat循环、label跳转、match loop这些控制结构。但不管多复杂,解析的时候都是先找结构,再逐段处理。
2.2 Advantest V93000的pattern格式要求
爱德万V93000这边的pattern格式,就要“讲究”一些。V93000上常用的pattern描述方式有原生格式,也有通过STIL、WGL这类标准语言导入。实际量产中,SmarTest软件加载pattern后,最终会把它编译成测试机能直接执行的二进制形式。脚本要做的,是生成一个SmarTest能正确导入的文本pattern文件。
V93000的pattern文件,核心内容也可以分成几块:
- PATTERN头:声明pattern名字,有时还带版本、注释。
- PIN声明:列出当前pattern用到的所有引脚,一般用测试机通道名或DUT引脚名。
- TIMING/WAVEFORM区:定义测试周期、各引脚在不同操作时刻的波形边沿位置。
- LEVEL区:定义各信号的高电平值、低电平值、期望电平、负载条件等。
- VECTOR区:按周期排列的激励和期望值,每列对应一个引脚。
有个关键点要特别注意:V93000的vector区通常不是简单的一列一个bit,而是按“每个引脚一个字符位”的方式排布,且引脚顺序必须和PIN声明一致。转换脚本如果只搬运数据、不处理引脚顺序,结果就是SmarTest导入时报pin count mismatch或者数据错位。
2.3 转换前必做的字段差异对照
两类格式拆解完,先别急着写代码,把差异列成一张对照表,后面每一步都对着它来,能避免很多低级错误。
| 对比项 | Chroma导出格式(以我接触的版本为例) | Advantest V93000 pattern格式 |
|---|---|---|
| 文件头 | //注释,信息较随意 | 有严格的PATTERN声明块 |
| 引脚顺序 | 按导出时设置的顺序 | 必须与PIN声明块严格一致 |
| 周期定义 | 单独PERIOD字段 | 通常放在TIMING/WAVEFORM区 |
| 逻辑值符号 | 0/1/L/H/Z/X | 按波形格式映射,可能用0/1/0/1/Z/X或自定义字符 |
| 期望值表示 | L/H直接标在向量里 | 需要区分drive和compare,常通过波形格式体现 |
| 循环控制 | 文件内文本形式展开或macro调用 | 有专门的loop/repeat语法 |
这张表看起来简单,但每一条背后都有坑。比如期望值表示这一条,Chroma里可能在向量位直接写L表示期望低电平,而V93000里需要通过波形格式来区分激励和比较时刻。如果脚本直接把L原样输出,导入后可能被当成当前时刻的驱动值,而不是比较值,功能测试必然失败。
3. 转换脚本的实现思路与关键代码
3.1 脚本语言选型和整体框架设计
这个转换任务本质上是“文本解析+数据重排+文本生成”,用Python最合适。原因很简单:字符串处理方便、正则表达式成熟、不用编译、改起来快。而且测试工程师大多能看懂Python,后续维护不需要额外学习成本。
脚本整体架构我设计成四个模块,各管一段,方便单独调试:
- 解析模块:读入Chroma原始pattern文件,把引脚、时序、电平、向量数据抽出来,存成统一的内存结构。
- 映射模块:把Chroma的引脚名映射到爱德万的测试通道名,把时序参数、电平参数按规则转换成V93000的对应描述。
- 转换模块:把中间结构的向量数据,按V93000的引脚顺序和波形格式重排,生成目标pattern内容。
- 校验模块:对转换前后的文件做静态一致性检查,包括引脚数量、向量行数、关键时序值等。
这种模块化设计最大的好处是,即使后面碰到一个新的Chroma导出格式变体,只需要改解析模块里的解析规则,其他部分基本不动。
3.2 解析模块:把Chroma原始文件读成中间结构
解析的关键是识别“结构边界”。不能一上来就逐行处理向量,得先找到引脚定义在哪里结束、时序定义在哪里结束、向量数据从哪里开始。我建议用“状态机+正则”的方式,一行一行扫过去,通过关键字把文件分成几个区段。
下面是我用的核心解析代码骨架,去掉了一些跟具体产品绑定的细节,保留框架逻辑:
import re from dataclasses import dataclass, field @dataclass class PatternData: pins: list = field(default_factory=list) period_ns: float = 0.0 levels: dict = field(default_factory=dict) vectors: list = field(default_factory=list) # 每项是["0000", "000", "1", "L"] def parse_chroma_pattern(filepath): pat = PatternData() section = None with open(filepath, "r", encoding="utf-8", errors="ignore") as f: for raw_line in f: line = raw_line.strip() if not line or line.startswith("//"): continue if line.startswith("PIN"): section = "pin" pat.pins = line.split()[2:] # 去掉 "PIN:" 两个字符 continue if line.startswith("PERIOD"): m = re.search(r"([\d.]+)\s*(ns|us|ps)?", line) if m: value = float(m.group(1)) unit = m.group(2) or "ns" if unit == "us": value *= 1000 elif unit == "ps": value /= 1000 pat.period_ns = value continue if line.startswith("LEVEL"): section = "level" continue if line.startswith("VECTOR"): section = "vector" continue if section == "level": for item in line.split(): if "=" in item: k, v = item.split("=") pat.levels[k] = float(v) elif section == "vector": parts = line.split() if parts: pat.vectors.append(parts) return pat这段代码做的事情很朴素:识别关键字,把数据放进统一的PatternData结构里。但有几个细节值得说说。第一,errors="ignore"是必须的,因为很多从测试机导出的文本文件,编码不一定干净,里面可能混着特殊字符。第二,向量数据我保留成字符串列表,不做数值转换。原因很实际:address这一列可能是十六进制表示的,也可能带负数偏移做repeat循环,如果一进来就转int,后面处理反而麻烦。
3.3 映射模块:引脚、时序、电平三件套
解析只是第一步,真正的转换难点在映射。这里说的映射包含三个层面:引脚映射、时序映射、电平映射。
引脚映射是最容易出错的。Chroma导出文件里的引脚名,比如A0, A1, DIN, CLK,在爱德万测试机上对应的是通道名,比如CH001, CH002这一类的物理通道,也可能是测试程序里重新定义过的逻辑引脚名。转换脚本里必须有一张映射表,把两边对应关系列清楚。我建议把这张映射表放到一个独立的配置文件里,而不是硬编码在脚本中。因为不同项目的硬件接口不一样,引脚映射关系几乎必然不同。
def map_pins(chroma_pins, pin_map_config): """根据配置文件把Chroma引脚名映射为Advantest引脚名""" mapped = [] for p in chroma_pins: if p not in pin_map_config: raise ValueError(f"引脚 {p} 没有在映射表中定义") mapped.append(pin_map_config[p]) return mapped时序映射这个东西,听着抽象,说穿了就是“周期和波形边沿怎么从Chroma的表示变成V93000的表示”。比如Chroma里定义了一个时钟信号,上升沿在周期的20%位置,下降沿在40%位置,采样点在60%位置。到了V93000里,需要把这些百分比或绝对时间值填到对应的波形定义里。这里有个容易忽视的点:时延基准不同。Chroma的边沿位置有时候是相对整个周期开始的,有时候是相对某个参考信号的沿。转换前一定要确认清楚,否则差半拍是常态。
电平映射相对简单,但也不能轻视。VIH/VIL/VOH/VOL这些值,两边单位通常都是伏特,直接搬就行。但要注意Chroma导出的电平值可能带电压档位描述、过冲参数、负载电流等附加信息,这些在V93000的level区里不一定有对应的字段,需要决定是忽略还是转换成合理默认值。我的做法是,凡是目标格式里没有明确对应关系的字段,统一在输出文件里用注释标注出来,人工确认一遍,绝不让脚本自作主张丢掉。
3.4 输出模块:生成爱德万可识别的pattern文件
输出模块负责把中间结构的数据,按照V93000的格式要求写出去。这里我直接给出一个简化但结构完整的输出示例,实际生产环境里SmarTest导入时需要的头字段比我这个要多,但基本骨架是一致的:
def write_advantest_pattern(pat, mapped_pins, outpath): with open(outpath, "w", encoding="utf-8") as f: f.write("PATTERN %s;\n" % pat.name) f.write("PIN: %s;\n" % " ".join(mapped_pins)) f.write("TIMING:\n") f.write(" PERIOD %sns;\n" % pat.period_ns) # 这里按实际项目需要逐pin写波形格式 for pin in mapped_pins: f.write(" WAVE %s: DRIVE0 @0ns, DRIVE1 @25ns, COMB @60ns;\n" % pin) f.write("LEVEL:\n") for k, v in pat.levels.items(): f.write(" %s %sV;\n" % (k, v)) f.write("VECTOR:\n") for vec in pat.vectors: # 按mapped_pins顺序重排每列数据 reordered = reorder_vector(vec, pat.pins, mapped_pins) f.write(" %s\n" % " ".join(reordered))reorder_vector这个函数是整个转换正确性的核心。它的逻辑是:拿到Chroma原始向量里的每一列,根据原始引脚顺序找到对应的逻辑信号名,再根据映射后的引脚顺序放回新的位置。说白了,就是把“按Chroma引脚顺序排的数据”重排成“按V93000引脚顺序排的数据”。这一步做对了,转换工作就完成了大半。
输出格式这块我有句经验之谈:不要试图一次生成最终完美格式。第一次跑通脚本,先输出一个简化版pattern,能导入SmarTest就行。然后逐步补齐字段,每补一个字段就跑一次SmarTest的编译检查。这样比一次性生成完整文件再回头排查要高效得多。
4. 转换后的校验与回归测试怎么做
4.1 静态校验:内容对得上不等于能用
脚本跑完,第一反应肯定是打开转换后的文件看一眼。但我建议别急着人工看数据,先跑静态校验脚本来做几项硬性检查。
第一项是引脚数量和顺序。Chroma原始引脚数必须等于映射后的引脚数,且顺序必须严格按映射表排列。第二项是向量行数。转换前后,去掉注释和空白行后,向量总数要一致。第三项是特殊符号集检查。把所有向量数据里出现过的字符收集起来,跟目标格式允许的逻辑值符号集合做比对,一旦出现目标格式不认识的字符,直接报错。
静态校验脚本我一般这么写:
def static_check(src_pat, dst_pat, mapping): errors = [] if len(src_pat.pins) != len(dst_pat.pins): errors.append("引脚数量不一致: %d -> %d" % (len(src_pat.pins), len(dst_pat.pins))) if len(src_pat.vectors) != len(dst_pat.vectors): errors.append("向量行数不一致: %d -> %d" % (len(src_pat.vectors), len(dst_pat.vectors))) allowed_chars = set("01LHZX") for i, vec in enumerate(dst_pat.vectors): for token in vec: for ch in token: if ch.upper() not in allowed_chars: errors.append("第%d行出现非法符号: %s" % (i + 1, ch)) break return errors这些校验全通过,只能说明“文件结构上内容对得上”,不能说明“测试结果会一致”。接下来还得做更接近真实行为的验证。
4.2 动态验证:模拟比对与上机回归
动态验证我分成两步走。
第一步是模拟比对。如果你的测试环境里有EDA仿真工具,可以把转换前后的pattern分别喂给同一个DUT仿真模型,比对仿真输出是否一致。这一步能在上机前拦掉大部分功能性错误,尤其是向量重排导致的信号错位问题。没有仿真环境的项目,退而求其次,可以写一个简单的pattern diff工具,逐行对比转换前后的向量,但要把“合法变化”排除掉,比如引脚顺序变化、字符大小写变化、注释位置变化。
第二步是上机回归。上机验证不是随便跑一遍看着pass就完事。正确的做法是:在Chroma和爱德万上分别跑同一个DUT(尽量用同一批芯片),把每一颗的pass/fail结果、关键测量值记录下来,做一致性对比。尤其要关注边界条件附近的芯片——在Chroma上fail边缘的芯片,在爱德万上如果变成pass或是反过来,都说明转换后的timing或level跟原平台有细微差异。这种差异在上机数据里是藏不住的。
我还习惯做一步“反向抽样”验证:从转换后的爱德万pattern里随机抽几十行,手工跟Chroma原始文件比对。随机抽样别看只查几十行,它能在最短时间内暴露系统性错误,比如整体偏移一位、某几列数据串位这种问题。
5. 转换过程中最常见的5个坑与排查方法
5.1 Pattern mismatch最常见的起因
转换上线后,最常被测试工程师挂在嘴边的问题就是“pattern mismatch”。这个词听着吓人,其实九成以上的mismatch在转换脚本里都有明确出处。我排查mismatch问题时不会一上来就怀疑脚本算法,而是先做三件事:查第一列向量、查引脚映射表、查复位序列。
很多pattern的第一行是复位或初始化序列,里面的值往往跟后续功能向量长得不一样。如果转换脚本对首行的特殊处理不到位,第一行错了,后面全部连锁错误。检查完第一行,再看引脚映射表有没有配错。我遇到过最阴的错是:两个引脚名在映射表里写反了,导致A0和A1数据整个交换,所有向量在边界情况下就不停mismatch。最后是通过随机抽样比对才发现规律。
5.2 时序边沿差一拍的问题
还有一个高频坑是时序边沿“差半拍”。Chroma里波形边沿如果是相对时钟下降沿定义的,而V93000里默认是相对周期起点或者某个参考沿,那么转换后所有信号的建立时间、保持时间都会偏差。症状就是功能测试低频pass、高频fail,或者在不同温度条件下不稳定。
排查这类问题,要把转换前后的波形定义打印出来逐参数比对,特别关注三类数据:时钟上升沿和下降沿的位置、数据信号相对时钟的建立保持时间、输出采样点的位置。任何一个参数在多平台间对不上,都可能导致边缘性mismatch。
5.3 电平值没按要求映射
电平问题也容易踩。比如DUT的输出是开漏结构,期望高阻态Z;Chroma里对应位可能是Z,但转换脚本如果没把Z正确映射到V93000的波形格式里,可能被解释成X(不比较)或者强行驱动低电平,结果就是输出对比异常。
另外常见的是VOH/VOL和VOL/VOH写反。两个平台的level区字段顺序不同,直接从Chroma里搬过去如果不加映射逻辑,很容易把高低电平值对调。这种错误在静态校验里很难发现,因为数值都在、字段也都在,只有在低频测试里看到输出逻辑反了才暴露。
5.4 循环和子程序展开丢失
再讲一个更隐蔽的坑:循环和子程序。Chroma里如果pattern用了repeat 100这样的循环结构,而转换脚本没有做展开转换,那转换后的总向量行数跟原始逻辑行数对不上。行数对不上,静态校验立刻能报错,这反而好查。怕的是脚本做了展开,但展开次数计算错了。
展开次数这种问题,建议不要用眼睛数,直接在解析模块里把循环展开后的逻辑向量总数打印出来,跟原始文件里的循环声明交叉验证。比如原始文件里写了repeat 100,展开后至少要比不展开多99行,这个数差可以自动检查。
5.5 排查速查表
把踩过的坑整理成一张速查表,遇到问题先对着查,比盲目翻代码高效得多。
| 异常现象 | 优先排查方向 | 检查方法 |
|---|---|---|
| 从第一个向量开始mismatch | 复位/初始化序列 | 手动比对转换前后前10行 |
| 固定某一列或某几列数据错误 | 引脚映射表 | 抽查映射表,反向查回原始文件 |
| 低频pass高频fail | 时序边沿定义 | 对比波形参数,确认基准沿 |
| 输出逻辑整体反相 | 电平映射VOH/VOL | 核查level区字段顺序 |
| vector行数不一致 | 循环/子程序展开 | 打印展开前后统计数字 |
| SmarTest导入报错 | 头字段缺失或语法错 | 逐段检查PATTERN/PIN/TIMING块 |
6. 把脚本做成能复用的工具:我的几点体会
6.1 接口设计成可配置
项目做完,脚本能跑只是及格线。真正让它值钱的,是能不能在下一次接到类似转换需求时直接复用。我的经验是把所有跟具体项目相关的参数都抽出来做成配置文件:引脚映射表、电平字段映射规则、时序基准定义、输出格式模板,全部外部化。脚本主体保持“纯逻辑”,不写死任何一个项目专属的值。这样换一个产品、换一台测试机,改配置文件就行,不用动代码。
6.2 保留日志和diff能力
第二个建议是脚本一定要留完整日志。我最初版本只管转换、不管记录,结果出问题时根本想不起来当时跑了什么参数。后来加了日志模块,每次转换自动生成一个带时间戳的diff报告,记录原始文件名、配置文件版本、映射表内容、转换后的校验结果。这个习惯救了我好几次,客户说“这个pattern好像有问题”,我能直接调出当时的转换日志跟他对。
6.3 后续扩展方向
这个脚本后续还能往几个方向扩展。一是支持更多格式:很多Chroma导出文件实际上是STIL或WGL变体,可以让脚本直接支持导入STIL、导出STIL,这样对接其他平台也方便。二是增加批量转换能力:一个产品通常有几十个pattern文件,批量跑完统一输出一份汇总报告。三是接入测试程序版本管理:转换脚本跟测试程序一起入版本库,每版pattern变更都能追踪到。
我个人在写完这个脚本之后最大的体会是:跨平台pattern转换这件事,难点从来不在“0和1怎么搬”,而在“搬的时候那些看不到的约定怎么处理”。引脚顺序、时序基准、电平映射、循环展开,每一个细节都藏着坑。把规则明确到配置文件里,把校验做进脚本流程里,再用上机数据兜底验证,这套思路不仅适用于Chroma转爱德万,也适用于任何两个ATE平台之间的pattern迁移。希望这篇能帮后来的人少走几步弯路。