GTKWave 脚本化导出 PNG:无头服务器上的波形出图工作流实测
更新时间:2026-10-25。本文是「EDA 实验室 · 波形」层的一篇实验文。iverilog VCD 波形 解决了「VCD 怎么从仿真里生出来、怎么瘦身」,波形查看工具对比 在有 Xvfb 的容器里对比过三条查看路线;本篇聚焦一个更具体的工程动作——在完全没有显示器的 CI/云主机上,把 VCD 变成一张能放进文档的波形图。我们按教科书做法先走
gtkwave --script脚本化导出 PNG,实跑抓到无头环境的真实报错,再走 VCD→WaveJSON 的兜底路线,两条路的边界用实测划清。
本文要回答的问题
- GTKWave 的
--scriptTcl 脚本导出 PNG 标准流程长什么样? - 无头(无 DISPLAY)环境下直接跑会得到什么报错?报错之后 GTKWave 干了什么?
- 不能出 PNG 时,VCD 转 WaveJSON 的兜底路线怎么走?产物质量如何?
- 三条出图路线(GTKWave 脚本、Xvfb 虚拟显示、WaveJSON)各自适合什么场景?
先给结论卡,数字全部来自本次真实运行日志:
读这张卡抓主线:本实验不是「成功出图秀」,而是一次路线边界实测——gtkwave --script 这条路在没有显示环境时产出是 0 张,报错信息直白但行为有坑(它不退出而是打印用法继续等);真正救场的是把 VCD 解析成 WaveJSON 交给 wavedrom 渲染。下面按实跑顺序展开。
一、工作流全景:从 VCD 到文档插图的三个环节
先把整条工作流画出来,后面每节对应图里一个环节(红色是本次实测抓到的断点):
读这张图抓住两条路线:上支路是「渲染路线」——GTKWave 本质上是个 GUI 程序,脚本模式只是不给它开窗口,渲染管线仍然依赖 GTK 图形栈,所以它对显示环境的要求是硬依赖;下支路是「解析路线」——把 VCD 当纯文本解析成值变化序列,重写成 WaveJSON,再让 wavedrom 这个纯数据驱动的渲染器出图,全程不碰任何图形库。红色节点就是本次实验的核心发现:两条路线的分水岭不在功能,在「环境里有没有 GTK」。
二、仿真产 VCD:与路线无关的第一步
第一步没有悬念,用 iverilog 跑一个带 clk / req / ack / cnt 四个信号的小 testbench:
iverilog -g2012 -o wave.vvp tb_wave.v && vvp wave.vvp仿真结束后 ls -l 确认产物 wave_demo.vcd 共 744 字节——刻意做小的教具:一个时钟、一对请求/应答握手、一个 4 位计数器,信号少到能在一屏波形里全看清,又覆盖单比特电平与多比特总线两种形态,足够检验出图路线对两类信号的处理。req/ack 这对握手信号是特意加的:出图路线如果连「两拍请求、三拍后应答」这种基本时序关系都画不清楚,就不配进文档。VCD 怎么控制大小、层级与时间窗,见 iverilog VCD 波形,本篇不再展开。
三、教科书路线实跑:gtkwave --script 在无头环境下翻车
GTKWave 官方提供的脚本化出图方式是写一个 Tcl 脚本,典型的 dump.tcl 内容骨架如下:
# dump.tcl —— GTKWave 脚本化出图骨架
set dumpfile [lindex $argv 0]
gtkwave::loadFile $dumpfile
# 把要展示的信号逐个加进波形窗口
gtkwave::addSignalsFromList {tb.clk tb.req tb.ack tb.cnt}
# 缩放到全波形(zoom full),保证截图信息完整
gtkwave::/Time/Zoom/Zoom_Full
# 导出当前波形视图为 PNG
gtkwave::/File/Export/Write_Image out.png脚本的核心动作就三个:加载波形、加信号、导出图像,中间用 zoom full 保证所有信号完整入画。然后一条命令执行:
gtkwave --script dump.tcl --dumpfile out.png wave_demo.vcd在有显示器的桌面上这条路能走通;本次在云主机容器里真实执行的结果如下:
读这份实录抓三个位置。第一,Could not initialize GTK! Is DISPLAY env var/xhost set?——GTKWave 在初始化图形栈时直接失败,这发生在它读 VCD 之前,所以后面无论脚本写什么都没用。第二,报错之后它不是安静退出,而是把整屏 40 多行用法帮助打出来——这个行为在 CI 里是个坑:命令退出码与日志都会显得「跑过了」,只有检查产物文件是否存在才能发现失败,所以我们的脚本里用 [ -s out.png ] 做产物判定,而不是看退出码。第三,判定失败后脚本打印 FALLBACK: ... using wavedrom JSON path 转入兜底路线。报错原文摘录:
+ gtkwave --script dump.tcl --dumpfile out.png wave_demo.vcd
+ tee gtkwave.log
Could not initialize GTK! Is DISPLAY env var/xhost set?
Usage: gtkwave [OPTION]... [DUMPFILE] [SAVEFILE] [RCFILE]
-n, --nocli=DIRPATH use file requester for dumpfile name
-f, --dump=FILE specify dumpfile name
-F, --fastload generate/use VCD recoder fastload files
...顺带说清另一个常见误解:有资料说「GTKWave 出图用 xvfb-run 包一层就行」——这是对的,但前提是镜像里装得下 Xvfb 和 GTK 运行库,波形查看工具对比 的 geek-eda:0.2 容器就是这么干的,代价是镜像体积与依赖链明显变大。本次实测的镜像是刻意保持精简的 CI 镜像,没有这套图形栈,于是逼出了下一条路线。
把「为什么 gtkwave 无头失败不是配置问题」再说透一层:--script 只是让 GTKWave 不弹交互窗口、改为执行脚本,但它的波形渲染仍然走 GTK 的绘图后端,进程启动时必须连上一个 X display——没有 DISPLAY 变量、没有 Xvfb 虚拟屏,GTK 初始化这关就过不去。换句话说这不是「缺个参数」的问题,而是「缺整个图形栈」的问题,这也正是判断一条出图路线能否进 CI 的第一性问题:数它的进程级依赖里有没有图形库。
四、兜底路线:VCD 解析成 WaveJSON
解析路线的核心是一个几十行的 Python 脚本 vcd2wavedrom.py。它的工作原理分两步:先读 VCD 头部的 $var 定义段,拿到信号名、位宽与 VCD 内部标识符(如 !、"、# 这类单字符代号)的映射;再扫值变化段,每条记录是 #时间戳 行加上 b1010 x(多比特)或 0a(单比特)这样的取值更新,脚本按命令行给定的窗口与采样格数把连续的值变化「量化」成每格一个符号的 wave 串——单比特信号用 0/1 电平符号,多比特总线用 = 数据相位并配 data 数组记录每拍的十六进制值。真实执行输出摘录:
+ /opt/eda-venv/bin/python /tmp/evidence-src/exp-waveform-png-gtkwave-surfer/vcd2wavedrom.py wave_demo.vcd wave.json clk,req,ack,cnt 10000 40 cnt:4
wrote wave.json with signals: clk,req,ack,cnt
clk wave=0000000000000000111111111111111111111111 data=
req wave=0001110011100000000000000000000000000000 data=
ack wave=0000011000110000000000000000000000000000 data=
cnt wave========================================= data=['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13']这几行是转换的自证:四个信号全部提取成功,cnt 作为多比特信号走了数据相位路径,14 个采样值 0 到 13 一字排开。命令行的参数语义是:信号清单 clk,req,ack,cnt、采样窗口 10000(时间单位)、40 个采样格、cnt:4 表示 cnt 按 4 位宽总线处理。产物 wave.json 交给文档构建期的 wavedrom 渲染器画成自包含 SVG——也就是本站所有波形图用的同一条管线。WaveJSON 的写法细节(wave 串周期位、data 数组、分组)在本站 wavedrom 配图规范里有完整说明,这里只强调它对文档场景的两个天然优势:产物是文本、可 diff 可 review;渲染不依赖任何 GUI 库、CI 友好。
兜底路线的局限也要说透:它适合「已知要展示哪几个信号、哪段窗口」的文档插图场景——信号清单和窗口是写死在命令里的;它替代不了 GTKWave 的交互探索(拖时间轴、临时加信号、游标测量)。两者的关系是「出片」与「探案」,不是互相取代。
再看一眼产物的实际形态,下面是从 wave.json 头部原样摘录的一段(节选):
{
"signal": [
{
"name": "clk",
"wave": "0000000000000000111111111111111111111111"
},
{
"name": "req",
"wave": "0001110011100000000000000000000000000000"
},这个 JSON 本身就是「文档素材」的含义所在:clk 前 18 格为低后 18 格为高,正好对应 testbench 里前半程时钟未起振、后半程以固定周期翻转的设计;req 的三段高电平脉冲对应三次请求。整份文件 40 行上下,进 git 后任何一次波形改动都是可读的文本 diff——这是 PNG 路线给不了的工程属性。文档构建期 wavedrom 渲染器拿到这份 JSON 后直接画成自包含 SVG,页面上看到的波形与这里的 wave 串一一对应,中间没有任何不可审计的环节。
本次运行还留了一个可下载的中间产物 wave.json,连同日志一起归档——将来想换窗口、换信号重新出图,不需要重跑仿真,改一行命令参数对这个 JSON 再生成即可。这种「仿真一次、素材多次再生」的分层,是文档型实验区别于一次性调试的核心工程习惯。
五、三条路线怎么选
把本次实测与既有经验折成一张选型表:
| 场景 | 路线 | 依据 |
|---|---|---|
| 桌面/本地调试,顺手截张图 | GTKWave GUI 里直接导出 | 交互确定窗口与信号后一键出图 |
| CI/云主机,镜像里有 Xvfb | xvfb-run gtkwave --script ... | 保真度最高,代价是镜像带整套 GTK |
| CI/文档站,精简镜像 | VCD → WaveJSON → wavedrom | 零图形依赖,产物文本化可进 git |
| 大波形交互探索 | GTKWave GUI(FST 格式) | 脚本/解析路线都不为探索而生 |
两条实操提醒。其一,任何脚本化出图都要做产物判定([ -s out.png ] 或检查文件大小),GTKWave 失败时打帮助继续跑的行为会让「看退出码」的脚本误判成功。其二,文档场景优先 WaveJSON:图随源码走 review,改一行信号清单就能重出,不存在「那张 PNG 当时是用哪个 save 文件截的」这类考古问题。
把这套工作流落成日常习惯,推荐的动作序列是:仿真脚本里固定 tee 出仿真日志 → 出图脚本先探 command -v gtkwave 与 DISPLAY 是否可用 → 可用走脚本化 PNG、不可用直接走 vcd2wavedrom → 无论走哪条,最后都用「产物文件非空 + 关键词 grep」双判定收尾并打印一行 RESULT: PASS。本次实验的 run.sh 就是这个序列的最小实现,全程 118 行日志、三段 step 标记清晰可分,放在 CI 里每晚跑也没有负担。
这一篇在知识体系里的位置
- 上游产波形:iverilog VCD 波形(
$dumpvars层级控制与瘦身); - 横向对比:波形查看工具对比(wavedrom / GTKWave+Xvfb / 文本三轨的渲染效果实测);
- 仿真入口:iverilog 仿真入门(编译运行全流程)。
一句话总结
GTKWave 脚本化导出 PNG 在有显示环境一条命令即可,但无头容器里 gtkwave --script 会在初始化 GTK 时失败、打用法帮助却不报错退出——必须靠产物文件判定成败;精简镜像下的正解是把 VCD 解析成 WaveJSON 走 wavedrom 渲染,零图形依赖、产物文本化可进 git,本次实测 744 字节 VCD 提取 4 个信号 14 拍数据全程 PASS。
实验环境
- 机器/系统:云主机 S6.4XLARGE32 16C32G / Ubuntu 22.04(eda-golden-v1 镜像,无 X 显示环境)
- 工具版本:Icarus Verilog(iverilog -g2012)、GTKWave(/usr/bin/gtkwave)、Python(/opt/eda-venv/bin/python),照抄日志真实路径
- 日期:2026-10-25
- 完整命令:
bash run.sh(内部执行iverilog -g2012 -o wave.vvp tb_wave.v && vvp wave.vvp,随后gtkwave --script dump.tcl --dumpfile out.png wave_demo.vcd失败触发兜底,python vcd2wavedrom.py wave_demo.vcd wave.json clk,req,ack,cnt 10000 40 cnt:4)
代码获取
testbench、dump.tcl、vcd2wavedrom.py 与运行脚本已同步到公开仓库:geek-doc.cn/experiments/exp-waveform-png-gtkwave-surfer,克隆后进入该目录执行 bash run.sh 即可复现本文全部结果(有显示环境时 gtkwave 路线会直接出 PNG)。