Skip to content

RTL Design Patterns · Chapter 6 · Memories & Register Files

ROMs & Initialization

Not every memory is a scratchpad you fill at run time. A huge amount of hardware needs constant data baked in before the first clock: sine tables, gamma curves, FIR coefficients, microcode, character fonts, CRC lookup tables. That is a ROM, and it is nothing more than the RAM inference template with its contents pinned at build, as a case of constants or an array preloaded from a file. The twin question is initialization, how a memory gets known contents at power-up, and it hides the most dangerous synthesis trap in this chapter. An initial block or a readmemh load simulates perfectly and works on an FPGA, where memory contents are written into the configuration bitstream. Port that same design to an ASIC, which has no free power-up state, and the array comes up undefined while every simulation still passes. This lesson builds constant-table ROMs, file-based init, and the silicon-safe fix in SystemVerilog, Verilog, and VHDL.

Intermediate14 min readRTL Design PatternsROMMemory InitializationreadmemhLookup TableSynthesis Inference

Chapter 6 · Section 6.4 · Memories & Register Files

1. The Engineering Problem

You are building a direct-digital-synthesis (DDS) oscillator. To emit a sine wave you need, every cycle, the amplitude of sin at the current phase — and you cannot afford to compute a sine in one clock. The standard answer is a lookup table: precompute 1024 sample points of one quarter (or full) period, store them in a memory, and each cycle read the sample at the current phase index. The table never changes at run time; it is constant data, decided before the chip ever runs. You do not need a RAM you can write — you need a ROM: the same addressable array as a RAM, holding fixed contents you only ever read.

This is not a niche. Constant-data memories are everywhere in real hardware: a gamma/tone curve in a display or camera pipeline, the coefficient bank of a fixed FIR or a windowing function, a microcode/microsequence store that drives a controller's datapath, a character-generator font ROM, a byte-wise CRC lookup table that turns a slow bit-serial computation into a one-cycle table read. Each is the same need — store many precomputed words, address them, read one per cycle — with the contents fixed at build. The mirror-image need is initialization: even a writable memory (a register file that must power up zeroed, a coefficient RAM that boots with defaults before software reprograms it) often needs known contents at power-up, not the undefined array 6.1 left you with.

The trap is where that initial data actually comes from in silicon. In RTL you can write $readmemh("sine.hex", rom) or an initial block and it will simulate perfectly. On an FPGA it works too — block RAM contents are written into the configuration bitstream, so the array powers up loaded. But an ASIC has no free power-up state: a compiled SRAM comes up undefined, and there is no bitstream to preload it. The same RTL that gave you a perfect DDS table on the FPGA gives you an array full of X on the ASIC — and every functional simulation still passes, because in simulation the initial block ran. Getting the contents right is easy; getting them into silicon on the target you actually ship is the whole problem.

the-need.v — a sine table: constant data, but WHERE does it live at power-up?
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // The need: read a precomputed sine sample every cycle at the current phase.
   //   reg [15:0] rom [0:1023];        // 1024 constant amplitude samples
   //   always @(posedge clk) dout <= rom[phase];   // registered read (a ROM)
 
   // Two ways to get the constants in — but they are NOT equivalent in silicon:
   //   (A) $readmemh("sine.hex", rom); // preload from a file  -> sim + FPGA config
   //   (B) case (phase) ... constants  // baked into the LOGIC -> real ROM, any target
 
   // The bug hiding here: (A) as the ONLY init works in sim and on the FPGA
   // (bitstream-loaded block RAM), but on an ASIC the array powers up UNDEFINED.
   // "It initialized in simulation" is not "it initialized in silicon."

2. Mental Model

3. Pattern Anatomy

The ROM-and-init family is built from the 6.1 memory template plus a small set of decisions about where the contents come from and whether that source exists on the target.

The constant-table ROM — a case of constants or a preloaded array. A ROM is the 2D array of 6.1 (reg [WIDTH-1:0] rom [0:DEPTH-1]) with the contents fixed at build. Two spellings: a case of constantscase (addr) 0: dout = 16'h0000; 1: dout = 16'h0192; … — where the constants become synthesized logic (a small decode-and-mux, good for tiny or highly irregular tables); or a preloaded array — declare the array and fill it once ($readmemh, or an initial loop that computes each entry), then index it (good for large, dense tables that map to block ROM). Either way the read is the same as a RAM's.

Registered vs combinational read — the same crux as 6.1. A ROM read can be combinational (assign dout = rom[addr], zero latency) or registered (dout <= rom[addr] inside a clocked block, one-cycle latency). For a large table that must map to a block RAM, the read must be registered — the block ROM is a synchronous array with a one-cycle read latency, exactly as in 6.1, and a combinational read cannot map to it. A tiny case ROM can be combinational, but then it is pure logic, not a block ROM. The one-cycle latency of a registered ROM is a real, load-bearing fact: your phase-to-sample pipeline is one stage deeper than the naive "read rom[phase] this cycle" mental picture.

Initialization — three descriptions, one target question. How the contents get in:

  • $readmemh / $readmemb from a file (SV/Verilog) — $readmemh("sine.hex", rom) reads hex (or $readmemb, binary) words from a file into the array. Clean separation of data from RTL; the classic table-load. The file's path and format must match exactly, or the load silently leaves zeros/X (§6).
  • An initial block / computed load (SV/Verilog) — an initial loop that assigns each entry (a constant, or a value computed at elaboration). No external file; the table is generated in RTL.
  • A VHDL constant / signal init arrayconstant rom_c : rom_t := ( … ) or a signal initial value; the VHDL idiom for the same "print these pages."

All three describe initial contents. The question that decides whether they survive is the target.

The FPGA-vs-ASIC init reality — the load-bearing distinction. The same init description maps to three different silicon realities:

  • FPGA block RAM — inits at configuration: the bitstream carries the contents, so the array powers up loaded. $readmemh/initial/VHDL init all work, for free, at config time. This is why the bug hides.
  • ASIC SRAM macro — has no free power-up state: a compiled SRAM comes up undefined, and there is no bitstream to preload it. To get known contents you need either a true ROM macro (contents etched into the mask — genuinely constant, any target) or an explicit reset-driven load (a small state machine or a $readmemh-equivalent boot ROM that writes the values in after reset).
  • A target that does neither — the array powers up X. Every functional simulation passes (the initial ran in sim); the hardware outputs garbage.

