Visualizing Digital Signals: Using GTKWave with Python
A practical, code-first guide to generating waveform (VCD) files in Python and viewing, styling, and automating them in GTKWave.
What is GTKWave?
GTKWave is a free, open-source waveform viewer used heavily in digital hardware design and verification. It reads standard trace formats like VCD (Value Change Dump), FST, and LXT2, and displays how digital (and analog) signals change over simulated time — essentially an oscilloscope for your simulation data.
It's most commonly paired with HDL simulators (Verilog/VHDL via Icarus Verilog, Verilator, GHDL, ModelSim, etc.), but you don't need an HDL simulator at all. Because VCD is just a well-documented plain-text format, Python can generate VCD files directly — which makes GTKWave useful for visualizing any time-based, discrete-event data: FSM states, protocol traces, sensor logs, algorithm internals, or actual digital logic simulated purely in Python.
- Installing GTKWave and the Python tooling
- Generating a VCD file from Python (manually and with the
pyvcdlibrary) - A worked example: simulating a simple digital counter and viewing it
- Loading, arranging, and saving views in GTKWave (
.gtkwsave files) - Automating GTKWave from Python (TCL scripting + subprocess)
- Tips, gotchas, and where to go next
1. Installing GTKWave and Python Tools
Install GTKWave
Ubuntu / Debian:
sudo apt update
sudo apt install gtkwave
macOS (Homebrew):
brew install --cask gtkwave
Windows: download the installer from the GTKWave SourceForge page, or install it as part of a toolchain like OSS CAD Suite.
Install the Python VCD library
We'll use pyvcd, a lightweight pure-Python library purpose-built for writing VCD files (no HDL simulator required).
pip install pyvcd
2. Writing a VCD File by Hand (Understanding the Format)
Before relying on a library, it helps to see what a VCD file actually looks like — it's just structured plain text:
$date
2026-08-15
$end
$version
Python hand-written example
$end
$timescale 1ns $end
$scope module top $end
$var wire 1 ! clk $end
$var wire 1 " reset $end
$var wire 8 # counter $end
$upscope $end
$enddefinitions $end
#0
0!
1"
b00000000 #
#5
1!
#10
0!
0"
#15
1!
b00000001 #
#20
0!
#25
1!
b00000010 #
#30
0!
Each signal gets a short identifier (!, ", #), timestamps are prefixed with #,
and value changes are logged only when they happen — that's what makes VCD compact. This is exactly the structure
pyvcd generates for you programmatically.
3. Generating a VCD File with Python (pyvcd)
Let's simulate something concrete: a clock, an active-low reset, and a free-running 4-bit counter that increments on every rising clock edge.
import vcd
with open('counter.vcd', 'w') as f:
writer = vcd.VCDWriter(f, timescale='1 ns', date='2026-08-15')
# Register signals
clk = writer.register_var('top', 'clk', 'wire', size=1)
reset = writer.register_var('top', 'reset_n', 'wire', size=1)
counter = writer.register_var('top', 'counter', 'wire', size=4)
count_val = 0
time = 0
# Hold reset low for the first 10ns
writer.change(reset, time, 0)
writer.change(clk, time, 0)
writer.change(counter, time, 0)
time = 10
writer.change(reset, time, 1) # release reset
# Run 20 clock cycles, period = 10ns
for cycle in range(20):
# Rising edge
time += 5
writer.change(clk, time, 1)
count_val = (count_val + 1) % 16
writer.change(counter, time, count_val)
# Falling edge
time += 5
writer.change(clk, time, 0)
writer.close()
print("counter.vcd written successfully.")
Run it:
python3 generate_counter_vcd.py
Then open the result directly in GTKWave:
gtkwave counter.vcd
pyvcd also supports real/analog values, vectors, string ("event") types, and hierarchical scopes,
so you can model buses, FSM state names, or even floating-point sensor data — not just single bits.
4. Viewing the Waveform in GTKWave
Once GTKWave opens:
- In the SST pane (top-left), click on
topto expand the module scope. - Select
clk,reset_n, andcounterfrom the signal list. - Click Insert (or drag them) into the waveform pane on the right.
- Right-click
counter→ Data Format → choose Unsigned Decimal or Hex to read bus values easily instead of raw binary. - Use the zoom controls (or scroll wheel) to fit the full trace, and the toolbar's Zoom Fit button for a quick reset.
5. Saving and Reusing Your View (.gtkw files)
After arranging signals, colors, and zoom level the way you like, save the session:
File → Write Save File (or Ctrl+S) — this produces a .gtkw file, which is itself a TCL-like
script GTKWave replays to restore your exact layout.
gtkwave counter.vcd counter.gtkw
This means you can commit .gtkw files alongside your Python scripts so teammates (or future you) get the same
signal layout every time, without manually re-adding signals.
6. Automating GTKWave from Python
GTKWave has a built-in TCL console and supports being driven via TCL scripts and its VCD "shared memory" interface. For most Python workflows, the simplest and most robust approach is to generate a TCL init script from Python and pass it to GTKWave on launch.
Example: auto-load signals via a generated TCL script
tcl_script = """
gtkwave::/File/Open_Trace {counter.vcd}
gtkwave::addSignalsFromList {top.clk top.reset_n top.counter}
gtkwave::/Edit/Set_Trace_Max_Time
gtkwave::/Time/Zoom/Zoom_Full
"""
with open('load_view.tcl', 'w') as f:
f.write(tcl_script)
Then launch GTKWave with the script:
gtkwave -T load_view.tcl counter.vcd
Launching GTKWave from a Python script
import subprocess
subprocess.run(['gtkwave', '-T', 'load_view.tcl', 'counter.vcd'])
This is handy for a one-command workflow: run a Python script that (1) simulates your logic, (2) writes the VCD, (3) writes a matching TCL layout script, and (4) pops open GTKWave already configured — no manual clicking required.
import vcd
import subprocess
def simulate_and_write_vcd(path='counter.vcd'):
with open(path, 'w') as f:
writer = vcd.VCDWriter(f, timescale='1 ns', date='2026-08-15')
clk = writer.register_var('top', 'clk', 'wire', size=1)
counter = writer.register_var('top', 'counter', 'wire', size=4)
val, t = 0, 0
for cycle in range(16):
t += 5
writer.change(clk, t, 1)
val = (val + 1) % 16
writer.change(counter, t, val)
t += 5
writer.change(clk, t, 0)
writer.close()
def write_tcl_layout(path='load_view.tcl'):
with open(path, 'w') as f:
f.write("""
gtkwave::addSignalsFromList {top.clk top.counter}
gtkwave::/Time/Zoom/Zoom_Full
""")
def open_gtkwave(vcd_path='counter.vcd', tcl_path='load_view.tcl'):
subprocess.run(['gtkwave', '-T', tcl_path, vcd_path])
if __name__ == '__main__':
simulate_and_write_vcd()
write_tcl_layout()
open_gtkwave()
7. Practical Tips
- Use FST for large traces. VCD is human-readable but bulky. For long simulations, convert with
vcd2fst(bundled with GTKWave) for much faster loading. - Group related signals in GTKWave with Edit → Insert Blank/Comment to visually separate clocks, buses, and control signals.
- Use markers (press
Mthen click a signal edge) to measure timing between two events. - Save your
.gtkwfile into version control alongside the Python script that generates the VCD, so the view is reproducible. - If you only need static, ready-made images for documentation rather than an interactive viewer, GTKWave can also export views to PostScript/PDF via File → Print to File.
8. Where to Go Next
If you're working with actual HDL (Verilog/VHDL) rather than pure-Python models, tools like cocotb let you write testbenches in Python that drive an HDL simulator (Icarus Verilog, Verilator, GHDL) and automatically dump VCD — GTKWave then visualizes the results exactly as shown above, no manual VCD writing required.
Written for a Blogger post — feel free to copy this HTML directly into Blogger's "HTML view" editor.

0 Comments