A complete asynchronous FIFO design with a UVM-based constrained-random verification environment for validating safe data transfer across clock domain crossings (CDC). The project demonstrates industry-standard verification methodology applied to a Gray-code-synchronized dual-clock FIFO.
Clock Domain Crossing is one of the most error-prone areas in digital design. This project pairs a classical async FIFO implementation (Gray code pointer synchronization over dual-port RAM) with a structured UVM testbench that exercises corner cases, randomized traffic patterns, and flag integrity — making it a practical reference for both RTL design and functional verification.
- Dual-clock FIFO — Independent write (
wclk@ 100 MHz) and read (rclk@ ~71.4 MHz) domains with Gray code synchronizers - 16×8 Dual-Port RAM — 16 entries, 8 bits wide
- Full UVM 1.2 environment — Agent, driver, monitor, scoreboard, sequences, and test
- Constrained-random stimulus — Weighted randomization with 70/30 write/read distribution
- Self-checking scoreboard — Reference FIFO queue model with automatic data integrity checks and vacuous-pass detection
- Status flag coverage —
full,empty,overrun, andunderrundetection and validation
Write Domain (wclk) Read Domain (rclk)
┌──────────────────────┐ ┌──────────────────────┐
│ Write Pointer (bin) ──► Gray ──► 2-FF Sync ──► Full │
│ │ │ │ Logic │
│ ▼ │ │ │
│ Dual-Port RAM │ │ Dual-Port RAM │
│ (write port) │ │ (read port) │
│ │ │ ▲ │
│ Empty ◄── 2-FF Sync ◄── Gray ◄── Read Pointer (bin) │
│ Logic │ │ │
└──────────────────────┘ └──────────────────────┘
| Parameter | Value |
|---|---|
| Data Width | 8 bits |
| FIFO Depth | 16 entries |
| Address Width | 4 bits (+1 MSB for full/empty) |
| Write Clock | 10 ns period (100 MHz) |
| Read Clock | 14 ns period (~71.4 MHz) |
Binary pointers are converted to Gray code before crossing into the opposite domain via a 2-stage flip-flop synchronizer. This guarantees only a single bit toggles per pointer increment, eliminating metastability-induced corruption during sampling.
| Flag | Condition | Domain |
|---|---|---|
empty |
Gray read pointer == synchronized write pointer | rclk |
full |
Gray write pointer == synchronized read pointer (top 2 MSBs inverted) | wclk |
underrun |
Read attempted while FIFO is empty | rclk |
overrun |
Write attempted while FIFO is full | wclk |
tb_top (SystemVerilog module)
├── DUT instantiation (top)
├── fifo_if (virtual interface)
└── UVM Test
└── fifo_test
└── fifo_env
├── fifo_agent (active)
│ ├── fifo_sequencer ─── fifo_sequence
│ ├── fifo_driver
│ └── fifo_monitor ──── analysis port
│ ├── monitor_write (wclk domain)
│ └── monitor_read (rclk domain)
└── fifo_scoreboard ◄───── analysis export
The transaction class models a single FIFO operation. Fields are registered with the UVM field automation macros for built-in print(), copy(), compare(), and pack()/unpack() support.
| Field | Type | Description |
|---|---|---|
write |
rand bit |
Write enable |
read |
rand bit |
Read enable |
data |
rand bit [7:0] |
Write data / observed read data |
Generates 500 constrained-random transactions with weighted distribution:
write dist {1:=70, 0:=30}; // 70% chance of write per txn
read dist {1:=70, 0:=30}; // 70% chance of read per txn
Transactions follow the UVM sequencer protocol: start_item() → randomize() → finish_item(). This ensures the sequencer has granted the item before randomization occurs, which is UVM best practice.
This distribution is tuned to stress the FIFO by keeping it moderately loaded, maximizing the probability of hitting full, empty, and simultaneous read/write scenarios under asynchronous clocking.
Drives transactions onto the DUT interface with the following protocol:
- Write path — Waits for
posedge wclk, then checksfull. If not full, assertswenanddinvia NBA, waits one morewclkcycle, then deassertswen. - Read path — Waits for
posedge rclk, then checksempty. If not empty, assertsrenvia NBA, waits one morerclkcycle, then deassertsren. - Idle path — If neither write nor read is requested, the driver advances by one
wclkcycle to prevent zero-delay simulation loops.
Both operations are guarded by DUT status flags to respect backpressure. The driver retrieves the virtual interface handle from uvm_config_db during the build_phase.
Uses two independent forked processes, one per clock domain, to avoid CDC sampling races:
monitor_write()— Triggers onposedge wclk, waits#1to let the driver's NBA assignments resolve, then checkswen && !full. On a valid write, it capturesdininto a transaction withwrite=1, read=0and broadcasts it via the analysis port.monitor_read()— Triggers onposedge rclk, waits#1for NBA resolution, then checksren && !empty. On a valid read, it waits one additionalrclkcycle for the registered RAM output (rdata) to become valid, then capturesdoutinto a transaction withwrite=0, read=1and broadcasts it.
The #1 delay after each clock edge is critical: since the driver uses non-blocking assignments (<=), these values resolve in the NBA region after the active region where the monitor's @(posedge) trigger fires. Without the delay, the monitor would sample stale values and miss all transactions.
Implements a reference model using an internal SystemVerilog queue (bit [7:0] model_q[$]) that mirrors expected FIFO behavior:
On write txn → push din to reference queue
On read txn → pop front of reference queue, compare against captured dout
Mismatch → uvm_error("SB", "DATA MISMATCH")
The scoreboard also tracks num_writes, num_reads, and num_matches counters. In the UVM check_phase, it prints a summary and raises UVM_ERROR if either counter is zero, catching vacuous passes where the test completes without any actual data integrity checks.
===== SCOREBOARD SUMMARY =====
Writes captured : 245
Reads captured : 245
Matches : 245
Queue remaining : 0
==============================
Encapsulates the driver, monitor, and sequencer into a reusable agent. Connections are made in the connect_phase:
driver.seq_item_port ──► sequencer.seq_item_export
monitor.mon_ap ──► scoreboard.sb_ap (via env)
Instantiates and wires the agent and scoreboard. The connect_phase links the monitor's analysis port to the scoreboard's analysis export, completing the data path from DUT observation to reference model checking.
Top-level test class that builds the environment, raises an objection, starts the sequence on the agent's sequencer, waits for drain time (#1000), and drops the objection to end the simulation.
┌─────────────┐ ┌────────────┐ ┌──────────┐
│ Sequence │────►│ Driver │────►│ DUT │
│ (randomized)│ │ (stimuli) │ │ (FIFO) │
└─────────────┘ └────────────┘ └──────────┘
│
┌──────────────────┤
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Monitor │ │ Monitor │
│ (wclk) │ │ (rclk) │
│ captures din│ │captures dout│
└──────┬──────┘ └──────┬──────┘
│ │
▼ ▼
┌────────────────────────────────┐
│ Scoreboard │
│ (ref model + vacuous-pass │
│ detection in check_phase) │
└────────────────────────────────┘
| Scenario | How It's Covered |
|---|---|
| Basic write → read data integrity | Scoreboard queue comparison on every read |
| FIFO full condition | Constrained-random write-heavy traffic fills FIFO; driver respects full flag |
| FIFO empty condition | Read-heavy bursts drain FIFO; driver respects empty flag |
| Overrun detection | Write attempted when full → overrun flag asserted |
| Underrun detection | Read attempted when empty → underrun flag asserted |
| CDC metastability safety | Mismatched clock frequencies (100 MHz vs ~71.4 MHz) exercise async boundaries |
| Simultaneous read/write | 70/30 distribution ensures high probability of concurrent operations |
| Vacuous pass prevention | check_phase asserts that writes and reads were actually observed |
cdc-fifo/
├── ram_top.sv # Dual-port RAM (16×8), independent R/W clocks
├── top.sv # Async FIFO top — pointers, Gray code, synchronizers, flags
├── tb.sv # UVM testbench (interface, txn, seq, drv, mon, sb, agent, env, test)
└── README.md
A Verilog/SystemVerilog simulator with UVM 1.2 support:
- Synopsys VCS
- Cadence Xcelium
- Mentor Questa / ModelSim
- Icarus Verilog (with UVM plugin)
VCS:
vcs -full64 -sverilog -ntb_opts uvm-1.2 \
ram_top.sv top.sv tb.sv \
-o simv -timescale=1ns/1ps
./simv +UVM_TESTNAME=fifo_test +UVM_VERBOSITY=UVM_MEDIUM
Questa:
vlog -sv +incdir+$UVM_HOME/src ram_top.sv top.sv tb.sv
vsim -c tb_top +UVM_TESTNAME=fifo_test -do "run -all; quit"
Xcelium:
xrun -sv -uvm -uvmhome CDNS-1.2 \
ram_top.sv top.sv tb.sv \
-timescale 1ns/1ps +UVM_TESTNAME=fifo_test
A passing simulation will complete with no UVM_ERROR or UVM_FATAL messages and a non-zero scoreboard summary:
UVM_INFO ... [RNTST] Running test fifo_test...
...
UVM_INFO ... [SB]
===== SCOREBOARD SUMMARY =====
Writes captured : 245
Reads captured : 245
Matches : 245
Queue remaining : 0
==============================
...
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO : XX
UVM_WARNING : 0
UVM_ERROR : 0
UVM_FATAL : 0
Any DATA MISMATCH errors from the scoreboard indicate a functional failure in the DUT. A VACUOUS PASS error indicates the monitor failed to capture transactions — check interface connectivity and clock generation.
- Functional coverage — Add
covergroupfor pointer states, flag transitions, and FIFO occupancy bins - Assertions (SVA) — Protocol checks on Gray code properties, flag timing, and no data loss guarantees
- Multiple sequences — Add targeted sequences for burst writes, burst reads, back-to-back fill/drain, and reset-mid-operation
- Parameterization — Make data width, depth, and synchronizer stages configurable with verification across multiple configs
- Regression suite — Multiple test classes with different seeds and stimulus profiles for broader coverage closure