ROM & init — the same array, three targets, and where the contents actually come from

data flow
ROM & init — the same array, three targets, and where the contents actually come from2D arrayrom[DEPTH][WIDTH]the 6.1 template, contents fixed at buildcase of constantsbaked into LOGIC → true ROM, any targetreadmemh / initial / VHDL initreadmemh /initial / VHDL…describes power-up contentsFPGA block RAMinits at CONFIG (bitstream) → loaded, freeASIC + reset-load/ ROM macroexplicit load or etched contents → silicon-safeASIC, init-onlyno free power-up state → array is X → the bug
A ROM is the 6.1 array with fixed contents. Bake the contents into a case and they become logic — a true ROM on any target. Describe them with readmemh/initial/VHDL init and the SAME description reaches three different silicon realities: an FPGA block RAM loads them for free at bitstream configuration; an ASIC with a real ROM macro or an explicit reset-driven load gets them into silicon deliberately; but an ASIC (or any target) that relies on init-only with no load powers up UNDEFINED — the array is X, functional sim still passes, and the block outputs garbage. Initial contents are a target-dependent synthesis concern, not a language feature. This distinction is the same in SystemVerilog, Verilog, and VHDL.

One thing is deferred on purpose, and naming it marks the boundary of this pattern. Read-during-write — what a writable, initialized memory shows when you read and write the same address in the same cycle — is Section 6.2; a ROM is never written, so the question does not arise, and an initialized RAM here is loaded before operation begins. This page teaches the two halves that make constant and preloaded memories real: the fixed-content ROM (the 6.1 template minus the write), and the target-dependent truth about where initial contents come from.

4. Real RTL Implementation

This is the core of the page. We build three code families — (a) a constant-table ROM (a case of constants and a preloaded array, registered read, WIDTH/DEPTH-generic), (b) a RAM initialized from a file / init, and (c) the init-only-in-sim/FPGA bug vs the silicon-safe fix — and each is shown in SystemVerilog, Verilog, and VHDL, with both an RTL block and a self-checking testbench that reads known table entries. The idea is identical in all three; the syntax differs, and seeing them side by side is what makes the pattern language-independent in your head.

One discipline runs through every RTL block on this page: the read is registered (one cycle of latency, exactly as in 6.1), and the initial contents are described deliberately — either baked into logic (a case) or loaded in a way that exists on the target. That single rule is what makes each block infer correctly and initialize in silicon, and it is exactly what family (c) violates.

4a. The constant-table ROM — a case of constants and a preloaded array, three ways

Start with the ROM itself. We build a small identity/sine-shaped lookup table: a fixed table indexed by addr, read through a register so dout carries the addressed constant one cycle later. Two spellings appear together — a case of constants (which becomes logic, a true ROM on any target) and a preloaded array indexed the same way. WIDTH and DEPTH are generic. The SystemVerilog form uses an initial loop to compute the preloaded array (a quarter-wave-shaped table) and a registered read.

rom_lut.sv — constant-table ROM (preloaded array, registered read, WIDTH/DEPTH-generic)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rom_lut #(
       parameter int WIDTH = 16,                 // sample width
       parameter int DEPTH = 16,                 // number of table entries
       localparam int AW    = $clog2(DEPTH)      // address width = clog2(DEPTH)
   )(
       input  logic             clk,
       input  logic [AW-1:0]    addr,            // table index (phase)
       output logic [WIDTH-1:0] dout             // registered read — valid NEXT cycle
   );
       // The 2D array holds CONSTANT data — a ROM is the 6.1 template with no write.
       logic [WIDTH-1:0] rom [DEPTH];
 
       // Build the table once, at elaboration. Here: a simple monotonic ramp standing
       // in for a precomputed curve (a real design would $readmemh a sine/gamma file).
       initial begin
           for (int i = 0; i < DEPTH; i++)
               rom[i] = WIDTH'(i * i);           // fixed contents, decided at build
       end
 
       // REGISTERED read (one-cycle latency, exactly as a RAM) → maps to a block ROM.
       always_ff @(posedge clk)
           dout <= rom[addr];
   endmodule

The alternative spelling — a case of constants — bakes the table into logic, which is a true ROM on any target (no init step to survive). It is the right choice for a tiny or highly irregular table; for a large dense table the preloaded array is denser.

rom_case.sv — the SAME ROM as a case of constants (baked into logic, any target)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rom_case (
       input  logic       clk,
       input  logic [3:0] addr,                  // 16-entry table
       output logic [15:0] dout                  // registered read
   );
       logic [15:0] c;
       // A case of constants: the contents are LOGIC, not a preloaded array. This is a
       // true ROM — it needs no init step, so it is silicon-safe on any target.
       always_comb begin
           unique case (addr)
               4'd0: c = 16'h0000; 4'd1: c = 16'h0001; 4'd2: c = 16'h0004;
               4'd3: c = 16'h0009; 4'd4: c = 16'h0010; 4'd5: c = 16'h0019;
               4'd6: c = 16'h0024; 4'd7: c = 16'h0031; 4'd8: c = 16'h0040;
               4'd9: c = 16'h0051; 4'd10: c = 16'h0064; 4'd11: c = 16'h0079;
               4'd12: c = 16'h0090; 4'd13: c = 16'h00A9; 4'd14: c = 16'h00C4;
               default: c = 16'h00E1;             // addr 15 → total assignment, no latch
           endcase
       end
       always_ff @(posedge clk) dout <= c;        // register the read → 1-cycle latency
   endmodule

The SystemVerilog testbench is clocked and its shape is the template for every §4 ROM testbench: read known table entries and assert dout matches the expected constant — offsetting the check by the one-cycle read latency. Because a ROM's contents are fixed and known, the reference model is the same formula that filled the table, so the check is exact.

