1. Each register stage adds one edge of delay
Two registers written with <= in one clocked block are two stages. At every rising edge the first
takes the input and the second takes what the first held before that edge, because both right-hand
sides are read before either register updates. A value sampled into s1 at one edge is in s2
after the next one.
always @(posedge clock) begin
s1 <= in;
s2 <= s1; // the s1 from before this edge
endCheck
in is sampled 1 at some rising edge. After how many rising edges, counting that one, does s2 show 1?
2. Decide where you are looking before you compare numbers
A clocked design has one value before the edge and another after it, and a testbench can look at either. If a trace disagrees with your prediction, check which value was printed before deciding that the design is wrong.
| How the testbench looks | What it sees | Why |
|---|---|---|
$display inside always @(posedge clock) | the value before the edge | $display runs in the active region; <= updates afterwards in the same time step |
$strobe in the same block | the value after the edge | $strobe prints at the end of the time step, after all updates |
#1 after the edge, or @(negedge clock) | the value after the edge | time has moved on; every update of the edge is done |
| a waveform | every change, with the time it happened | the simulator logs each selected signal as it changes |
The rule: before reading a number, decide which row of this table produced it. The scheduling regions behind the first two rows, active, nonblocking update and postponed, are in Registers and nonblocking assignment.
3. A waveform is a log of changes
A waveform is a record of when and how every selected signal changed during a simulation. A
testbench prints only the values you decided to print; a waveform also keeps the problems you did
not anticipate, such as a pulse that is one cycle late, an output that moves between clock edges,
or a register that never leaves x. Verilog simulators write waveforms as VCD (value change dump)
files, a plain-text format defined by IEEE Std 1364-2005.
A waveform is written during simulation. Elaboration decides which signals exist and what their
hierarchical names are; the simulator then writes a line to the file every time a selected signal
changes. The lab picks up dump.vcd from the run's working directory along with the output and
draws it as a waveform below the output.
3.1 Header and body
A VCD file has a header that declares the signals and a body that logs value changes in time order.
| Part | Content |
|---|---|
| Header | $timescale, then nested $scope module ... $end blocks. Inside each scope, a $var line declares a signal's kind, width, a short identifier such as !, and its name |
| Body | A line such as #25 sets the current time; the lines after it record only the signals whose value changed at that time. A 1-bit signal is written like 1!, a vector like b11 ": the value, then the identifier |
This testbench records a 1-bit en and a 2-bit n.
module tb;
reg en = 0;
reg [1:0] n = 0;
initial begin
$dumpfile("dump.vcd");
$dumpvars(0, tb);
#10 en = 1;
#10 n = 2'd3;
#5 $finish;
end
endmoduleThe interesting part of the dump.vcd that Icarus writes for it looks like this.
$timescale 1s $end $scope module tb $end $var reg 1 ! en $end $var reg 2 " n [1:0] $end $upscope $end $enddefinitions $end #0 $dumpvars b0 " 0! $end #10 1! #20 b11 " #25
| Line | Meaning |
|---|---|
$var reg 1 ! en $end | Declares the 1-bit reg en with identifier ! |
$var reg 2 " n [1:0] $end | Declares the 2-bit reg n with identifier " |
#0 and $dumpvars ... $end | The value of every signal when recording started |
#10, 1! | At time 10, en became 1 |
#20, b11 " | At time 20, n became 11 |
#25 | The last time, when $finish ran |
3.2 Only changes are logged
The body is not a table of samples taken at fixed intervals; it is a log of changes. A signal that does not change produces no lines at all. A viewer rebuilds each trace by holding a signal's last value until its next change.
Tip. The lab's viewer starts the time axis at the first recorded change, not at a header field. A signal that appears with the same identifier in several scopes is shown once, under its outermost scope.
4. Starting a recording
4.1 $dumpfile and $dumpvars
Syntax
$dumpfile("file_name");
$dumpvars(depth, scope, scope, ...);initial begin
$dumpfile("dump.vcd");
$dumpvars(0, tb);
end| Call | Effect |
|---|---|
$dumpfile("dump.vcd") | Names the output file. Without it, Icarus uses dump.vcd |
$dumpvars(0, tb) | Records the signals of tb and every level below it, starting from the moment this call runs |
Call $dumpfile before $dumpvars. Both usually sit at the very start of the testbench's
initial block, at time 0. Depth 0 means tb and every level below it; the other depths, and
recording a scope deeper in the hierarchy, are in
Recording waveforms.
4.2 Example: recording the counter
Counter.v is the synchronous-reset 4-bit counter from the last two chapters.
// A 4-bit counter with a synchronous, active-high reset.
module counter (
input clock,
input reset,
output reg [3:0] count
);
always @(posedge clock) begin
if (reset) count <= 4'd0;
else count <= count + 4'd1;
end
endmoduleAt the start of its initial block, Tb.v names the file with $dumpfile and records tb and
everything below it with $dumpvars(0, tb). It then holds reset for two rising edges, lowers it
on the falling edge at 20, waits six more rising edges, and prints one DONE line one time unit
after the eighth rising edge.
module tb;
reg clock = 0;
reg reset = 1;
wire [3:0] count;
counter dut (.clock(clock), .reset(reset), .count(count));
always #5 clock = ~clock;
initial begin
// Name the file first, then choose what to record: depth 0 means every level.
$dumpfile("dump.vcd");
$dumpvars(0, tb);
repeat (2) @(posedge clock);
@(negedge clock) reset = 0;
repeat (6) @(posedge clock);
#1 $display("DONE t=%0t count=%0d", $time, count);
$finish;
end
endmodulePredict before you run.
- Which scopes does the file contain, and which signals are in each?
- Between time 0 and the first rising edge, what value is
countdrawn with? - At which times does
countchange, and when doesresetfall?
Check
Between time 0 and the first rising edge at 5, what is count drawn as in the waveform?
Check the result. Press Run. The output starts with Icarus's own VCD info: line, which
confirms that the file was opened, followed by the DONE line at time 76 with count=6. Read the
waveform below it against the testbench.
| Time | What the waveform shows | Why |
|---|---|---|
| 0 | count is x, reset is 1, clock is 0 | The values when recording started; nothing has assigned the register yet |
| 5 | clock rises; count becomes 0 | The first rising edge with reset high |
| 20 | reset falls, at the same time as a falling clock edge | The testbench changes inputs on the falling edge |
| 25, 35, ..., 75 | count steps 1, 2, ..., 6, only at rising edges | A register with a synchronous reset changes only at its clock edge |
The DONE line is printed after #1, so it is the after-edge view of the same register, the third
row of the table in section 2, which is why it agrees with the last step of the trace.
The header holds two scopes, tb and tb.dut. The testbench's net count and the counter's
register count always carry the same value, but they are different objects in the hierarchy, so
they are recorded as two signals with different identifiers. The clock port of tb.dut, on the
other hand, is the same net as tb's clock and shares its identifier. The file's $timescale is
1s: these sources set no time unit, so each simulation time unit is recorded as one second.
Try it yourself.
| Change | What happens |
|---|---|
Delete the $dumpfile line | The waveform is unchanged, because dump.vcd is Icarus's default file name |
Move $dumpvars(0, tb); below repeat (2) @(posedge clock); | Recording starts at time 15. In the starting snapshot count is already 0, and the time axis begins at 15. The first edge and the x before it are not in the file |
Note. Other simulators may use a different default file name, so keep the $dumpfile call. A
waveform contains only what happened after recording started.
5. `timescale
5.1 Time unit and precision
The number after # is a count of time units. The `timescale compiler directive sets the
physical size of that unit and the precision, the finest step to which delays are rounded.
Syntax
`timescale unit / precision`timescale 1ns / 1ps
module tb;
initial begin
#5 $display("%0.1f", $realtime); // 5.0
#2.25 $display("%0.2f", $realtime); // 7.25
end
endmodule| Part | Meaning |
|---|---|
` | Starts a compiler directive. It is not a statement, so it has no ; |
1ns | The time unit. #5 means 5 ns |
1ps | The precision. Delays are rounded to whole picoseconds |
$realtime | Returns the current time as a real number in time units |
The directive applies to every module that follows it in the same compilation. Put the same directive at the top of both the design and the testbench files.
| Effect | With `timescale 1ns / 1ps | Without any `timescale |
|---|---|---|
#5 | 5 ns | 5 units; the Icarus default unit is 1 s |
#10.5 | Exactly 10.5 ns | Rounded to 11 units |
VCD $timescale | 1ps; every time is written in ps | 1s |
$realtime | A real number in ns | A count of units |
The VCD records time in the precision unit, so #5 appears in the file as #5000. The viewer
converts it back to ns.
5.2 Example: combinational versus registered paths
AndPaths.v computes a & b twice: y_comb with an assign, and y_reg with a register updated
at the rising edge. Both files start with the same `timescale 1ns / 1ps, so the clock period
is 10 ns and the testbench can move an input by half a nanosecond.
`timescale 1ns / 1ps
// The same AND function, once as a wire and once as a register.
module and_paths (
input clock,
input a,
input b,
output y_comb,
output reg y_reg
);
assign y_comb = a & b;
always @(posedge clock) y_reg <= a & b;
endmoduleTb.v runs a 10 ns clock, changes its inputs in the order below, and prints a DONE line with
$realtime. Rising edges fall at 5, 15, 25, 35, 45, and 55 ns.
| Time (ns) | Input change |
|---|---|
| 12 | a = 1 |
| 18 | b = 1 |
| 28.5 | b = 0 |
| 29.5 | b = 1, ending a 1 ns dip |
| 49.5 | a = 0 |
`timescale 1ns / 1ps
module tb;
reg clock = 0;
reg a = 0, b = 0;
wire y_comb, y_reg;
and_paths dut (.clock(clock), .a(a), .b(b), .y_comb(y_comb), .y_reg(y_reg));
// One time unit is now 1 ns, so the period is 10 ns.
always #5 clock = ~clock;
initial begin
$dumpfile("dump.vcd");
$dumpvars(0, tb);
#12 a = 1;
#6 b = 1;
// A pulse shorter than a clock period, between two rising edges.
#10.5 b = 0;
#1 b = 1;
#20 a = 0;
#10;
$display("DONE t=%0.1f ns y_comb=%b y_reg=%b", $realtime, y_comb, y_reg);
$finish;
end
endmodulePractice this concept
Use this concept in a short Verilog problem as your next step.
Predict before you run. Answer for each output:
- When does it first become 1?
- Does the short dip between 28.5 and 29.5 ns show up on it?
- When does it return to 0?
Check
b is 0 from 28.5 to 29.5 ns. Does y_reg show that dip?
Check
At what time, in ns, does y_reg first become 1?
Check the result. Press Run and read the two traces side by side. The DONE line reports
59.5 ns and both outputs at 0. The waveform matches this table.
| Time (ns) | Event | y_comb | y_reg |
|---|---|---|---|
| 5 | Rising edge | 0 | 0, replacing its initial x |
| 18 | b rises while a is already 1 | 1 at once | Unchanged |
| 25 | Rising edge | 1 | |
| 28.5 to 29.5 | b is 0 for 1 ns | 0, then 1 again | Unchanged: there is no edge in that window |
| 49.5 | a falls | 0 at once | Unchanged |
| 55 | Rising edge | 0 |
y_comb follows its inputs at every instant, including the 1 ns dip; in real hardware such a short
dip is called a glitch. y_reg changes only at rising edges, so it never sees the dip and trails
the combinational value by up to one clock period. In the file every time is written in ps, so the
dip appears as the two time lines #28500 and #29500.
The moment a signal changes is a hint about what kind of logic drives it. A change between edges
means a combinational path from an input that moved between edges. Changes only at edges are what
a register produces, but combinational logic fed only by registers changes only at edges too, and
an asynchronous reset or a negedge register changes a register between rising edges; the hint is
not a proof.
Try it yourself. Delete the `timescale line from both files, and predict before you run.
The time unit falls back to Icarus's default and the precision to one whole unit.
| Item | What happens |
|---|---|
#10.5 | Rounds to 11, so b falls at 29 and rises at 30 |
DONE line | Reports 60.0 |
VCD $timescale | 1s |
The dip on y_comb | Still there, but exactly one unit long |
Note. Without an explicit `timescale, these examples use Icarus's default unit and
precision, recorded as 1s in the VCD. Fractional delays can therefore round to different times.
6. A rising-edge detector compares two samples
A rise is a relation between the current sample and the previous one. Keep the previous sample in a register, and the pulse is "now 1, before 0":
always @(posedge clock) begin
prev <= level;
pulse <= level & ~prev; // prev still holds the previous sample here
endBecause both assignments are nonblocking, the second line reads the prev from before the edge even
though the first line assigns it. At an edge where level is 1 and prev is 0, pulse becomes 1
after that edge and stays 1 until the next edge: exactly one period. The model below keeps the same
prev; its rise is the combinational value in & !prev during the cycle, and the registered
pulse is that value captured at the edge that ends the cycle, visible one edge later. Fill in
prev after each edge and rise during each cycle.
Predict, then compare · rise-detect
Fill in each cell before comparing. Empty cells count as a mismatch.
| Cycle | ininputs sampled at the edge | prevafter the edge | riseOutputs |
|---|---|---|---|
| 0 | 0 | ||
| 1 | 1 | ||
| 2 | 1 | ||
| 3 | 1 | ||
| 4 | 0 | ||
| 5 | 1 |
7. Practice: one pulse per rise
The checkpoint is the Practice problem rise-pulse. The starter's pulse copies level, so a
level high for three cycles produces a three-cycle high output rather than a one-cycle pulse.
Its testbench changes level on falling edges and
prints on the following falling edge, so each line is the after-edge view. Write the expected
pulse column first, change the one line under the TODO, and then check the waveform, not only
the text: every pulse must span one rising edge to the next, and pulse must never move between
edges even when level does.
8. Summary
| Element | Meaning |
|---|---|
s1 <= in; s2 <= s1; | two stages: one edge of delay each |
$display at the edge | before-edge value; $strobe, #1, negedge give the after-edge value |
| VCD structure | the header declares scopes and signals; the body logs value changes in time order |
| change log | only changed signals are written; the body starts with a snapshot of every value |
$dumpfile, $dumpvars(0, tb) | name the file, then record tb and everything below it from that moment |
`timescale | sets the unit of # delays and the rounding precision; the VCD records time in the precision unit |
| changes between edges | a combinational path from a moving input; changes only at edges suggest a register but do not prove one |
pulse <= level & ~prev | a rise detected from two samples, one period wide |
Choosing scopes and depths, and the mistakes that leave a waveform empty or late, are in Recording waveforms.
9. Check your understanding
-
Why does the counter's VCD list
counttwice, and why do both traces always agree?Show answer
The net
countintband the registercountintb.dutare different objects in the hierarchy, so each is recorded. The testbench's net is connected to the counter's output port, so it always shows the register's value. -
In the counter waveform, why is
countxbefore time 5, and how does the file change if recording starts at time 15?Show answer
Before the first rising edge nothing has assigned the register, so it is
x. If recording starts at time 15, the starting snapshot is taken at 15, wherecountis already 0, so thexinterval and the first edge are not in the file. -
Why does the 1 ns dip in
bappear ony_combbut not ony_reg?Show answer
y_combis anassign, so it is recomputed the moment an input changes.y_regonly reads its input at rising edges, and there is no rising edge between 28.5 and 29.5 ns, so it never sees the dip. -
With
`timescale 1ns / 1ps, how much does a#2.5delay add to the time in the VCD body?Show answer
The VCD records time in ps, the precision unit, so 2.5 ns is 2500. The next time line is the previous time plus 2500.