Ask for a 6-deep FIFO and Icarus stops before simulation: ERROR: fifo_ip.sv:24: fifo_ip: DEPTH must be a power of two, at least 2. Make the same request through Verilator with -Wno-fatal, and the build succeeds, runs, and fails 507 cycles of the self-checking test. A reusable block has to refuse a bad configuration in every flow its users run, and it has to arrive with the evidence that it works.
This project takes the synchronous FIFO from the memory part, keeps its contract, and packages it with everything this part taught: a package, checked parameters, an interface with modports, assertions built into the RTL, a file list, a lint run with one justified waiver, and a bounded proof.
Read the figure as one core and the files around it.
Top: the package is compiled first. It holds the operation type and the depth check that the core calls while it elaborates.
Left and right: the push side and the pop side, as plain ports. Icarus, Verilator and Yosys can all read the core.
The two accent lines: the checks travel inside the RTL. Simulators read the SVA, Yosys reads the formal block, synthesis reads neither.
Bottom: the interface is a second view of the same ports, for flows that support interfaces.
Port | Direction | Width | Meaning |
|---|---|---|---|
clk | input | 1 | Rising-edge clock |
rst_n | input | 1 | Asynchronous reset, active low |
push_i | input | 1 | Push request, taken only while full_o is low |
din_i | input | WIDTH | Word stored by an accepted push |
full_o | output | 1 | level_o equals DEPTH |
pop_i | input | 1 | Pop request, taken only while empty_o is low |
dout_o | output | WIDTH | Popped word, registered; holds between pops |
dout_valid_o | output | 1 | High for the one cycle after each accepted pop |
empty_o | output | 1 | level_o equals 0 |
level_o | output | log2(DEPTH) + 1 | Words stored, 0 to DEPTH |
Parameters. WIDTH is at least 1. DEPTH is a power of two and at least 2. Any other value stops elaboration with a message that names the rule.
Reset. Clears both pointers, level_o, dout_o and dout_valid_o, so empty_o is 1 and full_o is 0. The storage array is not reset.
Acceptance. A push is accepted when push_i && !full_o, a pop when pop_i && !empty_o. At full, a push is refused even when a pop is accepted on the same edge: the conservative rule of the sync FIFO lesson.
Latency. A pop accepted at edge N puts its word on dout_o after edge N, with dout_valid_o high for that cycle. A word pushed at edge N can be popped at edge N + 1.
Level and order. level_o is accepted pushes minus accepted pops since reset. Words leave in the order they arrived, none lost and none repeated.
Deliverables. The package, the core, the interface view, a file list, a waiver file, a build script, and the checks in Done when.
The almost flags of the course FIFO are left out on purpose; adding one is a stretch goal.
Contract and Invariants: the FIFO rules this IP keeps.
Packages and Compile Order and Parameter Types and Guards: the package and the guards.
Common RTL property templates: the shape of every assertion below.
Your First Formal Run: the Yosys flow, reused unchanged.
The project also uses Generate loops and conditionals for the guards, Interfaces, Modports, and Portability for the bundle, Compile-Order and Package Errors, Decoded for the order check, and Waiver rules that scale for the waiver.
Each row is a check you can run yourself. The last column is what the reference solution printed.
Check | Command | Reference result |
|---|---|---|
Compiles in Icarus | iverilog -g2012 -Wall -f files.f tb_fifo_ip.sv | No warnings |
Lint clean | verilator --lint-only -Wall fifo_ip.vlt -f files.f | Exit 0, with one waiver (SYNCASYNCNET on rst_n) justified in the file |
Bad parameters refused | iverilog -g2012 -P fifo_ip.DEPTH=6 -f files.f | Elaboration error naming the rule; DEPTH = 1 and WIDTH = 0 also fail |
Bench, both simulators | vvp -n tb.vvp, then the Verilator binary | PASS for 8x4, 32x16 and 1x2, each reaching full and empty |
SVA silent, then firing | verilator --binary --assert, good RTL then a seeded bug | Good: no assertion message. Bug: a_bounds fails at time 75 |
Bounded proof | ./formal.sh W D 24 for the three sets | Status: PASSED; induction passes; c_full reached at steps 5, 17 and 3 |
No latch, expected flops | yosys: synth -top fifo_ip; stat, for 8x4 | 47 flops: 32 storage without reset, 15 control with reset; no latch cell |
Interface view | verilator --binary with tb_fifo_ip_if | PASS: 1065 words through the modports, all in order |
Each // name.sv comment starts a separate file. Line numbers in tool messages count from that comment, as in the full listing under Testbench.
// fifo_ip_pkg.sv: shared types and the parameter check
package fifo_ip_pkg;
// What one clock edge does, from the accepted push and pop.
typedef enum logic [1:0] {
OP_NONE = 2'b00, OP_POP = 2'b01, OP_PUSH = 2'b10, OP_BOTH = 2'b11
} fifo_op_t;
// A legal depth is a power of two and at least 2.
function automatic logic depth_ok(input int depth);
depth_ok = depth >= 2 && (depth & (depth - 1)) == 0;
endfunction
endpackage`fifo_op_t` names the four things an edge can do, so the control code compares names, not bit pairs.
`depth_ok` is a constant function: the core calls it during elaboration. (d & (d - 1)) == 0 is true only when one bit of d is set.
No `return`. Yosys 0.69 stopped at the return line with syntax error, unexpected TOK_ID. Assigning to the function name works in all three tools.
No `import`. Yosys 0.69 rejected import fifo_ip_pkg::*; in the module header and in the body (syntax error, unexpected TOK_IMPORT), so the core writes every package name in full, such as fifo_ip_pkg::depth_ok. Full names also rule out the two-package clash of case 3 in the errors lesson.
// snippet: fifo_ip.sv, lines 1 to 28 (line 1 is the file comment)
module fifo_ip #(
parameter int WIDTH = 8,
parameter int DEPTH = 4
) (
input logic clk,
input logic rst_n,
// push side
input logic push_i,
input logic [WIDTH-1:0] din_i,
output logic full_o,
// pop side
input logic pop_i,
output logic [WIDTH-1:0] dout_o,
output logic dout_valid_o,
output logic empty_o,
output logic [$clog2(DEPTH+1)-1:0] level_o
);
localparam int AW = $clog2(DEPTH);
localparam logic [AW:0] FULL_LEVEL = (AW + 1)'(DEPTH);
// Elaboration-time guards: a bad configuration does not build.
if (!fifo_ip_pkg::depth_ok(DEPTH)) begin : g_bad_depth
$error("fifo_ip: DEPTH must be a power of two, at least 2");
end
if (WIDTH < 1) begin : g_bad_width
$error("fifo_ip: WIDTH must be at least 1");
endThe guards are generate if blocks, so they exist only when their condition is true, and $error inside them fires at elaboration. Here is a request for DEPTH = 6 in both tools:
$ iverilog -g2012 -Wall -P fifo_ip.DEPTH=6 -o /dev/null -f files.f
ERROR: fifo_ip.sv:24: fifo_ip: DEPTH must be a power of two, at least 2
During elaboration Scope: g_bad_depth
1 error(s) during elaboration.
$ verilator --lint-only -Wall -GDEPTH=6 fifo_ip.vlt -f files.f
%Warning-USERERROR: fifo_ip.sv:24:5: fifo_ip: DEPTH must be a power of two, at least 2
...
%Warning-WIDTHTRUNC: fifo_ip.sv:35:20: Operator ASSIGNW expects 3 bits on the Assign RHS, but Assign RHS's SUB generates 4 bits.
...
%Error: Exiting due to 4 warning(s)Icarus treats $error as an error: exit status 1, and the scope g_bad_depth names the guard. Icarus 13 accepts only a single string here; a %0d argument gave sorry: Elaboration tasks currently only support a single string argument.
Verilator reports the same message as a warning, USERERROR. It stops the build only because warnings are fatal by default. The width warnings after it are the damage a bad depth does; the Common mistakes section shows what happens when that is switched off.
// snippet: fifo_ip.sv, lines 29 to 40
logic [WIDTH-1:0] mem [DEPTH];
logic [AW:0] wr_ptr, rd_ptr; // AW address bits + 1 lap bit
logic push_take, pop_take;
fifo_ip_pkg::fifo_op_t op;
assign level_o = wr_ptr - rd_ptr; // 0 to DEPTH, since DEPTH = 2**AW
assign full_o = (level_o == FULL_LEVEL);
assign empty_o = (level_o == '0);
assign push_take = push_i && !full_o;
assign pop_take = pop_i && !empty_o;
assign op = fifo_ip_pkg::fifo_op_t'({push_take, pop_take});Each pointer has AW address bits and one extra lap bit. The difference wr_ptr - rd_ptr is taken modulo 2^(AW+1), so it is the exact occupancy only when DEPTH = 2^AW. For DEPTH = 4 the pointers are 3 bits:
Moment | wr_ptr | rd_ptr | wr_ptr - rd_ptr | Flags |
|---|---|---|---|---|
After reset | 000 | 000 | 0 | empty |
Four pushes | 100 | 000 | 4 | full: same address, different lap bit |
Four pops, four pushes | 000 | 100 | 000 - 100 = 100 = 4 | full again, after wr_ptr wrapped |
The course FIFO accepts any depth by keeping a separate count register and a wrap function. This IP drops both: its control state is the two pointers, the output register and the valid flop, 15 flops at 8x4.
// snippet: fifo_ip.sv, lines 42 to 61
always_ff @(posedge clk) begin // storage: no reset
if (push_take) mem[wr_ptr[AW-1:0]] <= din_i;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= '0;
rd_ptr <= '0;
dout_o <= '0;
dout_valid_o <= 1'b0;
end else begin
dout_valid_o <= pop_take;
if (op == fifo_ip_pkg::OP_PUSH || op == fifo_ip_pkg::OP_BOTH)
wr_ptr <= wr_ptr + 1'b1;
if (op == fifo_ip_pkg::OP_POP || op == fifo_ip_pkg::OP_BOTH) begin
rd_ptr <= rd_ptr + 1'b1;
dout_o <= mem[rd_ptr[AW-1:0]];
end
end
endStorage has no reset, so a synthesis tool can map it to RAM. Empty guarantees no stale word is ever read.
Pointers move on the package's operation names. OP_BOTH moves both, and level_o is unchanged.
Output is registered: dout_o and dout_valid_o change on the edge that accepts the pop, as the contract requires.
// snippet: fifo_ip.sv, lines 63 to 98
`ifdef FORMAL
// Yosys reads immediate assertions only: the bounded proof.
logic f_past = 1'b0;
always_ff @(posedge clk) f_past <= 1'b1;
always @(*) if (!f_past) assume (!rst_n); // start in reset
always @(*) if (f_past) begin
f_bounds: assert (level_o <= FULL_LEVEL);
f_flags: assert (!(full_o && empty_o));
c_full: cover (full_o); // the bound reaches full
end
always_ff @(posedge clk) if (f_past && $past(rst_n) && rst_n) begin
f_no_ovf: assert (!$past(push_i && full_o) || wr_ptr == $past(wr_ptr));
f_no_udf: assert (!$past(pop_i && empty_o) ||
(rd_ptr == $past(rd_ptr) && !dout_valid_o));
f_count: assert (level_o ==
$past(level_o) + $past(push_take) - $past(pop_take));
end
`elsif SYNTHESIS
// synthesis: no checks
`elsif __ICARUS__
// Icarus 13 cannot parse concurrent SVA; the bench checks the same rules
`else
// Built-in SVA, for simulators that run concurrent assertions.
a_bounds: assert property (@(posedge clk) disable iff (!rst_n)
level_o <= FULL_LEVEL);
a_flags: assert property (@(posedge clk) disable iff (!rst_n)
!(full_o && empty_o));
a_no_ovf: assert property (@(posedge clk) disable iff (!rst_n)
push_i && full_o |=> $stable(wr_ptr));
a_no_udf: assert property (@(posedge clk) disable iff (!rst_n)
pop_i && empty_o |=> $stable(rd_ptr) && !dout_valid_o);
a_count: assert property (@(posedge clk) disable iff (!rst_n)
1'b1 |=> level_o == $past(level_o) + $past(push_take)
- $past(pop_take));
`endif
endmoduleOne file serves three tools, and each reads a different branch:
Tool | Macro it defines | What it reads |
|---|---|---|
Yosys, read_verilog -formal | FORMAL | Immediate asserts, the assume and the cover |
Yosys, synthesis | SYNTHESIS | No checks |
Icarus 13 | __ICARUS__ | No checks; the bench tests the same rules |
Verilator --assert | none of these | The five concurrent assertions |
Why two forms. Yosys 0.69 without a commercial front end does not parse clocked properties (syntax error, unexpected '@'), and Icarus 13 printed sorry: concurrent_assertion_item not supported and then syntax errors at |=>.
What they check. Bounds and flags every cycle; a refused push or pop leaves its pointer still; the level changes by exactly accepted pushes minus accepted pops. The bench's model checks data order.
`c_full` is a cover, not a check: it proves the bounded run is deep enough to reach full.
// snippet: fifo_if.sv and fifo_ip_if.sv (Verilator flows)
// fifo_if.sv: one bundle, a view for each side
interface fifo_if #(parameter int WIDTH = 8, parameter int DEPTH = 4) (
input logic clk, rst_n
);
logic push, full, pop, dout_valid, empty;
logic [WIDTH-1:0] din, dout;
logic [$clog2(DEPTH+1)-1:0] level;
modport push_side (input clk, rst_n, full, output push, din);
modport pop_side (input clk, rst_n, dout, dout_valid, empty, level,
output pop);
modport fifo (input clk, rst_n, push, din, pop,
output full, dout, dout_valid, empty, level);
endinterface
// fifo_ip_if.sv: the same core behind the bundle
module fifo_ip_if #(parameter int WIDTH = 8, parameter int DEPTH = 4) (
fifo_if.fifo bus
);
fifo_ip #(.WIDTH(WIDTH), .DEPTH(DEPTH)) u_core (
.clk(bus.clk), .rst_n(bus.rst_n),
.push_i(bus.push), .din_i(bus.din), .full_o(bus.full),
.pop_i(bus.pop), .dout_o(bus.dout), .dout_valid_o(bus.dout_valid),
.empty_o(bus.empty), .level_o(bus.level));
endmoduleThe core keeps plain ports, which is the portable boundary the interfaces lesson recommends. Teams whose tools support interfaces wrap it. A test with a producer on push_side and a consumer on pop_side ran in Verilator; Icarus 13 rejects the interface port:
$ verilator --binary --assert -Mdir obj_if --top-module tb_fifo_ip_if fifo_ip.vlt -f files.f fifo_if.sv fifo_ip_if.sv tb_fifo_ip_if.sv
$ ./obj_if/Vtb_fifo_ip_if
PASS: 1065 words through the modports, all in order
$ iverilog -g2012 -Wall -o /dev/null -f files.f fifo_if.sv fifo_ip_if.sv tb_fifo_ip_if.sv
fifo_ip_if.sv:3: syntax error
fifo_ip_if.sv:3: Errors in port declarations.
...// files.f: packages first, then modules
fifo_ip_pkg.sv
fifo_ip.sv
// fifo_ip.vlt
`verilator_config
// Waiver: rule SYNCASYNCNET | scope: rst_n in fifo_ip.sv only
// Rationale: rst_n is an async reset for the flops; the "sync" use is the
// disable iff of the built-in assertions, which build no hardware.
// Owner: fifo_ip maintainer | Expiry: revisit if rst_n drives any new logic
lint_off -rule SYNCASYNCNET -file "*fifo_ip.sv" -match "*'rst_n'*"Without the waiver file, lint stops on one warning. It is the only one, and the waiver above carries the five parts from the waiver lesson: rule, scope, rationale, owner and expiry.
$ verilator --lint-only -Wall -f files.f
%Warning-SYNCASYNCNET: fifo_ip.sv:7:38: Signal flopped as both synchronous and async: 'rst_n'
fifo_ip.sv:47:10: ... Location of async usage
...
fifo_ip.sv:86:59: ... Location of sync usage
...
%Error: Exiting due to 1 warning(s)The build script runs every check in order. check_order.sh is the order check from Compile-Order and Package Errors, Decoded; with the two files swapped it prints ORDER: fifo_ip.sv uses fifo_ip_pkg:: but fifo_ip_pkg is not declared earlier in bad.f.
#!/bin/sh
# build.sh: order check, both simulators, lint, then the bounded proof
set -e
./check_order.sh files.f
iverilog -g2012 -Wall -o tb.vvp -f files.f tb_fifo_ip.sv
vvp -n tb.vvp
verilator --lint-only -Wall fifo_ip.vlt -f files.f
verilator --binary --assert -Mdir obj --top-module tb_fifo_ip \
fifo_ip.vlt -f files.f tb_fifo_ip.sv > obj.log
./obj/Vtb_fifo_ip
for cfg in "8 4" "32 16" "1 2"; do ./formal.sh $cfg 24; doneThe bench, listed under Testbench, has one checker module per configuration, each with its own clock and a ring-buffer model of the contract. Each runs the corners first (pop on empty, fill, push on full, push and pop on full, drain), then 4000 seeded random cycles in 200-cycle phases that lean towards pushing and then towards popping, so every configuration fills and drains many times. After every edge it compares dout_valid_o, dout_o, level_o, full_o and empty_o with the model. The same bench ran in both simulators with identical output:
$ ./build.sh
order OK: fifo_ip_pkg
fifo_ip 1x2: 1841 words in order, full 960 cycles, empty 944 cycles, 1368 refused
fifo_ip 8x4: 2004 words in order, full 740 cycles, empty 643 cycles, 1039 refused
fifo_ip 32x16: 2118 words in order, full 545 cycles, empty 485 cycles, 742 refused
PASS: 3 configurations, every word in order, flags and level match the model
...Full and empty counts prove both edges of every configuration were reached hundreds of times, not just once in the directed part.
Refused counts requests the FIFO turned down. Each one is a chance for an overflow or underflow bug to show.
The order of the lines does not matter: the three checkers run in parallel and each prints when its own run ends.
Remove the full guard, a plausible slip when editing the acceptance line:
// snippet: the seeded bug in fifo_ip.sv, line 38
assign push_take = push_i; // seeded bug: no full guard$ ./obj/Vtb_fifo_ip # Verilator, --assert
[75] %Error: fifo_ip.sv:86: Assertion failed in tb_fifo_ip.c2.dut.a_bounds: 'assert' failed.
%Error: fifo_ip.sv:86: Verilog $stop
$ vvp -n tb.vvp # Icarus, no SVA
FATAL: tb_fifo_ip.sv:64: FAIL: fifo_ip 1x2: 1738 bad cycles, first at 66
Time: 40106 Scope: tb_fifo_ip.c2The SVA stops at time 75, the first edge after the bad push, and names the rule and the instance: the 1x2 FIFO went past DEPTH.
The bench caught the same push at time 66, one time unit after the edge it checks, but reports only at the end of the run. Both point at the same edge; the assertion says which rule broke.
On the good RTL the Verilator run prints no assertion message at all.
formal.sh is the flow from Your First Formal Run, with chparam choosing the configuration. It runs a bounded check from reset, then induction, then the cover:
#!/bin/sh
# formal.sh WIDTH DEPTH STEPS: bounded check, induction, then cover
set -e
yosys -q -p "read_verilog -formal -sv fifo_ip_pkg.sv fifo_ip.sv;
chparam -set WIDTH $1 -set DEPTH $2 fifo_ip; prep -top fifo_ip;
async2sync; dffunmap; write_smt2 -wires fifo_ip.smt2"
yosys-smtbmc -s z3 -t $3 fifo_ip.smt2 | tail -1
yosys-smtbmc -s z3 -i -t $3 fifo_ip.smt2 | tail -1
yosys-smtbmc -s z3 -c -t $3 fifo_ip.smt2 | grep Reached$ ./formal.sh 8 4 24
## 0:00:00 Status: PASSED
## 0:00:00 Status: PASSED
## 0:00:00 Reached cover statement in step 5 at fifo_ip: c_full
$ ./formal.sh 32 16 24
## 0:00:01 Status: PASSED
## 0:00:00 Status: PASSED
## 0:00:04 Reached cover statement in step 17 at fifo_ip: c_full
$ ./formal.sh 1 2 24
## 0:00:00 Status: PASSED
## 0:00:00 Status: PASSED
## 0:00:00 Reached cover statement in step 3 at fifo_ip: c_fullFirst line: no input sequence breaks the five assertions in 24 cycles from reset.
Second line: induction passes, so the bounded result holds for every cycle. f_bounds needs no helper: from any state with the level at most DEPTH, the guards keep it there.
Third line: full is reached at step 1 + DEPTH (one reset step, then DEPTH pushes). With 12 steps the 32x16 cover was Unreached, and the overflow checks would never have seen a full FIFO.
On the seeded bug the bounded check fails at step 6 for 8x4: Assert failed in fifo_ip: f_bounds, a fifth push into a full FIFO.
The proof covers the occupancy and flag invariants. Data order is checked by the bench's model, not proved.
First-word fall-through. Add parameter bit FWFT = 0 and a generate if that chooses the registered output or a fall-through output. Add an assertion that dout_valid_o equals !empty_o in FWFT mode. See Generate loops and conditionals.
Almost full. Add AF_LEVEL with a guard for 1 to DEPTH, as in Almost Flags and Parameter Edges, and a formal check that the flag equals level_o >= AF_LEVEL.
AXI-Stream wrapper. Map tvalid and tready onto push and !full_o on the input side, and onto !empty_o and pop on the output side, following Core AXI-Stream Signals. The registered output needs a small skid stage to keep tdata stable while tready is low.
Its pointers carry a lap bit and the level is wr_ptr - rd_ptr modulo 2^(AW+1). That is exact only when DEPTH = 2^AW. At DEPTH = 6 the 3-bit address reaches slots 6 and 7, which do not exist, and the pointers wrap at 16 instead of 12. The course FIFO pays for any depth with a wrap function and a count register.
In 12 steps from reset the FIFO cannot fill: it needs one reset step and 16 pushes, and the cover reports c_full as unreached. The overflow checks only fire when full_o is high, so at 12 steps they passed without ever being tested. Induction passing makes the proof complete here, but the cover is how you know the bound itself was meaningful.
Tools: Icarus Verilog 13.0, Verilator 5.048, Yosys 0.69+post (git sha1 143eb14f9) with yosys-smtbmc and z3, run on 2026-10-17. Files: fifo_ip_pkg.sv, fifo_ip.sv, tb_fifo_ip.sv (the Testbench listing), fifo_if.sv, fifo_ip_if.sv, tb_fifo_ip_if.sv, files.f, fifo_ip.vlt, build.sh, formal.sh, and check_order.sh from the compile-order errors lesson.
Bench: iverilog -g2012 -Wall -o tb.vvp -f files.f tb_fifo_ip.sv; vvp -n tb.vvp, and verilator --binary --assert --top-module tb_fifo_ip fifo_ip.vlt -f files.f tb_fifo_ip.sv: identical PASS output in both; 8x4 2004 words, 32x16 2118, 1x2 1841, each reaching full and empty. Lint: verilator --lint-only -Wall fifo_ip.vlt -f files.f exit 0; without the .vlt one SYNCASYNCNET warning. Interface test (Verilator only): PASS, 1065 words; Icarus 13 rejects the interface port.
Guards: iverilog -P fifo_ip.DEPTH=6, DEPTH=1 and WIDTH=0 each fail elaboration; verilator --lint-only -Wall -GDEPTH=6 exits 1 on USERERROR plus width warnings. With the 8x4 checker changed to 8x6, verilator --binary --assert -Wno-fatal built (exit 0) and the bench failed: 507 bad cycles, first at 226; adding -Werror-USERERROR stops the build. Seeded bug (push_take = push_i): Verilator a_bounds fails at time 75; Icarus bench FAIL, 1738 bad cycles, first at 66.
Formal: formal.sh W D 24 (read_verilog -formal -sv; chparam; prep; async2sync; dffunmap; write_smt2; yosys-smtbmc -s z3 with -t 24, -i -t 24, -c -t 24) for 8x4, 32x16 and 1x2: BMC PASSED, induction successful, c_full reached at steps 5, 17 and 3; at -t 12 the 32x16 cover is unreached. Seeded bug: BMC failed at step 6 on f_bounds. Synthesis: yosys synth -top fifo_ip; stat at 8x4: 125 cells, 47 flops (32 $_DFFE_PP_, 14 $_DFFE_PN0P_, 1 $_DFF_PN0_), no latch.
Scope and limits: the proof covers occupancy, flag and pointer-hold invariants for three configurations, not data order, which the bench checks against a model with seeded random traffic. Yosys generic cells only, no timing. Concurrent SVA ran only in Verilator; Icarus and Yosys read no concurrent properties.
The package, the core and the bench as one listing. Each // name.sv comment starts a separate file. The interface view and its test are in The interface view above.
// fifo_ip_pkg.sv: shared types and the parameter check
package fifo_ip_pkg;
// What one clock edge does, from the accepted push and pop.
typedef enum logic [1:0] {
OP_NONE = 2'b00, OP_POP = 2'b01, OP_PUSH = 2'b10, OP_BOTH = 2'b11
} fifo_op_t;
// A legal depth is a power of two and at least 2.
function automatic logic depth_ok(input int depth);
depth_ok = depth >= 2 && (depth & (depth - 1)) == 0;
endfunction
endpackage
// fifo_ip.sv: synchronous FIFO, registered output, power-of-two depth
module fifo_ip #(
parameter int WIDTH = 8,
parameter int DEPTH = 4
) (
input logic clk,
input logic rst_n,
// push side
input logic push_i,
input logic [WIDTH-1:0] din_i,
output logic full_o,
// pop side
input logic pop_i,
output logic [WIDTH-1:0] dout_o,
output logic dout_valid_o,
output logic empty_o,
output logic [$clog2(DEPTH+1)-1:0] level_o
);
localparam int AW = $clog2(DEPTH);
localparam logic [AW:0] FULL_LEVEL = (AW + 1)'(DEPTH);
// Elaboration-time guards: a bad configuration does not build.
if (!fifo_ip_pkg::depth_ok(DEPTH)) begin : g_bad_depth
$error("fifo_ip: DEPTH must be a power of two, at least 2");
end
if (WIDTH < 1) begin : g_bad_width
$error("fifo_ip: WIDTH must be at least 1");
end
logic [WIDTH-1:0] mem [DEPTH];
logic [AW:0] wr_ptr, rd_ptr; // AW address bits + 1 lap bit
logic push_take, pop_take;
fifo_ip_pkg::fifo_op_t op;
assign level_o = wr_ptr - rd_ptr; // 0 to DEPTH, since DEPTH = 2**AW
assign full_o = (level_o == FULL_LEVEL);
assign empty_o = (level_o == '0);
assign push_take = push_i && !full_o;
assign pop_take = pop_i && !empty_o;
assign op = fifo_ip_pkg::fifo_op_t'({push_take, pop_take});
always_ff @(posedge clk) begin // storage: no reset
if (push_take) mem[wr_ptr[AW-1:0]] <= din_i;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= '0;
rd_ptr <= '0;
dout_o <= '0;
dout_valid_o <= 1'b0;
end else begin
dout_valid_o <= pop_take;
if (op == fifo_ip_pkg::OP_PUSH || op == fifo_ip_pkg::OP_BOTH)
wr_ptr <= wr_ptr + 1'b1;
if (op == fifo_ip_pkg::OP_POP || op == fifo_ip_pkg::OP_BOTH) begin
rd_ptr <= rd_ptr + 1'b1;
dout_o <= mem[rd_ptr[AW-1:0]];
end
end
end
`ifdef FORMAL
// Yosys reads immediate assertions only: the bounded proof.
logic f_past = 1'b0;
always_ff @(posedge clk) f_past <= 1'b1;
always @(*) if (!f_past) assume (!rst_n); // start in reset
always @(*) if (f_past) begin
f_bounds: assert (level_o <= FULL_LEVEL);
f_flags: assert (!(full_o && empty_o));
c_full: cover (full_o); // the bound reaches full
end
always_ff @(posedge clk) if (f_past && $past(rst_n) && rst_n) begin
f_no_ovf: assert (!$past(push_i && full_o) || wr_ptr == $past(wr_ptr));
f_no_udf: assert (!$past(pop_i && empty_o) ||
(rd_ptr == $past(rd_ptr) && !dout_valid_o));
f_count: assert (level_o ==
$past(level_o) + $past(push_take) - $past(pop_take));
end
`elsif SYNTHESIS
// synthesis: no checks
`elsif __ICARUS__
// Icarus 13 cannot parse concurrent SVA; the bench checks the same rules
`else
// Built-in SVA, for simulators that run concurrent assertions.
a_bounds: assert property (@(posedge clk) disable iff (!rst_n)
level_o <= FULL_LEVEL);
a_flags: assert property (@(posedge clk) disable iff (!rst_n)
!(full_o && empty_o));
a_no_ovf: assert property (@(posedge clk) disable iff (!rst_n)
push_i && full_o |=> $stable(wr_ptr));
a_no_udf: assert property (@(posedge clk) disable iff (!rst_n)
pop_i && empty_o |=> $stable(rd_ptr) && !dout_valid_o);
a_count: assert property (@(posedge clk) disable iff (!rst_n)
1'b1 |=> level_o == $past(level_o) + $past(push_take)
- $past(pop_take));
`endif
endmodule
// tb_fifo_ip.sv: one checker per configuration, each against a model
module fifo_ip_check #(
parameter int WIDTH = 8, parameter int DEPTH = 4, parameter int SEED = 1
) (output logic done);
localparam int LW = $clog2(DEPTH + 1);
logic clk = 1'b0, rst_n = 1'b0;
logic push_i = 1'b0, pop_i = 1'b0;
logic [WIDTH-1:0] din_i = '0, dout_o;
logic dout_valid_o, full_o, empty_o;
logic [LW-1:0] level_o;
fifo_ip #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (.*);
always #5 clk = ~clk;
// Reference model: a ring of DEPTH words and a count.
logic [WIDTH-1:0] model [DEPTH];
int head = 0, tail = 0, count = 0;
int errors = 0, words = 0, full_cycles = 0, empty_cycles = 0, refused = 0;
logic [31:0] rnd = SEED;
time first_bad;
task automatic step(input logic push, pop, input logic [WIDTH-1:0] data);
logic take_push, take_pop;
logic [WIDTH-1:0] expected;
@(negedge clk);
push_i = push; pop_i = pop; din_i = data;
take_push = push && (count < DEPTH); // refused when full, even with a pop
take_pop = pop && (count > 0); // refused when empty, even with a push
if ((push && !take_push) || (pop && !take_pop)) refused++;
expected = model[head];
if (take_push) begin model[tail] = data; tail = (tail + 1) % DEPTH; end
if (take_pop) begin head = (head + 1) % DEPTH; words++; end
count = count + int'(take_push) - int'(take_pop);
@(posedge clk); #1;
if (dout_valid_o !== take_pop || (take_pop && dout_o !== expected) ||
level_o !== LW'(count) || full_o !== (count == DEPTH) ||
empty_o !== (count == 0)) begin
if (errors == 0) first_bad = $time;
errors++;
end
if (full_o) full_cycles++;
if (empty_o) empty_cycles++;
endtask
initial begin
done = 1'b0;
repeat (2) @(posedge clk);
@(negedge clk) rst_n = 1'b1;
step(0, 1, '0); // pop on empty
for (int i = 0; i < DEPTH; i++) step(1, 0, WIDTH'(i + 1)); // fill
step(1, 0, '1); // push on full
step(1, 1, '1); // both on full
for (int i = 0; i < DEPTH; i++) step(0, 1, '0); // drain
for (int n = 0; n < 4000; n++) begin // seeded random traffic,
logic busy, half, fill; // 200-cycle phases that
rnd = rnd * 32'd1664525 + 32'd1013904223; // lean to push, then pop
busy = rnd[31] | rnd[30]; // 3 in 4
half = rnd[29]; // 1 in 2
fill = ((n / 200) % 2 == 0);
step(fill ? busy : half, fill ? half : busy, WIDTH'({rnd[23:8], rnd[23:8]}));
end
while (count > 0) step(0, 1, '0);
if (errors != 0)
$fatal(1, "FAIL: fifo_ip %0dx%0d: %0d bad cycles, first at %0t",
WIDTH, DEPTH, errors, first_bad);
if (full_cycles == 0 || empty_cycles == 0)
$fatal(1, "FAIL: fifo_ip %0dx%0d never full or never empty", WIDTH, DEPTH);
$display("fifo_ip %0dx%0d: %0d words in order, full %0d cycles, empty %0d cycles, %0d refused",
WIDTH, DEPTH, words, full_cycles, empty_cycles, refused);
done = 1'b1;
end
endmodule
module tb_fifo_ip;
logic d0, d1, d2;
fifo_ip_check #(.WIDTH(8), .DEPTH(4), .SEED(11)) c0 (.done(d0));
fifo_ip_check #(.WIDTH(32), .DEPTH(16), .SEED(22)) c1 (.done(d1));
fifo_ip_check #(.WIDTH(1), .DEPTH(2), .SEED(33)) c2 (.done(d2));
initial begin
wait (d0 === 1'b1 && d1 === 1'b1 && d2 === 1'b1);
$display("PASS: 3 configurations, every word in order, flags and level match the model");
$finish;
end
initial begin #2_000_000; $fatal(1, "FAIL: timeout"); end
endmodule