Skip to main content

Arrays and Memories

Data Types introduced the array declaration reg [7:0] mem [0:255]; — 256 elements, each 8 bits wide — without saying what it's for. This page is that answer: arrays declared this way are how Verilog models ROM and RAM behaviorally, directly connecting to Digital Design's Memory Fundamentals and Read-Only Memory (ROM) pages.

Reading the declaration​

reg [7:0] mem [0:255];

Two independent bracket pairs, meaning two independent things:

  • [7:0] — the width of each element: 8 bits.
  • [0:255] — the depth: 256 separately addressable elements, indexed 0 through 255.

mem[10] accesses element 10 (an 8-bit value); mem[10][3] accesses bit 3 of element 10. Mixing these up — writing mem[7:0] expecting a bit-slice of element 0 the way a vector slice would work — is a common error, since array-depth indexing and vector-width slicing use identical-looking syntax for different concepts.

Modeling a ROM​

A ROM (read-only memory) needs its contents fixed at compile time and read combinationally by address, matching the truth-table-driven lookup behavior from Read-Only Memory (ROM):

module rom_16x8 (
input [3:0] addr,
output [7:0] data
);

reg [7:0] mem [0:15];

initial begin
mem[0] = 8'h00;
mem[1] = 8'h01;
// ... remaining 14 entries
mem[15] = 8'hFF;
end

assign data = mem[addr];

endmodule

addr[3:0] feeds a 16 x 8 ROM (fixed table), producing data[7:0]

The initial block here is doing something different from its simulation-only role in Behavioral Modeling — for ROM contents specifically, many synthesis flows do treat an initial-populated array as a fixed lookup table synthesizable into real ROM hardware (or, on an FPGA, initialized block-RAM content), rather than ignoring it outright. That support is tool- and target-dependent, so real ROM content is more commonly loaded via a $readmemh/ $readmemb file-read system task (covered in the next page, on system tasks and directives) than hardcoded line-by-line as above — the structure is the same either way, only how the array gets populated differs.

Modeling a RAM​

A RAM additionally needs to be written, which means the array must be updated procedurally, synchronously, matching Memory Fundamentals's synchronous-write RAM organization:

module ram_256x8 (
input clk,
input we,
input [7:0] addr,
input [7:0] wdata,
output [7:0] rdata
);

reg [7:0] mem [0:255];

always @(posedge clk) begin
if (we)
mem[addr] <= wdata;
end

assign rdata = mem[addr]; // combinational read (asynchronous read port)

endmodule

addr[7:0], wdata[7:0], we, and clk feed a 256 x 8 RAM (mem array), producing rdata[7:0]

The write side is an ordinary synchronous always @(posedge clk) block — nonblocking assignment, per Combinational vs. Sequential always, because writing mem[addr] here is exactly a clocked register update, just addressed rather than a single fixed signal. The read side (assign rdata = mem[addr];) is combinational and asynchronous in this version — rdata changes immediately whenever addr changes, with no clock involved. Making the read synchronous instead (registering rdata on a clock edge) is a one-line change — always @(posedge clk) rdata <= mem[addr]; — and is the far more common real-world choice, since a registered read port is what most synthesis flows can actually map onto dedicated block-RAM hardware on an FPGA, rather than large arrays of flip-flops.

Read-during-write: what happens when read and write addresses collide​

In ram_256x8 above, what does rdata show if addr is written and read on the same clock edge? Because the write (mem[addr] <= wdata;) is nonblocking, mem[addr] doesn't actually update until after the clock edge's nonblocking updates settle — the combinational read (assign rdata = mem[addr];) sees the old contents of that address during the cycle the write is happening, and only reflects wdata from the next cycle onward. This is called read-old-data (or "read-before-write") behavior. The alternative, read-new-data ("write-first") behavior — where a colliding read immediately sees the value just written — needs extra logic (typically comparing the read and write addresses and bypassing mem with wdata directly when they match) rather than falling out of the array template automatically.

Why array-based memory doesn't scale to huge sizes​

Big behavioral arrays don't synthesize the way you might expect

reg [7:0] mem [0:255] (2 Kbit) synthesizes reasonably as flip-flops or maps to a small block-RAM. reg [63:0] mem [0:1000000] — a 64 Mbit array — describing it this way is technically legal Verilog, but no synthesis tool will turn that into 64 million flip-flops; realistic memory sizes are implemented using dedicated memory compilers/IP (or FPGA block-RAM primitives) that a synthesis tool infers from recognizable patterns like the RAM template above, not arbitrary behavioral code. Keep behaviorally-modeled memories at a size that's plausible as actual flip-flop-based storage, and treat anything larger as needing a real memory macro instantiated directly, not inferred.

What's next​

Section D has now covered the complete synthesizable-RTL toolkit: combinational/sequential inference rules, FSM coding styles, parameterized/generated structure, and memory modeling. Section E turns to practical, everyday tooling — system tasks for debugging, compiler directives, a minimal testbench, and a full worked example tying the whole topic together.