Waveforms & Debugging
Dumping VCD files, reading them in GTKWave, X and Z values, and a field guide to the bugs every beginner meets.
1. When a test fails: look inside
The self-checking testbenches of Chapter 10 tell you whether a design works. When they print FAIL, you need to know why, and the fastest way is to look at every signal over time. A simulator can record each value change into a waveform file; a viewer then draws it like a logic analyser with unlimited probes. This chapter shows how to dump and read waveforms, explains the strange values X, Z and U that appear in them, and then walks through a field guide of the bugs every beginner meets β each with a broken and a fixed version that we actually ran.
2. Dumping waveforms: VCD, FST, GHW
The Value Change Dump (VCD) format is defined by the Verilog standard and read by every viewer. It is plain text: a header lists the signals, then the file records only the changes, each preceded by the time at which it happened. We reuse the 4-bit counter of Chapter 10 as the design to look at:
// Counter with synchronous reset and enable; wrap pulses on the last count
module counter #(
parameter WIDTH = 4
) (
input wire clk,
input wire rst,
input wire en,
output reg [WIDTH-1:0] count = 0,
output wire wrap
);
assign wrap = en && (count == {WIDTH{1'b1}});
always @(posedge clk) begin
if (rst)
count <= 0;
else if (en)
count <= count + 1'b1;
end
endmoduleIn Verilog you ask for a VCD file with two system tasks in the testbench:
`timescale 1ns/1ps
module counter_wave_tb;
reg clk = 0, rst = 1, en = 0;
wire [3:0] count;
wire wrap;
counter #(.WIDTH(4)) dut (.clk(clk), .rst(rst), .en(en), .count(count), .wrap(wrap));
always #4 clk = ~clk;
initial begin
$dumpfile("counter.vcd"); // name of the waveform file
$dumpvars(0, counter_wave_tb); // 0 = this module and everything below it
repeat (2) @(negedge clk);
rst = 0;
en = 1;
repeat (20) @(negedge clk);
en = 0;
repeat (3) @(negedge clk);
$finish;
end
endmodule$dumpfile names the file; $dumpvars(levels, scope) chooses what to record: level 0 means the scope and all modules below it, level 1 only the signals of that module. The VHDL testbench needs nothing in the code β GHDL dumps from the command line:
# Verilog: the testbench calls $dumpfile/$dumpvars iverilog -g2012 -o tb counter.v counter_wave_tb.v && vvp -n tb vvp -n tb -fst # same, but write the compact FST format # VHDL: choose the format at run time ghdl -a --std=08 counter.vhd counter_wave_tb.vhd ghdl -e --std=08 counter_wave_tb ghdl -r --std=08 counter_wave_tb --vcd=counter.vcd # standard VCD ghdl -r --std=08 counter_wave_tb --wave=counter.ghw # GHDL's own format, keeps VHDL types ghdl -r --std=08 counter_wave_tb --fst=counter.fst # compact binary ghdl -r --std=08 my_tb --stop-time=10us --vcd=my.vcd # for a testbench that never ends
VCD files grow quickly β every toggle of a 125Β MHz clock is a line β so for long runs use FST, a compressed binary format GTKWave and Surfer read natively, or limit what you dump. GHW is GHDL's own format: it keeps enumerated state names and records, which VCD cannot represent (Chapter 12 relies on that).
Here is the start of the Icarus VCD for the counter:
$timescale 1ps $end $scope module counter_wave_tb $end $var wire 1 ! wrap $end $var wire 4 " count [3:0] $end $var reg 1 # clk $end ... $enddefinitions $end #0 $dumpvars b0 " count = 0 0# clk = 0 ... #4000 1# at 4 ns, clk rises #16000 1$ at 16 ns, en = 1 and rst = 0 0% #20000 b1 " at 20 ns, count = 1
Each signal gets a short identifier made of printable characters (!, ", #β¦). A 1-bit change is written as the value followed by the identifier (1#); a vector as b, the binary value, a space and the identifier. Times are in the units of $timescale. That is the whole format β simple enough that Section 8 parses it in 30 lines of Python.
3. Reading waveforms in GTKWave
Open the file with gtkwave counter.vcd (or surfer counter.vcd). The window has three parts: a hierarchy tree of the design on the top left, the list of signals in the selected scope below it, and the wave area on the right.
- Click a scope in the hierarchy (the testbench, or
dutto see inside the design). - Select signals in the list and Append them (or drag them) to the wave area.
- Press Zoom Fit to see the whole run, then zoom into the region of interest. Click in the wave area to place a marker; the names column shows every value at that instant.
- Right-click a bus and change its Data Format (binary, hexadecimal, decimal, signed decimal). File β Write Save File stores your signal selection in a
.gtkwfile; next time rungtkwave counter.vcd counter.gtkw.
GTKWave draws a bus as a band with its value written inside, and X or unknown values in red. That red is your first clue in most debugging sessions.
4. Four values in Verilog, nine in VHDL
Real wires carry a voltage. Simulators add values for situations where the voltage is not a clean 0 or 1. In Verilog every bit is one of four values:
| Value | Meaning | Typical cause |
|---|---|---|
| 0, 1 | logic low, logic high | normal operation |
| x | unknown: could be 0 or 1 | uninitialised reg, two drivers disagree, X spreading through logic |
| z | high impedance: nothing drives the net | undriven wire, released tri-state bus |
VHDL's std_logic has nine values. The ones you will see are '0', '1', 'Z', 'X' (conflict or unknown) and 'U' β uninitialised, the value every std_logic signal has before anything assigns it. 'U' is extra information Verilog does not have: it tells you the signal has never been written, rather than written with something unknown. The others ('W', 'L', 'H', '-') model weak pull-ups and don't-cares and rarely matter on an FPGA.
This testbench produces each kind of unknown on purpose:
`timescale 1ns/1ps
// Where do X and Z come from? Four small experiments.
module xz_demo_tb;
reg [3:0] r; // (1) never assigned: starts as X
wire [3:0] floating; // (2) no driver at all: Z
wire net; // (3) two drivers that disagree
reg [7:0] mem [0:3]; // (4) read past the end of an array
reg [1:0] sel = 2'd1;
reg [2:0] idx = 3'd6;
wire [3:0] sum = r + 4'd1; // X spreads through arithmetic
assign net = sel[0]; // driver A says 1 ...
assign net = sel[1]; // ... driver B says 0
initial begin
mem[0] = 8'h11; mem[1] = 8'h22; mem[2] = 8'h33; mem[3] = 8'h44;
#1;
$display("(1) uninitialised reg r = %b", r);
$display(" X propagates r + 1 = %b", sum);
$display("(2) undriven wire floating = %b", floating);
$display("(3) 1 and 0 on one net net = %b", net);
$display("(4) mem[6], only 0..3 mem[idx] = %b", mem[idx]);
$display(" r == 4'b0000 gives %b, r === 4'b0000 gives %b", r == 4'b0000, r === 4'b0000);
$finish;
end
endmoduleIcarus GHDL
(1) uninitialised reg r = xxxx (1) uninitialised signal r = UUUU
X propagates r + 1 = xxxx arithmetic on it r + 1 = XXXX
(2) undriven wire floating = zzzz (2) released tri-state tri = Z
(3) 1 and 0 on one net net = x (3) '1' and '0' on one net = X
(4) mem[6], only 0..3 mem[idx] = xxxxxxxx r = "0000" is false
r == 4'b0000 gives x, r === 4'b0000 gives 0Case (4) has no VHDL twin: reading an array outside its range is not an X in VHDL but a run-time error β GHDL stops with index (6) out of bounds (0 to 3). That is stricter and often more helpful. Out-of-range reads in Verilog quietly return X, and out-of-range writes are silently ignored.
How X spreads follows a simple rule: the output is X only if the unknown input could change it. \(0 \,\&\, x = 0\) and \(1 \,|\, x = 1\), but \(1 \,\&\, x = x\). Arithmetic is pessimistic: any X bit makes the whole sum X, because a carry could ripple anywhere.
if (x_signal) takes the elsebranch in Verilog, so X can be hidden and simulation looks more optimistic than hardware.GHDL's VCD writer keeps U by default; with --vcd-4states it maps the nine values to Verilog's four so that every viewer understands them.
5. Bug field guide, part 1: combinational logic
Each entry below shows the broken version and the fix in one module pair. A single testbench (bugs_tb, at the end of Section 7) runs all of them side by side in each language; the outputs quoted are from that run.
Bug 1 β incomplete sensitivity list (simulation/synthesis mismatch)
A process re-runs only when a signal in its sensitivity list changes. Here b is missing, so when b rises while a stays 1, the simulated output does not update. Synthesis tools ignore the sensitivity list and build an AND gate β GHDL's synthesiser turns sens_bad into exactly y <= a and b β so the hardware does the right thing and the simulation is wrong. You have verified a different circuit from the one you built.
// BUG: b is missing from the sensitivity list
module sens_bad (input wire a, input wire b, output reg y);
always @(a)
y = a & b;
endmodule
// FIX: @(*) lists every signal read in the block (or use assign)
module sens_good (input wire a, input wire b, output reg y);
always @(*)
y = a & b;
endmodulesensitivity: a=1 b=1 bad y=0 good y=1
Bug 2 β the accidental latch
In combinational logic every output must be assigned on every path through the block. If one path leaves y alone, y must remember its old value, and the only way to build that without a clock is a latch β a level-sensitive storage element. Latches complicate timing analysis and are almost never what you meant. Synthesis prints a warning; GHDL's synthesiser refuses outright with latch infered for net "y".
// BUG: y is not assigned when sel == 2'b11, so it must remember: a latch
module latch_bad (
input wire [1:0] sel,
input wire a, b, c,
output reg y
);
always @(*) begin
if (sel == 2'b00) y = a;
else if (sel == 2'b01) y = b;
else if (sel == 2'b10) y = c;
end
endmodule
// FIX: a default assignment first, so every path assigns y
module latch_good (
input wire [1:0] sel,
input wire a, b, c,
output reg y
);
always @(*) begin
y = 1'b0;
if (sel == 2'b00) y = a;
else if (sel == 2'b01) y = b;
else if (sel == 2'b10) y = c;
end
endmodulelatch : sel=11 bad y=1 good y=0 (bad keeps old value)
Bug 3 β width truncation
Averaging 200 and 100 should give 150. But the sum 300 needs 9 bits. In Verilog the expression (a + b) >> 1 is evaluated with the width of its widest operand and of the destination β 8 bits β so the carry is lost before the shift: \(300 - 256 = 44\), and \(44/2 = 22\). In VHDL, numeric_std's + returns as many bits as the longer operand, with the same result. Neither language warns you.
// BUG: a + b is evaluated with 8 bits (the widest operand and the
// destination), so the carry is lost before the shift: 200 and 100 give 22
module avg_bad (input wire [7:0] a, input wire [7:0] b, output wire [7:0] avg);
assign avg = (a + b) >> 1;
endmodule
// FIX: make the sum 9 bits wide explicitly
module avg_good (input wire [7:0] a, input wire [7:0] b, output wire [7:0] avg);
wire [8:0] sum = {1'b0, a} + {1'b0, b};
assign avg = sum[8:1];
endmodulewidth : avg(200,100) bad=22 good=150
6. Bug field guide, part 2: sequential logic
Bug 4 β blocking assignment in clocked logic
In Verilog, = (blocking) updates the variable immediately; <= (non-blocking) schedules the update for the end of the time step, so every right-hand side sees the values from before the clock edge β exactly how flip-flops behave. With =, s2 = s1reads the new s1, and a two-stage shift register becomes a one-stage one. VHDL has the same trap with variables (:=), which update immediately, as opposed to signals (<=), which update after the process suspends.
// BUG: blocking '=' in a clocked block. s2 gets the NEW s1,
// so the two-stage shift register collapses into one stage.
module shift_bad (input wire clk, input wire d, output wire q);
reg s1 = 1'b0, s2 = 1'b0;
always @(posedge clk) begin
s1 = d;
s2 = s1;
end
assign q = s2;
endmodule
// FIX: non-blocking '<=': every right-hand side is read before the edge
module shift_good (input wire clk, input wire d, output wire q);
reg s1 = 1'b0, s2 = 1'b0;
always @(posedge clk) begin
s1 <= d;
s2 <= s1;
end
assign q = s2;
endmoduleshift : q rises bad after 1 edge(s), good after 2
Bug 5 β off-by-one counter
A counter that runs from 0 to \(M\) inclusive has \(M + 1\) states. To get a tick every \(N\) cycles, wrap at \(N - 1\). The broken version compares with 10 and ticks every 11 cycles: 10 ticks in 110 cycles instead of 11. Such a bug never shows as a wrong value in a single cycle β only as a wrong rate, which is why the testbench counts ticks over a long window.
// BUG: counts 0, 1, ..., 10 -- that is 11 states, so one tick every 11 cycles
module tick_bad (input wire clk, output reg tick = 1'b0);
reg [3:0] cnt = 4'd0;
always @(posedge clk) begin
tick <= 1'b0;
if (cnt == 4'd10) begin
cnt <= 4'd0;
tick <= 1'b1;
end else
cnt <= cnt + 1'b1;
end
endmodule
// FIX: for a period of N cycles, wrap at N - 1
module tick_good (input wire clk, output reg tick = 1'b0);
localparam N = 10;
reg [3:0] cnt = 4'd0;
always @(posedge clk) begin
tick <= 1'b0;
if (cnt == N - 1) begin
cnt <= 4'd0;
tick <= 1'b1;
end else
cnt <= cnt + 1'b1;
end
endmoduleoff-by-one : ticks/110cyc bad=10 good=11
Bug 6 β forgotten reset
The rst port exists but is never used, and the register has no initial value. In simulation it starts as X (U in VHDL), and \(X + 1 = X\) forever. On the board it would start from the bitstream's default (0) and probably work β the classic case of simulation being right to complain. Either a reset branch or an initial value fixes it; this course uses initial values for free-running logic and a synchronous rst when the design must be restartable.
// BUG: rst is a port but is never used, and cnt has no initial value
module noreset_bad (input wire clk, input wire rst, output reg [3:0] cnt);
always @(posedge clk)
cnt <= cnt + 1'b1; // X + 1 = X, forever
endmodule
// FIX: a synchronous reset branch (an initial value would also fix simulation)
module noreset_good (input wire clk, input wire rst, output reg [3:0] cnt);
always @(posedge clk)
if (rst)
cnt <= 4'd0;
else
cnt <= cnt + 1'b1;
endmodulereset : count bad=xxxx good=3 (GHDL prints XXXX)
7. Bug 7 β the unintended combinational loop
A loop through logic with no flip-flop in it has no stable meaning: cnt = cnt + 1 without a clock asks the hardware to increment as fast as the gates can switch, forever. In silicon this oscillates or settles unpredictably; timing analysis cannot handle it and Vivado reports a combinational loop.
// BUG: a "counter" with no clock. cnt depends on itself through
// combinational logic only: a loop with no register to break it.
module loop_bad (input wire en, output reg [3:0] cnt = 4'd0);
always @(*)
if (en)
cnt = cnt + 1'b1;
endmodule
// FIX: every feedback path goes through a flip-flop
module loop_good (input wire clk, input wire en, output reg [3:0] cnt = 4'd0);
always @(posedge clk)
if (en)
cnt <= cnt + 1'b1;
endmoduleThe two simulators react differently, and neither reaction is the truth:
Icarus: after en=1: cnt=1 GHDL: ghdl:info: simulation stopped @10ns by --stop-delta=5000
Icarus runs the always @(*) block once and does not retrigger it on its own write, so it shows a plausible but meaningless 1. GHDL keeps re-evaluating within the same time step β delta cycle after delta cycle β and gives up after 5000. Simulation time stuck at one instant, or a βdelta limitβ message, almost always means a combinational loop. The fix is to put a flip-flop in every feedback path.
The testbench that produced every βbugs_tbβ line above:
`timescale 1ns/1ps
// Runs every broken/fixed pair of the field guide side by side
module bugs_tb;
reg clk = 0, rst = 1;
reg a = 0, b = 0, c = 0, d = 0;
reg [1:0] sel = 2'b00;
reg [7:0] x = 8'd200, z = 8'd100;
wire y_sb, y_sg, y_lb, y_lg, q_b, q_g, t_b, t_g;
wire [7:0] avg_b, avg_g;
wire [3:0] cnt_b, cnt_g;
integer i, ticks_b = 0, ticks_g = 0, first_b = -1, first_g = -1;
sens_bad u1 (.a(a), .b(b), .y(y_sb));
sens_good u2 (.a(a), .b(b), .y(y_sg));
latch_bad u3 (.sel(sel), .a(a), .b(b), .c(c), .y(y_lb));
latch_good u4 (.sel(sel), .a(a), .b(b), .c(c), .y(y_lg));
shift_bad u5 (.clk(clk), .d(d), .q(q_b));
shift_good u6 (.clk(clk), .d(d), .q(q_g));
avg_bad u7 (.a(x), .b(z), .avg(avg_b));
avg_good u8 (.a(x), .b(z), .avg(avg_g));
tick_bad u9 (.clk(clk), .tick(t_b));
tick_good u10(.clk(clk), .tick(t_g));
noreset_bad u11(.clk(clk), .rst(rst), .cnt(cnt_b));
noreset_good u12(.clk(clk), .rst(rst), .cnt(cnt_g));
always #4 clk = ~clk;
initial begin
// 1. sensitivity list: a = 1 first, then b rises while a stays 1
a = 1; #1; b = 1; #1;
$display("sensitivity: a=1 b=1 bad y=%b good y=%b", y_sb, y_sg);
// 2. latch: y follows a, then sel = 11 is not covered
a = 1; b = 0; c = 0; sel = 2'b00; #1;
sel = 2'b11; #1;
$display("latch : sel=11 bad y=%b good y=%b (bad keeps old value)", y_lb, y_lg);
// 3. width: average of 200 and 100
$display("width : avg(200,100) bad=%0d good=%0d", avg_b, avg_g);
// 4. shift register: one pulse on d; after how many edges does q rise?
@(negedge clk); d = 1;
for (i = 1; i <= 3; i = i + 1) begin
@(negedge clk); d = 0;
if (q_b && first_b < 0) first_b = i;
if (q_g && first_g < 0) first_g = i;
end
$display("shift : q rises bad after %0d edge(s), good after %0d", first_b, first_g);
// 5. forgotten reset: release rst and look at both counters
rst = 0; repeat (3) @(negedge clk);
$display("reset : count bad=%b good=%0d", cnt_b, cnt_g);
// 6. off-by-one: count ticks in 110 cycles
for (i = 0; i < 110; i = i + 1) begin
@(negedge clk);
ticks_b = ticks_b + t_b;
ticks_g = ticks_g + t_g;
end
$display("off-by-one : ticks/110cyc bad=%0d good=%0d", ticks_b, ticks_g);
$finish;
end
endmodulesensitivity: a=1 b=1 bad y=0 good y=1 latch : sel=11 bad y=1 good y=0 (bad keeps old value) width : avg(200,100) bad=22 good=150 shift : q rises bad after 1 edge(s), good after 2 reset : count bad=xxxx good=3 off-by-one : ticks/110cyc bad=10 good=11
| Symptom in simulation | Look for |
|---|---|
| output does not react to an input | incomplete sensitivity list (use @(*) / list all inputs) |
| output "remembers" in combinational logic | missing else / default: a latch |
| result is exactly 2^n too small | width truncation: lost carry |
| pipeline one stage too short | blocking = (or VHDL variable) in clocked logic |
| right values, wrong rate | off-by-one terminal count |
| X or U from time 0, never recovers | missing reset or initial value |
| time stops advancing / delta limit | combinational loop |
| X appears on a bus mid-run | two drivers, or an out-of-range index |
8. Python: read a VCD and draw it yourself
Because VCD is plain text, you can process it with any language β to plot, to measure (pulse widths, rates), or to check properties automatically. The program below contains a VCD produced by Icarus from a short testbench (small_vcd_tb.v) that runs the counter of Chapter 10 next to the un-reset counter of Bug 6. It parses the file into a list of changes per signal and draws the waveform with matplotlib, painting unknown values red as GTKWave does.
`timescale 1ns/1ps
// Short run used to make the VCD embedded in vcd_plot.py
module small_vcd_tb;
reg clk = 0, rst = 1, en = 0;
wire [3:0] count, bad;
wire wrap;
counter #(.WIDTH(4)) dut (.clk(clk), .rst(rst), .en(en), .count(count), .wrap(wrap));
noreset_bad u_bad (.clk(clk), .rst(rst), .cnt(bad));
always #4 clk = ~clk;
initial begin
$dumpfile("small.vcd");
$dumpvars(1, small_vcd_tb); // 1 = this level only, not inside the DUTs
@(negedge clk); @(negedge clk);
rst = 0; en = 1;
repeat (17) @(negedge clk);
en = 0;
repeat (2) @(negedge clk);
$finish;
end
endmoduleClick Run to execute the Python code
Code will be executed with Python 3 on the server
The parser is a single pass over whitespace-separated tokens: $var lines build the table from identifier code to name and width, # tokens set the current time, and every other token is a value change. The printout confirms what the plot shows: wrap is high at 132Β ns, the cycle in which count holds 15 with en = 1, and bad has exactly one change β to X, at time 0.
Practice Problems
\$timescale 1ps \$end, then #20000 followed by b101 ". What happened, and when?1'b0 & 1'bx, (b) 1'b1 & 1'bx, (c) 1'b1 | 1'bz, (d) 4'b0011 + 4'b00x1. Explain (a) and (b) physically.always @(*) block assigns y in a case statement with branches for 2'b00, 2'b01 and 2'b10. Simulation looks fine. Vivado's synthesis log warns about a latch. (a) Why might simulation look fine? (b) Give two different fixes.acc <= acc + sample; with acc 12 bits and sample 8 bits. After adding 20 samples of value 255 the simulation shows 1004 instead of 5100. Name the bug and the minimum width that fixes it for up to 20 samples.parse_vcd from Section 8 into a tiny checker: return the time of the first rising edge of clk at which count holds a value containing X or Z, or None. Sketch the algorithm and say what it returns on small.vcd for count and for bad.