CASE STUDY / DIGITAL DESIGN / VERIFICATION

Small queue. Clear contract.

A parameterized synchronous FIFO in SystemVerilog, built to explore the corner cases behind seemingly simple hardware buffering.

THE PROJECT IN ONE MINUTE

My engineering work

Implemented a parameterized single-clock FIFO and a separate shift-array reference scoreboard to exercise reset, full/empty boundaries and simultaneous reads/writes.

Inspect first

Four passing configurations, including 800 fixed-seed transactions per setup; the page links directly to SystemVerilog RTL, testbench, simulator log and raw VCD.

Verification boundary

Simulation of a defined single-clock contract. Generic Yosys synthesis is documented below; device timing, FPGA deployment, and CDC remain future work.

01 / THE HARDWARE QUESTION

What happens at the boundaries?

FIFO buffers show up between producers and consumers throughout digital systems. The happy path is obvious: write data in, read it out in order. What matters just as much is what happens when the queue is already full or empty, or both requests arrive on the same clock.

I kept this experiment intentionally narrow: one clock domain, a simple registered read interface and testable rules for every boundary case. That leaves room to reason about the design rather than burying the behaviour inside a larger system.

02 / SPECIFY BEFORE SIMULATING

Define what each clock is allowed to do.

The interface accepts requests wr_en and rd_en at each rising edge. The output rd_data updates only for an accepted read. full, empty and count expose the occupancy after the clock edge.

EMPTY

Don't invent data.

A read when empty is blocked. If a write also arrives, only the write proceeds—this FIFO has no first-word fall-through.

FULL

Keep the stream moving.

A write when full is blocked, except when a valid read happens on that edge. Both may then proceed, preserving full occupancy.

RESET

Clear state, not storage.

Active-low asynchronous reset clears pointers, occupancy and registered output. Memory contents are not reset; zero occupancy prevents stale reads.

This contract is deliberately specific. It is not AXI-Stream, asynchronous CDC buffering or a general-purpose bus-ready/valid protocol.

03 / DESIGN DECISIONS

Make acceptance explicit.

Two combinational conditions decide what happens on the next edge. A read is accepted only if the FIFO is nonempty; a write is accepted if there is space, or if the same edge removes an existing word. This avoids special-case bookkeeping inside the occupancy counter.

REAL RTL / sync_fifo.svFull module ↗
wire take_read  = rd_en && !empty;
wire take_write = wr_en && (!full || take_read);

case ({take_write, take_read})
  2'b10: count <= count + 1'b1;
  2'b01: count <= count - 1'b1;
  default: count <= count;
endcase

The pointers wrap at DEPTH-1 instead of relying on binary overflow. That is why a three-entry instance works as well as an eight-entry instance.

01

Registered read data

Read output changes after a valid clocked pop and otherwise holds its last value. The design is not combinational-first-word fall-through.

02

Simultaneous transactions

On full, an accepted read and write replace the oldest word in one edge; an NBA-registered read returns the old word rather than the incoming data.

03

Parameterization

Pointer and occupancy widths derive from depth. The simulation regression covers depths 1, 3, 4 and 8 and a wider 16-bit data path.

04 / VERIFICATION EVIDENCE

Don't just drive signals. Check outcomes.

The testbench keeps an independent shift-array queue, unlike the RTL's circular pointer storage. After every transaction it checks the registered output, occupancy and flags. Directed cases exercise both empty and full boundaries; each configuration also runs 800 fixed-seed pseudo-random transactions.

SIMULATION / ICARUS VERILOG 12

4 / 4 configurations passed

Depths 1, 3 and 4 with eight-bit data, plus depth 8 with 16-bit data. Each test included reset, underflow/overflow prevention, simultaneous operations, wraparound and randomized push/pop traffic. These are simulation results, not FPGA tests.

ONE TEST / SOURCE TO RESULT

What happens when a full queue is written and read on the same edge?

  1. Contract → RTL

    At full occupancy, a standalone write is rejected. If a read is accepted on that edge, take_write = wr_en && (!full || take_read) allows the incoming word to replace the oldest one while the count stays full. The read returns the previous head, not the new word.

  2. Stimulus → independent check

    The public bench first fills the queue, tries an overflow, then sends simultaneous write/read requests. Its separate shift-array reference model checks output, occupancy and flags after the edge; it does not copy the DUT's circular-pointer algorithm.

  3. Recorded outcome → boundary

    The saved depth-4, eight-bit run reports PASS, 823 checked cycles, 385 accepted writes, 384 reads, 38 blocked writes and 60 blocked reads. These are finite simulator observations under one configuration, not a hardware throughput result.

