Skip to content

Memories in Verilog

Introduction

Memory is a fundamental building block in digital design — used to store data, coefficients, or states across clock cycles. In Verilog, memory modeling allows designers to describe RAM, ROM, and register arrays at various abstraction levels — from behavioral to gate-level.

This article explores how to model different types of memories, initialize them, simulate read/write operations, and infer synthesizable memory in hardware.

Introduction to Memories

In Verilog, memory elements are typically arrays of registers (reg data type). They represent a collection of storage locations, each holding a word of fixed width.

Declaration Syntax

reg [data_width-1:0] memory_array [0:depth-1];
  • data_width → number of bits in each word
  • depth → number of words in memory

Example

reg [7:0] mem [0:15]; // 16x8 memory

This example defines a memory with 16 locations, each 8 bits wide.

Types of Memories

There are three major types of memories commonly modeled in Verilog:

Memory Type Description Access Synthesizable
ROM (Read-Only Memory) Pre-initialized, no writes Read-only Yes
RAM (Random Access Memory) Read and write operations Read/Write Yes
Register Array Used for small storage or pipelining Parallel-access Yes

Modeling Read and Write Operations

Simple Read/Write Memory Example

module simple_ram (
  input clk,
  input we,               // write enable
  input [3:0] addr,
  input [7:0] data_in,
  output reg [7:0] data_out
);
  reg [7:0] mem [0:15]; // 16x8 memory

  always @(posedge clk) begin
    if (we)
      mem[addr] <= data_in;  // Write operation
    else
      data_out <= mem[addr]; // Read operation
  end
endmodule

Explanation:

  • Memory is updated only on the positive edge of clk.
  • When we (write enable) is 1, the data at data_in is written into mem[addr].
  • When we is 0, the memory content at addr is read to data_out.

Read-Only Memory (ROM)

ROM can be modeled by initializing the memory with predefined contents. It is typically read-only during simulation.

Example: 8x8 ROM

module rom_example (
  input [2:0] addr,
  output reg [7:0] data
);
  reg [7:0] rom [0:7]; // 8x8 ROM

  initial begin
    rom[0] = 8'hA1;
    rom[1] = 8'hB2;
    rom[2] = 8'hC3;
    rom[3] = 8'hD4;
    rom[4] = 8'hE5;
    rom[5] = 8'hF6;
    rom[6] = 8'h07;
    rom[7] = 8'h18;
  end

  always @(*) begin
    data = rom[addr];
  end
endmodule

Alternatively, ROM can be initialized from an external file.

Using $readmemh or $readmemb

initial begin
  $readmemh("rom_data.hex", rom);
end
  • $readmemh → reads hexadecimal file
  • $readmemb → reads binary file

This allows designers to easily load data from files instead of manually assigning values.

Dual-Port Memory

Many designs (like DSPs and FIFOs) use dual-port memories to allow simultaneous read/write operations from two ports.

Example: Dual-Port RAM

module dual_port_ram (
  input clk,
  input we_a, we_b,
  input [3:0] addr_a, addr_b,
  input [7:0] data_a, data_b,
  output reg [7:0] q_a, q_b
);
  reg [7:0] mem [0:15];

  always @(posedge clk) begin
    if (we_a)
      mem[addr_a] <= data_a;
    q_a <= mem[addr_a];

    if (we_b)
      mem[addr_b] <= data_b;
    q_b <= mem[addr_b];
  end
endmodule

This design enables independent read/write operations at both ports A and B.

Asynchronous vs. Synchronous Memory

Type Read Behavior Example Use
Synchronous Output changes only on clock edge RAM used in FPGA
Asynchronous Output updates immediately on address change Combinational ROM

Example: Asynchronous Read Memory

always @(*) begin
  data_out = mem[addr]; // No clock required
end

Synchronous memories are typically synthesizable, while asynchronous models are mainly for simulation.

Memory Initialization

Initialization ensures that memory has defined contents before simulation begins.

Inline Initialization

reg [7:0] mem [0:3] = '{8'h11, 8'h22, 8'h33, 8'h44};

File-Based Initialization

initial begin
  $readmemh("init.hex", mem);
end

File-based initialization is especially useful for large memories or ROMs.

Behavioral Memory Models (Simulation-Only)

Behavioral models provide quick verification before hardware synthesis.

Example: Behavioral RAM

module ram_behavioral;
  reg [7:0] mem [0:31];
  reg [7:0] data_out;
  integer i;

  initial begin
    for (i = 0; i < 32; i = i + 1)
      mem[i] = 0;
  end

  task write(input [4:0] addr, input [7:0] data);
    mem[addr] = data;
  endtask

  function [7:0] read(input [4:0] addr);
    read = mem[addr];
  endfunction
endmodule

These constructs are non-synthesizable but help validate logic functionality early in design.

Synthesis Considerations

When synthesizing memory designs:

Allowed

  • Indexed access: mem[address]
  • Single clock domain synchronous reads/writes
  • Proper write-enable and clock control

Avoid

  • Non-constant indices in for loops
  • Asynchronous writes
  • Mixed blocking and non-blocking assignments in the same block

Common FPGA Mapping

  • Simple RAM -> Block RAM (BRAM)
  • Small arrays -> LUT RAM
  • ROM -> Initialized LUTs or BRAM

Example: Synchronous RAM Testbench

module tb_sync_ram;
  reg clk, we;
  reg [3:0] addr;
  reg [7:0] din;
  wire [7:0] dout;

  simple_ram uut (.clk(clk), .we(we), .addr(addr), .data_in(din), .data_out(dout));

  always #5 clk = ~clk;

  initial begin
    clk = 0; we = 0;
    #10 we = 1; addr = 4'b0001; din = 8'hAA;
    #10 we = 1; addr = 4'b0010; din = 8'hBB;
    #10 we = 0; addr = 4'b0001;
    #10 we = 0; addr = 4'b0010;
    #10 $finish;
  end

  initial begin
    $monitor(\"Time=%0t Addr=%b WE=%b Din=%h Dout=%h\", $time, addr, we, din, dout);
  end
endmodule

This testbench writes and reads memory values while displaying results.

Modeling Multi-Dimensional Memories

Verilog allows multidimensional memory arrays useful in image buffers or matrices.

reg [7:0] mem [0:3][0:7]; // 4x8 two-dimensional memory

Access with:

mem[row][col] = data;

Although supported in simulation, multi-dimensional synthesis is tool-dependent — check FPGA/ASIC tool documentation.

Advanced Memory Features

Error Detection (Parity/ECC)

You can append a parity bit to each word for error detection.

reg [8:0] mem [0:15]; // 8 data bits + 1 parity bit

Burst Access

You can design burst reads/writes by incrementing the address automatically.

addr = addr + 1; // Sequential burst read/write

Memory Banking

Large memories can be split into smaller banks to improve access speed or support parallelism.

Best Practices

  • Use non-blocking (<=) assignments inside clocked processes
  • Initialize memories before simulation
  • Separate read and write logic
  • Use parameterized designs for reusability
  • Prefer $readmemh for large memory initialization

Summary

Memory modeling in Verilog provides the foundation for designing register files, caches, buffers, and storage arrays. By understanding how to declare, read/write, and initialize memory arrays, designers can simulate complex storage systems accurately and infer efficient hardware in synthesis.

Key Takeaways:

  • Use reg [width-1:0] mem [0:depth-1] for declaration.
  • Initialize using initial block or $readmemh.
  • Use always @(posedge clk) for synchronous reads/writes.
  • Verify functionality using testbenches with $monitor or $display.