Clocking blocks and sampled views

Use a clocking block to sample DUT outputs from just before the edge and to drive inputs just after it, and never mix raw signals with sampled ones.

Lesson
6.3
Time
25 min
Runs on
Verilator
Check
5 questions

A monitor waits with @(bus.cb) but then reads the raw signal bus.rdata instead of the sampled bus.cb.rdata. The DUT updates rdata with a nonblocking assignment at the same edge, so by the time the monitor runs, the raw signal already holds the next beat. Every recorded transfer pairs this beat's handshake with the next beat's data, and the scoreboard reports a mismatch on every read.

Raw and sampled values at the same edges
012clk edge123rdata before the edge001122bus.cb.rdata (sampled)001122bus.rdata read after @(bus.cb)112233raw read sees the next value

The clocking event fires after the DUT's nonblocking update, so the raw signal has already moved on.

A clocking block fixes when to sample and when to drive

A clocking block names a clock event and a set of signals, and states when each signal is sampled or driven relative to that event. It usually sits inside the interface, next to the signals it times.

clocking cb @(posedge clk); default input #1step output #2; input rdata; output rvalid; endclocking declares one. Inputs are signals the testbench reads, outputs are signals it drives, and the skews say how far from the edge each happens.

Each signal in the block becomes a clockvar, reached as bus.cb.rdata. Reading a clockvar gives the sampled value; writing one, always with <=, schedules a drive.

Input skew: the value from just before the edge

An input skew of #1step samples the signal in the Preponed region of the clocking event's time step. That is the value the signal held just before anything happened at the edge, which is exactly the value the DUT's flip-flops capture.

#1step means one step of the simulation's time precision, not one clock cycle. The sample therefore stays the same however many processes run at the edge and in whatever order.

The clocking event itself, which @(bus.cb) waits for, is triggered in the Observed region, after the NBA region. By then the DUT's registers have their new values, and only the clockvars still hold the pre-edge view.

After @(bus.cb) at an edgeReadsMatches what the DUT captured?
bus.cb.rdatavalue from just before the edgeyes
bus.rdatavalue after the edge's nonblocking updatesno: it is the next value

Two views of one signal in the same time step.

Output skew: drive just after the edge

Writing bus.cb.rvalid <= 1; does not change the pin at once. The drive happens at the output skew after the clocking event: with output #2, two time units later, well clear of the edge.

With an output skew of #0, the drive happens in the Re-NBA region of the same time step. That is after the DUT has sampled its inputs at this edge, so the DUT sees the new value at the next edge, just as it would from a flip-flop.

Either way, a testbench that drives through clocking-block outputs cannot race the DUT's sampling, which removes the race shown in the previous lesson without any hand-placed delays.

Use one view per process

Within one driver or monitor, use the clocking block for everything: wait with @(bus.cb), read bus.cb.x, and drive bus.cb.y <= v. Mixing in raw reads or @(posedge clk) brings back the timing questions the clocking block was meant to settle.

Wait on the clocking event, read the raw signal

@(bus.cb);
if (bus.cb.rvalid && bus.cb.rready)
  got.push_back(bus.rdata);   // raw

The handshake is from before the edge, the data from after it: each beat is paired with the next one.

Read everything through the clocking block

@(bus.cb);
if (bus.cb.rvalid && bus.cb.rready)
  got.push_back(bus.cb.rdata);

Handshake and data come from the same instant, just before the edge.

Separate clocking blocks for the driver and the monitor keep their roles apart. A modport can then export just the right block, as in modport source (clocking drv_cb);.

Choosing the skews

For inputs, #1step is almost always right: it gives exactly the values the DUT captured. A larger input skew, such as #2, samples two time units before the edge, which is useful only to model a real setup time.

For outputs, a small nonzero skew such as #2 makes waveforms easy to read, because testbench changes appear just after each edge instead of on top of it. A skew of #0 is equally correct and keeps the waveform tight.

Counting cycles with a default clocking

Declaring one clocking block as the default, as in default clocking cb @(posedge clk); ... endclocking, enables the cycle-delay operator. ##2 then waits for two clocking events, so drivers can count cycles instead of edges.

Cycle delays follow the clocking block's event, so they fit naturally with clockvar drives and samples. Mixing them with raw @(posedge clk) waits brings back the same timing questions as mixing raw and sampled reads.

Common slips

Reading a clockvar right after @(posedge clk) instead of @(bus.cb) can return the sample from the previous edge. Clockvars are updated only when the clocking event fires, later in the time step, so wait on the clocking block's own event.

Writing a clockvar with = is illegal; clocking-block drives always use <=. And driving the same signal both raw and through the clocking block makes two drivers fight, so pick one path for each signal.

Try it

At every clocking event the raw signal already showed the value the DUT had just written, while the clockvar showed the value from before the edge. The two differ by one beat at the same time.

The drive through the clocking block reached the pin two time units after the edge, as its output skew says. Change the skew to output #0 and predict both rvalid lines.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=5 raw rdata=11, sampled bus.cb.rdata=00 t=15 raw rdata=22, sampled bus.cb.rdata=11 t=25 raw rdata=33, sampled bus.cb.rdata=22 t=26 one unit after the edge: rvalid=0 t=27 two units after the edge: rvalid=1

Check yourself

Predict the outputThe register q starts at 40 and adds 1 at every rising edge. Predict the exact output.
interface ifc (input logic clk);
  logic [7:0] q = 8'h40;
  clocking cb @(posedge clk);
    default input #1step;
    input q;
  endclocking
endinterface

module tb;
  logic clk = 0;
  always #5 clk = ~clk;
  ifc b (clk);
  always @(posedge clk) b.q <= b.q + 1;
  initial begin
    repeat (2) @(b.cb);
    $display("%h %h", b.q, b.cb.q);
    $finish;
  end
endmodule
ConceptWhat does an input skew of #1step mean?
ConceptA driver writes bus.cb.rvalid <= 1; with default output #0. When does a DUT register clocked on posedge clk first capture rvalid = 1?
Read the waveformThe DUT register rdata changes with a nonblocking assignment at every rising edge. A monitor waits @(bus.cb) and reads the raw bus.rdata. What does it record at edge 3?
rdata around three edges
0123edge1234rdata before the edgea0a1a2a3rdata after the edgea1a2a3a4

Values just before and just after each rising edge.

Spot the bugEach option is a monitor process for the clocking block cb. The DUT register rdata goes 00, 11, 22 at successive edges, and the monitor must record the value each edge captured: 00, 11, 22. Which option records the wrong values?

Remember

  • A clocking block states when each signal is sampled and driven relative to one clock event.
  • With #1step, a clockvar holds the value from just before the edge, which is what the DUT captured.
  • @(bus.cb) resumes after the nonblocking updates, so raw signals already show post-edge values.
  • Clocking-block outputs are driven with <= at the output skew; a #0 skew drives in the Re-NBA region.
  • In one driver or monitor, read and write only through the clocking block.

Practice