Build the generator and driver

Describe each clock cycle of stimulus as a transaction object, generate directed and random ones, and drive them onto the pins through a mailbox and a clocking block.

Lesson
7.3
Time
30 min
Runs on
Verilator
Check
5 questions

A generator created one fifo_op object and, for each write, changed its data and put it in the mailbox again. The mailbox held three handles to the same object, so the driver sent 0x33 three times while the test plan listed 0x11, 0x22 and 0x33. The FIFO was blamed for losing data it had never been given.

Three puts, one object
012data the generator meant112233handle in the mailboxopwrite_data driven33driver takes the first handle

One column per operation. By the time the driver reads the object, the generator has overwritten it.

One operation per clock: the fifo_op class

A transaction describes one unit of stimulus without saying how the pins move. Here it is one clock cycle of requests, so fifo_op holds reset, write_en, read_en and write_data. It is built like the transaction classes of Module 4, with a copy() that makes a new object and a show() for messages.

The fields are rand, so Module 5's tools apply: randomize() picks the requests, and the constraint reset dist {0 := 40, 1 := 1} keeps resets rare without ruling them out. A fifo_op says what is offered, never what is accepted. Whether a write goes in depends on the FIFO's state, which only the FIFO and the scoreboard know.

Directed first, then random

The generator starts with the directed cases from the test plan. It sends a reset, then five writes into a FIFO of depth 4, so the last one is refused, then a read and a write together at full. Seeded random operations follow, and finally five reads drain whatever is left.

The directed cases come first for a practical reason. If the FIFO gets one of them wrong, the failure appears within the first dozen cycles, in a short log that is easy to read, before any random traffic.

Each send() creates a new fifo_op, which is the fix for the opening story: once an object is in the mailbox, it belongs to the driver. Random objects are new as well, and every randomize() call is checked, as Module 5 requires.

The generator seeds its own process with process::self().srandom(seed) before creating anything. Every object it creates is then seeded from that process, so the same seed gives the same operations. Lesson 7.6 prints the seed for exactly this reason.

Seeding the process, rather than each object, keeps one number in charge of the whole run, so there is exactly one value to log and to replay. The show() method earns its place in messages: op.show() turns a handle into one readable line such as rst=0 w=1 r=0 d=14.

One object, reused

fifo_op op = new();
foreach (data[i]) begin
  op.write_en   = 1;
  op.write_data = data[i];
  req.put(op);
end

Every message is the same object, so the driver sees whatever was written last.

A new object per operation

foreach (data[i]) begin
  fifo_op op = new();
  op.write_en   = 1;
  op.write_data = data[i];
  req.put(op);
end

Each message keeps its own data, whenever the driver gets to it.

A bounded mailbox between them

The generator and the driver are separate processes joined by mailbox #(fifo_op) req = new(1), the bounded channel of Module 3. The bound of 1 means the generator can never be more than one operation ahead of the driver. So when it sets done, the end of the stimulus is at most two clock cycles away.

The generator counts the operations it has sent in sent, and the driver counts the ones it has driven in driven. The stimulus is finished when the generator is done and the two counts match; lesson 7.6 adds the checks that come after that.

The driver owns the pins

The driver is the only code that writes the FIFO's inputs. It holds a virtual interface to fifo_if and drives through the clocking block drv_cb, as in Module 6. It waits for @(vif.drv_cb), just after a rising edge, and assigns the four inputs.

The clocking block's output #2 makes each value land 2 time units after that edge, well before the next edge samples it. So an operation taken from the mailbox at one edge is acted on by the FIFO at the following edge, with no race against the FIFO's own always block.

The driver never filters requests. It sends a write to a full FIFO exactly as the generator asked, because the test exists to see the FIFO refuse it. A driver that quietly skipped such requests would make the most important boundary impossible to test.

Where the testbench stands

LayerComponentJobReusesStatus
Testfifo_test, fifo_configchooses depth, seed and length, runs the environment, gives the verdictModule 4 classeslesson 7.7
Generatorfifo_generatorcreates fifo_op objects: directed, then seeded randomModules 4 and 5this lesson
Mailboxesmailbox #(fifo_op), mailbox #(fifo_obs)carry operations and observations between processesModule 3this lesson
Driverfifo_driverdrives one operation per clock through drv_cbModule 6this lesson
Monitorfifo_monitorsamples the pins through mon_cb into a new fifo_obsModule 6lesson 7.4
Scoreboardfifo_scoreboardpredicts each edge with the queue exp_q and comparesModules 2 and 4lesson 7.4
Assertionsfifo_propschecks timing rules on the pins at every edgeModule 6lesson 7.4
Coveragefifo_coverage, fifo_scenariosmeasures which situations the stimulus reachedModule 5 stimuluslesson 7.5
DUTfifothe design under testlesson 7.1 rulesbuilt in 7.2

The layered testbench after lesson 7.3.

There is no checking yet. The example prints what the pins carried at the first ten edges, so you can compare the stimulus with the plan before any checking is added.

Try it

