VCD Basics - Reading VCD files¶
A VCD (Value Change Dump) file is a text-based format used to record signal value changes over time, starting with a header that defines metadata (e.g. the used timescale), and signal declarations.
Signals are declared using unique identifiers within a hierarchical scope, mapping human-readable names to compact symbols (e.g. a signal CLK becomes ^, RESET becomes !).
After the header, the value change section lists timestamped updates that show how each signal’s value changes as simulation time advances.
Simulating testbenches and dumping all signal traces to VCD format, it enables simulation-assisted optimization. This is done e.g. by extracting the signal traces and comparing them against each other, to find signals that always have the same values and thus form signal groups. If only a single VCD file is considered, these signals may have been only coincidentally equal all the time. The more different VCD data from test bench runs are analyzed, the more meaningful the individual signal groups become.
Signals of a signal group may be interchangeable, i.e. all signals but one of the group can be replaced by the remaining signal. Using a Logic Equivalence Check (e.g. using Yosys EQY), this transformation can be matched against the original circuit to prove whether the replaced signal(s) truly are functionally equivalent. If they are still equivalent, the transformation is valid. This transformation may allow for further simplifications (i.e. removing now-unused logic, that was related to the removed signals). Accordingly, the number of wires and instances withing the circuit may be reduced.
In this notebook, some basic functionality is shown on how to handle VCD files and apply simulation data to circuit elements.
Subsequently it is shown how to use these informations to automatically extract signal groups from the simulation data for optimization.
A simple example VCD file is given in files/sim/tb_adder_basics.vcd, which was generated by simulating the testbench files/sim/tb.sv.
from netlist_carpentry.io.vcd import VCDWaveform
wf = VCDWaveform("../files/sim/tb_adder_basics.vcd")
repr(wf)
'Waveform(1 Top Scope, 13 Variables)'
A waveform consists of scopes and variables.
The top scopes are the scopes on testbench level, e.g. the DUT instantiation, or other modules that are instantiated in the testbench.
Within a scope, there may be subscopes, e.g. the instances inside the DUT.
The variables are all variables tracked in the VCD file.
Execute the cell below to get informations about the top scopes of the given VCD waveform.
print(f"The waveform has {len(wf.top_scopes)} top scopes.")
print(f"The names of the top scopes are: {', '.join(scope.name for scope in wf.top_scopes)}")
print(f"The first scope has {len(wf.top_scopes[0].vars)} variables.")
print(f"In total, the waveform has {len(wf.all_vars)} variables.")
The waveform has 1 top scopes. The names of the top scopes are: tb_adder_basics The first scope has 6 variables. In total, the waveform has 13 variables.
Since it is now known that the waveform only contains a single scope, the scope can be further analyzed. Every scope has a list of variables that exist in this scope, i.e. the signals associated with this hierarchy layer. For a module instance, this is comparable with the wires existing inside the module. Furthermore, each scope may have subscopes. These are comparable with the submodule instances inside a module. Execute the cell below to retrieve the VCD variables from the scope as well as any subscopes.
scope = wf.top_scopes[0]
print(f"Scope {scope} has {len(scope.scopes)} subscope(s): {', '.join(subscope.name for subscope in scope.scopes)}")
for subscope in scope.scopes:
print(f"Subscope {subscope.name} has {len(subscope.vars)} variables:")
for var in subscope.vars:
print(f"\tVariable {var.name}, type {var.var_type}")
Scope module(tb_adder_basics) has 1 subscope(s): adder Subscope adder has 7 variables: Variable clk, type Wire Variable in1, type Wire Variable in2, type Wire Variable rst, type Wire Variable __0__out__8__0__, type Wire Variable WIDTH, type Parameter Variable out, type Reg
There are some more useful attributes to both the scope and variable classes.
Both feature a full_name attribute, which returns the full hierarchical path to the scope or signal.
This is comparable with instance paths and wire paths.
Furthermore, scopes also have a scope_type attribute, similar to the var_type attribute of VCD variables.
The scope type defines the type of the scope (big surprise), which normally is "module" for module instances, or "task" for testbench tasks.
Execute the cell below to retrieve the full name of the first (and only) subscope of the top scope, along with its type.
subscope = scope.scopes[0]
print(f"Full name of scope {subscope.name} is {subscope.full_name}.")
print(f"Scope type is '{subscope.scope_type}'.")
Full name of scope adder is tb_adder_basics.adder. Scope type is 'module'.
The most important information of the VCD waveform is probably how the signals behave.
This can be traced for each VCD variable using the all_changes attribute.
This attribute returns a list of 2-tuples, in which each signal value change is listed, where the first entry is the timestamp (based on the specified timescale) and the second element is the new signal value.
Execute the cell below to view the signal changes for all variables of the inner scope.
for var in subscope.vars:
print(f"{var.name}: {var.all_changes}")
clk: [(0, 'x'), (610, 0), (650, 1), (700, 0), (750, 1), (800, 0), (850, 1), (900, 0), (950, 1), (1000, 0), (1050, 1), (1100, 0), (1150, 1), (1200, 0), (1250, 1), (1300, 0), (1350, 1), (1400, 0), (1450, 1), (1500, 0), (1550, 1), (1600, 0), (1650, 1), (1700, 0), (1750, 1), (1800, 0), (1850, 1), (1900, 0), (1950, 1)] in1: [(0, 'xxxxxxxx'), (1350, 165), (1550, 111), (1750, 175)] in2: [(0, 'xxxxxxxx'), (1350, 15), (1550, 197), (1750, 207)] rst: [(0, 'x'), (310, 1), (610, 0), (1050, 1)] __0__out__8__0__: [(0, 'xxxxxxxxx'), (1350, 180), (1550, 308), (1750, 382)] WIDTH: [(0, 8)] out: [(0, 'xxxxxxxxx'), (310, 0), (650, 'xxxxxxxxx'), (1050, 0)]
If only the change times are relevant (i. e. the first element of the tuple), the change_times attribute can be used instead, which returns a list of integer values, representing the signal change timestamps.
for var in subscope.vars:
print(f"{var.name} changes at these times: {var.change_times}")
clk changes at these times: [0, 610, 650, 700, 750, 800, 850, 900, 950, 1000, 1050, 1100, 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600, 1650, 1700, 1750, 1800, 1850, 1900, 1950] in1 changes at these times: [0, 1350, 1550, 1750] in2 changes at these times: [0, 1350, 1550, 1750] rst changes at these times: [0, 310, 610, 1050] __0__out__8__0__ changes at these times: [0, 1350, 1550, 1750] WIDTH changes at these times: [0] out changes at these times: [0, 310, 650, 1050]