Verilog Arrays and Memories: A Practical Guide
Every real chip stores data somewhere: a FIFO buffering packets, a register file inside a CPU, a lookup table holding filter coefficients. In Verilog, all of these are modelled with arrays — and the single biggest source of confusion for beginners is that an array is not the same thing as a vector, even though both use square brackets. This chapter clears that up, then walks through declaring memories, reading and writing them, loading them from files with $readmemh, and using multidimensional arrays. Every runnable example below was compiled and simulated with Icarus Verilog, and the log shown is the actual tool output.
Vectors vs arrays: one value vs many values
A vector is a single value that happens to be more than one bit wide. reg [15:0] bus; is one 16-bit quantity: you can assign all of it in one statement, slice it with a part-select like bus[15:8], and pass it through a port.
An array is a collection of separate values. reg [7:0] scratch [0:7]; declares eight independent 8-bit words. The range before the name is the width of each word; the range after the name is how many words exist. You cannot assign the whole array in one statement in Verilog-2005, and you cannot put an entire array on a module port — you access it one word at a time.
| Vector | Array | |
|---|---|---|
| Declaration | reg [15:0] bus; | reg [7:0] scratch [0:7]; |
| What it holds | One 16-bit value | Eight separate 8-bit values |
| Assign everything at once | Yes: bus = 16'hA5C3; | No — loop or $readmemh |
| Allowed on a port | Yes | No (Verilog-2005) |
| Index meaning | bus[3] is bit 3 | scratch[3] is word 3 |
Here is both side by side. Note the last line: Verilog-2001 and later let you select bits inside an array word by indexing twice.
// vector_vs_array_tb.v -- a vector is ONE value; an array is MANY values
module vector_vs_array_tb;
reg [15:0] bus; // vector: one 16-bit value
reg [7:0] scratch [0:7]; // array : eight separate 8-bit words
integer i;
initial begin
bus = 16'hA5C3;
$display("bus = %h (whole vector in one assignment)", bus);
$display("bus[15:8] = %h (part-select of a vector)", bus[15:8]);
$display("bus[0] = %b (bit-select of a vector)", bus[0]);
for (i = 0; i < 8; i = i + 1)
scratch[i] = i * 8'h11; // each word written individually
$display("scratch[3] = %h (word-select of an array)", scratch[3]);
$display("scratch[3][7:4] = %h (bit-select inside one word)", scratch[3][7:4]);
$finish;
end
endmodulebus = a5c3 (whole vector in one assignment) bus[15:8] = a5 (part-select of a vector) bus[0] = 1 (bit-select of a vector) scratch[3] = 33 (word-select of an array) scratch[3][7:4] = 3 (bit-select inside one word) /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp137p8mf9/vector_vs_array_tb.v:18: $finish called at 0 (1s)
Declaring a memory
A memory in Verilog is simply an array of reg vectors. The classic declaration you will see in every RAM, ROM and register-file model is:
reg [7:0] mem [0:255]; // 256 words x 8 bits -- a byte memory reg [31:0] rf [0:31]; // 32 words x 32 bits -- a register file reg [15:0] frame [0:3][0:7]; // 4 x 8 two-dimensional array wire [7:0] taps [0:3]; // net array (each element needs a driver)
Read reg [7:0] mem [0:255]; from the inside out: each element is a [7:0] vector (8 bits), and there are elements at addresses 0 through 255. Total storage is 256 × 8 = 2048 bits. Two details that trip people up in interviews:
- Order matters.
reg [0:255] mem [7:0];is legal Verilog but means something completely different: eight words, each 256 bits wide. - Depth range is up to you.
[0:255]and[255:0]both give 256 words; sticking to[0:low-to-high]matches how$readmemhfills addresses by default and is the convention most teams use.
Reading and writing a memory
Writing a word is an assignment to an indexed element, usually clocked. Reading is just using the indexed element in an expression. The most common RTL pattern — the one that maps onto FPGA block RAM and is the basis of almost every memory interview question — is synchronous write, asynchronous read:
// ram_rw.v -- 256x8 single-port RAM: synchronous write, asynchronous read
module ram_256x8 (
input wire clk,
input wire we, // write enable
input wire [7:0] addr,
input wire [7:0] wdata,
output wire [7:0] rdata
);
reg [7:0] mem [0:255]; // the memory: 256 words of 8 bits
always @(posedge clk)
if (we)
mem[addr] <= wdata; // write happens on the clock edge
assign rdata = mem[addr]; // read is combinational (asynchronous)
endmodule
module ram_rw_tb;
reg clk = 0;
reg we;
reg [7:0] addr, wdata;
wire [7:0] rdata;
ram_256x8 dut (.clk(clk), .we(we), .addr(addr), .wdata(wdata), .rdata(rdata));
always #5 clk = ~clk; // 10 ns clock period
initial begin
// write 0xDE to address 10, then 0xAD to address 200
we = 1; addr = 8'd10; wdata = 8'hDE;
@(posedge clk) #1; // hold inputs through the rising edge
addr = 8'd200; wdata = 8'hAD;
@(posedge clk) #1;
we = 0;
addr = 8'd10; #1 $display("mem[ 10] read back = %h", rdata);
addr = 8'd200; #1 $display("mem[200] read back = %h", rdata);
addr = 8'd55; #1 $display("mem[ 55] (never written) = %h", rdata);
$finish;
end
endmodulemem[ 10] read back = de mem[200] read back = ad mem[ 55] (never written) = xx /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmp5qc8o1sg/ram_rw.v:39: $finish called at 19 (1s)
Three things to notice in that log:
- Addresses 10 and 200 return exactly what was written. Each array element behaves like its own register.
- Address 55 returns
xx. Aregarray starts as all-unknown in simulation; nothing initializes it for you. Reading a location you never wrote givesx, which is often the first clue when debugging a design whose output is mysteriously unknown. - The testbench changes inputs
#1after the clock edge. If you changeaddrat the exact moment of the edge with blocking assignments, you create a race with thealways @(posedge clk)block and the write can land at the wrong address.
For a registered-read (synchronous-read) RAM, move the read into the clocked block too: rdata_q <= mem[addr];. Synthesis tools infer block RAM more reliably from that style; the purely combinational read above often becomes distributed/LUT RAM on FPGAs.
Initializing memories: $readmemh and $readmemb
Loading a ROM image, a test vector set, or a CPU program word-by-word in an initial block gets old fast. Verilog provides two system tasks that fill an array from a text file:
$readmemh("file", mem);— file contains hexadecimal values$readmemb("file", mem);— file contains binary values
The file format is simple: values separated by whitespace or newlines, // comments allowed, and an optional @hh directive that jumps to a new address (always interpreted in hex, even in a $readmemb file). Values may contain x and z. You can also restrict the load range: $readmemh("f.hex", mem, 64, 127); fills only addresses 64 to 127.
This example writes the hex file from the testbench itself so the whole thing runs standalone — in a real project boot.hex would come from your assembler or a Python script:
// rom_readmemh_tb.v -- initialize a memory from a hex file with $readmemh
module rom_readmemh_tb;
reg [7:0] rom [0:15]; // 16x8 ROM
integer f, i;
initial begin
// Step 1: create boot.hex (in a real project this file already exists)
f = $fopen("boot.hex", "w");
$fwrite(f, "// boot.hex - first four opcodesn");
$fwrite(f, "3E 10 D3 07n"); // values separated by whitespace
$fwrite(f, "@0A FFn"); // @0A jumps to address 10 (hex)
$fclose(f);
// Step 2: load it into the array
$readmemh("boot.hex", rom);
for (i = 0; i < 12; i = i + 1)
$display("rom[%0d] = %h", i, rom[i]);
$finish;
end
endmodulerom[0] = 3e rom[1] = 10 rom[2] = d3 rom[3] = 07 rom[4] = xx rom[5] = xx rom[6] = xx rom[7] = xx rom[8] = xx rom[9] = xx rom[10] = ff rom[11] = xx /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpiqnq5uox/rom_readmemh_tb.v:19: $finish called at 0 (1s)
The log confirms the behaviour: addresses 0–3 get the four listed values, the @0A directive places FF at address 10, and every address the file never mentions stays xx. One practical warning: the width of each file entry should match the word width of the array. A value wider than the word is truncated and most simulators only warn, so a mismatched file can silently corrupt your ROM contents.
Is $readmemh synthesizable? For ASIC flows, no — it is a simulation construct, and real ROMs come from the foundry or memory compiler. Most FPGA tools, however, honour it inside an initial block to set the power-up contents of block RAM, which is why you see it in production FPGA code.
Multidimensional arrays
Verilog-2001 added arrays with more than one unpacked dimension. They are ideal for anything naturally organised as a grid: a frame buffer, a cache organised as sets × ways, or a bank of register files. Index them one dimension at a time, left to right:
// multidim_tb.v -- a 2-D array: 4 rows x 8 columns of 8-bit pixels
module multidim_tb;
reg [7:0] frame [0:3][0:7]; // frame[row][col]
integer r, c;
initial begin
for (r = 0; r < 4; r = r + 1)
for (c = 0; c < 8; c = c + 1)
frame[r][c] = r*16 + c; // encode position into the value
$display("frame[0][0] = %h", frame[0][0]);
$display("frame[2][5] = %h", frame[2][5]);
$display("frame[3][7] = %h", frame[3][7]);
// print one full row
$write("row 1 =");
for (c = 0; c < 8; c = c + 1)
$write(" %h", frame[1][c]);
$write("n");
$finish;
end
endmoduleframe[0][0] = 00 frame[2][5] = 25 frame[3][7] = 37 row 1 = 10 11 12 13 14 15 16 17 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpdqqn1tiz/multidim_tb.v:20: $finish called at 0 (1s)
Two restrictions to remember: you must index every unpacked dimension before you can touch bits (frame[2] alone is not a usable value in Verilog-2005), and $readmemh works only on one-dimensional memories — to load a 2-D array from a file, loop over rows yourself. SystemVerilog relaxes several of these rules, but in plain Verilog they are hard limits that interviewers like to probe.
Mistakes that cost marks in interviews
- Writing
mem = 0;to clear a memory. Illegal — arrays have no whole-array assignment in Verilog. Use aforloop in aninitialor reset block, and remember that looping over thousands of words on a reset edge is fine in simulation but describes a very expensive reset network in hardware. - Putting an array on a port.
output reg [7:0] mem [0:255]will not compile in Verilog-2005. Expose an address/data interface instead — which is exactly how real memories work. - Swapping the two ranges.
reg [0:255] mem [7:0]compiles and then wastes an afternoon of debugging. - Expecting zero-initialized memory. Simulation starts at
x; silicon starts at whatever the power-up state happens to be. If your design needs known contents, load them explicitly. - Forgetting the synthesis mapping. A small array of a few words synthesizes to flip-flops; a large
reg [7:0] mem [0:255]with a clocked write infers RAM. Knowing which one you are describing — and saying so — is what separates a confident answer from a memorised one.
Work through all four examples by changing one thing at a time — widths, depths, the @ directive, the read style — and re-running the simulation. Arrays and memories appear in practically every block you will ever design or verify, so fluency here pays off immediately in the FIFO, register-file and cache designs that follow.
Quick check
What is the difference between reg [7:0] a; and reg a [7:0];?
The first is a vector: one 8-bit value you can assign in a single statement and slice with part-selects. The second is an array of eight separate 1-bit regs: each element must be accessed individually, and the whole thing cannot be assigned at once or passed through a port in Verilog-2005.
How many bits of storage does reg [15:0] buffer [0:1023]; declare?
1024 words of 16 bits each, so 16,384 bits total. The range before the name is the word width; the range after the name is the depth (number of words).
In a $readmemh data file, what does @0A mean, and what happens to addresses the file never mentions?
@0A sets the load address to hex 0A (decimal 10), so the next value lands there. Address directives are always hexadecimal, even in $readmemb files. Any location the file does not cover keeps its previous contents — for a fresh reg array, that means unknown (x).
A memory is modelled with a clocked write (mem[addr] <= wdata) and a continuous-assign read. Why might reading an address return x even though the design is correct?
Because that address was never written. Verilog reg arrays power up as all-x in simulation, and nothing initializes them automatically. The fix is to write the location first, load the array with $readmemh/$readmemb, or initialize it in an initial block or reset loop.
Share your question in comments or talk to our mentor team for batch guidance.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses