Skip to content
code0.HDL

Register stages, observation and waveforms

Chain registers into stages, learn what a testbench sees at the edge, after #1, with $strobe and in a waveform, start a VCD recording with $dumpfile and $dumpvars, give its times a unit with `timescale, and build a rising-edge detector whose pulse is exactly one cycle wide.

In this chapter

  • Chain two registers into stages and say after which edge a value appears in each stage
  • Decide from how a testbench prints whether a line shows the value before or after the edge
  • Start a VCD recording with $dumpfile and $dumpvars, read its header and change log, and give its times a unit with `timescale
  • Tell a combinational path from a register by when its trace changes, and build a one-cycle rising-edge pulse
Concepts in this chapter
  • Register stages and observation time
Prerequisite concepts

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
end

Check

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 looksWhat it seesWhy
$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 blockthe 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 edgetime has moved on; every update of the edge is done
a waveformevery change, with the time it happenedthe 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.

PartContent
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
BodyA 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
endmodule

The 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
LineMeaning
$var reg 1 ! en $endDeclares the 1-bit reg en with identifier !
$var reg 2 " n [1:0] $endDeclares the 2-bit reg n with identifier "
#0 and $dumpvars ... $endThe value of every signal when recording started
#10, 1!At time 10, en became 1
#20, b11 "At time 20, n became 11
#25The 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
CallEffect
$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.

Counter.v
// 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
endmodule

At 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.

Tb.v
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
endmodule

Predict before you run.

  1. Which scopes does the file contain, and which signals are in each?
  2. Between time 0 and the first rising edge, what value is count drawn with?
  3. At which times does count change, and when does reset fall?

Check

Between time 0 and the first rising edge at 5, what is count drawn as in the waveform?

Your answer

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.

TimeWhat the waveform showsWhy
0count is x, reset is 1, clock is 0The values when recording started; nothing has assigned the register yet
5clock rises; count becomes 0The first rising edge with reset high
20reset falls, at the same time as a falling clock edgeThe testbench changes inputs on the falling edge
25, 35, ..., 75count steps 1, 2, ..., 6, only at rising edgesA 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.

ChangeWhat happens
Delete the $dumpfile lineThe 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
PartMeaning
`Starts a compiler directive. It is not a statement, so it has no ;
1nsThe time unit. #5 means 5 ns
1psThe precision. Delays are rounded to whole picoseconds
$realtimeReturns 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.

EffectWith `timescale 1ns / 1psWithout any `timescale
#55 ns5 units; the Icarus default unit is 1 s
#10.5Exactly 10.5 nsRounded to 11 units
VCD $timescale1ps; every time is written in ps1s
$realtimeA real number in nsA 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.

AndPaths.v
`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;
endmodule

Tb.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
12a = 1
18b = 1
28.5b = 0
29.5b = 1, ending a 1 ns dip
49.5a = 0
Tb.v
`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
endmodule

Practice this concept

Use this concept in a short Verilog problem as your next step.

Predict before you run. Answer for each output:

  1. When does it first become 1?
  2. Does the short dip between 28.5 and 29.5 ns show up on it?
  3. When does it return to 0?

Check

b is 0 from 28.5 to 29.5 ns. Does y_reg show that dip?

Your answer

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)Eventy_comby_reg
5Rising edge00, replacing its initial x
18b rises while a is already 11 at onceUnchanged
25Rising edge1
28.5 to 29.5b is 0 for 1 ns0, then 1 againUnchanged: there is no edge in that window
49.5a falls0 at onceUnchanged
55Rising edge0

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.

ItemWhat happens
#10.5Rounds to 11, so b falls at 29 and rises at 30
DONE lineReports 60.0
VCD $timescale1s
The dip on y_combStill 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
end

Because 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.

State table
Cycleininputs sampled at the edgeprevafter the edgeriseOutputs
00
11
21
31
40
51

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

ElementMeaning
s1 <= in; s2 <= s1;two stages: one edge of delay each
$display at the edgebefore-edge value; $strobe, #1, negedge give the after-edge value
VCD structurethe header declares scopes and signals; the body logs value changes in time order
change logonly 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
`timescalesets the unit of # delays and the rounding precision; the VCD records time in the precision unit
changes between edgesa combinational path from a moving input; changes only at edges suggest a register but do not prove one
pulse <= level & ~preva 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

  1. Why does the counter's VCD list count twice, and why do both traces always agree?

    Show answer

    The net count in tb and the register count in tb.dut are 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.

  2. In the counter waveform, why is count x before 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, where count is already 0, so the x interval and the first edge are not in the file.

  3. Why does the 1 ns dip in b appear on y_comb but not on y_reg?

    Show answer

    y_comb is an assign, so it is recomputed the moment an input changes. y_reg only 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.

  4. With `timescale 1ns / 1ps, how much does a #2.5 delay 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.

Checkpoint

Submit the Practice problem for grading

Grading runs on the server against hidden tests; the result is recorded in this browser only.

Sources

Licences and notices for the sources this site follows