Back to components

Memory Components

Synchronous write, asynchronous read RAM with configurable depth and width. From small register files to large memories.

RAM Sync Write Async Read Configurable

Memory Architecture

Memories in digital circuits serve as storage arrays, holding data that can be written and read by other components. RHDL's memory model uses synchronous writes and asynchronous reads, which is the most common pattern for register-file style memories in FPGAs and ASICs.

With synchronous writes, data is stored into the memory array on the rising edge of the clock when a write-enable signal is asserted. This ensures clean, predictable write behavior without race conditions. Asynchronous reads return data immediately based on the current read address, without waiting for a clock edge. This means the read output is a combinational function of the read address, making it available within the same clock cycle. This model maps naturally to FPGA distributed RAM (LUT-based) and is ideal for register files and small lookup tables.

Write Port

addr, data, enable (clocked)

Storage Array

depth × width bit cells

Read Port

addr → data (async)

Configuration

ParameterTypeDescription
depthIntegerNumber of addressable entries in the memory array. Address width is automatically computed as ceil(log2(depth)).
widthIntegerBit width of each memory entry. Determines the size of read and write data ports.
initArray / HashOptional initial values for the memory contents. Can be a flat array of values or a hash mapping addresses to values. Unspecified entries default to zero.
read_portsIntegerNumber of independent asynchronous read ports. Default is 1. Multiple read ports allow simultaneous access from different components.
write_portsIntegerNumber of independent synchronous write ports. Default is 1. Multiple write ports require arbitration logic to handle simultaneous writes to the same address.

Memory DSL

RHDL provides a dedicated DSL for declaring and using memories. The memory keyword creates a storage array, while sync_write and async_read define the write and read interfaces. This keeps the intent clear and maps cleanly to hardware primitives.

A basic RAM module with one write port and one read port. Writes happen on the clock edge when write-enable is high. Reads are combinational and always available.

simple_ram.rb
class SimpleRAM < RHDL::Sim::Component
  parameter :DEPTH, default: 256
  parameter :WIDTH, default: 8

  input  :clk
  # Write port
  input  :wr_addr,  width: clog2(DEPTH)
  input  :wr_data,  width: WIDTH
  input  :wr_en
  # Read port
  input  :rd_addr,  width: clog2(DEPTH)
  output :rd_data,  width: WIDTH

  # Declare the memory array
  memory :mem,
    depth: DEPTH,
    width: WIDTH

  # Synchronous write on clock edge
  sync_write clock: :clk do
    mem[wr_addr] <= wr_data when wr_en
  end

  # Asynchronous read (combinational)
  async_read do
    rd_data <= mem[rd_addr]
  end
end

A memory with initial values, useful for instruction ROMs, lookup tables, or boot code storage. The init parameter accepts an array or a hash for sparse initialization.

init_rom.rb
class LookupTable < RHDL::Sim::Component
  input  :clk
  input  :index,  width: 4
  output :value,  width: 8

  # Sine wave lookup (quarter wave)
  memory :sine_lut,
    depth: 16,
    width: 8,
    init: [
      0x00, 0x19, 0x32, 0x4B,
      0x64, 0x7C, 0x93, 0xA8,
      0xBC, 0xCE, 0xDE, 0xEB,
      0xF5, 0xFB, 0xFE, 0xFF,
    ]

  async_read do
    value <= sine_lut[index]
  end
end

Register Files

A register file is a multi-ported memory used inside processors to hold general-purpose register values. It typically has two read ports (for source operands) and one write port (for the result). In RISC-V, register x0 is hardwired to zero, which requires special handling. This example shows a complete 32-entry register file suitable for a RISC-V processor.

register_file.rb
class RegisterFile < RHDL::Sim::Component
  parameter :NUM_REGS, default: 32
  parameter :WIDTH,    default: 32

  input  :clk
  # Read ports (asynchronous)
  input  :rs1_addr,  width: 5
  input  :rs2_addr,  width: 5
  output :rs1_data,  width: WIDTH
  output :rs2_data,  width: WIDTH
  # Write port (synchronous)
  input  :wr_addr,   width: 5
  input  :wr_data,   width: WIDTH
  input  :wr_en

  memory :regs,
    depth: NUM_REGS,
    width: WIDTH

  # Synchronous write (x0 is always zero)
  sync_write clock: :clk do
    regs[wr_addr] <= wr_data when wr_en & wr_addr.neq?(0)
  end

  # Asynchronous dual read with x0 hardwired to 0
  async_read do
    rs1_data <= cond(rs1_addr.eq?(0), 0, regs[rs1_addr])
    rs2_data <= cond(rs2_addr.eq?(0), 0, regs[rs2_addr])
  end
end