A ROM holds fixed contents with no write port. The array style suits large tables and the case style suits small ones.
The full listing sits under Testbench below.
Style | Yosys result | Use for |
|---|---|---|
| 1 memory cell, no write port, initialised contents: a ROM | Tables larger than a few dozen words: sine, CRC, font, microcode |
| 3 comparators and a | Tiny tables (up to ~16 entries) where a LUT is cheaper than a block |
Read the left half as one block with its contents loaded at configuration, and the right half as plain gates. The table above gives the Yosys result for each.
Left: the array has no write port, and its clocked read lets it map to a block ROM: worth it for tables larger than a few dozen words.
Right: a 4-entry case costs a few comparators and a mux, and reads in the same cycle. A ROM block would waste most of its size.
For FPGAs, the initial block (or $readmemh) becomes the memory's initial contents in the bitstream. For ASICs, a ROM macro is a separate library cell with fixed contents, and the RTL array is only a simulation model.
Check which flow the project targets.
Use a function or a file to generate contents so the table can be regenerated; hand-typed numbers rot.
Prefer a synchronous read (as in rom_sine) so the table maps to a block ROM. A combinational read turns it into logic.
If the table is read in only a few places, a case may be smaller than a memory block with its control overhead.
Size limits: a function with $sin is evaluated at elaboration time; keep it deterministic and check that your synthesis tool accepts the system functions.
case than as a ROM array?A block RAM has a large fixed minimum size and timing overhead; four entries cost just a few gates as logic. The case also reads combinationally with no extra cycle.
The testbench compares all 64 sine entries with the generator, adds independent quarter-wave anchors and monotonicity and symmetry checks with a one-code tolerance for truncation, and checks all four case entries.
// A 64-entry sine ROM (unsigned, 8 bit): contents defined by a function, read synchronously.
module rom_sine (
input logic clk,
input logic [5:0] addr,
output logic [7:0] dout
);
logic [7:0] mem [64];
// Contents are fixed at elaboration time. Tools turn this into a ROM (block RAM with an init file, or LUTs).
initial begin
for (int i = 0; i < 64; i++)
mem[i] = 8'($rtoi(127.5 + 127.5 * $sin(6.283185307179586 * i / 64.0)));
end
always_ff @(posedge clk) dout <= mem[addr]; // synchronous read, no write port
endmodule
// Same idea with a case statement: good for small tables, tools infer a LUT/ROM, no array needed.
module rom_case (
input logic [1:0] sel,
output logic [7:0] dout
);
always_comb begin
unique case (sel)
2'd0: dout = 8'h3C;
2'd1: dout = 8'h5A;
2'd2: dout = 8'hA5;
2'd3: dout = 8'hC3;
default: dout = 8'h00;
endcase
end
endmodule
Source checker, run with Icarus Verilog 13.0 (2026-10-16): prints PASS. It compares all 64 sine entries with the generator, adds independent quarter-wave anchors, monotonicity and symmetry checks with a one-code tolerance for truncation, and checks all four case entries. Target ROM mapping is not checked, and these finite properties do not prove every numerical value.