Implement the FIFO and its boundaries
Write FIFO RTL that follows the rules of lesson 7.1 exactly, with state that changes only on accepted operations and pointers that wrap at any depth.
- Lesson
- 7.2
- Time
- 25 min
- Runs on
- Verilator
- Check
- 5 questions
A FIFO passed every test at depth 4 and returned the wrong data the first time it was built with depth 3. Its write pointer was a 2-bit counter that wrapped by overflowing, so after slot 2 it moved to slot 3, which a depth-3 FIFO does not have. The bug needed both a full lap of the ring and a depth that is not a power of two, and the old tests had neither.
One column per accepted write, with reads in between keeping the FIFO from filling.
Offers and accepted operations
The RTL starts from the two decisions of lesson 7.1. take_w is write_en && used < DEPTH and take_r is read_en && used > 0, where used is the number of stored items. Both are continuous assignments of the values before the edge.
Everything that changes state is gated by them: the memory write, both pointers and used. An offer that is refused, such as a write while full, must change nothing at all, or a later accepted operation lands in the wrong place.
Named wires also help when debugging. In a waveform, take_w and take_r show at a glance which offers the FIFO accepted, so you rarely have to work it out from used.
The flags come from the same used: full is used == DEPTH and empty is used == 0. Because acceptance and flags read one number, a testbench never sees the FIFO accept a write while full says it cannot.
One decision for the occupancy
At an edge that takes both a write and a read, used must stay the same. Writing that as two separate nonblocking assignments looks right but is wrong.
Two updates
if (take_w) used <= used + 1;
if (take_r) used <= used - 1;
When both fire, the later assignment wins, so used drops by 1 although the count of stored items did not change.
One case on both decisions
case ({take_w, take_r})
2'b10: used <= used + 1;
2'b01: used <= used - 1;
default: used <= used;
endcase
Each combination of decisions has exactly one result, and both together leave used alone.
Both nonblocking assignments in the left version are scheduled at the same edge, and the last one scheduled sets the final value (Module 6). The bug appears only when both operations happen together, which is why the test plan includes that case at every fill level.
A useful habit is to write the next value of every register as one expression or one case. Each combination of conditions then has exactly one result, and you can read it off the page.
A registered response
read_valid is assigned at every edge that is not a reset: read_valid <= take_r;. Writing it only inside if (take_r) would leave it stuck at 1 after the first read, because nothing would ever clear it.
read_data changes only when a read is accepted, and it takes the item at the read pointer. Between reads it keeps its last value, which the rules allow. Reset clears used, both pointers, read_valid and read_data in one step.
Suppose the FIFO answered within the same edge, through a path straight from the memory to read_data. The output could then change whenever the inputs changed, and every reader would have to allow for that. Registering it trades one cycle of delay for an output that holds still.
The memory itself has no reset. After a reset, the old items are still in it, but used is 0 and both pointers are back at slot 0, so nothing can read them before they are overwritten.
Wrap at the depth
The pointers walk around a ring of DEPTH slots. Each one moves only on its own accepted operation and wraps with an explicit test: wp <= (wp == DEPTH - 1) ? 0 : wp + 1;.
A counter that simply overflows wraps at the next power of two. That happens to be correct when DEPTH is 4 and the counter is 2 bits wide, and wrong for every other depth. So test the FIFO at a depth such as 3, for at least two laps of the ring.
Here the pointers are int unsigned, far wider than they need to be, so they cannot overflow by accident. The explicit test against DEPTH - 1 is what keeps them in range, at every depth.
A first test at the pins
The example drives the pins directly from a task: it sets the requests on a falling edge, lets the FIFO act at the rising edge, and prints what it sees. It peeks at dut.used to show the occupancy, which a real testbench would not do. Lesson 7.3 replaces this task with a driver.
The task changes the inputs on the falling edge, half a cycle away from the rising edge where the FIFO reads them, so the test never races the design. Module 6's clocking blocks do the same job more neatly, which is why the driver in lesson 7.3 uses one.
| 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 | this lesson |
The layered testbench after lesson 7.2.
Try it
Each line shows the requests of one edge and the outputs just after it. The write of 44 is refused at full. At full with both requests, 11 comes out and 55 is refused. With two stored, 22 comes out and 66 goes in, and 66 is the last item read, after the pointers have wrapped.
Reads at empty are refused and read_valid stays 0 while read_data keeps 66. Reset clears everything. Now change wp to wrap as (wp + 1) % 4 and predict which line changes first.
w=0 r=0 d=00 rst=1 | full=0 empty=1 valid=0 data=00 used=0 w=1 r=0 d=11 rst=0 | full=0 empty=0 valid=0 data=00 used=1 w=1 r=0 d=22 rst=0 | full=0 empty=0 valid=0 data=00 used=2 w=1 r=0 d=33 rst=0 | full=1 empty=0 valid=0 data=00 used=3 w=1 r=0 d=44 rst=0 | full=1 empty=0 valid=0 data=00 used=3 w=1 r=1 d=55 rst=0 | full=0 empty=0 valid=1 data=11 used=2 w=1 r=1 d=66 rst=0 | full=0 empty=0 valid=1 data=22 used=2 w=0 r=1 d=00 rst=0 | full=0 empty=0 valid=1 data=33 used=1 w=0 r=1 d=00 rst=0 | full=0 empty=1 valid=1 data=66 used=0 w=0 r=1 d=00 rst=0 | full=0 empty=1 valid=0 data=66 used=0 w=0 r=1 d=00 rst=0 | full=0 empty=1 valid=0 data=66 used=0 w=1 r=1 d=77 rst=0 | full=0 empty=0 valid=0 data=66 used=1 w=1 r=0 d=88 rst=1 | full=0 empty=1 valid=0 data=00 used=0 w=0 r=1 d=00 rst=0 | full=0 empty=1 valid=0 data=00 used=0Check yourself
Remember
- Gate every state change with
take_wandtake_r; a refused offer changes nothing. - Compute acceptance and the flags from the same pre-edge count.
- Update the occupancy with one decision on both operations, never two competing assignments.
- Assign
read_validon every edge, and wrap pointers with an explicit test againstDEPTH - 1. - Test at a depth that is not a power of two, for at least two laps.
Practice
- The read that was valid on an empty FIFO Fix · Easy
- Arbitrary-depth FIFO with a fill level Implement · Medium