The VCD format
You will learn
What a .vcd file actually contains, and how to read its dump of every signal.
A waveform file is text, and reading it as text is a skill. A .vcd declares its timescale, names its scopes and variables, then records only changes: a timestamp, an identifier, a value. The lesson's spec, vcd_trace, models exactly that -- headers, changes, durations -- with tests over earliest and latest change. The widget is a real trace wrapped up as a card: the testbench toggled clk 585 times and left 4 signals at X for the whole run, both facts counted from the file, not remembered from the screen.
Try it
Read the wrapped trace card: 585 toggles, 4 signals at X; then find in vcd_trace the function that counts changes at one timestamp.

Drop a .vcd from any simulator and get a Wrapped-style card: value changes, the busiest net, signals stuck at X or Z, the quietest ones and the time span, read in your browser.
specs/fpga/vcd_trace.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/fpga/vcd_trace.t27
// T27 VCD Trace Emission Specification
// Emits Value Change Dump traces from HIR simulation
// IEEE 1364-2001 VCD format with variable sections
// Uses flat arrays + count fields (parser-compatible)
// phi^2 + 1/phi^2 = 3 | TRINITY
module VcdTrace {
// === VCD variable kind ===
pub const VcdVarKind = enum(i8) {
wire = 0,
reg = 1,
integer = 2,
parameter = 3,
}
// === VCD scope kind ===
pub const VcdScopeKind = enum(i8) {
module_scope = 0,
task_scope = 1,
function_scope = 2,
}
// === VCD variable entry ===
pub struct VcdVar {
kind : i8,
size : u32,
name : &str,
ident : &str,
}
fn vcd_var(kind: i8, size: u32, name: &str, ident: &str) -> VcdVar {
return VcdVar{
.kind = kind,
.size = size,
.name = name,
.ident = ident,
};
}
fn var_wire(size: u32, name: &str, ident: &str) -> VcdVar {
return vcd_var(0, size, name, ident);
}
fn var_reg(size: u32, name: &str, ident: &str) -> VcdVar {
return vcd_var(1, size, name, ident);
}
// === VCD value change entry ===
pub struct VcdChange {
timestamp_ps : u64,
ident : &str,
value : u32,
bit_width : u32,
}
fn vcd_change(ts: u64, ident: &str, value: u32, width: u32) -> VcdChange {
return VcdChange{
.timestamp_ps = ts,
.ident = ident,
.value = value,
.bit_width = width,
};
}
// === VCD header config ===
pub struct VcdHeader {
date : &str,
version : &str,
timescale : &str,
comment : &str,
_dummy : u32,
}
fn vcd_header(version: &str, timescale: &str) -> VcdHeader {
return VcdHeader{
.date = "2026-04-10",
.version = version,
.timescale = timescale,
.comment = "T27 Trinity VCD",
};
}
// === VCD trace ===
pub struct VcdTrace {
header : VcdHeader,
end_time_ps : u64,
}
fn vcd_trace(version: &str) -> VcdTrace {
return VcdTrace{
.header = vcd_header(version, "1 ps"),
.end_time_ps = 0,
};
}
// === Query functions ===
fn var_ident_index(idx: u32) -> &str {
return "!";
}
fn format_binary(value: u32, width: u32) -> &str {
return "b0";
}
fn changes_at_timestamp(changes: [VcdChange], count: u32, ts: u64) -> u32 {
var found : u32 = 0;
var i : u32 = 0;
while i < count {
if changes[i].timestamp_ps == ts {
found = found + 1;
}
i = i + 1;
}
return found;
}
fn earliest_change(changes: [VcdChange], count: u32) -> u64 {
if count == 0 {
return 0;
}
var min_ts : u64 = changes[0].timestamp_ps;
var i : u32 = 1;
while i < count {
if changes[i].timestamp_ps < min_ts {
min_ts = changes[i].timestamp_ps;
}
i = i + 1;
}
return min_ts;
}
fn latest_change(changes: [VcdChange], count: u32) -> u64 {
if count == 0 {
return 0;
}
var max_ts : u64 = changes[0].timestamp_ps;
var i : u32 = 1;
while i < count {
if changes[i].timestamp_ps > max_ts {
max_ts = changes[i].timestamp_ps;
}
i = i + 1;
}
return max_ts;
}
fn trace_duration_ps(changes: [VcdChange], count: u32) -> u64 {
if count == 0 {
return 0;
}
return latest_change(changes, count) - earliest_change(changes, count);
}
// === Validation ===
fn validate_var(v: VcdVar) -> u32 {
var errors : u32 = 0;
if v.name == "" {
errors = errors + 1;
}
if v.ident == "" {
errors = errors + 1;
}
return errors;
}
fn validate_change(c: VcdChange) -> u32 {
var errors : u32 = 0;
if c.ident == "" {
errors = errors + 1;
}
if c.bit_width == 0 {
errors = errors + 1;
}
return errors;
}
// === Tests ===
test vcd_var_creation
given v = var_wire(32, "counter", "!")
then v.kind == 0
and v.size == 32
and v.name == "counter"
test var_wire_creation
given v = var_wire(1, "clk", "!")
then v.kind == 0
test var_reg_creation
given v = var_reg(8, "data", "!")
then v.kind == 1
test vcd_change_creation
given c = vcd_change(1000, "!", 1, 1)
then c.timestamp_ps == 1000
and c.ident == "!"
and c.value == 1
test vcd_header_creation
given h = vcd_header("t27c v0.1", "1 ps")
then h.version == "t27c v0.1"
and h.timescale == "1 ps"
test vcd_trace_creation
given t = vcd_trace("t27c v0.1")
then t.end_time_ps == 0
and t.header.version == "t27c v0.1"
test changes_at_timestamp
given c1 = vcd_change(100, "!", 0, 1)
and c2 = vcd_change(100, "!", 1, 1)
and c3 = vcd_change(200, "!", 1, 1)
then changes_at_timestamp([c1, c2, c3], 3, 100) == 2
test changes_at_timestamp_none
given c1 = vcd_change(100, "!", 0, 1)
then changes_at_timestamp([c1], 1, 999) == 0
test earliest_change
given c1 = vcd_change(500, "!", 0, 1)
and c2 = vcd_change(100, "!", 1, 1)
and c3 = vcd_change(300, "!", 0, 1)
then earliest_change([c1, c2, c3], 3) == 100
test latest_change
given c1 = vcd_change(500, "!", 0, 1)
and c2 = vcd_change(100, "!", 1, 1)
and c3 = vcd_change(300, "!", 0, 1)
then latest_change([c1, c2, c3], 3) == 500
test trace_duration
given c1 = vcd_change(100, "!", 0, 1)
and c2 = vcd_change(500, "!", 1, 1)
then trace_duration_ps([c1, c2], 2) == 400
test trace_duration_empty
then trace_duration_ps([], 0) == 0
test validate_var_ok
given v = var_wire(1, "sig", "!")
then validate_var(v) == 0
test validate_var_empty_name
given v = VcdVar{.kind = 0, .size = 1, .name = "", .ident = "!"}
then validate_var(v) > 0
test validate_var_empty_ident
given v = VcdVar{.kind = 0, .size = 1, .name = "sig", .ident = ""}
then validate_var(v) > 0
test validate_change_ok
given c = vcd_change(0, "!", 0, 1)
then validate_change(c) == 0
test validate_change_empty_ident
given c = VcdChange{.timestamp_ps = 0, .ident = "", .value = 0, .bit_width = 1}
then validate_change(c) > 0
test validate_change_zero_width
given c = VcdChange{.timestamp_ps = 0, .ident = "!", .value = 0, .bit_width = 0}
then validate_change(c) > 0
// === Invariants ===
invariant trace_duration_non_negative
given c1 = vcd_change(100, "!", 0, 1)
and c2 = vcd_change(500, "!", 1, 1)
assert trace_duration_ps([c1, c2], 2) >= 0
invariant validate_non_negative
given v = var_wire(1, "sig", "!")
assert validate_var(v) >= 0
// === Benchmarks ===
bench vcd_emit_latency
measure: nanoseconds for vcd_change(1000, "!", 42, 32)
target: < 50ns
}
// phi^2 + 1/phi^2 = 3 | TRINITY
All lessons
Module 1 · Why verify
Designs that compile and are wrong, the model that decides, and the plan written before the code.
Module 2 · Testbenches
Stimulus, checks and a verdict, written as one spec beside the design it judges.
Module 3 · Waveforms
A trace of every signal, read the way a hardware engineer reads it, and two runs compared.
Module 4 · Conformance vectors
Cases with the answer written beside them, kept where the compiler can reach them.
Module 5 · Cosimulation
Spec, simulator and board agreeing on the bench Artix-7 XC7A200T, and what to do when they do not.
Module 6 · Coverage
What the tests touched: lines, toggles, states, and what that number hides.
Module 7 · Formal
Assertions that hold every cycle, bounded search for a counterexample, and why a proof needs induction.
Module 8 · Mutation
Break the design on purpose and count what the tests catch.
Module 9 · Sign-off
One command, every receipt, a clean verdict you can show.