Start from the FIFO rules

Write the FIFO's rules down precisely enough to execute them, and plan the layered testbench that will check a real design against them.

Lesson
7.1
Time
25 min
Runs on
Verilator
Check
5 questions

A new scoreboard reported 300 errors on its first night, all at moments when the FIFO was full and the test asked to read and write together. The checker let the read free a slot first and then accepted the write, while the FIFO refused the write, as its designer intended. Each side followed its own idea of the rule, because nobody had ever written the rule down.

Read and write together at full
0123held before the edge343write_enread_enFIFO took the writechecker expected a writeboth requested at full

One column per rising edge. The checker and the FIFO disagree only at edge 2.

The FIFO in one table

Module 7 builds one complete testbench for one small design: a synchronous FIFO, a first-in, first-out buffer that stores bytes in the order they arrive. Every lesson reuses something you built in Modules 1 to 6.

PortDirectionMeaning
clkinputevery decision happens at a rising edge
resetinputsynchronous, active high, and stronger than every request
write_en, write_datainputasks to store write_data at this edge
read_eninputasks to remove the oldest stored item at this edge
full, emptyoutputcomputed from how many items are stored right now
read_validoutput1 for one cycle after an edge that accepted a read
read_dataoutputthe item that read returned; meaningless while read_valid is 0

Ports of the FIFO, with DEPTH entries of WIDTH bits.

These are the ports and rules of hardware_fifo, the FIFO used by the problem bank. That version adds an initialize pin for its test fixture and 32-bit data pins, so what you build here carries straight over to those problems.

Every decision uses the state before the edge

At each rising edge, the FIFO looks at how many items it held just before the edge. It accepts a write if write_en is 1 and it is not full, and it accepts a read if read_en is 1 and it is not empty.

Both decisions use that one number. So a read in the same edge never makes room for a write, and a write never gives a read in the same edge something to return. That single sentence settles the argument in the opening story.

Held before the edgeRequestsAcceptedRead returns
0 (empty)write and readthe write onlynothing
between 0 and DEPTHwrite and readboththe item that was oldest before the edge
DEPTH (full)write and readthe read onlythe oldest item
DEPTH (full)writenothingnothing
anyanything, with resetnothing; the FIFO emptiesnothing

Decisions at the boundaries. Every other case follows from the two acceptance rules.

Reset is synchronous and wins over everything: at an edge with reset at 1, the FIFO discards what it holds, clears read_valid and accepts neither request.

What read_valid promises

A read accepted at one edge shows its item on read_data after that edge, with read_valid at 1 until the next edge. At every edge that accepts no read, read_valid returns to 0.

Between reads, read_data may keep showing the last item. A checker must ignore it then, because an old value is not a new answer. This one-edge delay also shapes the scoreboard in lesson 7.4: it predicts at one edge and checks the answer at the next.

Why register the answer at all? A registered output changes only just after a clock edge, so whatever reads it sees a steady value for a whole cycle. The cost is the one-cycle delay, which every part of the testbench has to respect.

The rules as code

The example turns the rules into a class with a queue, built like the scoreboard queue exp_q from Module 2. Each call to step() is one rising edge: decide both acceptances from the size before the edge, then pop the read and push the write.

Read first, then decide the write

if (read_en && exp_q.size() > 0)
  data = exp_q.pop_front();
take_w = write_en && exp_q.size() < depth;

At full, the pop makes room, so the model accepts a write the FIFO refuses: the opening story.

Decide both, then change the queue

take_w = write_en && exp_q.size() < depth;
take_r = read_en  && exp_q.size() > 0;
if (take_r) data = exp_q.pop_front();
if (take_w) exp_q.push_back(write_data);

Both decisions see the occupancy from before the edge, exactly as the rule says.

Writing step() also forces decisions that prose tends to skip. A refused read changes nothing at all. A read accepted at one edge is still answered if the next edge is a reset, because reset acts only at its own edge.

A model you can run is a stronger statement of the rules than prose. When the FIFO and the model disagree later, you can point at a line of the model and ask which of the two is wrong.

From rules to a test plan

Each rule needs at least one test that would fail if the rule were broken. Write that list before any testbench code, and let it decide what the generator must produce.

