Memory
Formal Reference
For a concise normative summary of all MEM rules, port types, and access syntax, see the Memory Formal Reference. This page provides detailed explanations, examples, and practical guidance.
Overview
- MEM declares internal arrays (word_width × depth) synthesized as Block RAM, Distributed RAM, or vendor-specific memories.
- MEM supports multiple named read (
OUT) ports (ASYNC or SYNC), synchronous write (IN) ports, and combined read/write (INOUT) ports. - Address widths are derived as
clog2(depth)with a minimum of 1 bit. Runtime addresses are exact-width signals unless you widen or slice explicitly. - Initialization can be a sized literal (same value for every word) or a file (
@file(...)). - MEM access obeys synchronous vs. asynchronous rules depending on port kind and block context.
Memory Port Modes
JZ-HDL MEM declarations express all common BSRAM operating modes through port type combinations. The compiler analyzes port declarations to determine the required BSRAM mode.
| Port Configuration | BSRAM Mode | Description |
|---|---|---|
OUT only | Read Only Memory | Read-only, initialized at power-on |
INOUT ×1 | Single Port | One shared-address port for read and write |
IN + OUT | Semi-Dual Port | Separate write port (Port A) and read port (Port B) |
INOUT ×2 | Dual Port | Two independent read/write ports |
Notes:
INOUTports cannot be mixed withINorOUTports in the same MEM declaration.- A MEM uses either
IN/OUTports (Semi-Dual Port or Read Only) orINOUTports (Single Port or Dual Port), never both.
Canonical Syntax
Use inside a @module body:
text
MEM(type=[BLOCK|DISTRIBUTED]) {
<name> [<word_width>] [<depth>] = <init> {
OUT <port_id> [ASYNC | SYNC];
IN <port_id> { WRITE_MODE = <mode>; }; // or: IN <port_id>;
INOUT <port_id>; // combined read/write port
};
...
}typeoptional; compiler may infer when omitted.- Word width and depth are positive integers or module-local
CONSTnames (compile-time). - The init value is a sized literal,
@file("path"),@file(CONST_NAME), or@file(CONFIG.NAME). - Each MEM must include at least one port.
INOUTports cannot be mixed withINorOUTports in the same MEM declaration.
Declaration Rules
- Memory name must be unique in the module.
- Port names must be unique within the MEM and must not clash with module-level identifiers.
- If
type=BLOCKis specified, the compiler verifies constraints (e.g., all OUT ports must beSYNCif required by target). - Address width =
clog2(depth)(compile-time). Minimum address width is 1 bit; a single-word memory (depth=1) uses a 1-bit address (the sole word lives at address1'b0). - Word init literals must not contain
x. - File-based init files must not contain undefined bits.
INOUTports cannot be mixed withINorOUTports in the same MEM declaration.- All port names (
IN,OUT,INOUT) must be distinct within the MEM block.
Port Types
Runtime memory addresses are ordinary runtime bit-vectors. Compile-time constructs such as IDX are elaborated earlier and do not use these runtime width rules.
OUT port_id ASYNC- Asynchronous read: combinational address → data path.
- Used in
ASYNCHRONOUSlogic (=/=>/<=as allowed). - Access syntax:
mem.port[address_expr](RHS). address_exprmust have exact address width.
OUT port_id SYNC- Synchronous read: address sampled on clock; data available on a latched read output next cycle.
- Exposes two pseudo-fields:
.addrand.data. .addris assigned inSYNCHRONOUSblocks with<=..addrmust have exact address width; use<=zfor narrower values or an explicit slice for wider values..datais the read result, valid the cycle after.addris sampled..datais readable in any block (ASYNCHRONOUS or SYNCHRONOUS).mem.port[addr]indexing is illegal for SYNC ports (use.addr/.datainstead)..addrmay be assigned at most once per execution path.
IN port_id- Write port(s) are always synchronous: address and data sampled at clock.
- Access in
SYNCHRONOUSblocks:mem.port[address_expr] <= data_expr; address_exprmust have exact address width.- Bare
<=requiresdata_exprwidth to equalword_width; use<=zfor narrower values and slice wider values explicitly. - Each
INport may be written at most once perSYNCHRONOUSblock (per Exclusive Assignment Rule).
INOUT port_id- Combined read/write port with shared address.
- Always synchronous (no
ASYNC/SYNCkeyword;SYNCis implicit). - Exposes three pseudo-fields:
.addr— address input, set via<=inSYNCHRONOUSblocks.data— read data output (1 cycle latency), readable in any block.wdata— write data input, assigned via<=inSYNCHRONOUSblocks
- If
.wdatais not assigned in a given execution path, no write occurs (read-only cycle). .addrand.wdatamay each be assigned at most once per execution path..addrmust have exact address width. Bare<=to.wdatarequires exactword_width; use<=zfor narrower values and slice wider values explicitly.mem.port[addr]indexing syntax is illegal onINOUTports (must use.addr).- Write modes (
WRITE_FIRST,READ_FIRST,NO_CHANGE) apply toINOUTports.
Access Syntax and Context
Asynchronous read (ASYNCHRONOUS blocks):
dst = mem.port[index];indexwidth must exactly match the derived address width.dstwidth must equalword_width.
Synchronous read via SYNC OUT (SYNCHRONOUS blocks):
mem.port.addr <= index;schedules the address to be sampled.reg_or_net <= mem.port.data;reads the registered output.- The
.dataoutput is valid one cycle after.addris sampled.
Synchronous write via IN (SYNCHRONOUS blocks):
mem.port[index] <= data;indexmust have exact address width.- Bare
<=requiresdatato have exactword_width; use<=zfor narrower values and slice wider values explicitly. - Conditional writes via
IF/SELECTare allowed (must obey Exclusive Assignment Rule).
INOUT access (SYNCHRONOUS blocks):
mem.port.addr <= index;sets the shared address.reg_or_net <= mem.port.data;reads the data (1 cycle latency).mem.port.wdata <= data;writes data at the current address.- If
.wdatais not assigned, no write occurs (read-only cycle).
General width behavior:
- Runtime addresses must match the derived address width exactly.
- Bare
<=writes require exactword_widthdata. - Use
<=zwhen you want explicit zero-extension for narrower synchronous addresses or write data. - Use explicit slices when an address or data expression is wider than required.
Read / Write Semantics
- Reads and writes are independent ports; a memory may have multiple OUT and IN ports.
- When a read and a write target the same address in the same cycle, the observed read value depends on the corresponding write port’s
WRITE_MODE:WRITE_FIRST(default): read returns the newly written data.READ_FIRST: read returns the old data (pre-write).NO_CHANGE: read retains its previous output value for that cycle.
- On subsequent cycles the stored word equals the write data regardless of mode.
Write Modes
You may declare write mode per IN or INOUT port:
Shorthand:
text
IN wr; // default WRITE_FIRST
IN wr WRITE_FIRST;
IN wr READ_FIRST;
IN wr NO_CHANGE;
INOUT rw; // default WRITE_FIRST
INOUT rw WRITE_FIRST;
INOUT rw READ_FIRST;
INOUT rw NO_CHANGE;Attribute form:
text
IN wr {
WRITE_MODE = READ_FIRST;
};
INOUT rw {
WRITE_MODE = READ_FIRST;
};Meaning:
- WRITE_FIRST — newly written data visible on same-cycle reads.
- READ_FIRST — old data visible on same-cycle reads.
- NO_CHANGE — read output unchanged for that cycle.
For INOUT ports, write mode controls what .data shows when .wdata is assigned in the same cycle.
Initialization
- Literal initialization: the init value is a sized literal (must not contain
x) and applies to all words.- Example:
mem [8] [256] = 8'h00 { ... }
- Example:
- File-based initialization:
= @file("path"),= @file(CONST_NAME), or= @file(CONFIG.NAME)- The path argument may be a literal string, a module-local string CONST, or a project-level string CONFIG reference.
- Using a numeric CONST/CONFIG where a string path is expected is a compile error (
CONST_NUMERIC_IN_STRING_CONTEXT). - Supported formats:
.bin,.hex(Intel HEX),.mif,.coe,.mem(Verilog memory format with0/1and//comments), and tool-specific formats. - File size must be ≤
depth × word_widthbits. Smaller files are zero-padded. Larger files → compile error. - Files must not encode unknown bits; any undefined bits cause a compile error.
- Initialization evaluated at compile time.
Derived Address Width and Bounds
- Address width W =
clog2(depth)(compile-time). - Minimum address width is 1 bit; a single-word memory (depth=1) uses a 1-bit address (the sole word lives at address
1'b0). - If an address expression is statically provable ≥
depth→ compile-time error. - Runtime addresses still follow the exact-width rules above; if you need widening or truncation, make it explicit in source.
Constraints & Rules Summary
- MEM must have at least one declared port.
- OUT ASYNC reads may not be used in SYNCHRONOUS blocks as RHS without appropriate
<=semantics. - IN ports may only be written in SYNCHRONOUS blocks. Writing in ASYNCHRONOUS → compile error.
- Each
INport: at most one write perSYNCHRONOUSblock (per-path exclusivity applies). - Each MEM port name unique per MEM and distinct from module identifiers.
- Literal init must not contain
x. - CONST names in word_width/depth resolved in module
CONSTscope; must be resolvable at compile time.
Examples
Simple ROM (async read)
text
MEM {
sine_lut [8] [256] = @file("sine_table.hex") {
OUT addr ASYNC;
};
}
ASYNCHRONOUS {
data = sine_lut.addr[index];
}Synchronous register-file style
text
MEM {
regfile [32] [32] = 32'h0000_0000 {
OUT rd_a ASYNC;
OUT rd_b ASYNC;
IN wr;
};
}
ASYNCHRONOUS {
rd_a_out = regfile.rd_a[rd_addr_a];
rd_b_out = regfile.rd_b[rd_addr_b];
}
SYNCHRONOUS(CLK=clk) {
IF (wr_en) {
regfile.wr[wr_addr] <= wr_data;
}
}Synchronous read (registered output)
text
MEM {
cache [32] [1024] = 32'h0000_0000 {
OUT rd SYNC;
};
}
SYNCHRONOUS(CLK=clk) {
cache.rd.addr <= addr;
read_data <= cache.rd.data;
}Dual-write prohibition (illegal)
text
SYNCHRONOUS(CLK=clk) {
mem.wr[a] <= x;
mem.wr[b] <= y; // ERROR: same IN port written twice in the same block
}Single Port Memory (INOUT)
text
MEM(TYPE=BLOCK) {
mem [16] [256] = 16'h0000 {
INOUT rw;
};
}
SYNCHRONOUS(CLK=clk) {
mem.rw.addr <= addr;
rd_data <= mem.rw.data;
IF (wr_en) {
mem.rw.wdata <= wr_data;
}
}True Dual Port Memory (2× INOUT)
text
MEM(TYPE=BLOCK) {
mem [16] [256] = 16'h0000 {
INOUT port_a;
INOUT port_b;
};
}
SYNCHRONOUS(CLK=clk) {
mem.port_a.addr <= addr_a;
rd_data_a <= mem.port_a.data;
IF (wr_en_a) {
mem.port_a.wdata <= wr_data_a;
}
mem.port_b.addr <= addr_b;
rd_data_b <= mem.port_b.data;
IF (wr_en_b) {
mem.port_b.wdata <= wr_data_b;
}
}Note: For True Dual Port memories, write behavior when both ports write to the same address in the same cycle is undefined (hardware-dependent).
Synchronous FIFO
text
@module sync_fifo_8x32
CONST {
WIDTH = 8;
DEPTH = 32;
ADDR_WIDTH = 5;
}
PORT {
IN [8] din;
OUT [8] dout;
IN [1] wr_en;
IN [1] rd_en;
IN [1] clk;
OUT [1] full;
OUT [1] empty;
}
REGISTER {
wr_ptr [ADDR_WIDTH + 1] = {(ADDR_WIDTH + 1){1'b0}};
rd_ptr [ADDR_WIDTH + 1] = {(ADDR_WIDTH + 1){1'b0}};
}
MEM {
fifo_mem [8] [32] = 8'h00 {
OUT rd ASYNC;
IN wr;
};
}
ASYNCHRONOUS {
full = (wr_ptr[ADDR_WIDTH] != rd_ptr[ADDR_WIDTH]) &
(wr_ptr[ADDR_WIDTH - 1 : 0] == rd_ptr[ADDR_WIDTH - 1 : 0]);
empty = (wr_ptr == rd_ptr) ? 1'b1 : 1'b0;
dout = fifo_mem.rd[rd_ptr[ADDR_WIDTH - 1 : 0]];
}
SYNCHRONOUS(CLK=clk) {
IF (wr_en & ~full) {
fifo_mem.wr[wr_ptr[ADDR_WIDTH - 1 : 0]] <= din;
wr_ptr <= wr_ptr + 1;
}
IF (rd_en & ~empty) {
rd_ptr <= rd_ptr + 1;
}
}
@endmodRegistered Read Cache (Semi-Dual Port with SYNC read)
text
@module l1_cache_64x256
CONST {
LINE_WIDTH = 64;
NUM_LINES = 256;
}
PORT {
IN [8] read_addr;
OUT [64] read_data;
IN [8] write_addr;
IN [64] write_data;
IN [1] write_en;
IN [1] clk;
}
MEM {
cache_mem [64] [256] = 64'h0000_0000_0000_0000 {
OUT rd SYNC;
IN wr;
};
}
SYNCHRONOUS(CLK=clk) {
cache_mem.rd.addr <= read_addr;
read_data <= cache_mem.rd.data;
IF (write_en) {
cache_mem.wr[write_addr] <= write_data;
}
}
@endmodTriple-Port Memory (2 Read, 1 Write)
text
@module triple_port_mem_32x256
PORT {
IN [8] rd_addr_0;
IN [8] rd_addr_1;
OUT [32] rd_data_0;
OUT [32] rd_data_1;
IN [8] wr_addr;
IN [32] wr_data;
IN [1] wr_en;
IN [1] clk;
}
MEM {
mem [32] [256] = 32'h0000_0000 {
OUT rd_0 ASYNC;
OUT rd_1 ASYNC;
IN wr;
};
}
ASYNCHRONOUS {
rd_data_0 = mem.rd_0[rd_addr_0];
rd_data_1 = mem.rd_1[rd_addr_1];
}
SYNCHRONOUS(CLK=clk) {
IF (wr_en) {
mem.wr[wr_addr] <= wr_data;
}
}
@endmodQuad-Port Memory (2 Read, 2 Write)
text
@module quad_port_mem_16x128
PORT {
IN [7] rd_addr_0;
IN [7] rd_addr_1;
OUT [16] rd_data_0;
OUT [16] rd_data_1;
IN [7] wr_addr_0;
IN [7] wr_addr_1;
IN [16] wr_data_0;
IN [16] wr_data_1;
IN [1] wr_en_0;
IN [1] wr_en_1;
IN [1] clk;
}
MEM {
mem [16] [128] = 16'h0000 {
OUT rd_0 ASYNC;
OUT rd_1 ASYNC;
IN wr_0;
IN wr_1;
};
}
ASYNCHRONOUS {
rd_data_0 = mem.rd_0[rd_addr_0];
rd_data_1 = mem.rd_1[rd_addr_1];
}
SYNCHRONOUS(CLK=clk) {
IF (wr_en_0) {
mem.wr_0[wr_addr_0] <= wr_data_0;
}
IF (wr_en_1) {
mem.wr_1[wr_addr_1] <= wr_data_1;
}
}
@endmodConfigurable Memory with Parameters
text
@module param_mem
CONST {
WORD_WIDTH = 32;
DEPTH = 256;
ADDR_WIDTH = 8;
}
PORT {
IN [ADDR_WIDTH] rd_addr;
IN [ADDR_WIDTH] wr_addr;
IN [WORD_WIDTH] wr_data;
IN [1] wr_en;
OUT [WORD_WIDTH] rd_data;
IN [1] clk;
}
MEM {
storage [WORD_WIDTH] [DEPTH] = {WORD_WIDTH{1'b0}} {
OUT rd SYNC;
IN wr;
};
}
SYNCHRONOUS(CLK=clk) {
storage.rd.addr <= rd_addr;
rd_data <= storage.rd.data;
IF (wr_en) {
storage.wr[wr_addr] <= wr_data;
}
}
@endmodMEM in Module Instantiation
Memories are internal to modules and cannot be directly accessed from parent modules. To expose memory operations, wrap them in a module interface:
text
@module memory_wrapper
PORT {
IN [1] clk;
IN [8] addr;
IN [16] wr_data;
IN [1] wr_en;
OUT [16] rd_data;
}
MEM(TYPE=BLOCK) {
mem [16] [256] = 16'h0000 {
INOUT rw;
};
}
SYNCHRONOUS(CLK=clk) {
mem.rw.addr <= addr;
rd_data <= mem.rw.data;
IF (wr_en) {
mem.rw.wdata <= wr_data;
}
}
@endmodThe parent module instantiates the wrapper and accesses memory through its ports.
CONST Evaluation in MEM
Numeric CONST names may be used in word_width and depth expressions. These are resolved in the module's CONST scope at compile time:
text
CONST {
WIDTH = 32;
DEPTH = 1024;
}
MEM {
mem [WIDTH] [DEPTH] = {WIDTH{1'b0}} {
OUT rd SYNC;
IN wr;
};
}String CONST names may be used in @file() path arguments:
text
CONST {
WIDTH = 32;
DEPTH = 1024;
INIT_FILE = "firmware.bin";
}
MEM {
rom [WIDTH] [DEPTH] = @file(INIT_FILE) {
OUT rd ASYNC;
};
}Project-level string CONFIG references may also be used: @file(CONFIG.FIRMWARE).
When used with OVERRIDE, the overriding module's CONST values apply, allowing the same module to be instantiated with different memory sizes.
Common Errors and Diagnostics
Declaration Errors
- Invalid or duplicate MEM name.
- Invalid
word_widthordepth(≤ 0 or unresolved CONST). - Missing initialization clause.
- Port name duplicates or conflicts with module identifiers.
INOUTports mixed withINorOUTports in the same MEM declaration.INOUTport declared withASYNCkeyword (not supported).
Access Errors
- Asynchronous read used in SYNCHRONOUS incorrectly (forgetting
<=). - Write in ASYNCHRONOUS block.
- Multiple writes to the same
INport in oneSYNCHRONOUSblock (Exclusive Assignment violation). - Address or data width mismatch (too wide) — truncation is not implicit.
- Constant out-of-range address (address literal ≥ depth) — compile error.
- Using
mem.port[addr]indexing syntax onINOUTports (must use.addr). - Assigning
.wdatain ASYNCHRONOUS block. - Assigning
.addrin ASYNCHRONOUS block. - Multiple
.addrassignments to the sameINOUTport per execution path. - Multiple
.wdataassignments to the sameINOUTport per execution path.
- Asynchronous read used in SYNCHRONOUS incorrectly (forgetting
Initialization Errors
- Init literal overflow (literal intrinsic width > declared word width).
- Init file not found or too large for memory depth.
- Init literal or file contains
x/undefined bits. - Numeric CONST/CONFIG used in
@file()path (CONST_NUMERIC_IN_STRING_CONTEXT). - String CONST/CONFIG used in width/depth expression (
CONST_STRING_IN_NUMERIC_CONTEXT).
Behavioral Warnings
- Port declared but never accessed.
- Partial initialization (file smaller than memory) — zero-padding warning.
- Using ASYNC reads in combinational paths that create loops — flow-sensitive loop detection may flag cycles.
Synthesis and Implementation Notes
- Compiler infers FPGA/ASIC memory primitives from MEM declarations.
- ASYNC vs SYNC read ports influence whether the tool implements combinational read paths or registered outputs.
type=BLOCKguides inference toward block RAMs; compiler may validate constraints (e.g., read port timing).
- Write modes map to vendor BRAM settings:
WRITE_FIRST,READ_FIRST,NO_CHANGE→ vendor BRAM write-mode attributes.
- File-based initialization may be passed to the backend (vendor tools) as memory init files.
- For small depths use DISTRIBUTED RAM; for larger depths prefer BLOCK BRAM. If omitted, compiler makes a choice based on
depthand port timing.
Port Type Inference:
ASYNCOUTports → combinational read (data available same cycle)SYNCOUTports → registered read (data available next cycle)INports → synchronous write (captured at clock edge)INOUTports → synchronous read/write with shared address (read data available next cycle)
MEM as a Register Array Replacement
Looking for register arrays?
JZ-HDL does not support multi-dimensional REGISTER syntax (e.g., name [depth] [width]). Use MEM instead — it provides the same functionality with explicit port semantics.
A MEM(type=DISTRIBUTED) with an OUT ASYNC read port is the direct equivalent of a register array in other HDLs. Both synthesize to flip-flops plus a read mux, with identical timing:
- Read latency: zero additional cycles (combinational, same as a register read)
- Write timing: synchronous, captured at the clock edge (same as a register write)
- Synthesis result: LUT-based storage with mux/decoder (same fabric resources)
The only difference is that MEM requires explicit port declarations, which makes the number of read ports, write ports, and their timing (ASYNC vs SYNC) unambiguous in the source code.
Example: 8-entry, 32-bit register file with 2 read ports and 1 write port
text
MEM(type=DISTRIBUTED) {
regfile [32] [8] = 32'h0000_0000 {
OUT rd_a ASYNC;
OUT rd_b ASYNC;
IN wr;
};
}
ASYNCHRONOUS {
read_data_a = regfile.rd_a[rd_addr_a];
read_data_b = regfile.rd_b[rd_addr_b];
}
SYNCHRONOUS(CLK=clk) {
IF (wr_en) {
regfile.wr[wr_addr] <= wr_data;
}
}This is equivalent to what reg [31:0] regfile [0:7] would provide in Verilog, but with the read/write port structure made explicit.
Best Practices
- Always declare
WORD_WIDTHandDEPTHusing numericCONSTwhen parameterizing modules. - Use string
CONSTorCONFIGfor@file()paths to make initialization files configurable across builds. - Prefer explicit
clog2(DEPTH)orADDR_WIDTHconstants for address signals so widths are consistent and self-documenting. - Use synchronous reads (
SYNC) when you need registered, timing-stable outputs or to pipeline memory reads. - Guard dynamic indices when they might be out-of-range or ensure index width covers
clog2(depth). - For register files, prefer ASYNC read ports for zero-latency reads and a single synchronous write port; ensure write-first/read-first behavior matches desired architectural semantics.
- Avoid reading and writing the same address from different ports in the same cycle unless the write mode semantics are explicitly what you require.
Troubleshooting Checklist
- If you see a floating/undefined read value:
- Verify read port and write ports address widths and that at least one driver provides known data.
- Check for mistaken ASYNCHRONOUS write attempts (illegal).
- If synthesis maps memory into many small LUTs:
- Consider changing
typeor increasing depth to encourage BRAM inference or supply vendor-specific attributes.
- Consider changing
- If reads return unexpected values on same-cycle read/write:
- Confirm the
INport’sWRITE_MODEand whether you intended WRITE_FIRST (default) or READ_FIRST.
- Confirm the