Synchronous write, asynchronous read RAM with configurable depth and width. From small register files to large memories.
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.
addr, data, enable (clocked)
depth × width bit cells
addr → data (async)
| Parameter | Type | Description |
|---|---|---|
| depth | Integer | Number of addressable entries in the memory array. Address width is automatically computed as ceil(log2(depth)). |
| width | Integer | Bit width of each memory entry. Determines the size of read and write data ports. |
| init | Array / Hash | Optional 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_ports | Integer | Number of independent asynchronous read ports. Default is 1. Multiple read ports allow simultaneous access from different components. |
| write_ports | Integer | Number of independent synchronous write ports. Default is 1. Multiple write ports require arbitration logic to handle simultaneous writes to the same address. |
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.
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.
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
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.
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