RuleDirected caseBuilt in
a read at empty is refusedread with nothing stored7.3
a write at full is refusedfill past full7.3
both at full: read onlyread and write at full7.3
both at empty: write onlyread and write at empty7.6
both in between: old head returnedread and write with 1 to DEPTH-1 stored7.6
reset empties and cancelsreset while holding data7.6
order survives the ringtwo laps at depth 37.6

The test plan. Random traffic in lesson 7.6 adds volume on top of these cases.

The testbench you will build

The testbench has layers, and each layer has one job. The next six lessons build them in order, and each lesson shows this table with its progress.

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 5lesson 7.3
Mailboxesmailbox #(fifo_op), mailbox #(fifo_obs)carry operations and observations between processesModule 3lesson 7.3
Driverfifo_driverdrives one operation per clock through drv_cbModule 6lesson 7.3
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 ruleslesson 7.2

The layered testbench after lesson 7.1.

The layers talk to each other only through objects: fifo_op for an operation to apply and fifo_obs for what was seen on the pins. No layer reaches into another, so each can be read, tested and replaced on its own.

Only the DUT changes when you move to another FIFO. The generator, driver, monitor and scoreboard talk about operations and observations, so they carry over to a different design with the same rules.

Try it

Each line is one edge. At empty only the write is taken; between, both are taken and the read returns 11, the oldest item; at full only the read is taken; reset takes nothing and empties the model.

Move the pop_front() line above the take_w line, so the read is done before the write is decided, and predict which line changes.

RUNVerilator · simulator service
Edit the code, then run it
Expected: both at empty: held 0 -> 1, took write=1 read=0 write: held 1 -> 2, took write=1 read=0 write: held 2 -> 3, took write=1 read=0 both between: held 3 -> 3, took write=1 read=1, read returns 11 write: held 3 -> 4, took write=1 read=0 write at full: held 4 -> 4, took write=0 read=0 both at full: held 4 -> 3, took write=0 read=1, read returns 22 reset, both: held 3 -> 0, took write=0 read=0

Check yourself

Predict the outputThe model from this lesson runs with a depth of 2. Predict the exact output.
// The FIFO rules as an executable model: a queue, like the scoreboard queue in module 2.
class fifo_model;
  int       depth;
  bit [7:0] exp_q [$];                 // what the FIFO holds, oldest first
  function new(int depth);
    this.depth = depth;
  endfunction
  // One rising edge. Returns {took_write, took_read}; 'data' gets the value read.
  function bit [1:0] step(bit reset, bit write_en, bit read_en, bit [7:0] write_data,
                          output bit [7:0] data);
    bit take_w, take_r;
    data = 0;
    if (reset) begin
      exp_q.delete();                  // reset wins: nothing is taken
      return 2'b00;
    end
    take_w = write_en && exp_q.size() < depth;   // both decisions use the
    take_r = read_en  && exp_q.size() > 0;       // occupancy before the edge
    if (take_r) data = exp_q.pop_front();
    if (take_w) exp_q.push_back(write_data);
    return {take_w, take_r};
  endfunction
endclass

module tb;
  fifo_model m = new(2);
  bit [7:0] r1, r2, unused;
  initial begin
    void'(m.step(0, 1, 0, 8'h11, unused));
    void'(m.step(0, 1, 0, 8'h22, unused));
    void'(m.step(0, 1, 1, 8'h33, r1));      // read and write at full
    void'(m.step(0, 1, 1, 8'h44, r2));      // read and write with 1 stored
    $display("returned %h then %h, holding %0d: %h", r1, r2, m.exp_q.size(), m.exp_q[0]);
  end
endmodule
Spot the bugEach option is the reset handling at the top of step(), before the acceptance decisions. Reset must empty the model and accept nothing at that edge. Which option breaks the rule?
ConceptThe FIFO is empty. At the next edge, both write_en and read_en are 1. What happens?
ConceptAfter an edge that accepted no read, read_valid is 0 and read_data shows 8'h5a. What should a checker do with 8'h5a?
ConceptA read is accepted at the rising edge at t=25, with a 10 ns clock. When is read_valid 1?

Remember

  • Write the rules down as code: a queue model that accepts and returns exactly what the FIFO should.
  • Both acceptance decisions use the occupancy from before the edge; a read never makes room for a write in the same edge.
  • Reset is synchronous, wins over every request, and empties the FIFO.
  • read_data means something only while read_valid is 1, one cycle after the accepting edge.
  • Turn every rule into a directed test before building the testbench.

Practice