RTL acceptance logic ↗Stimulus and scoreboard ↗Actual four-run log ↗

CAPTURED POST-CLOCK SAMPLES / DEPTH 4SIMULATED SIGNALS, NOT PHYSICAL MEASUREMENTS
Cycle-sampled simulation trace showing requested reads and writes, occupancy, full and empty flags and last read data, derived from an actual VCD capture.
Selected first 26 samples from the real Icarus waveform, taken 1 ns after successive rising clock edges. The visual is generated from the VCD, not drawn to imply a result. Download the raw VCD ↓

Read the captured edge, not just the PASS line. In the linked depth-4 waveform, sample 09 requests a write of EE while full and without a read; count stays four. At sample 10, simultaneous read and write of F1 keep count four while rd_data=01, the previous head. Samples 11–14 read 02, 03, 04, F1 in order. This is a cycle-sampled simulator trace, not measured silicon timing.

REPRODUCE / ICARUS VERILOGPublic build instructions ↗
bash projects/fifo-rtl/run_tests.sh

The public source + testbench bundle ↓ preserves the original project paths and runner. Unzip and run the command from the extracted root with Icarus Verilog available. The archived log was generated from these files; a future new run may differ in non-functional simulator diagnostics.

05 / A REPRODUCIBLE DEBUGGING EXERCISE

One missing condition. One dropped write.

This controlled mutation exercise makes a real failure observable: remove the simultaneous-read exception from the existing write-acceptance rule, then run the same independent testbench. It illustrates diagnosis and regression protection; it is not a claim of a historical bug I discovered in production.

  1. Symptom.

    At cycle 10, a full depth-four queue receives a read and a write. The faulty variant returns the old head but drops the new word: occupancy becomes three when the contract requires four.

  2. Root cause.

    wr_en && !full rejects a write using the pre-edge full flag, even though a read frees a slot on that same edge.

  3. Correction.

    The existing RTL uses wr_en && (!full || take_read). It accepts replacement while preserving the old-head read and full occupancy.

  4. Regression.

    The unchanged scoreboard rejects the mutant with expected count=4, actual=3. The corrected RTL completes 823 checked cycles. The four-configuration regression also passes.

OBSERVED TESTBENCH FAILURE / CONTROLLED VARIANT
cycle=10 DEPTH=4
expected count/full/empty/data=4/1/0/1
actual=3/0/0/1

Read the failing and passing runs ↗ · Download the failing waveform ↓ · Download the reproduction script ↓

python3 scripts/reproduce_fifo_debug.py /tmp/fifo-debug
06 / BEYOND SIMULATION

Through a real synthesis flow.

Yosys 0.52 successfully elaborated and synthesized the same FIFO RTL for all four tested configurations. check -assert reported zero structural problems. The -noabc run produces generic logic cells, so these counts are not FPGA LUT counts, clock rates, area estimates, or timing closure.

Generic synthesis results
DepthData widthGeneric cells
1829
38113
48150
816480

Read actual Yosys output ↗ · Download measured counts ↓ · Download the synthesis runner ↓

bash projects/fifo-rtl/run_synthesis.sh /tmp/fifo-synthesis

The next step is target-device mapping, constraints, and board verification. This completes a small synthesis milestone while keeping physical performance claims separate.

07 / ENGINEERING BOUNDARIES

Correct in simulation isn't the finish line.

This is a single-clock register-array FIFO, verified in simulation and checked through generic synthesis. Hardware timing, SRAM inference, power, formal proof, FPGA deployment, and cross-clock safety remain outside its demonstrated scope. The passing testbench supports the documented interface contract under the tested cases and parameters; it does not prove correctness in every environment.

Next steps are target-device mapping and timing constraints, followed by a board demonstration. Ready/valid integration and formal properties are further design directions.

The lesson is straightforward: a hardware block is only as convincing as its precisely stated behaviour and the checks supporting it.

Discuss the design