rom_lut_tb.sv — clocked self-check: read known entries, assert dout one cycle late
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rom_lut_tb;
       localparam int WIDTH = 16;
       localparam int DEPTH = 16;
       localparam int AW    = $clog2(DEPTH);
       logic              clk = 0;
       logic [AW-1:0]     addr;
       logic [WIDTH-1:0]  dout;
       int                errors = 0;
 
       rom_lut #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut (.clk(clk), .addr(addr), .dout(dout));
 
       always #5 clk = ~clk;
 
       function automatic logic [WIDTH-1:0] expect_rom(int a);  // the golden table formula
           return WIDTH'(a * a);
       endfunction
 
       initial begin
           addr = 0;
           @(negedge clk);
           // Read every address; dout is valid ONE cycle after addr is presented (registered
           // read), so present addr, take an edge, then compare on the following sample.
           for (int a = 0; a < DEPTH; a++) begin
               addr = a[AW-1:0];
               @(posedge clk); #1;                          // registered read updates dout here
               assert (dout === expect_rom(a))
                   else begin $error("rom a=%0d dout=%h exp=%h", a, dout, expect_rom(a)); errors++; end
               @(negedge clk);
           end
           if (errors == 0) $display("PASS: rom_lut returns the known table entry (1-cycle latency)");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in intent; the differences are reg/wire typing and the classic memory-array declaration. The initial fills the table, and the registered read infers the same block ROM with the same one-cycle latency.

rom_lut.v — constant-table ROM in Verilog (initial-filled array, registered read)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rom_lut #(
       parameter WIDTH = 16,
       parameter DEPTH = 16,
       parameter AW    = 4                        // = clog2(DEPTH); set by the instantiator
   )(
       input  wire             clk,
       input  wire [AW-1:0]    addr,
       output reg  [WIDTH-1:0] dout               // `reg` — registered read
   );
       reg [WIDTH-1:0] rom [0:DEPTH-1];           // the constant-data array
       integer i;
 
       // Fill the table once. A real design would use $readmemh("sine.hex", rom) here;
       // the computed loop keeps this example self-contained and its contents known.
       initial begin
           for (i = 0; i < DEPTH; i = i + 1)
               rom[i] = (i * i);                  // fixed contents
       end
 
       always @(posedge clk)
           dout <= rom[addr];                     // REGISTERED read → block ROM, 1-cycle latency
   endmodule
rom_lut_tb.v — clocked self-check; read known entries with 1-cycle latency
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rom_lut_tb;
       parameter WIDTH = 16;
       parameter DEPTH = 16;
       parameter AW    = 4;
       reg              clk;
       reg  [AW-1:0]    addr;
       wire [WIDTH-1:0] dout;
       reg  [WIDTH-1:0] exp;
       integer          a, errors;
 
       rom_lut #(.WIDTH(WIDTH), .DEPTH(DEPTH), .AW(AW)) dut (.clk(clk), .addr(addr), .dout(dout));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; addr = 0;
           @(negedge clk);
           for (a = 0; a < DEPTH; a = a + 1) begin
               addr = a[AW-1:0];
               @(posedge clk); #1;                        // registered read updates dout here
               exp = (a * a);                             // golden table formula
               if (dout !== exp) begin
                   $display("FAIL: rom a=%0d dout=%h exp=%h", a, dout, exp);
                   errors = errors + 1;
               end
               @(negedge clk);
           end
           if (errors == 0) $display("PASS: rom_lut returns the known entry (1-cycle latency)");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the table is a constant init array — the ROM contents are a constant of the array type, computed in a helper function at elaboration, which is genuinely constant (a true ROM, no init step to survive). The read lives in a rising_edge(clk) process, so dout is registered exactly as in SV/Verilog. The generic plays the role of the Verilog parameter.

rom_lut.vhd — constant-table ROM in VHDL (constant init array + registered read)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity rom_lut is
       generic (
           WIDTH : positive := 16;                          -- sample width
           DEPTH : positive := 16                           -- number of entries
       );
       port (
           clk  : in  std_logic;
           addr : in  std_logic_vector(natural(ceil(log2(real(DEPTH))))-1 downto 0);
           dout : out std_logic_vector(WIDTH-1 downto 0)    -- registered read, valid next cycle
       );
   end entity;
 
   architecture rtl of rom_lut is
       type rom_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
 
       -- Build the CONSTANT table at elaboration. Because it is a `constant`, the
       -- contents are genuinely fixed — a true ROM, silicon-safe on any target.
       function init_rom return rom_t is
           variable r : rom_t;
       begin
           for i in 0 to DEPTH-1 loop
               r(i) := std_logic_vector(to_unsigned((i * i) mod 2**WIDTH, WIDTH));
           end loop;
           return r;
       end function;
 
       constant rom : rom_t := init_rom;                    -- fixed contents
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               dout <= rom(to_integer(unsigned(addr)));     -- REGISTERED read → 1-cycle latency
           end if;
       end process;
   end architecture;
rom_lut_tb.vhd — clocked self-check (assert report severity), 1-cycle read latency
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity rom_lut_tb is
   end entity;
 
   architecture sim of rom_lut_tb is
       constant WIDTH : positive := 16;
       constant DEPTH : positive := 16;
       constant AW    : positive := 4;                      -- = clog2(16)
       signal clk  : std_logic := '0';
       signal addr : std_logic_vector(AW-1 downto 0) := (others => '0');
       signal dout : std_logic_vector(WIDTH-1 downto 0);
   begin
       dut : entity work.rom_lut
           generic map (WIDTH => WIDTH, DEPTH => DEPTH)
           port map (clk => clk, addr => addr, dout => dout);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable exp : std_logic_vector(WIDTH-1 downto 0);
       begin
           wait until falling_edge(clk);
           for a in 0 to DEPTH-1 loop
               addr <= std_logic_vector(to_unsigned(a, AW));
               wait until rising_edge(clk);                 -- registered read updates dout here
               wait for 1 ns;
               exp := std_logic_vector(to_unsigned((a * a) mod 2**WIDTH, WIDTH));
               assert dout = exp
                   report "rom_lut mismatch (latency accounted?)" severity error;
               wait until falling_edge(clk);
           end loop;
           report "rom_lut self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. A RAM initialized from a file / init — $readmemh and a VHDL init array

Now the writable memory that needs known power-up contents: a coefficient RAM that boots with defaults, later reprogrammable. It is the 6.1 single-port RAM plus an initialization step. In SystemVerilog and Verilog the classic idiom is $readmemh (or $readmemb) reading a hex/binary file into the array; here we use an initial loop so the example is self-contained and its contents are known to the testbench. The load happens before operation; the read is registered as always.

