☰
WaveDrom离线包:JSON时序图进Git与批量SVG渲染
2026/10/27 6:27:10 网站建设 项目流程

简介:面向电子设计、FPGA开发与数字电路学习者,Wavedrom Editor v2.3.2 Windows 64位版提供可离线使用的时序图绘制环境,缓解官网访问不稳、在线工具受限的问题。软件以简洁文本语法描述波形,支持上升沿、下降沿、高低电平与脉冲等元素,并可添加注释、标签和颜色,编辑时实时预览,完成后可导出 SVG、PNG 或复制代码嵌入文档。资源包共124个文件,约83.72MB,涵盖pak与info资源文件、dll运行库、exe主程序以及html说明页等,目录完整,解压后即可在本地运行。已有2170人学习下载,适合需要反复绘制总线时序、接口波形和实验报告的工程师与学生,也便于在无网络环境中整理设计资料、复核信号关系并提升绘图效率。

1. wavedrom-editor 这个离线包,解决的是一张时序图进不了版本库的问题

接手一份别人写的接口文档,时序图只有截图,采样点偏了半个周期,评审会上三个人对着同一张图得出三种结论——这种事我遇到过不止一次。wavedrom-editor-v2.3.2-win-x64.zip正是针对这类情况:把 WaveDrom 那套「用 JSON 描述波形、由渲染器画图」的方案,打成一个 Windows x64 免安装桌面程序。解压后双击就能进编辑器,左边写文本,右边实时出图,改一个字符图就跟着变。它解决的其实更偏「时序图能不能进版本库、能不能 diff、能不能批量重出」,跟「画得好看」关系不大——图从二进制资产变成一行行可审阅的文本。适合写 FPGA/ASIC 接口时序的、维护芯片手册的、做嵌入式协议文档的,以及受够了拖拽式画图、想把图塞进 Git 的人。下面按解压启动、语法、脚本化、排错、团队化五段往下走。

2. 解压即用:win-x64 绿色包从落地到画出第一张图

2.1 先认清这是什么:Electron 打包产物,不是一个安装程序

这个 zip 里装的是一整套 Chromium + Node 运行时加上编辑器前端代码。常见目录结构大致如下,具体名字随打包配置略有出入,但分层逻辑是一致的:

