HspiceToolbox 入门:用 Python 批量解析 HSPICE 仿真数据与波形
简介面向电路设计工程师与研究人员的MATLAB Hspice Toolbox辅助工具包将Hspice仿真能力无缝接入MATLAB环境简化仿真脚本编写、波形查看与数据处理流程适用于模拟/混合信号电路验证、噪声分析、电源完整性评估及敏感性分析等场景。压缩包共47个文件大小仅91KB以m脚本为主另含dll动态库、mexglx/mexsol平台接口、C源文件、说明文档与许可证文件等便于跨平台扩展与参考。已有457人学习下载适合希望提升电路仿真效率、开展参数优化与自动化分析的MATLAB用户。通过该工具用户可自动生成并解析Hspice仿真脚本利用MATLAB可视化波形并定制报告附带的说明文档和示例脚本也可辅助快速上手显著缩短从电路建模到仿真的周期。1. HspiceToolbox 是什么别再手工复制瞬态波形数据了做模拟电路或混合信号验证时HSPICE 跑完一波仿真留下的是一堆 .tr0、.mt0.csv、.sw0 文件。很多同学的第一反应是用文本编辑器打开搜索某个节点电压然后手工复制到 Excel 画波形。数据量大一点就露馅一次 10ms 的瞬态仿真采样点几万个复制粘贴根本做不了更别说多 corner、多参数版本对比时光是整理数据就要占掉大半天。HspiceToolbox 要解决的就是这件事把 HSPICE 的仿真输出变成可批量读取、可计算、可绘制的结构化数据再往上一层就是参数扫描、批量跑仿真、自动收集指标。它适合三类人需要反复看波形的模拟设计工程师做 post-layout 仿真的验证人员以及想搭自动化回归环境的 CAD 工程师。它不是某个固定软件的专属功能而是一套可以按自己项目需求堆起来的脚本集合。2. 先搞清 HSPICE 输出文件的品类tr0、mt0.csv 和 sw0 到底谁好啃动手写工具箱之前必须先搞清楚 HSPICE 到底吐出了哪些文件。因为工具的核心不是仿真器本身而是对仿真结果的解析。解析错了后面所有画图和统计都是白搭。2.1 HSPICE 默认输出文件类型与适用场景HSPICE 运行完成后常见输出包括 .lis打印列表、.tr0瞬态波形、.mt0.csv测量结果表、.sw0扫描参数表。近几年版本默认也会生成对应 csv 后缀的瞬态波形文件比如 .tr0.csv。它们之间存在明显的用途差异我梳理了一个实际工作中最常用的对照。文件内容解析难度最常用场景.lis日志、网表回显、错误信息低判断仿真是否收敛查报错.tr0瞬态波形二进制或 csv二进制版较高看时域波形、眼图、纹波.mt0.csv.measure 语句的汇总表低提取增益、带宽、功耗等特征值.sw0参数扫描与直流扫描结果中DC 扫描、PVT 扫描曲线我一般优先把 .mt0.csv 作为主数据源因为它是纯文本表格头部信息规整用 pandas 或 csv 模块都能直接读。至于二进制 .tr0不同版本的 HSPICE 头部结构不完全一样自己写通用解析器很容易踩到版本兼容性问题。常见做法是第一版工具箱先支持 .mt0.csv 和 .sw0把指标收集自动化跑通再考虑直接接二进制 .tr0。如果你手头只有 .tr0 二进制文件可以先用 HSPICE 自带的打印选项让结果同时输出 csv或者用新版命令将瞬态数据以文本形式导出这样工具始终只需要和 csv 打交道。2.2 从 .mt0.csv 入手第一行就要处理单位后缀.mt0.csv 打开之后长这样$DATA1 SOURCEHSPICE VERSIONX-2020.06 $DATE... $TITLE... $CURVE_DATA i(vdd), fout 2.3e-3, 1.5000e00 2.2e-3, 1.4999e00这里的坑不在格式而在单位。HSPICE 里 m 表示毫MEG 表示兆u 表示微n 表示纳。如果你按网表里的“C1 1pF”直接把数值塞进表里画横轴时发现时间轴差了 3 个数量级那就是单位后缀没换算。最稳妥的做法是统一转成国际单位后入库。我在工具里会维护一张换算表后缀名称换算系数T太1e12G吉1e9MEG兆1e6k千1e3m毫1e-3u微1e-6n纳1e-9p皮1e-12f飞1e-15注意网表中的 M 是毫而不是兆这与工程习惯相反。如果你本意是 1MΩ必须写 1MEG。这个设计经常导致别人从 .mt0.csv 读出来的电阻值小 1000 倍。我实际解析的做法是先抓符号和系数再做单位映射。下面的代码粗略展示了这个逻辑import re UNIT_SCALE { f: 1e-15, p: 1e-12, n: 1e-9, u: 1e-6, m: 1e-3, k: 1e3, meg: 1e6, g: 1e9, t: 1e12 } def parse_value(text): text text.strip().lower() match re.match(r^([-]?\d\.?\d*(?:[eE][-]?\d)?)([a-z])?$, text) if not match: raise ValueError(f无法解析数值: {text}) num float(match.group(1)) unit match.group(2) if unit: if unit not in UNIT_SCALE: raise ValueError(f未识别后缀: {unit}) num * UNIT_SCALE[unit] return num这段代码的核心是把“2.3e-3”和“2.3m”都转成 0.0023。匹配规则里先按科学计数法解析数字再单独提取后缀字母。如果文件中出现 X 或 L 这类兼容符号我的做法是直接报错而不是静默处理因为后续画图数据错了很难追查。注意HSPICE 对单位后缀大小写不敏感但 .mt0.csv 里输出的单位通常会写成完整拼写或缩写。如果遇到类似 MEG 这种三个字母后缀parse_value 里必须单独列出来否则匹配会失败。2.3 二进制 .tr0 不是不能读但不建议自己做通用解析器网上有一些公开解析工具能直接读二进制 .tr0但它们的头部解析与版本强相关。某次我帮别人看一个旧工艺库的仿真结果同样的解析代码在他机器上正常在我机器上读出的时间轴开头多出 8 个字节的偏移。后来发现是不同版本对节点名编码方式不同。如果你非要读二进制我见过的最稳做法是先用 HSPICE 命令行把 .tr0 转成 csv 再解析。比如hspice -i input.sp -o output再把输出设置成列表打印模式让瞬态节点数据直接落到 .lis 里然后从 .lis 按节点名抽取。代价是文件体积膨胀十几倍但至少不会因为二进制头解析失败导致整条链路卡死。所以这里给出一个原则能用文本格式解决的问题不要碰二进制。工具箱的第一版建议直接面向 .mt0.csv 和 .sw0 文本文件开发。3. 用 Python 搭一个最小可用的 HspiceToolbox从目录扫描到画图这一章给出一个可以直接抄作业的版本。我不喜欢一上来就引入重量级框架先用标准库加 matplotlib 就能把链路跑通。3.1 写一个最基础的 hspice_toolbox.py 脚本框架我习惯把这类工具组织成三个函数扫描目录、解析多个结果文件、汇总成表。下面展示一个精简但能用的脚本。import os import csv import glob from collections import defaultdict def find_results(root_dir, suffixmt0.csv): files [] for pattern in [**/*. suffix, **/* suffix.upper()]: files.extend(glob.glob(os.path.join(root_dir, pattern), recursiveTrue)) return sorted(files) def parse_mt0(path): rows [] with open(path, r, encodingutf-8, errorsreplace) as f: reader csv.reader(f) is_data False header None for line in reader: stripped [x.strip() for x in line if x.strip()] if not stripped: continue if stripped[0].startswith($): if stripped[0].startswith($CURVE): is_data True elif stripped[0].startswith($END): is_data False continue if not is_data: continue if header is None: header stripped[0].split(,) if len(header) 2: continue else: rows.append(stripped[0].split(,)) return header, rows def summarize(root_dir): table defaultdict(list) for path in find_results(root_dir): header, rows parse_mt0(path) for row in rows: key os.path.basename(os.path.dirname(path)) table[key].append(dict(zip(header, row))) return table if __name__ __main__: import json result summarize(./sim_output) print(json.dumps(result, indent2))逻辑说明find_results 用 glob 递归扫描 sim_output 下所有 mt0.csv 文件。parse_mt0 逐行判断当前是否处于 $CURVE_DATA 与 $END 之间只有该区域内的数据才被收集。header 首次出现时记录列名后面所有行都按该列名对齐。summarize 以仿真目录名作为分组 key得到一份以 case 为索引的结果表。参数说明suffix 参数缺省是 mt0.csv如果你想要让工具也解析 sw0只需要把 suffix 改成 sw0 再调用同一套解析逻辑。parse_mt0 里我故意没有对数值做单位换算因为换算最好放在统一入口做避免每个调用方各自处理一遍。encoding 用了 utf-8 加 errorsreplace这一项是给 Windows 生成的老式输出留的后路比直接报错省事得多。3.2 读取 .mt0.csv 后直接画图的完整示例解析出数据还不够工程师最终要看波形。下面这段代码读取单个 mt0.csv并把两列画成曲线带中文标签和网格适合快速核对。import matplotlib.pyplot as plt import pandas as pd def plot_mt0(path, xtime, yv(out)): header, rows parse_mt0(path) df pd.DataFrame(rows, columnsheader) for col in [x, y]: if col not in df.columns: raise KeyError(f列 {col} 不存在) df[col] pd.to_numeric(df[col], errorscoerce) df df.dropna(subset[x, y]) fig, ax plt.subplots(figsize(8, 4)) ax.plot(df[x], df[y], linewidth1) ax.set_xlabel(x) ax.set_ylabel(y) ax.set_title(Waveform from path) ax.grid(True) fig.tight_layout() return fig if __name__ __main__: fig plot_mt0(sim_output/amp_v1/amp_v1.mt0.csv) fig.savefig(amp_v1.png, dpi150)这段代码的关键点先把列名与单位前缀剥掉再用 pd.to_numeric 把数字字符串转成 float。dropna 只删除目标列缺失的行不影响整条数据链路。如果 HSPICE 输出中包含 NaN 或 INFpandas 会保留为浮点特殊值画图时可能出现断线这里我先 dropna把无效点剔除。实际使用中我一般会再包一层参数让 x 轴列名默认取 sweep 变量名y 轴列名默认取第一个非 time 列。对于多输出节点的情况读列头后自动按列遍历全部画到同一张图上方便快速对比。注意如果 csv 里列名本身带括号、带斜杠直接当 DataFrame 列名使用没问题但要避免用 df.xxx 取列统一用 df[v(out)]。3.3 如果团队已经用 MATLAB用 textscan 也能做到同样的事很多老团队离不了 MATLAB。与其强行推 Python不如给一段可平替的 MATLAB 解析脚本。我列出核心函数function [header, data] parse_mt0_matlab(filename) fh fopen(filename, r); header {}; data []; in_data false; while ~feof(fh) line fgetl(fh); if isempty(line) continue; end if startsWith(line, $CURVE_DATA) in_data true; continue; end if startsWith(line, $END) in_data false; break; end if ~in_data continue; end parts strsplit(line, ,); parts strtrim(parts); if isempty(header) header parts; else data(end1, :) str2double(parts); %#okAGROW end end fclose(fh); end这里的逻辑和 Python 版完全对应先找数据区再分离表头和数据。MATLAB 的 strtrim 只去首尾空格不会动科学计数法的中间部分。str2double 会自动处理 1.5e00 这种写法。如果你遇到某行数据不全data(end1, :) 这行会因为列数不匹配而报错建议在赋值前先判断 length(parts) 是否等于 length(header)不等则丢弃整行并打印警告。4. 把 HspiceToolbox 变成批量仿真流水线扫参、并行跑、结果入库解析零散结果只是第一步真正让工具产生价值的是批量能力一个 netlist 模板用脚本把参数一换循环进 hspice再把所有结果汇总到一张大表。4.1 用参数替换批量生成 spice 网表最常见的扫参场景是温度、电源电压、负载电容。我通常把模板里要替换的参数写成大写的占位符例如 {TEMP}、{VDD}、{CLOAD}。下面这段 Python 可以批量生成网表文件import os template 模拟项目X的基本netlist示例 .param vdd{vdd} .param temp{temp} .param cload{cload} VDD vdd 0 {vdd} Rin vin vdd 1k Cout vout 0 {cload} .tran 10n 10u .measure tran t50 when v(vout)vdd/2 cross1 .end cases [ {vdd: 1.8, temp: 25, cload: 2p}, {vdd: 1.8, temp: 85, cload: 2p}, {vdd: 1.62, temp: 25, cload: 2p}, ] for idx, case in enumerate(cases): content template.format(**case) filename fcase_{idx}.sp with open(filename, w) as f: f.write(content)这里用 str.format 直接替换占位符。注意 cload 我传的是字符串 2p而不是数字否则 format 出来变成 2网表里就缺少单位后缀。vdd 如果是 1.8 会输出 1.8HSPICE 能读但更稳妥的做法是全参数用字符串传值比如 1.8V 里的 V 在 HSPICE 里是单位后缀可写可不写。生成网表后下一步是逐条调用 hspice。4.2 批量执行 hspice 并判读返回状态不只看退出码很多人写循环调用时只看 subprocess 返回值是否为 0这是不全面的。HSPICE 在仿真中途遇到非收敛但能继续跑完的情况下退出码依然可能是 0。更可靠的做法是同时检查两个信号子进程退出码以及 .lis 文件里是否出现正常结束标记。#!/bin/bash for sp in case_*.sp; do log${sp%.sp}.lis hspice -i $sp -o ${sp%.sp} $log 21 ret$? if [ $ret -ne 0 ]; then echo [ERROR] $sp exit code $ret continue fi # 在 .lis 中查找正常终止标记不同版本关键词并不完全相同 if grep -qE simulation completed|job finished|End of Simulation $log; then echo [OK] $sp else echo [WARN] $sp 退出码为0但日志未见正常结束标记请人工检查 fi done逻辑说明每次调用 hspice 时用 -o 指定输出前缀这样 .lis、.tr0、.mt0.csv 的名字统一方便后续解析。ret 记录进程退出码非 0 直接跳过并打日志。退出码为 0 的并不代表收敛因此 grep 关键词再查一层避免漏网。关键词说明不同版本、不同工艺库的结束语不一致我的 grep 写了三种常见说法。如果你所在的版本输出固定在最后一行可以用 tail -n 1 查看后再手动确认一次比我这种多关键词匹配更可靠。4.3 把 .measure 结果合并成一张大表汇总所有 corner批量仿真后的收敛产物是每个 case 一个 .mt0.csv。下一步是把每个文件里的指标行汇总成一张 csv 表每行代表一个 case。这里直接给出 pandas 版import pandas as pd import glob records [] for mt0 in glob.glob(case_*.mt0.csv): header, rows parse_mt0(mt0) for row in rows: row_data dict(zip(header, row)) row_data[source_file] mt0 records.append(row_data) df pd.DataFrame(records) df.to_csv(all_summary.csv, indexFalse)所有 case 的测量结果被合并后可以直接按“温度”“电压”分组看趋势。如果某个指标缺失源数据里根本没那个列pandas 会自动补 NaN后续画图时注意 dropna。这里我故意把所有列都转成字符串后再输出是因为 .mt0.csv 里不同列的单位后缀不一样先保持原样统计时再按列转换。5. HspiceToolbox 避坑指南最容易翻车的 5 个细节这个领域技术本身不难难在结果可信。下面这几条都是实战中踩过的坑按现象、原因、解决写清楚值得贴在工位上。5.1 退出码为 0但仿真其实被中断了现象批量跑完所有 case 退出码都正常结果表也生成了。整理数据时发现某几个 case 的指标缺失严重对比 .lis 才发现仿真中途被 kill。原因HSPICE 在某些情况下捕获到信号后直接终止但 shell 层面的退出码可能仍为 0或者被脚本忽略。我遇到过服务器负载过高导致进程被杀的情况脚本里没有检查 .lis 中是否包含正常结束标记导致坏数据混入。解决所有批量脚本都必须双重判断退出码加日志关键词。如果 .lis 最后几行出现 aborted、terminated、alarm 等字样即使退出码为 0 也标记为失败。我的经验是把这段检查逻辑封装成一个函数每次调完 hspice 立刻执行不要等到最后统一排查否则出错时定位成本很高。5.2 Windows 环境下生成的 .mt0.csv 最后一行数据丢失现象同一套工具在 Windows 上跑完的仿真结果拷到 Linux 上解析最后一行数据永远是空值或直接少一行。原因Windows 写文本用 CRLF某些版本的 HSPICE 在末尾不写换行符。解析脚本按行读时最后一行可能因为缺少 LF 被某些库忽略或因为结尾带 \r 导致 float 转换失败。解决统一在脚本里做换行符归一化。读取时用content open(path, rb).read().decode(utf-8, errorsreplace) content content.replace(\r\n, \n).rstrip(\n)或者直接在 csv 模块的 open 参数里指定 newline让 csv 库处理换行。如果你用 pandas 的 read_csv加上 dtypestr 和 keep_default_naFalse 能减少很多奇怪的空值问题。5.3 m 和 MEG 之间差了 1000 倍单位换算是最大数据污染源现象功耗曲线整体比手册值小 1000 倍一开始以为是仿真模型问题查了半天发现是解析工具把 2.3m 当成 2.3 处理了。原因HSPICE 网表里只有 M 表示毫兆要用 MEG。手工写网表的人如果按 SI 直觉把 M 当兆部分仿真器版本会当成毫。更隐蔽的是输出 csv 中有些列自带 M 后缀有些列直接写原始数值加单位注释解析脚本不统一换算后续统计就全错。解决工具中强制所有单位换算。解析入口只接受两个状态要么全部按原始数值保存并带单位列要么全部转成国际单位。我选择后者因为后续计算方便。换算表放在一个独立文件任何新增单位后缀都改一处。同时对网表中的 M 特别注意如果电路里有 1M 电阻这种写法必须和设计者确认是 1MΩ 还是 1mΩ。5.4 不要用行首 * 判断网表注释现象批量生成网表时用脚本删掉所有以 * 开头的行结果把正常的电阻行删了仿真直接网表错误。原因HSPICE 兼容性设计里星号在行首是注释标记但如果星号后面跟的是具体器件名部分版本行为不一致。更常见的是连续多行注释中间夹了空行空行没法用 * 判断。解决注释处理交给 HSPICE 自己不要在工具链里做注释行过滤。如果非要清理用 .param 和 $ 型注释配合或者生成网表时直接不写注释行。我现在的做法是模板里不放多余注释所有参数说明放到网表外的描述文件里既安全又可追溯。5.5 路径分隔符和大小写混用导致结果文件找不到现象在 Linux 上跑通的工具拷到 Windows 上 glob 找不到文件路径里有反斜杠和正斜杠混在一起。原因Python 的 os.path.join 在不同平台默认分隔符不同Windows 下反斜杠在字符串里又被当作转义符。解决所有路径拼接统一用 pathlib.Path或只用正斜杠Python 的 glob 默认接受正斜杠。写路径时不写死绝对路径全部相对于脚本所在目录。我经常加一行 basename() 检查确认文件系统里确实存在预期文件不存在就报错而不是静默跳过。6. 最后一步让工具箱的数据可信靠的是交叉验证习惯工具能跑通不难难的是让所有人都信你从 HspiceToolbox 里拿出来的数字。我现在的做法是给工具链加上两道验证关卡。集成验证第一招是对关键指标做双重解析。我不只依赖自己的解析脚本还会用 HSPICE 官方的打印功能再导出一份结果两份结果放到一起做 diff。我习惯每次跑完批量仿真后用哈希对 .mt0.csv 原始文件做快照记录进 JSON 文件。下次重跑时对比哈希能看到哪些结果发生变化、哪次数据被覆盖。这对多版本对比的帮助很大。验证第二招是画图时叠加频谱检查。时域波形如果采样点不足画出来是平滑了但高频分量可能被丢光。我一般对瞬态输出先做一次 fft看最高幅值频点是否偏离预期。如果采样率差了 10 倍频谱上会出现明显混叠此时第一反应不是看波形而是回到仿真命令检查时间步长不要在设计层面找锅。哈希检查简单示例import hashlib import json def file_hash(path): h hashlib.sha256() with open(path, rb) as f: for chunk in iter(lambda: f.read(8192), b): h.update(chunk) return h.hexdigest() def snapshot(results_dir): snap {} for p in find_results(results_dir): snap[p] file_hash(p) with open(snapshot.json, w) as f: json.dump(snap, f, indent2)这段脚本把每个结果文件的哈希写入 snapshot.json。下次批量仿真前先做一次 snapshot跑完后再做一次用 diff 对比两个 JSON能一眼看出哪个 case 的输出发生了意外变化。这个方法成本低但能在多版本对比中救回不少头发。我现在的习惯是每次任何指标写入最终报告前必须能回答三个问题。第一个问题这份数据是哪个版本仿真器跑的。第二个问题解析脚本有没有对单位做过换算换算表是不是当前版本。第三个问题源文件哈希值是不是和第一次分析时一致。这三个问题问完数据可信度就有基本保障。这个习惯是从一个数据事故里学来的当时我把没跑完的仿真结果当正常数据处理了后面整个迭代周期都被一份错误数据带偏。从那以后哈希快照和结束标记检查就成了工具箱的默认动作再小的项目也不例外。希望这份记录对你搭自己的 HspiceToolbox 有帮助。本文还有配套的精品资源点击获取