ram_init.sv — RAM with power-up contents (readmemh / initial), registered read
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module ram_init #(
       parameter int WIDTH = 16,
       parameter int DEPTH = 16,
       localparam int AW    = $clog2(DEPTH)
   )(
       input  logic             clk,
       input  logic             we,              // still writable — init is only the POWER-UP state
       input  logic [AW-1:0]    addr,
       input  logic [WIDTH-1:0] din,
       output logic [WIDTH-1:0] dout             // registered read
   );
       logic [WIDTH-1:0] mem [DEPTH];
 
       // POWER-UP CONTENTS. In production: $readmemh("coeff.hex", mem) loads a hex file.
       // Here an initial loop makes the contents known to the testbench.
       // NOTE: this init exists in simulation and, on an FPGA, at bitstream config —
       // but NOT on an ASIC (see 4c). For a writable RAM, a reset-load is the safe form.
       initial begin
           // $readmemh("coeff.hex", mem);       // <- the production one-liner
           for (int i = 0; i < DEPTH; i++)
               mem[i] = WIDTH'(16'hC000 + i);    // default coefficients
       end
 
       always_ff @(posedge clk) begin
           if (we) mem[addr] <= din;             // still writable (software may reprogram)
           dout <= mem[addr];                    // REGISTERED read → 1-cycle latency
       end
   endmodule

The testbench first verifies the initial contents (read every address before any write and assert it equals the known default), then writes a new value and confirms the write overrides the default — proving both that the memory came up loaded and that it is still writable.