位置内容能不能动
根目录主程序.exe启动入口,等同于一个定制过的浏览器外壳不能改名后指望快捷方式还生效
resources/app.asar打包后的界面与渲染逻辑,asar 是个归档不要改,改了下次升级全丢
resources/下的其他资源图标、内置示例、默认配置一般不动
若干.dll/*.binChromium、V8、图形栈的运行时不要单独替换
locales/界面语言资源可删多余语言,省体积但没必要

认清这一点很关键:它不写注册表、不装服务、不需要管理员权限,「卸载」就是把目录删掉。这也意味着,任何「我改了界面里某个默认值想让它全局生效」的想法都不成立——编辑器本身没有集中式配置中心,你的配置都放在你保存的.json文件里。

另一层含义是,它的启动速度和稳定性,跟你机器上的显卡驱动、杀毒软件、路径长度直接挂钩,而不是跟这个软件本身有多重有关。后面避坑章节里一大半的「翻车」都出在这一层。

2.2 解压路径:为什么我从不把它放进「下载」文件夹

三个现实约束,随便撞上一个就够折腾半小时。

第一是路径长度。Electron 应用解压出来是成千上万个小文件,嵌套层级深,Windows 传统路径上限 260 字符,放在C:\Users\某某\Downloads\新建文件夹 (2)\这种路径下,解压到一半报「文件名过长」是常态。第二是中文与空格,命令行渲染器、构建脚本在处理非 ASCII 路径时偶尔会出问题,问题还特别隐蔽,报错指向的是「文件不存在」。第三是同步盘,OneDrive、坚果云这类目录会实时扫描新文件,解压上万个文件的同时被扫描,速度能慢到你以为卡死了。

所以路径策略很简单:短、纯英文、不参与同步。D:\tools\wavedrom这种就行。

# 1) 建一个短路径、纯 ASCII 的落地目录,避开「下载」和同步盘 $dst = 'D:\tools\wavedrom' New-Item -ItemType Directory -Force -Path $dst | Out-Null # 2) 去掉 zip 上的「来自网络」标记,否则解压后每个文件都带 Zone.Identifier # 这一步能顺带免掉后面启动时的「Windows 已保护你的电脑」弹窗 Unblock-File -LiteralPath '.\wavedrom-editor-v2.3.2-win-x64.zip' # 3) 用 Expand-Archive 而不是资源管理器双击预览,长路径和大量小文件更稳 Expand-Archive -LiteralPath '.\wavedrom-editor-v2.3.2-win-x64.zip' -DestinationPath $dst -Force # 4) 自检:顶层文件和总大小,跟压缩包比明显偏小就是解压被截断了 Get-ChildItem -LiteralPath $dst | Select-Object Name, Length '{0:N1} MB' -f ((Get-ChildItem -LiteralPath $dst -Recurse -File | Measure-Object Length -Sum).Sum / 1MB)

-LiteralPath而不是-Path,是因为源码目录里经常出现方括号(比如wave[0-9].json),-Path会把方括号当通配符,导致「明明在却找不到」。-Force是覆盖已存在文件,重解压时省一次手工清理。最后那行统计体积不是为了好看:如果数字只有压缩包的一半,说明解压中途断了,直接删目录重来,别急着排查「为什么打不开」——那是在给一个不完整的东西找 bug。

2.3 第一次启动:三个开关和一次自检

主程序名以你上一步Get-ChildItem -Filter *.exe看到的结果为准,不同打包配置下名字不一样。用脚本取第一个 exe 是最省事的做法,也避免了你手抄错名字。

Set-Location 'D:\tools\wavedrom' # 取根目录第一个 exe 作为启动目标,避免猜名字 $exe = (Get-ChildItem -Filter *.exe | Select-Object -First 1).FullName $exe # 常规启动 & $exe # 场景 A:虚拟机、远程桌面、老显卡上窗口出来了但一片白 —— 关掉 GPU 加速 & $exe --disable-gpu # 场景 B:受限沙箱环境里进程直接退出,且没有任何窗口 —— 临时关沙箱验证 & $exe --no-sandbox

--disable-gpu关的是 Chromium 的硬件合成,代价是滚动和缩放会稍微发涩,但换来的是在各种虚拟显卡下都能出图,这是我给同事配环境时的默认建议。--no-sandbox只用来做一次「是不是权限问题」的定性判断,验证完就该撤掉,不要写进快捷方式天天用——它会让渲染进程失去隔离,长期跑一个开着未知来源 JSON 的编辑器,没必要冒这个险。

启动成功后做一次自检:新建文件,粘一段最小波形,看右侧是否立刻出图。出不来先别怀疑语法,把内容清空只留一个{"signal":[{"name":"a","wave":"01"}]},如果这个都不出图,问题在环境层,直接跳去看第 5 章。

2.4 建工作区:源文件和产物必须分家

我见过最常见的坏习惯,是把导出的 SVG、PNG 和源.json混在同一个文件夹里,然后一起提交。半年后没人说得清哪张图是从哪份源文件出的,改一个信号得挨个打开十几个文件试。

从一开始就分开:

project/ wave/ # 手写的源文件,进版本库 spi_read.json i2c_start.json build/ # 渲染产物,.gitignore 掉 svg/ png/ render.js # 批量渲染脚本

wave/里的每个文件对应一张图,文件名用「模块名_场景名」的格式。build/随时可删可重建,不进版本库。这条约定看着朴素,但它是后面第 4 章脚本化、第 6 章回归校验的前提——如果产物和源混在一起,git diff会被一堆二进制变化淹掉,你根本看不见「到底哪个信号被改了」。

编辑器里的操作流程也顺着这个来:打开wave/xxx.json,改,看图,另存回原路径;需要给别人看的时候再导出到build/。不要把编辑器当成唯一的存储,它没有自动保存,也没有回收站。

3. WaveDrom 语法:把一段握手协议压进十几行 JSON

3.1 最小骨架:signal 数组 + name + wave

整个 WaveDrom 文档就是一个对象,核心只有一个键signal,值是一个数组,数组里每个元素是一「行」波形。一行最少需要两个字段:name(左侧标签)和wave(波形字符串)。

{ signal: [ { name: "clk", wave: "p...." }, { name: "req", wave: "0.1.." }, { name: "ack", wave: "0..1." } ] }

wave字符串的第 n 个字符,对应时间轴上的第 n 个时间片。所以三行的长度应该对齐,否则图会按最长的一行撑开,短的行右边留白——不算错误,但看起来像漏画了。p是正边沿时钟,它自己会展开成周期性方波,后面的.只是占位延续;0.1..读作「低、保持、拉高、保持、保持」。

这套写法的核心价值在于「可读的文本即图形」。你把0.1..改成0.1.0,任何一个做数字逻辑的人扫一眼就知道「req 拉高后又落了」,不需要打开图片。

3.2 wave 字符表:这十来个符号够画九成协议

不同版本对个别字符的支持略有差异,以编辑器自带的示例文件为准。常用的这批是稳的:

字符含义典型用法
0/1低电平 / 高电平控制信号、使能、复位
.延续上一个时间片的状态占位,保持长度对齐
x不确定 / 无关项总线空闲、上电未定
z高阻双向总线的释放态
u/d弱上拉 / 弱下拉上拉电阻、开漏输出
p/P正边沿时钟,P末端带箭头时钟主干
n/N负边沿时钟下降沿采样的接口
l/h低/高电平但保留时钟边沿样式门控时钟
=或2~9数据段(不同数字是不同配色)总线、状态字

x和z是最容易被忽略、又最能体现专业度的两个:一个是「这里还没定」,一个是「这里没人驱动」,语义完全不同。很多手绘图把总线空闲期画成一条横线,读者只能猜是 0 还是高阻;用z画出来,多主总线的仲裁过程一下子就清楚了。

l/h用在门控时钟上:信号本身是常高的,但你要表达「它的边沿跟时钟对齐」,这时候用它比用1加标注更省事。

3.3 数据总线与 data 数组的对齐规则

一旦wave里出现=或2~9,就必须配data数组,数组元素按数据段出现的先后顺序一一对应。

{ signal: [ { name: "clk", wave: "p........." }, { name: "valid", wave: "0.1....0." }, { name: "data", wave: "x.3..4..x", data: ["ADDR", "DATA"] } ] }

data里只有两个元素,因为wave里只出现了3和4两个数据段,x不算。这是最容易出错的地方:如果你写了三个data元素但只有两个数据段,多出来的那个会被静默丢弃;反过来数据段比data多,后面的段会画成空白格子——不报错,只是图错了,而且错得很像「本来就这样」。改完图记得数一遍。

数据段从它出现的位置一直延伸到下一个数据段或x/z出现的位置。想让一段数据只占一格,就在它后面接x或者另一个数据段。想让标签在格子里偏左或偏右,用phase,见下一节。

另外,name也可以给数组,用来做两行标签,比如name: ["data", "DATA[7:0]"],上面一行短名、下面一行位宽,文档里很实用。

3.4 边沿箭头、node 与 phase/period 的配合

时序图上最值钱的信息往往在「谁跟着谁变」。WaveDrom 用node打锚点、用edge连箭头来表达这种因果关系。

{ signal: [ { name: "clk", wave: "p...........", period: 1 }, { name: "req", wave: "0.1........0.", node: "..a.......c." }, { name: "ack", wave: "0.......1...", node: "........b..." }, { name: "data", wave: "x.3....4..x.", data: ["D0", "D1"] }, { name: "state", wave: "2.........3.", data: ["IDLE", "DONE"] } ], edge: ["a~>b"], head: { text: "一次最小握手" }, foot: { text: "data 必须在 ack 拉高前稳定" }, config: { hscale: 2 } }

node字符串的长度必须和同一行的wave等长,字母是锚点名,.是空位。锚点的位置决定箭头端点落在哪一格,所以a放在req上升沿那一格(索引 2),b放在ack上升沿那一格(索引 8),箭头就从「请求拉高」指向「应答拉高」。

edge里的语法是起点~>终点,~画曲线,->画折线,-~之类的组合用来表达「先横后弯」。这条线一画上去,读者立刻知道这两个事件是因果关系而不是巧合。

几个参数的分工要分清,混了就白调:

参数写在哪改的是什么别和谁混
hscale顶层config整张图横向拉伸倍数,只影响版面period
period单行 signal 项该行时钟的周期长度,改变波形本身hscale
phase单行 signal 项标签在格内的水平偏移,0~1 之间period
skin顶层config配色与线条风格(default/narrow/lowkey/dark)无

hscale调大图变宽、字相对变小,是解决「信号太多挤在一起」的首选;period调大是「这个时钟就是慢一拍」,属于语义修改,别拿它当排版手段。phase的典型场景是数据标签:总线的数据有效窗口通常跨好几格,标签默认居中,用phase: 0.5或者-0.5把它推到窗口起始位置,看起来才像真实采样点。

foot那一行文本很重要,我自己定的规矩是:任何一张放进文档的时序图,foot里必须写一句约束条件,比如「data 必须在 ack 拉高前稳定」。图只能表达「发生了」,文字才能表达「必须这样」。两者缺一,读者就会把示意图当规范用。

4. 走出编辑器:把几十张图批量渲成 SVG 的流水线

4.1 为什么一定要脚本化

编辑器里一张一张点「导出」在十张图以内是可行的,超过二十张就开始出错——不是漏导,就是导出的版本对不上源文件的版本。更麻烦的是文档更新:接口改了一版,源文件改了五个,你得记住是哪五个,然后逐个打开导出。人记不住这个。

脚本化的收益不是省那点点击,而是把「渲染」变成一个可重复、可校验、可以在 CI 里跑的动作。源文件是唯一真相,产物随时可由源文件重建,这条链一旦建立,你就再也不会纠结「这张图是不是最新的」。

前提是有一个能命令行调用的渲染器。常见做法是装 Node 环境,用社区提供的 WaveDrom 命令行渲染器(wavedrom-cli),它接收一个输入文件、一个输出路径、可选的格式与尺寸参数。具体开关名各版本有差异,动手前先跑一次--help,以你本地那份为准,下面的脚本也把命令路径抽成了常量,方便替换。

4.2 批量渲染脚本:一次跑完整个目录

// render.js —— 把 wave/*.json 批量渲染成 build/svg/*.svg const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); const SRC = path.resolve(__dirname, 'wave'); const OUT = path.resolve(__dirname, 'build/svg'); // 命令行渲染器的路径,按你本地实际安装位置改; // Windows 下 npm 装的全局命令通常带 .cmd 后缀 const CLI = process.env.WAVEDROM_CLI || 'wavedrom-cli'; fs.mkdirSync(OUT, { recursive: true }); const files = fs.readdirSync(SRC).filter(f => f.endsWith('.json')); let ok = 0; const failed = []; for (const f of files) { const input = path.join(SRC, f); const output = path.join(OUT, f.replace(/\.json$/, '.svg')); try { // stdio 用 pipe,避免几十个文件刷屏;失败时再把 stderr 拿出来 execFileSync(CLI, ['-i', input, '-s', output], { stdio: 'pipe' }); ok += 1; } catch (err) { // 关键:不要遇到第一个错误就退出,否则你一次只能修一个问题 failed.push({ file: f, reason: (err.stderr ? err.stderr.toString() : err.message).split('\n')[0] }); } } console.log(`rendered ${ok}/${files.length}`); if (failed.length) { console.table(failed); process.exitCode = 1; // 让 CI 能感知失败,本地跑也不会误判为成功 }

几个设计取舍值得说清楚。

用execFileSync而不是execSync:前者不经过 shell,文件名里有空格、括号、方括号都不会被解释成命令的一部分。这在批量处理时是硬需求,我吃过这个亏——一个叫spi (v2).json的文件让整个脚本在第三张图上停住,报的错还完全指向别处。

失败收集而不是快速失败:批量脚本最容易犯的错是「出错就throw」,结果你修一个跑一次,十张图修十轮。把失败收集到数组里,一次跑完一次看全,效率差好几倍。

process.exitCode而不是process.exit(1):前者让 Node 自然退出,console.table的输出能完整刷出来;后者有可能在输出缓冲还没写完时就掐断进程,你看到的是半张表。

-i是输入文件,-s是输出 SVG,此外通常还有输出 PNG 的开关和尺寸参数。渲染尺寸这类东西我建议写在源文件里(config.hscale),不要靠命令行参数覆盖——参数只存在于你的终端历史里,源文件才会跟着 Git 走。半年后别人重跑脚本,得到的结果必须和你一样,否则这条流水线就是不可信的。

4.3 输出格式与参数选择:SVG 还是 PNG

维度SVGPNG
缩放无损,任意放大放大会糊
git diff能看到路径和文本变化只显示「二进制文件不同」
字体依赖依赖打开方的字体已栅格化,不依赖
适合场景内部文档、网页、需要审阅改动对外交付、贴进 Word/PPT

我的做法是两份都出:build/svg/进版本库用于审阅,build/png/按 2 倍像素密度导出用于交付。这样评审时能看见「哪条线动了」,交付时对方也不会因为缺字体看到一堆方块。

尺寸上有一个经验值:hscale设在 2 到 3 之间,出来的 SVG 宽度大概能塞进 A4 竖版的正文区。超过 4 就得考虑拆图——一张横向要滚动的时序图,读者大概率只会看第一屏。

4.4 把图嵌回文档:路径稳定比什么都重要

渲染脚本跑通之后,文档里引用的应该是相对路径,并且这个相对路径在仓库内保持稳定:

![SPI 读时序](../build/svg/spi_read.svg)

这里有个小坑:不少 Markdown 渲染器对 SVG 的支持不一致,有的能内联、有的只认 PNG。稳妥做法是文档里引 PNG(交付形态),同时在同目录留一份同名 SVG 供审阅,靠脚本保证两者永远同源。别手工维护两份,那必然不同步。

5. 避坑排查:白屏、整图不出、标签串位的五个现场

5.1 启动层的两个坑

现象一:双击 exe 弹出「Windows 已保护你的电脑」,点「更多信息」也没有「仍要运行」。

原因是 zip 从网络下载后带了 Mark-of-the-Web 标记,解压出来的每个文件都继承了Zone.Identifier附加数据流。部分企业策略下这个提示会被直接拦死,连绕过入口都不给。

解决:解压之前对 zip 本身执行Unblock-File,再解压,标记就不会传播。已经解压了的,对整个目录递归解除:

Get-ChildItem -LiteralPath 'D:\tools\wavedrom' -Recurse -File | Unblock-File

现象二:窗口能出来,标题栏也在,内容区一片白,等十分钟也不变。

八成是 GPU 合成在虚拟显卡或远程桌面会话里崩了,渲染进程没起来。判断方法很简单:加--disable-gpu重启,如果立刻出图,就是这个原因。

解决:日常就用带--disable-gpu的快捷方式。如果关掉 GPU 还是白屏,检查是不是杀毒软件拦了子进程——Electron 会拉起多个渲染进程,某些策略下它们被逐个干掉,表现出来就是「主窗口在,内容空白」。把安装目录加进白名单再试。

5.2 语法层的两个坑

现象三:整张图完全不出来,右侧一片空白,连坐标轴都没有。

不是渲染器坏了,是 JSON 解析失败。常见诱因有三个:一是用了单引号或者键名没加引号(严格 JSON 不允许);二是末尾多了逗号;三是写了//注释。.json5后缀在部分版本里能吃注释和尾逗号,但.json不行,而很多人是从别处复制的片段,后缀和内容对不上。

还有一个更隐蔽的:edge里引用了不存在的node名。整份文档语法完全合法,但渲染阶段找不到锚点,直接整体失败。

解决:二分法定位。先把edge和head/foot全删掉,只留signal,能出图说明问题在附加字段;再把signal一行一行删,删到某一行突然好了,问题就在上一行。这套流程比盯着屏幕找逗号快得多,也是我处理这类「黑匣子式空白」的固定套路。

现象四:data标签整体串位,第一个标签跑到了第二个格子上。

data数组长度和数据段数量不一致。注意x、z、u、d都不消耗data,只有=和2~9消耗。很多人在总线上用x表示空闲、用3表示数据,数的时候把x也算进去了,于是整体偏移一位。

解决:把wave字符串抄出来,逐字符标一遍哪些是数据段,数完再跟data.length对。别靠眼睛扫,写下来数。

5.3 输出与协作层的一个坑

现象五:导出的 PNG 右边被切掉一截,或者中文脚注在同事机器上变成方块。

前者是hscale太大而导出宽度被固定住了,图超出画布就被裁。后者是 SVG 里的文字只存了font-family名字,栅格化和渲染都依赖打开方那一侧装了什么字体。

解决:裁切问题优先改hscale而不是加宽画布,因为加宽画布只是把问题推到下一张更宽的图;实在要保留宽图,导出 SVG 再用矢量工具转 PNG,让转换器按内容自适应。字体问题只有一个可靠答案——对外交付用 PNG,并且在自己的机器上完成栅格化;对内协作用 SVG,但约定一套大家都装了的等宽字体。

6. 进阶:把时序图做成能 diff、能回归的团队资产

单机画图做到第 4 章就够了,但要让它在一个团队里活过两个版本迭代,还得再往前一步:把图当代码管。

我现在的做法是三条约定加一个校验脚本。约定一,文件名格式固定为模块名_场景名.json,模块名和源码目录一致,这样搜一个模块能同时搜到 RTL 和时序图。约定二,任何一张图的foot必须有一句约束,没有约束的图不允许进文档评审。约定三,build/全部gitignore,产物由脚本重建,任何人本地看到的图都必须能由wave/复现。

校验脚本其实很短,核心就一句:渲染前后比对哈希,源文件没变而产物变了,说明渲染器版本或环境被人动过。

// check.js —— 源文件未变时,产物必须不变 const fs = require('fs'); const crypto = require('crypto'); const path = require('path'); const hash = p => crypto.createHash('sha1') .update(fs.readFileSync(p)).digest('hex'); const SRC = 'wave', OUT = 'build/svg'; const drift = []; for (const f of fs.readdirSync(SRC).filter(f => f.endsWith('.json'))) { const svg = path.join(OUT, f.replace(/\.json$/, '.svg')); if (!fs.existsSync(svg)) { drift.push([f, 'missing']); continue; } // 把源文件哈希写进产物注释里(渲染后追加即可),下次比对就能查出漂移 const stamp = hash(path.join(SRC, f)).slice(0, 8); if (!fs.readFileSync(svg, 'utf8').includes(stamp)) drift.push([f, 'stale']); } if (drift.length) { console.table(drift); process.exitCode = 1; }

这段脚本不追求严谨,追求的是「有人改了源文件忘了重渲染」这件事一定能被发现。真实项目里,图过时比图画错更常见,也更难查——图画错了评审能看出来,图过时了没人知道。

最后一个我自己的习惯:每张图的源文件顶部留一个// rev: 2024-xx-xx by A的注释行(.json不支持注释就放在同目录的.md里)。不是因为工具需要,是因为半年后你会想知道这张图当初为什么画成这样、对应哪版接口。血泪经验,图本身不会告诉你这些上下文,只有当时留下的那行字会。

这套东西做下来,投入大概是半天配环境加一天写脚本,换来的是每次接口变更后十分钟内全量重出文档。值不值得,取决于你一年要改几次时序。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询