干IC测试这行最烦的一件事就是换测试平台。前阵子接了个活,客户的原厂程序一直跑在Chroma(致茂)机台上,因为产能分配和成本考虑,要把整个测试方案挪到爱德万(Advantest)平台上。设备到位、测试项目一个个移植都还顺利,唯独编译好的pattern文件让人头疼到不行。
Chroma的pattern和爱德万的pattern,语法不通用、时序定义方式不一样、电平命名规则也两模两样。手动重写几千行的pattern,不光慢,还特别容易在某个循环嵌套、某个掩码位上出岔子。所以我花了几天时间写了个Python脚本,专门做这种格式转换:输入Chroma的pattern,输出爱德万能直接识别和编译的pattern格式。这个脚本用下来至少省了一周的人力,而且转换后的pattern在机台上验证基本一遍过。今天把整个思路、代码框架和踩过的坑完整记录下来。
1. 为什么Chroma的pattern不能直接扔给爱德万:两大平台的格式差异根源
先解决一个最基础的问题:同样是ATE测试机,同样是测数字芯片,为什么pattern文件不能通用?
1.1 同为ATE,但两者描述“测试图形”的语言完全不同
Chroma的pattern本质上是一种逐向量(vector-by-vector)的文本格式。每一行描述一个测试周期内所有引脚的状态,包括驱动值、期望值、掩码位,以及这个周期的时间信息。行与行之间按顺序执行,逻辑非常直白,类似下面这种表达方式:
SIGNAL CLK 15; SIGNAL D0 12; SIGNAL D1 11; ... VECTOR 1 0 1 M H L;而爱德万的pattern格式则更偏向块结构(block-structured)。它把pattern组织成若干个命名的pattern块,向量行可以引用预先定义好的波形(waveform)和时间集(timing set),整体上更像一种“带编译逻辑的测试程序”。同样一条向量,在爱德万里可能要拆成“波形定义 + 电平定义 + 向量引用”三部分,初看会很不习惯。
两者最核心的差异可以概括为:
| 对比维度 | Chroma pattern | 爱德万 pattern |
|---|---|---|
| 组织形式 | 逐行向量文本 | 块结构 + pattern命名 |
| 时序描述 | 每行携带时间信息 | 独立timing set,向量行引用 |
| 逻辑电平 | 直接用H/L/Z/M等字符 | 符号化电平 + level set |
| 控制流 | 循环/跳转指令相对简单 | 循环/子程序/宏结构更丰富 |
| 注释风格 | //或#行注释 | 块注释或行注释 |
| 编译依赖 | 相对独立 | 依赖测试程序里的类型定义 |
看到这个对照表你就明白了,字符替换只能解决信号名的问题,解决不了语义层的差异。
1.2 转换的真正难点不在“翻译”,而在“语义对齐”
我一开始以为这个脚本就是一个大号的查找替换,把Chroma的信号名映射成爱德万的信号名,再把H/L/Z这些字符换成对应的符号就行了。真正动手才发现,最麻烦的是两边的语义不完全对等。
举一个最简单的例子。Chroma里一个vector行可能只写了“这一周期CLK为H,D0为L”,但爱德万要求你明确这个H/L对应的是哪一种波形格式(NRZ、RZ、RO、SBC等),以及它的有效沿在哪里。如果只是机械地把H换成一个逻辑值,不把时序信息带过去,那转换出来的pattern在爱德万上要么编译报错,要么能编译但测试结果完全不对。
我当时就踩过这个坑:某次只是简单替换了信号名和逻辑值,没有处理时序集,转换完的pattern跑在爱德万上,良率直接从98%掉到80%。排查了整整一天,最后发现是一个信号的掩码位没有跟着时序集一起转过去,导致无效区间的毛刺被当成了真实故障。
**所以做这个转换脚本,本质上是做一套“语义映射引擎”,而不是字符串翻译器。**这句话是整篇文章最重要的认知。
2. 动手前先做的事:摸清Chroma pattern的文件结构,建好信号映射
在写第一行转换代码之前,我花了小半天专门“读文件”。不要小看这一步,pattern文件不像普通代码,它往往藏着很多隐含信息,不读透直接写脚本,后面返工是必然的。
2.1 拿到文件后的“三读”:头部、信号表、向量区
一般Chroma导出的pattern文件,结构上可以分成三块:
- 头部声明:记录测试程序的版本、生成日期、DUT型号、pattern名称、时间单位、周期默认值等。
- 信号表(Signal List):定义所有用到的引脚名和对应的通道号(channel pin)。
- 向量区(Vector Data):真正的测试图形,每一行对应一个测试周期的引脚状态。
我拿到文件后,先用编辑器打开,从头到尾扫一遍,把这三块区域在文件中分别从哪里开始、到哪里结束标记清楚。这一步非常关键,因为不同版本的Chroma软件导出的文件格式其实有差异,有的带SIGNAL段,有的带PATTERN段,有的信号表顺序是乱的,只有实际看到文件才能确定后面的解析策略。
2.2 用正则把信号表抽出来
解析信号表我直接用Python的正则表达式搞定。Chroma信号表的典型形态是SIGNAL 引脚名 通道号;。假设第一版格式是这样,那解析逻辑可以写成:
import re def parse_signal_declaration(text): """从Chroma pattern文本中提取信号表""" signal_map = {} pattern = re.compile(r"SIGNAL\s+(\w+)\s+(\d+)\s*;", re.IGNORECASE) for match in pattern.finditer(text): signal_name = match.group(1) channel_id = int(match.group(2)) signal_map[signal_name] = channel_id return signal_map这里有个小技巧:不要假设信号表是按通道号排序的。我遇到过一次,Chroma导出的文件里信号表顺序跟实际通道号不一致,写脚本时如果默认“第一个信号就是通道0”,后面的向量数据全部会错位。所以解析完后,建议先按通道号排个序,再打印出来人工确认一遍。
2.3 建立“信号映射表”而不是写死在代码里
Chroma的引脚名跟爱德万的引脚名几乎不可能一模一样。常见的情况是:Chroma里叫U1_A0,爱德万里叫T1_A0;Chroma里叫DQ0,爱德万里叫D0。这种映射关系必须单独维护。
我强烈建议把信号映射放在外置的JSON配置文件里,而不是硬编码在Python脚本中。理由很简单:不同项目的板卡、探针卡和DUT封装不一样,引脚映射规则是随项目变的,脚本不该跟着每个项目改。
配置文件示例:
{ "mapping": { "U1_A0": {"target": "T1_A0", "direction": "input", "channel": 15}, "U1_A1": {"target": "T1_A1", "direction": "input", "channel": 14}, "DQ0": {"target": "D0", "direction": "bidir", "channel": 3}, "DQ1": {"target": "D1", "direction": "bidir", "channel": 4} }, "default_direction": "unknown", "unmapped_action": "error" }这个映射文件里还可以带上direction信息。为什么要带?因为在转换过程中,输入引脚、输出引脚和双向引脚的处理逻辑是不同的。比如输入引脚通常不需要mask,输出引脚要考虑期望值,双向引脚在读写切换时需要特别小心Z状态的位置。没有方向信息,脚本在遇到状态转换时就没法做合理的判断。
2.4 向量行的语义:必须先建立“状态字典”
向量区里每个引脚的状态,不同平台有不同符号。Chroma常见的状态字有:
H:驱动高电平/期望高电平(取决于该引脚是输入还是输出)L:驱动低电平/期望低电平Z:高阻M:掩码(不比较)X:未知/不关心
而爱德万的向量状态字可能是1、0、Z、M、X,也可能用别的形式。这个映射关系我建议做成一个状态字典,放在脚本开头,方便随时调整:
STATE_MAP_CHROMA_TO_ADVANTEST = { "H": "1", "L": "0", "Z": "Z", "M": "M", "X": "X", }这里要说个重点:状态字的语义和引脚方向是耦合的。同一个H,在输入引脚上意味着“测试机驱动高电平给DUT”,在输出引脚上意味着“DUT输出高电平,测试机采样并与期望高比较”。所以转换时不能只看状态字,还得结合信号映射表里的direction字段一起判断。这个我在第4章展开细讲。
3. 脚本的骨架设计:解析-映射-生成三段式拆开讲解
如果你问我转换脚本的核心架构是什么,我会毫不犹豫说六个字:解析、映射、生成。这三个阶段各干各的,千万别揉在一起。
3.1 为什么用三段式而不是逐行翻译
很多人写这种转换脚本,第一反应是“读一行Chroma,然后输出一行爱德万”,看起来效率高,代码也短。但实际项目里这么写必死。原因有三个:
第一,很多语义需要跨行判断。比如Chroma里一个循环块的起始和结束,要读到后面才知道前面那行是循环头,逐行输出时根本没法处理。
第二,调试成本高。如果转换逻辑和输出逻辑混在一起,中间某一行的信号没有映射,报错时你很难分清是“解析问题”还是“映射问题”还是“输出问题”。
第三,复用性差。今天你在Chroma 3380上转爱德万T2000,明天可能要在Chroma 3360上转爱德万V93000。解析器可以复用,映射规则可以复用,只要改生成器就行。
所以我选择了清晰的三个阶段:
- 解析器(Parser):读入Chroma文件,输出一个中间表示(IR,Intermediate Representation)
- 映射器(Mapper):把IR里的信号名、状态字、时序信息,按照配置映射成爱德万的语义
- 生成器(Generator):把映射后的数据结构,拼接成爱德万格式的文本文件
3.2 解析器:用“行分类器”代替一把梭的正则
Chroma pattern文件的每一行,可能有不同的角色。我建议写一个LineClassifier,先判断每行属于哪种类型,再分发到对应的处理函数:
class LineType: HEADER = "header" SIGNAL = "signal" TIMING = "timing" VECTOR = "vector" CONTROL = "control" # loop / repeat / jump COMMENT = "comment" BLANK = "blank" def classify_line(line): stripped = line.strip() if not stripped: return LineType.BLANK if stripped.startswith("//") or stripped.startswith("#"): return LineType.COMMENT if re.match(r"SIGNAL\s+\w+\s+\d+\s*;", stripped, re.IGNORECASE): return LineType.SIGNAL if re.match(r"LOOP|REPEAT|JUMP|END", stripped, re.IGNORECASE): return LineType.CONTROL if re.match(r"VECTOR", stripped, re.IGNORECASE): return LineType.VECTOR return LineType.HEADER看到这里你可能觉得,这不就是简单的if-else吗?没错,但实际项目里TIMING行的判断没那么简单,因为Chroma不同型号导出的时序格式差异很大。我建议在解析阶段先把所有内容原样存进一个RawBlock结构,然后再用状态机识别哪些行构成一个完整的时序段。时序段不解析成结构,后面没法做单位换算和格式映射。
3.3 映射器:核心转换逻辑全在这里
解析完成后,数据变成了Python对象,比如一个ChromaVector列表:
class ChromaVector: def __init__(self, time_unit, states, control=""): self.time_unit = time_unit # 该向量的时间单位 self.states = states # 引脚状态字典,如 {"U1_A0": "H", ...} self.control = control # 控制指令,如 REPEAT 100映射器的职责就是遍历这些对象,结合信号映射表和状态字典,输出一个新的数据结构:
class AdvantestVector: def __init__(self, pin_states, timing_set, control=""): self.pin_states = pin_states # 已映射的爱德万引脚状态 self.timing_set = timing_set # 引用的时间集 self.control = control映射器里最核心的三个函数是:
def map_signal_name(chroma_name, mapping): if chroma_name in mapping: return mapping[chroma_name]["target"] else: raise KeyError(f"Unmapped signal: {chroma_name}") def map_signal_state(chroma_state, direction): if chroma_state in STATE_MAP_CHROMA_TO_ADVANTEST: base_state = STATE_MAP_CHROMA_TO_ADVANTEST[chroma_state] # 根据方向做额外处理,例如输出引脚是否要自动加MASK return apply_direction_rule(base_state, direction) else: raise ValueError(f"Unknown state: {chroma_state}") def map_timing(chroma_timing, timing_config): # 把Chroma的时间单位换算成爱德万的时间单位 return convert_time(chroma_timing, timing_config["unit"], timing_config["precision"])这种结构的好处是,每一类映射都是独立的函数,哪个环节出问题,报错信息就能直接告诉你是信号没映射、状态字不认识,还是时序单位换算出错。
3.4 生成器:模板字符串+逐行拼装
生成器比较机械,但也最容易忽略细节。我建议先把爱德万pattern的头部模板和尾部模板单独抽出来,不要混在循环里:
HEADER_TEMPLATE = """Pattern {pattern_name}; LevelSet {level_set_name}; TimingSet {timing_set_name}; VectorData; """ VECTOR_TEMPLATE = "{pin_states} ; {control}" FOOTER_TEMPLATE = """EndVectorData; EndPattern; """ def generate_advantest_pattern(vectors, output_path, pattern_name="CONVERTED"): with open(output_path, "w") as f: f.write(HEADER_TEMPLATE.format( pattern_name=pattern_name, level_set_name="LS_DEFAULT", timing_set_name="TS_DEFAULT", )) for vec in vectors: f.write(VECTOR_TEMPLATE.format( pin_states=" ".join(vec.pin_states.values()), control=vec.control, )) f.write("\n") f.write(FOOTER_TEMPLATE)这里有个细节:引脚顺序必须以爱德万那边的channel map为准。Chroma文件里信号行的顺序跟爱德万要求的vector行引脚顺序很可能不同。所以生成器在输出前,一定要先按照一个ordered_pin_list把pin_states重新排序。这个ordered_pin_list从哪里来?从爱德万测试程序里导出的通道列表来,不要用Chroma的顺序。
4. 最容易翻车的细节:时序换算、电平映射和掩码处理
这一章专门讲转换脚本里最容易出bug的三个细节。这三个细节如果处理不好,轻则编译不过,重则测试结果错误但表面上一切正常。
4.1 时序单位换算:别小看“取整”这个动作
不同机台的时间分辨率不一样,这是转换过程中绕不开的问题。Chroma某些机台的pattern时间精度可能是2ns步进,爱德万那边的timing set精度可能是1ns或500ps。假设Chroma里一个周期定义在100ns,沿位置在30ns,直接换算到爱德万时,要先把30ns除以爱德万的精度单位,得到整数步数。
def convert_time(time_ns, precision_ns): # 把时间转换为目标机台的步数 steps = round(time_ns / precision_ns) actual_time = steps * precision_ns if abs(actual_time - time_ns) > precision_ns / 2: raise ValueError(f"Time {time_ns}ns cannot be represented at {precision_ns}ns precision") return steps我一开始写的版本用的是int()而不是round(),结果所有边界沿的位置都往0偏了半个精度单位,测出来的时序跟原平台差了1ns,在小时序裕量的测试项上直接边缘失败。换平台的pattern转换,时序必须以目标机台能精确表示的值为准,并且要在日志里记录每次换算的舍入误差。这是机台与机台之间“可移植性”的关键所在。
另外要注意的是,Chroma的向量行如果用的是相对时间或周期计数器,那要先还原成绝对时间,再换算到爱德万的时间集,否则循环中的累计误差会一层层叠加,最后偏到离谱。
4.2 电平映射:不能只转字符,还要带上“电平集”
逻辑电平字符只是表象,真正决定驱动电压和比较阈值的是背后的电平定义。Chroma的pattern文件里一般不直接写电压值,而是引用测试程序里的某个电平组(level group)。但在转换到爱德万时,这些电压值必须明确出现在爱德万的level set里。
我在做转换时,先在Chroma测试程序里找到对应电平组的定义,整理成一个JSON:
{ "level_set": { "VIH": 3.3, "VIL": 0.0, "VOH": 2.5, "VOL": 0.5, "VREF": 0.9, "VMID": 1.65 } }然后生成器在输出爱德万文件的头部时,直接根据这个JSON生成对应的level set声明。这里最容易忽略的是“输出高/低比较阈值”和“高阻态比较电压”的区别。有的工程师只转了VIH/VIL,把VOH/VOL漏了,结果爱德万默认输出阈值跟Chroma不一致,导致数字输出引脚的功能测试失败。
另外,爱德万某些机型对电平符号有特殊要求,比如低电平比较阈值写作负数形式,或者需要显式声明VOH/VOL是“开漏式”还是“推挽式”。这些细节不写进映射配置里,很难第一次就跑对。
4.3 掩码处理:Z状态自动转成“比较掩码”是非常危险的
掩码(mask)是pattern转换里最容易出问题的地方。Chroma里一个引脚状态写M或Z,很多人第一反应是“Z就转成Z”,但实际测试场景远比这复杂:
- 如果DUT引脚是开漏输出,在某个周期处于高阻态,测试机如果不去主动上拉,这个引脚上的电压是浮动的。到底是当前周期就mask掉比较,还是等下一个驱动周期再比较,不同平台的处理逻辑不一样。
- 如果DUT引脚是双向口,在读写切换的那一个周期,通常需要mask掉输入比较,只做方向切换。这个“切换周期”在Chroma里可能用
Z表示,在爱德万里可能需要一个额外的“mask标志”才能表达。
我建议的状态映射规则是:
def apply_direction_rule(base_state, direction, is_read_write_transition=False): if base_state == "Z" and direction == "output": # 开漏输出的高阻态:比较端必须mask,否则浮空采样 return "M" if base_state == "Z" and direction == "bidir": if is_read_write_transition: return "M" # 方向切换周期:不比较 else: return "Z" return base_state这个规则的前提是信号映射表里有准确的direction信息。没有方向信息的Z状态转换,一律按“报错”处理,而不是悄悄猜一个映射值。宁可让脚本停下来,也不能输出一份带隐患的pattern上机。
4.4 控制流的转换:循环嵌套深度要提前核查
pattern里常见的控制流包括循环(LOOP/REPEAT)、跳转(JUMP)和注释标签。Chroma和爱德万在控制流上的语法差异也很大。
有一回我转换一个比较复杂的pattern,里面嵌套了三层循环,代码逻辑完全没问题,结果爱德万编译器报了一个“loop nesting exceeds limit”的错误。一查才发现,爱德万那种型号对循环嵌套层数有硬性限制,而Chroma那边没有。这种情况就必须提前在映射器里加一个静态检查规则:扫描所有的循环开始/结束对,统计最大嵌套深度,超出限制直接报错,并提示用户手动重构或扁平化这些循环。
控制流转换的另一个坑是循环体长度。爱德万有些机型的循环体有长度上限,Chroma里一个很长的循环块直接搬过去会超限。这种情况不能自动处理,最好的办法是脚本输出一条明确的warning,让测试工程师决定怎么拆分,而不是自作主张改逻辑。
5. 转换完怎么验证才敢上机:从静态比对到真机回读
写完转换脚本,生成出第一版爱德万pattern时,千万别急着上机。我经历过太多次“以为转换对了,结果一上机全挂”的情况。验证步骤得一步一步来。
5.1 静态校验:脚本自带的自检函数
生成器输出文件之后,我建议脚本立刻做一轮静态自检,比较转换前后的几个关键指标:
- 向量总数:Chroma源文件的向量行数,和爱德万输出文件的向量行数,必须完全一致(除非用户手动指定了展开循环,那也要有明确的计数报告)。
- 信号数量:所有信号名都被正确映射,没有遗漏。
- 状态字合法性:输出的状态字必须在爱德万的合法字符集内。
- 控制流闭合:所有
LOOP都有对应的END,所有JUMP的目标标签都存在。
这些自检逻辑写成一个函数,放在生成器之后:
def validate_output(orig_vectors, output_vectors): errors = [] if len(output_vectors) != len(orig_vectors): errors.append(f"Vector count mismatch: {len(orig_vectors)} vs {len(output_vectors)}") orig_pins = set() for vec in orig_vectors: orig_pins.update(vec.states.keys()) mapped_pins = set() for vec in output_vectors: mapped_pins.update(vec.pin_states.keys()) if not orig_pins.issubset(mapped_pins): missing = orig_pins - mapped_pins errors.append(f"Missing pins in output: {missing}") return errors自检报告我建议保存为JSON或CSV,作为转换记录的一部分存档。一旦后面测试出问题,回溯时能直接看到“那一版的pattern转换时有没有告警、有没有强制跳过的项目”。
5.2 用仿真器回放比对:不花钱的最优验证方式
如果你们公司有爱德万配套的pattern仿真工具,那是最好的第二道关卡。把转换后的pattern加载进仿真器,让它按pattern跑一遍,看有没有时序违例、有没有状态冲突。
没有仿真器的话,也可以做一个相对原始的离线回放:写个小脚本,逐条读取转换后的pattern数据和转换前的数据,按统一的“周期、引脚状态”抽象模型重新对齐,自动比较两者在一个个测试周期上的语义是否一致。这种方法虽然不能验证时序在真实电路上的表现,但能抓出不少“状态字错位”的问题。
我当时用这个方法抓到一个很隐蔽的bug:Chroma里某些向量行的状态字顺序跟信号表顺序一致,但爱德万的输出文件需要按“先所有输入引脚,再所有输出引脚”重排,我生成器里有一个索引数组在配置更新后没有同步,导致从第200行开始所有引脚状态集体错位。这种问题不上仿真器、不做离线回放,肉眼根本看不出来。
5.3 真机验证的稳妥三步法
静态校验和离线回放都通过后,就是上机真验证。我强烈建议按下面的顺序来,别嫌麻烦:
- 选一颗已知良好(Known Good Device),先跑Chroma原平台的pattern,记录全部pass的bin。
- 在同一颗DUT上跑转换后的爱德万pattern,对比bin结果和输出波形。这个阶段不要急着调新pattern,而是想尽办法让“转换后的pattern”复现“原pattern”的结果。
- 用shmoo plot对比关键配置测试项。供电电压、频率、输出负载边界等扫描测试,如果shmoo形状跟原平台趋势一致,说明时序与电平映射基本正确。shmoo形状偏差很大,比如某个区域原平台是pass,新平台是fail,就要回头检查对应的时序沿或电平阈值是不是转换错了。
上机这一步,最容易出现的情况是“整体能跑,但某些bin的边缘测试结果不一样”。这一般不是脚本转换错误,而是两个平台本身的硬件精度差异导致的——这也正常,转换脚本能保证的是逻辑语义一致,不能保证两台不同精度的测试机测出完全相同的裕量。遇到这种情况,需要测试工程师手动微调timing或level,让两边匹配。
5.4 别忘了版本管理:pattern的转换日志比pattern本身还重要
转换脚本迭代过程中,我吃了不少“方案B改坏了方案A”的亏。所以后来养成了一个习惯:每个pattern文件在转换时,单独生成一份转换日志,连同源文件、输出文件一起提交到Git。
日志里至少包含以下内容:
- 源文件的MD5哈希值
- 输出文件的MD5哈希值
- 转换脚本的版本号或Git commit id
- 关键配置参数(信号映射文件版本、电平集版本、时序换算舍入误差)
- 转换过程中的所有warning和error
为什么坚持记录MD5?因为pattern文件不定什么时候被谁重新导出过,或者txt文件被Excel打开后自动改了换行符。这些细微变化如果没记录,等到测试结果对不上时,很难说是pattern变了、脚本变了还是配置变了。有了MD5和git版本记录,这类问题一般十分钟内就能定位。
6. 脚本本身的工程化维护:配置、日志与回归用例
最后一个部分,聊一聊这个转换脚本写完之后的工程化问题。很多工程师会写脚本,但脚本用三个月之后再去维护,往往是灾难——当初自己都忘了每个if分支在干嘛。所以趁着记忆还在,把维护相关的经验也一并写出来。
6.1 配置与代码彻底分离,别让工程师改代码来换项目
我前面已经提过信号映射表放JSON。实际上所有可能随项目变化的配置都应该外置:
# config.yaml timing: target_precision_ns: 1.0 allow_rounding_error_ns: 0.5 max_loop_nesting: 4 level: level_set_name: "LS_CUSTOM" default_vih: 3.3 default_vil: 0.0 default_voh: 2.5 default_vol: 0.5 output: pin_order_file: "./config/pin_order_advantest.csv" header_template: "./templates/header_advantest.tpl" footer_template: "./templates/footer_advantest.tpl"把模板文件也外置,这样新项目的爱德万pattern头部格式有变化时,只需要改模板文件,不需要动脚本主体。我用YAML而不是JSON做配置,主要是允许注释,方便在配置里写各种说明。
6.2 日志设计:打印到终端的同时必须落盘
转换脚本平时可能在Windows命令行跑,也可能在Linux服务器上跑。我建议用Python标准库的logging模块,配置两个handler:一个输出到终端,一个输出到文件。日志级别要能通过命令行参数控制:
python convert_chroma_to_advantest.py \ --input ./patterns/chroma_dut1.pattern \ --output ./patterns/advantest_dut1.pattern \ --config ./config/project_dut1.yaml \ --log-level DEBUG日志文件命名带上时间戳,例如convert_log_20240115_143022.log。这个文件可以直接作为测试记录的一部分归档,也可以回溯问题时当证据。
6.3 回归用例:用一个“黄金pattern”套住脚本的底线
脚本改一改就弄坏了之前正常的功能,这是工程上最常见的问题。我建议在脚本仓库里放一个test/目录,里面存几组典型的“Chroma源文件 + 人工确认过正确的爱德万目标文件”作为回归基准。
每次改完脚本,跑一遍回归测试:
pytest test/regression/test_conversion.py回归用例至少覆盖这几种场景:
- 一个最简单的全输入pattern
- 一个带开漏输出和mask位的pattern
- 一个带三层循环嵌套的复杂pattern
- 一个信号名需要重新映射、引脚顺序需要重排的pattern
我每次改完代码都会跑这组用例,通过后再集成到正式转换流程。这一招帮我挡住了至少三次“改了这个功能结果那个功能坏了”的尴尬。
6.4 我踩过的最后几个小坑
最后补充几个跟具体格式相关的小坑,都是我曾经花过半小时以上排查的:
第一,大小写敏感问题。Chroma某些版本导出的信号名全大写,爱德万那边可能区分大小写。如果映射表里写的引脚名大小写跟文件里不完全一致,直接替换就会漏。建议在解析阶段统一upper()处理,映射配置里也统一大写。
第二,BOM和换行符问题。Windows下用记事本另存过Chroma的pattern文件后,可能带上UTF-8 BOM,爱德万的编译器遇到BOM可能直接报错或解析异常。脚本读取文件时最好显式处理编码:
with open(filepath, "r", encoding="utf-8-sig") as f: content = f.read()换行符也建议统一转成目标平台能接受的格式。
第三,空行和注释的保留策略。转换后的爱德万pattern不需要完整保留Chroma注释,但有些工程师习惯靠注释里的信息来追踪pattern行来源。我建议转换脚本默认把Chroma里的标签类注释(比如label1:这种)转成爱德万的标签,而普通说明性注释默认丢弃,只在控制流关键位置自动生成带行号的注释信息。
第四,二进制pattern要先转文本。有些老型号的Chroma机台,pattern导出时可能包含二进制段或压缩段,脚本直接按文本读会乱码。遇到这种情况,需要先用Chroma自带的工具或插件把pattern转成完整的文本格式,再交给转换脚本。这一步没有现成代码可以给,因为跟你们厂里的工具版本强相关,但方向一定要清楚。
写在最后的实际操作体会
这套转换脚本从最开始一个粗糙的正则加替换,逐步迭代到现在这个结构,前后大概用了两周的碎片时间。最大的感受是:写转换脚本最值钱的不是那些转换逻辑,而是你肯不肯花时间去理解两个平台各自的“表达习惯”。
很多做测试的同事一听“写脚本做pattern转换”,都觉得是个体力活,代码量不大,跑通就行。但真正在这个行业里干久了就会知道,pattern本身就是一套精密的时序和状态协议,里面一个掩码位错了、一个时序沿偏了,最后在产线上就是批量性的良率损失,排查起来论小时计。
我现在每次做平台迁移,都会先把这套脚本跑一遍,再用第5章说的三步验证法在机台上确认,整个过程下来非常稳。如果你也在做Chroma到爱德万的pattern移植,可以先按“解析-映射-生成”的骨架搭一版,遇到具体格式差异再逐步往映射规则里补。等你把第一批几十个pattern全部转完还能一遍通过的时候,就会理解这套方法的价值在哪里。