ram_init_tb.sv — verify the loaded pattern, then that writes still work
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module ram_init_tb;
       localparam int WIDTH = 16, DEPTH = 16, AW = $clog2(DEPTH);
       logic              clk = 0, we;
       logic [AW-1:0]     addr;
       logic [WIDTH-1:0]  din, dout;
       int                errors = 0;
 
       ram_init #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut
           (.clk(clk), .we(we), .addr(addr), .din(din), .dout(dout));
 
       always #5 clk = ~clk;
 
       function automatic logic [WIDTH-1:0] init_val(int a);  // the golden power-up value
           return WIDTH'(16'hC000 + a);
       endfunction
 
       initial begin
           we = 0; addr = 0; din = 0;
           @(negedge clk);
           // PHASE 1 — confirm the memory came up with the loaded pattern (before any write).
           for (int a = 0; a < DEPTH; a++) begin
               addr = a[AW-1:0];
               @(posedge clk); #1;
               assert (dout === init_val(a))
                   else begin $error("init a=%0d dout=%h exp=%h", a, dout, init_val(a)); errors++; end
               @(negedge clk);
           end
           // PHASE 2 — a write overrides the default; the RAM is still writable.
           addr = 3; din = 16'hBEEF; we = 1; @(negedge clk);
           we = 0; addr = 3; @(posedge clk); #1;
           assert (dout === 16'hBEEF)
               else begin $error("write failed: dout=%h exp=BEEF", dout); errors++; end
           if (errors == 0) $display("PASS: ram_init came up loaded and is still writable");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form uses the same initial load (or $readmemh) and the same registered read. The comment marks the exact spot where a production file load would go.

ram_init.v — RAM initialized from a file/init in Verilog (registered read)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module ram_init #(
       parameter WIDTH = 16,
       parameter DEPTH = 16,
       parameter AW    = 4
   )(
       input  wire             clk,
       input  wire             we,
       input  wire [AW-1:0]    addr,
       input  wire [WIDTH-1:0] din,
       output reg  [WIDTH-1:0] dout
   );
       reg [WIDTH-1:0] mem [0:DEPTH-1];
       integer i;
 
       // POWER-UP CONTENTS. Production: $readmemh("coeff.hex", mem);
       // Simulation/FPGA-config init — NOT present on an ASIC (see 4c).
       initial begin
           // $readmemh("coeff.hex", mem);
           for (i = 0; i < DEPTH; i = i + 1)
               mem[i] = (16'hC000 + i);          // default coefficients
       end
 
       always @(posedge clk) begin
           if (we) mem[addr] <= din;             // still writable
           dout <= mem[addr];                    // REGISTERED read → 1-cycle latency
       end
   endmodule
ram_init_tb.v — verify loaded pattern, then a write overrides it
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module ram_init_tb;
       parameter WIDTH = 16, DEPTH = 16, AW = 4;
       reg              clk, we;
       reg  [AW-1:0]    addr;
       reg  [WIDTH-1:0] din;
       wire [WIDTH-1:0] dout;
       integer          a, errors;
 
       ram_init #(.WIDTH(WIDTH), .DEPTH(DEPTH), .AW(AW)) dut
           (.clk(clk), .we(we), .addr(addr), .din(din), .dout(dout));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; we = 0; addr = 0; din = 0;
           @(negedge clk);
           // PHASE 1 — confirm the loaded power-up pattern.
           for (a = 0; a < DEPTH; a = a + 1) begin
               addr = a[AW-1:0];
               @(posedge clk); #1;
               if (dout !== (16'hC000 + a)) begin
                   $display("FAIL: init a=%0d dout=%h exp=%h", a, dout, (16'hC000 + a));
                   errors = errors + 1;
               end
               @(negedge clk);
           end
           // PHASE 2 — a write overrides the default.
           addr = 3; din = 16'hBEEF; we = 1; @(negedge clk);
           we = 0; addr = 3; @(posedge clk); #1;
           if (dout !== 16'hBEEF) begin
               $display("FAIL: write did not take, dout=%h", dout);
               errors = errors + 1;
           end
           if (errors == 0) $display("PASS: ram_init loaded at power-up and still writable");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the initialized RAM uses a signal init array — the mem signal is given an initial value from the same constant-building function, then written and read in a rising_edge(clk) process. The signal init is VHDL's $readmemh/initial equivalent, and it carries the same target caveat.

ram_init.vhd — RAM with power-up contents in VHDL (signal init array, registered read)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity ram_init is
       generic ( WIDTH : positive := 16; DEPTH : positive := 16 );
       port (
           clk  : in  std_logic;
           we   : in  std_logic;
           addr : in  std_logic_vector(natural(ceil(log2(real(DEPTH))))-1 downto 0);
           din  : in  std_logic_vector(WIDTH-1 downto 0);
           dout : out std_logic_vector(WIDTH-1 downto 0)     -- registered read
       );
   end entity;
 
   architecture rtl of ram_init is
       type ram_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
 
       function init_ram return ram_t is
           variable r : ram_t;
       begin
           for i in 0 to DEPTH-1 loop
               r(i) := std_logic_vector(to_unsigned((16#C000# + i) mod 2**WIDTH, WIDTH));
           end loop;
           return r;
       end function;
 
       -- Signal INITIAL VALUE = VHDL's readmemh/initial. Present in sim + FPGA config;
       -- NOT free on an ASIC (see 4c) — a writable RAM there needs a reset-load.
       signal mem : ram_t := init_ram;
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if we = '1' then
                   mem(to_integer(unsigned(addr))) <= din;   -- still writable
               end if;
               dout <= mem(to_integer(unsigned(addr)));       -- REGISTERED read
           end if;
       end process;
   end architecture;
ram_init_tb.vhd — verify loaded pattern then a write (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity ram_init_tb is
   end entity;
 
   architecture sim of ram_init_tb is
       constant WIDTH : positive := 16;
       constant DEPTH : positive := 16;
       constant AW    : positive := 4;
       signal clk  : std_logic := '0';
       signal we   : std_logic := '0';
       signal addr : std_logic_vector(AW-1 downto 0) := (others => '0');
       signal din  : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
       signal dout : std_logic_vector(WIDTH-1 downto 0);
   begin
       dut : entity work.ram_init
           generic map (WIDTH => WIDTH, DEPTH => DEPTH)
           port map (clk => clk, we => we, addr => addr, din => din, dout => dout);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable exp : std_logic_vector(WIDTH-1 downto 0);
       begin
           wait until falling_edge(clk);
           -- PHASE 1 — confirm the loaded power-up pattern.
           for a in 0 to DEPTH-1 loop
               addr <= std_logic_vector(to_unsigned(a, AW));
               wait until rising_edge(clk);
               wait for 1 ns;
               exp := std_logic_vector(to_unsigned((16#C000# + a) mod 2**WIDTH, WIDTH));
               assert dout = exp
                   report "ram_init: wrong power-up value" severity error;
               wait until falling_edge(clk);
           end loop;
           -- PHASE 2 — a write overrides the default.
           addr <= std_logic_vector(to_unsigned(3, AW)); din <= x"BEEF"; we <= '1';
           wait until falling_edge(clk);
           we <= '0'; addr <= std_logic_vector(to_unsigned(3, AW));
           wait until rising_edge(clk); wait for 1 ns;
           assert dout = x"BEEF"
               report "ram_init: write did not take" severity error;
           report "ram_init self-check complete" severity note;
           wait;
       end process;
   end architecture;

4c. The init-only-in-sim/FPGA bug — buggy vs silicon-safe fix, in all three HDLs

Family (c) is the signature failure of this section, shown here as buggy vs fixed RTL in each language, then dramatized narratively in §7. The bug is the same everywhere: a coefficient/microcode memory relies on an initial block (or $readmemh) as its only source of power-up contents. It simulates perfectly (the initial runs in sim) and works on an FPGA (block RAM contents come from the bitstream at configuration). Ported to an ASIC — which has no free power-up state — the array powers up undefined, and the block outputs garbage until (never) written. Every functional simulation still passes. The fix is to make the initial contents exist on the target: use a true ROM for genuinely constant data, or an explicit reset-driven load for a writable memory that must boot with defaults.

rom_init_bug.sv — BUGGY (init-only) vs FIXED (reset-driven load)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: `initial` is the ONLY source of contents. Works in sim and on an FPGA
   //        (bitstream config loads block RAM); on an ASIC the array powers up X and
   //        dout is garbage forever. Every functional sim PASSES — the bug is invisible.
   module coeff_mem_bad #(parameter int WIDTH = 16, DEPTH = 16, localparam int AW = $clog2(DEPTH))(
       input  logic clk,
       input  logic [AW-1:0] addr,
       output logic [WIDTH-1:0] dout
   );
       logic [WIDTH-1:0] mem [DEPTH];
       initial for (int i = 0; i < DEPTH; i++) mem[i] = WIDTH'(16'hC000 + i);  // sim/FPGA-only
       always_ff @(posedge clk) dout <= mem[addr];        // ASIC: reads UNDEFINED memory
   endmodule
 
   // FIXED: a reset-driven load writes the defaults into the array after reset, so the
   //        contents exist in SILICON on any target. A small counter walks every address
   //        during a `loading` phase and writes the (here computed; in practice ROM-sourced)
   //        default value; normal reads begin once `loaded` is high.
   module coeff_mem_good #(parameter int WIDTH = 16, DEPTH = 16, localparam int AW = $clog2(DEPTH))(
       input  logic clk,
       input  logic rst_n,                                // active-low synchronous reset
       input  logic [AW-1:0] addr,
       output logic          loaded,                      // high once the load sequence is done
       output logic [WIDTH-1:0] dout
   );
       logic [WIDTH-1:0] mem [DEPTH];
       logic [AW-1:0]    lcnt;                            // load-sequence address counter
       function automatic logic [WIDTH-1:0] def_val(logic [AW-1:0] a);
           return WIDTH'(16'hC000 + a);                   // a real design reads this from a ROM
       endfunction
       always_ff @(posedge clk) begin
           if (!rst_n) begin
               lcnt   <= '0;
               loaded <= 1'b0;
           end else if (!loaded) begin
               mem[lcnt] <= def_val(lcnt);                // WRITE the defaults in — exists in silicon
               if (lcnt == AW'(DEPTH-1)) loaded <= 1'b1;
               lcnt <= lcnt + 1'b1;
           end
           dout <= mem[addr];                             // registered read (valid after `loaded`)
       end
   endmodule

The testbench is what actually distinguishes the two on the target: it binds to the good module, waits for the loaded flag, and only then reads and checks the defaults — proving the contents were written into silicon, not merely declared. (On the buggy module the same defaults appear in RTL sim, which is exactly why the bug survives to the ASIC.)

rom_init_bug_tb.sv — wait for the reset-load, then verify contents exist in silicon
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rom_init_bug_tb;
       localparam int WIDTH = 16, DEPTH = 16, AW = $clog2(DEPTH);
       logic              clk = 0, rst_n;
       logic [AW-1:0]     addr;
       logic              loaded;
       logic [WIDTH-1:0]  dout;
       int                errors = 0;
 
       // Bind to coeff_mem_good → the reset-load makes the defaults real on any target.
       coeff_mem_good #(.WIDTH(WIDTH), .DEPTH(DEPTH)) dut
           (.clk(clk), .rst_n(rst_n), .addr(addr), .loaded(loaded), .dout(dout));
 
       always #5 clk = ~clk;
 
       initial begin
           rst_n = 0; addr = 0;
           @(negedge clk); @(negedge clk); rst_n = 1;
           wait (loaded);                                 // let the load sequence finish
           @(negedge clk);
           for (int a = 0; a < DEPTH; a++) begin
               addr = a[AW-1:0];
               @(posedge clk); #1;
               assert (dout === WIDTH'(16'hC000 + a))
                   else begin $error("a=%0d dout=%h exp=%h", a, dout, WIDTH'(16'hC000 + a)); errors++; end
               @(negedge clk);
           end
           if (errors == 0) $display("PASS: coeff_mem_good loaded defaults into silicon via reset-load");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story with reg outputs and always @(posedge clk). The bug is initial-only; the fix walks a load counter after reset so the contents are actually written into the array.

rom_init_bug.v — BUGGY (initial-only) vs FIXED (reset-load) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: initial is the ONLY init → sim + FPGA only; ASIC powers up X.
   module coeff_mem_bad #(parameter WIDTH = 16, DEPTH = 16, AW = 4)(
       input  wire clk,
       input  wire [AW-1:0] addr,
       output reg  [WIDTH-1:0] dout
   );
       reg [WIDTH-1:0] mem [0:DEPTH-1];
       integer i;
       initial for (i = 0; i < DEPTH; i = i + 1) mem[i] = (16'hC000 + i);  // sim/FPGA-only
       always @(posedge clk) dout <= mem[addr];       // ASIC: reads undefined memory
   endmodule
 
   // FIXED: reset-driven load writes the defaults in → contents exist in silicon.
   module coeff_mem_good #(parameter WIDTH = 16, DEPTH = 16, AW = 4)(
       input  wire clk,
       input  wire rst_n,
       input  wire [AW-1:0] addr,
       output reg           loaded,
       output reg  [WIDTH-1:0] dout
   );
       reg [WIDTH-1:0] mem [0:DEPTH-1];
       reg [AW-1:0]    lcnt;
       always @(posedge clk) begin
           if (!rst_n) begin
               lcnt   <= {AW{1'b0}};
               loaded <= 1'b0;
           end else if (!loaded) begin
               mem[lcnt] <= (16'hC000 + lcnt);        // WRITE defaults in (real design: from ROM)
               if (lcnt == (DEPTH-1)) loaded <= 1'b1;
               lcnt <= lcnt + 1'b1;
           end
           dout <= mem[addr];                         // registered read (valid after loaded)
       end
   endmodule
rom_init_bug_tb.v — wait for the load sequence, then verify silicon contents
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module rom_init_bug_tb;
       parameter WIDTH = 16, DEPTH = 16, AW = 4;
       reg              clk, rst_n;
       reg  [AW-1:0]    addr;
       wire             loaded;
       wire [WIDTH-1:0] dout;
       integer          a, errors;
 
       coeff_mem_good #(.WIDTH(WIDTH), .DEPTH(DEPTH), .AW(AW)) dut
           (.clk(clk), .rst_n(rst_n), .addr(addr), .loaded(loaded), .dout(dout));
 
       initial clk = 1'b0;
       always #5 clk = ~clk;
 
       initial begin
           errors = 0; rst_n = 0; addr = 0;
           @(negedge clk); @(negedge clk); rst_n = 1;
           wait (loaded);
           @(negedge clk);
           for (a = 0; a < DEPTH; a = a + 1) begin
               addr = a[AW-1:0];
               @(posedge clk); #1;
               if (dout !== (16'hC000 + a)) begin
                   $display("FAIL: a=%0d dout=%h exp=%h", a, dout, (16'hC000 + a));
                   errors = errors + 1;
               end
               @(negedge clk);
           end
           if (errors == 0) $display("PASS: reset-load put defaults into silicon on any target");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the bug is the same: a signal init as the only source of contents is sim/FPGA-only. The fix replaces it with a reset-driven load process that walks a counter and writes the defaults, so the array is loaded in silicon regardless of target.

rom_init_bug.vhd — BUGGY (signal-init only) vs FIXED (reset-load) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   -- BUGGY: the signal init is the ONLY source of contents. Sim + FPGA config only;
   --        on an ASIC the array powers up undefined and dout is garbage forever.
   entity coeff_mem_bad is
       generic ( WIDTH : positive := 16; DEPTH : positive := 16 );
       port ( clk  : in  std_logic;
              addr : in  std_logic_vector(3 downto 0);
              dout : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of coeff_mem_bad is
       type ram_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
       function init_ram return ram_t is
           variable r : ram_t;
       begin
           for i in 0 to DEPTH-1 loop
               r(i) := std_logic_vector(to_unsigned((16#C000# + i) mod 2**WIDTH, WIDTH));
           end loop;
           return r;
       end function;
       signal mem : ram_t := init_ram;                     -- sim/FPGA-only init
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               dout <= mem(to_integer(unsigned(addr)));     -- ASIC: reads undefined memory
           end if;
       end process;
   end architecture;
 
   -- FIXED: a reset-driven load walks every address and WRITES the defaults, so the
   --        contents exist in silicon on any target.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   entity coeff_mem_good is
       generic ( WIDTH : positive := 16; DEPTH : positive := 16 );
       port ( clk    : in  std_logic;
              rst_n  : in  std_logic;
              addr   : in  std_logic_vector(3 downto 0);
              loaded : out std_logic;
              dout   : out std_logic_vector(WIDTH-1 downto 0) );
   end entity;
   architecture rtl of coeff_mem_good is
       type ram_t is array (0 to DEPTH-1) of std_logic_vector(WIDTH-1 downto 0);
       signal mem  : ram_t;                                 -- no free init — loaded after reset
       signal lcnt : integer range 0 to DEPTH-1 := 0;
       signal done : std_logic := '0';
   begin
       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   lcnt <= 0;
                   done <= '0';
               elsif done = '0' then
                   mem(lcnt) <= std_logic_vector(to_unsigned((16#C000# + lcnt) mod 2**WIDTH, WIDTH));
                   if lcnt = DEPTH-1 then done <= '1'; else lcnt <= lcnt + 1; end if;
               end if;
               dout <= mem(to_integer(unsigned(addr)));     -- registered read (valid after done)
           end if;
       end process;
       loaded <= done;
   end architecture;
rom_init_bug_tb.vhd — wait for the reset-load, verify silicon contents (assert report)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity rom_init_bug_tb is
   end entity;
 
   architecture sim of rom_init_bug_tb is
       constant WIDTH : positive := 16;
       constant DEPTH : positive := 16;
       constant AW    : positive := 4;
       signal clk    : std_logic := '0';
       signal rst_n  : std_logic := '0';
       signal addr   : std_logic_vector(AW-1 downto 0) := (others => '0');
       signal loaded : std_logic;
       signal dout   : std_logic_vector(WIDTH-1 downto 0);
   begin
       dut : entity work.coeff_mem_good
           generic map (WIDTH => WIDTH, DEPTH => DEPTH)
           port map (clk => clk, rst_n => rst_n, addr => addr, loaded => loaded, dout => dout);
 
       clk <= not clk after 5 ns;
 
       stim : process
           variable exp : std_logic_vector(WIDTH-1 downto 0);
       begin
           wait until falling_edge(clk);
           wait until falling_edge(clk);
           rst_n <= '1';
           wait until loaded = '1';                         -- let the load sequence finish
           wait until falling_edge(clk);
           for a in 0 to DEPTH-1 loop
               addr <= std_logic_vector(to_unsigned(a, AW));
               wait until rising_edge(clk);
               wait for 1 ns;
               exp := std_logic_vector(to_unsigned((16#C000# + a) mod 2**WIDTH, WIDTH));
               assert dout = exp
                   report "coeff_mem_good: default not loaded into silicon" severity error;
               wait until falling_edge(clk);
           end loop;
           report "rom_init_bug self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: describe initial contents in a way that exists on the target you ship — a case/true ROM for constant data, or a reset-driven load for a writable memory — because an initial/$readmemh that only ran in simulation did not put anything into silicon.

5. Verification Strategy

A ROM's correctness is a known table, and an initialized memory's correctness is a known power-up state — both are exactly the kind of thing a self-checking testbench proves by comparison, because you know the answer in advance. The single invariant that captures a correct ROM is:

Reading address A returns the known constant table entry for A — every time, one cycle after A is presented (registered read).

Everything below is that invariant, made checkable, and it maps directly onto the testbenches in §4.

  • Read the table and assert known entries (self-checking). The reference model is the formula (or file) that filled the table, so the check is exact: drive every addr, and after accounting for the one-cycle read latency assert dout === expect_rom(addr). For a small sine/identity table this is exhaustive — every entry is checked, not sampled. The three rom_lut testbenches in §4a do exactly this: SV assert (dout === expect_rom(a)), Verilog if (dout !== exp) $display("FAIL…"), VHDL assert dout = exp report … severity error.
  • Verify $readmemh loaded the expected pattern. For a file-loaded ROM/RAM, read the first few and last few addresses and assert they equal the known first/last words of the hex file. This is the check that catches the silent-zeros failure of §6: if the path or format was wrong, the reads come back all-zeros (or X) and the assertion fires immediately, instead of the design limping along on a blank table.
  • Verify the registered-read latency. The read is registered, so dout is valid one cycle after addr. The testbench must present addr, take a clock edge, then compare — a testbench that checks dout in the same cycle it sets addr will fail a correct ROM. Getting the latency accounting right is itself part of the verification (it is the same one-cycle offset as 6.1's RAM read).
  • Test all addresses. A ROM is small enough to sweep completely, so verify every entry, not a corner. A table that is right at address 0 and wrong at a mid-table entry (a transcription error in the case, a truncated .hex file) is only caught by a full sweep. Re-run the sweep for the parameterized WIDTH/DEPTH you actually instantiate — a table proven at 16 entries is not proof at 1024.
  • Validate init behaviour for the actual target. The most important check is the one RTL sim cannot make on its own: the initial contents must be verified for the target you ship. For an FPGA, confirm the bitstream carries the block RAM contents (a post-config readback or a hardware smoke test). For an ASIC, the init must come from a true ROM or a reset-load — and the §4c testbench proves the reset-load by waiting for the loaded flag before checking, i.e. it verifies contents that were written into the array, not merely declared. Expected waveform: after reset deasserts, the good module walks its load counter (loaded low), then raises loaded; only after that does dout return valid table entries. A design where dout is valid immediately on the target with no load and no ROM macro is the §7 bug on a waveform.

6. Common Mistakes

Relying on initial/$readmemh power-up init that does not exist on the target. The signature synthesis-awareness bug of this section. An initial block or a $readmemh load simulates perfectly and works on an FPGA (block RAM contents come from the bitstream at configuration), so it passes every functional test. But an ASIC has no free power-up state — the array comes up undefined — so on ASIC (or any target without config-time init) the memory is X and the block outputs garbage, while the sim stays green. Constant data must be a true ROM (baked into logic or a ROM macro); a writable memory that must boot with known contents needs an explicit reset-driven load. "It initializes in simulation" is not "it initializes in silicon."

Combinational ROM where a registered read was intended. Writing assign dout = rom[addr] (zero latency) for a large table means the tool cannot map it to a block ROM — it becomes a wide combinational decode/mux over DEPTH entries, an area and timing problem, exactly the 6.1 async-read failure applied to constant data. For a large table, register the read (dout <= rom[addr]) and accept the one-cycle latency, which is the block ROM's native behaviour. (A tiny case ROM may be combinational on purpose — but then it is logic, not a block ROM.)

A $readmemh file path or format mismatch — silent zeros/X. $readmemh is quiet about failure: if the file path is wrong, or the file has the wrong radix (hex vs binary — use $readmemb for binary), wrong word count, or stray whitespace/comments the format does not expect, the load can leave the array as zeros or X with no error, and the design runs on a blank table. Always verify the load in the testbench by asserting known first/last entries (§5); never assume $readmemh succeeded.

Forgetting the read latency of a registered ROM. A registered ROM read has one cycle of latency (addr this cycle → dout next cycle), the same as a RAM. A pipeline built as if the ROM read were combinational will be off by one cycle: the phase-to-sample path in a DDS, or the address-to-microword path in a sequencer, is one stage deeper than the naive picture. Account for the latency in the surrounding datapath, and in the testbench compare on the cycle after presenting the address.

A huge combinational case ROM that should be a block RAM. A case of constants is fine for a small or irregular table, but a large dense table written as a giant case (or a wide combinational lookup) synthesizes to a mountain of logic — a deep decode-and-mux that eats LUTs/gates and will not close timing, when the same table as a preloaded array with a registered read would map cleanly to one block ROM. Match the spelling to the size: case for tiny/irregular, preloaded-array-plus-registered-read for large/dense.

7. DebugLab

The coefficient table that worked on the FPGA and shipped garbage on the ASIC

The engineering lesson: memory initial contents are a target-dependent synthesis concern — "it initializes in simulation" is not "it initializes in silicon." An initial block or $readmemh describes a simulation event and, on an FPGA, a bitstream-configuration feature; neither exists on an ASIC, which has no free power-up state. The tell is a bug that appears only on one target and reproduces identically at every power-up while every functional sim and the FPGA build stay green — target-specificity in something you designed to be universal means you leaned on a feature that target does not have. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: for constant data use a true ROM (baked into logic or a ROM macro), and for a writable memory that must boot with known contents use an explicit reset-driven load — and validate power-up contents for the target you actually ship, not just in the RTL sim where the initial block always runs.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "where do the contents come from in silicon?" habit.

Exercise 1 — Pick the initialization strategy for the target

For each memory, state whether you'd use a case/true ROM, a $readmemh/initial preload, or an explicit reset-driven load, and why in one line, given the stated target: (a) a 1024-entry sine table in a DDS on an ASIC; (b) the same sine table on an FPGA; (c) a 256-entry coefficient RAM that boots with defaults but is reprogrammed by software, on an ASIC; (d) an 8-entry, highly irregular control-decode table on any target. (Hint: constant vs writable, and whether the target initializes memory for free.)

Exercise 2 — Predict the silicon, then the bug

Two engineers implement the same 512-entry microcode ROM. Engineer A writes a preloaded array with $readmemh("ucode.hex", rom) and a registered read. Engineer B writes the same 512 entries as a giant combinational case. Both simulate identically and both pass on the FPGA. Describe (i) how the synthesized hardware differs (block ROM vs logic, area/timing), (ii) which one is at risk on an ASIC and exactly why, and (iii) what one change makes A's version silicon-safe on the ASIC without abandoning the block-ROM mapping.

Exercise 3 — Break the $readmemh load on paper

A ROM is loaded with $readmemh("tbl.hex", rom) and the design misbehaves. State the three distinct ways the load can silently fail (wrong path, wrong radix/format, wrong word count/width) and what the array contains in each case. Then write the single check you would add to each of a SystemVerilog, a Verilog, and a VHDL testbench to catch a silent-zeros load before the design uses the table, and explain why a case-of-constants ROM is not exposed to this particular failure.

Exercise 4 — Design the reset-load sequence

You must give a 256×16 coefficient RAM known power-up defaults on an ASIC (no free init), while keeping it writable for later software reprogramming. Sketch the control: what states/counter you need, where the default values come from, how you signal "loading done" to the rest of the design so it does not read before the load finishes, and the cost the load introduces (cycles of latency after reset, extra logic). Then say how a testbench proves the defaults reached silicon rather than merely being declared.

Continue in RTL Design Patterns (sibling and forward links unlock as they ship):

  • Inferring RAM — Chapter 6.1; the memory-inference template (2D array, clocked write, registered read) that a ROM is, minus the write and with fixed contents. Read this first.
  • Memory Read Timing — Chapter 6.2; the registered-vs-combinational read choice and read-during-write behaviour — the read side of this page's ROM, deferred here.
  • Register Files (Chapter 6.3) and Dual-Port RAM (Chapter 6.5) — sibling memory patterns; unlock as they ship.
  • Datapath and DSP lookup-table patterns (later chapters) — where the sine/coefficient ROMs of this page feed real arithmetic; unlock as they ship.

Backward / method:

  • The Register Pattern (D-FF) — Chapter 2.1; the flop that registers the ROM read, giving it its one-cycle latency.
  • Synchronous Reset — Chapter 2.x; the reset discipline the silicon-safe reset-load in §4c is built on.
  • Reset Strategy — Chapter 2.5; how reset defines a known power-up state — the exact problem an initialized memory must solve on an ASIC.
  • Encoding & Conversions — the constant-mapping viewpoint: a ROM is a general lookup that turns any index into any precomputed value.
  • The RTL Design Mindset — Chapter 0.2; the synthesis-awareness lens ("what does this become in silicon?") that this page's init bug is the purest example of.
  • What Is an RTL Design Pattern? — Chapter 0.1; why constant-data memory earns the name "pattern" — a recurring need, a reusable form, a synthesis reality, and a signature failure.

Verilog v1 prerequisites (the grammar this pattern transcribes into):

  • Case Statements — the construct behind the case-of-constants ROM, and where unique/default (latch-free totality) live.
  • Initial and Always Blocks — the initial block that fills a preloaded array in simulation, and why it is a simulation construct — the heart of this page's init caveat.
  • Physical Data Types (Nets, Regs, Arrays) — the memory-array declaration (reg [W-1:0] rom [0:D-1]) that holds the table.

11. Summary

  • A ROM is the RAM template with fixed contents and no write. The same 2D array read through a register (one-cycle latency, exactly as in 6.1), with the contents baked into a case of constants (which becomes logic — a true ROM on any target) or a preloaded array ($readmemh/initial, which maps to a block ROM for large dense tables). Constant-data memories are everywhere: sine/gamma/coefficient tables, microcode, fonts, CRC tables.
  • Initial contents are a target-dependent synthesis concern, not a language feature. $readmemh, initial, and a VHDL constant/signal init all describe power-up contents, but whether that description survives depends on the target: an FPGA block RAM inits at bitstream configuration (free), while an ASIC has no free power-up state — it needs a true ROM macro or an explicit reset-driven load.
  • The signature bug: init that exists only in sim and on the FPGA. An initial/$readmemh used as the only source of contents passes every functional simulation and works on the FPGA (bitstream-loaded block RAM), then powers up undefined on an ASIC and outputs garbage forever. "It initializes in simulation" is not "it initializes in silicon" — the DebugLab of §7.
  • Silicon-safe by construction, in every HDL. For constant data use a true ROM (a case, or a ROM macro); for a writable memory that must boot with known contents use an explicit reset-driven load that writes the defaults into the array — so the contents exist regardless of target.
  • Verify the table, the load, the latency, and the target. Read known entries and assert dout matches the golden table one cycle late (registered read); assert $readmemh actually loaded the expected first/last words (catch the silent-zeros failure); account for the one-cycle read latency; and validate the init for the target you ship — the §4c reset-load testbench waits for the loaded flag so it checks contents written into silicon, not merely declared. Self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL.