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.
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.
| Port | Direction | Meaning |
|---|---|---|
clk | input | every decision happens at a rising edge |
reset | input | synchronous, active high, and stronger than every request |
write_en, write_data | input | asks to store write_data at this edge |
read_en | input | asks to remove the oldest stored item at this edge |
full, empty | output | computed from how many items are stored right now |
read_valid | output | 1 for one cycle after an edge that accepted a read |
read_data | output | the 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 edge | Requests | Accepted | Read returns |
|---|---|---|---|
| 0 (empty) | write and read | the write only | nothing |
| between 0 and DEPTH | write and read | both | the item that was oldest before the edge |
| DEPTH (full) | write and read | the read only | the oldest item |
| DEPTH (full) | write | nothing | nothing |
| any | anything, with reset | nothing; the FIFO empties | nothing |
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.
| Rule | Directed case | Built in |
|---|---|---|
| a read at empty is refused | read with nothing stored | 7.3 |
| a write at full is refused | fill past full | 7.3 |
| both at full: read only | read and write at full | 7.3 |
| both at empty: write only | read and write at empty | 7.6 |
| both in between: old head returned | read and write with 1 to DEPTH-1 stored | 7.6 |
| reset empties and cancels | reset while holding data | 7.6 |
| order survives the ring | two laps at depth 3 | 7.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.
| Layer | Component | Job | Reuses | Status |
|---|---|---|---|---|
| Test | fifo_test, fifo_config | chooses depth, seed and length, runs the environment, gives the verdict | Module 4 classes | lesson 7.7 |
| Generator | fifo_generator | creates fifo_op objects: directed, then seeded random | Modules 4 and 5 | lesson 7.3 |
| Mailboxes | mailbox #(fifo_op), mailbox #(fifo_obs) | carry operations and observations between processes | Module 3 | lesson 7.3 |
| Driver | fifo_driver | drives one operation per clock through drv_cb | Module 6 | lesson 7.3 |
| Monitor | fifo_monitor | samples the pins through mon_cb into a new fifo_obs | Module 6 | lesson 7.4 |
| Scoreboard | fifo_scoreboard | predicts each edge with the queue exp_q and compares | Modules 2 and 4 | lesson 7.4 |
| Assertions | fifo_props | checks timing rules on the pins at every edge | Module 6 | lesson 7.4 |
| Coverage | fifo_coverage, fifo_scenarios | measures which situations the stimulus reached | Module 5 stimulus | lesson 7.5 |
| DUT | fifo | the design under test | lesson 7.1 rules | lesson 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.
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=0Check yourself
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_datameans something only whileread_validis 1, one cycle after the accepting edge.- Turn every rule into a directed test before building the testbench.
Practice
- Acceptance truth table Implement · Easy
- Requests that expose broken FIFO rules Break it · Medium