Each line shows the pins just before a rising edge. The reset is sampled at t=15, the writes of 10 to 14 follow, and at t=65 the write of 14 meets a full FIFO. At t=75 the read and write together take out 10 and refuse ee, so read_valid is 1 at t=85.

From t=85 the operations are random, from seed 7. Change the seed in new(req, 20, 7) and predict which lines stay the same.

RUNVerilator · simulator service
Edit the code, then run it
Expected: t=5 rst=0 w=0 r=0 d=00 | full=0 empty=1 valid=0 data=00 t=15 rst=1 w=0 r=0 d=00 | full=0 empty=1 valid=0 data=00 t=25 rst=0 w=1 r=0 d=10 | full=0 empty=1 valid=0 data=00 t=35 rst=0 w=1 r=0 d=11 | full=0 empty=0 valid=0 data=00 t=45 rst=0 w=1 r=0 d=12 | full=0 empty=0 valid=0 data=00 t=55 rst=0 w=1 r=0 d=13 | full=0 empty=0 valid=0 data=00 t=65 rst=0 w=1 r=0 d=14 | full=1 empty=0 valid=0 data=00 t=75 rst=0 w=1 r=1 d=ee | full=1 empty=0 valid=0 data=00 t=85 rst=0 w=0 r=0 d=55 | full=0 empty=0 valid=1 data=10 t=95 rst=0 w=0 r=0 d=81 | full=0 empty=0 valid=0 data=10 generator sent 32 operations, driver drove 32

Check yourself

Predict the outputThe driver from this lesson receives one write at time 0, with a 10-unit clock whose first rising edge is at t=5. Predict the exact output.
// The FIFO's pins, with the clocking blocks from module 6.
interface fifo_if #(parameter int WIDTH = 8) (input bit clk);
  bit             reset, write_en, read_en;
  bit [WIDTH-1:0] write_data;
  bit             full, empty, read_valid;
  bit [WIDTH-1:0] read_data;
  clocking drv_cb @(posedge clk);
    default input #1step output #2;
    output reset, write_en, read_en, write_data;
    input  full, empty;
  endclocking
  clocking mon_cb @(posedge clk);
    default input #1step;
    input reset, write_en, read_en, write_data, full, empty, read_valid, read_data;
  endclocking
endinterface

// One clock cycle of requests: what the driver offers to the FIFO.
class fifo_op;
  rand bit       reset, write_en, read_en;
  rand bit [7:0] write_data;
  constraint c_reset { reset dist {0 := 40, 1 := 1}; }
  function fifo_op copy();
    copy = new();
    copy.reset = reset; copy.write_en = write_en;
    copy.read_en = read_en; copy.write_data = write_data;
  endfunction
  function string show();
    return $sformatf("rst=%0d w=%0d r=%0d d=%h", reset, write_en, read_en, write_data);
  endfunction
endclass

// Takes one operation per clock from the mailbox and drives it through drv_cb.
class fifo_driver;
  virtual fifo_if    vif;
  mailbox #(fifo_op) req;
  int                driven;
  function new(virtual fifo_if vif, mailbox #(fifo_op) req);
    this.vif = vif; this.req = req;
  endfunction
  task run();
    fifo_op op;
    forever begin
      req.get(op);
      @(vif.drv_cb);                           // just after a rising edge
      vif.drv_cb.reset      <= op.reset;       // lands 2 units later,
      vif.drv_cb.write_en   <= op.write_en;    // sampled at the next edge
      vif.drv_cb.read_en    <= op.read_en;
      vif.drv_cb.write_data <= op.write_data;
      driven++;
    end
  endtask
endclass

module tb;
  bit clk;
  always #5 clk = ~clk;
  fifo_if bus (clk);
  mailbox #(fifo_op) req = new(1);
  fifo_driver drv = new(bus, req);
  initial begin
    fifo_op op = new();
    op.write_en = 1;
    op.write_data = 8'h42;
    req.put(op);
    fork drv.run(); join_none
    @(posedge bus.write_en);
    $display("write_en rose at t=%0t", $time);
    $finish;
  end
endmodule
Spot the bugEach option is the body of a generator task that must put three writes, with data 11, 22 and 33, into an unbounded mailbox before the driver starts. Which option makes the driver send the wrong data?
ConceptA teammate makes the driver skip any write it sees while drv_cb.full is 1, "to keep the FIFO safe". What is wrong with that?
ConceptThe generator puts operations into mailbox #(fifo_op) req = new(1). When the generator sets done, what is true?
ConceptThe driver returns from applying an operation with write_en and read_en both 1. What does that tell you?

Remember

  • A fifo_op describes the requests of one clock cycle: offers, never outcomes.
  • Create a new object for every operation you put in a mailbox; after the put, it belongs to the receiver.
  • Generate the directed cases first, then seeded random ones, and check every randomize().
  • Only the driver writes the pins, through drv_cb, and it never filters illegal offers.
  • The stimulus is finished when the generator is done and the driver has driven everything it sent.

Practice