1. Verilog RAM - Single Port Async Design
A Single Port asynchronous RAM is the simplest memory you can write in Verilog — one bidirectional data port, address, and a pair of control signals. It's also a useful teaching design because it forces you to confront tristate buses, race conditions, and the gap between simulatable RTL and synthesizable, FPGA-friendly RTL. This post walks the design, shows the timing, and ends with where async RAM is the wrong choice (most modern silicon).
Where async RAM fits — and where it doesn't
| Async RAM | Synchronous RAM | |
|---|---|---|
| Read latency | Combinational (data appears as soon as o_en is high) | One clock cycle |
| Timing closure | Hard — read path is combinational across the array | Easy — read is registered |
| FPGA block RAM | Not natively supported — synthesizes to LUTs (huge area) | Native; what BRAM is built for |
| ASIC SRAM compilers | Rare — most generate sync-only macros | Standard |
| Use cases today | Legacy interfaces, small register files, behavioral models | Almost everything |
Modern designs use synchronous RAM. Async is what you'll find in a textbook, a 1990s-era IP block, or as a behavioral abstraction inside a verification model. Knowing the design is still valuable — many small register banks are async-style under the hood, and understanding the tristate semantics is essential when you read older code.
Features of this design
- Parameterized: WIDTH, DEPTH, and ADDR width are all
parameters - Asynchronous: no clock — reads and writes complete in zero simulation time
- Bidirectional data port: the same
databus serves both read (driven by RAM) and write (driven by external master) - Async reset: clears all locations to zero on reset assertion
Port description
| Port | Direction | Description |
|---|---|---|
data | inout | Bidirectional data bus [WIDTH-1:0] |
addr | input | Address bus [ADDR-1:0] |
reset | input | Asynchronous reset (active high) |
wr_en | input | Write enable |
o_en | input | Output enable (read) |
RTL Implementation
// Module: sp_ram.v
// Parameterized Single Port Asynchronous RAM
module sp_ram #(
parameter WIDTH = 8,
parameter DEPTH = 16,
parameter ADDR = 4
)(
inout [WIDTH-1:0] data,
input [ADDR-1:0] addr,
input reset,
input wr_en,
input o_en
);
// Memory array
reg [WIDTH-1:0] mem [DEPTH-1:0];
integer i;
// Read: drive data when o_en asserted and not writing
assign data = (o_en && !wr_en) ? mem[addr] : {WIDTH{1'bz}};
// Write / Reset (async, level-sensitive)
always @(reset, data, addr, wr_en, o_en) begin
if (reset) begin
for (i = 0; i < DEPTH; i = i + 1)
mem[i] = 0;
end
else if (wr_en && !o_en) begin
mem[addr] = data;
end
end
endmodule
Operation modes
| wr_en | o_en | Operation |
|---|---|---|
| 0 | 0 | Idle (data = Hi-Z) |
| 0 | 1 | Read from mem[addr] |
| 1 | 0 | Write to mem[addr] |
| 1 | 1 | Invalid (avoid — bus contention) |
Read/write timing
Race conditions to be aware of
- Write-then-read same address. Because the array update is in a level-sensitive
always, simulating a write immediately followed by a read at the same address can return the new or the old value depending on event scheduling. Use blocking assignments inside the always block (as shown), and put a small delay between write and read in your testbench. - wr_en and o_en both high. The bus is driven by both the master and the RAM — contention. The truth table marks this combination as Invalid; production code should add an assertion to catch it:
// synthesis_off,assert (!(wr_en && o_en)); - Hi-Z propagation. The read path drives
zwhen neither read nor write is active. Downstream logic that doesn't tolerateX/Zmust either tristate or register the bus. - Sensitivity list completeness. The
always @(reset, data, addr, wr_en, o_en)needs all signals it reads. Modern style: usealways @*and let the simulator infer.
Sketch of a self-checking testbench
module sp_ram_tb;
parameter WIDTH = 8;
parameter ADDR = 4;
reg [ADDR-1:0] addr;
reg reset, wr_en, o_en;
reg [WIDTH-1:0] wdata;
wire [WIDTH-1:0] data;
// Drive bus during writes, release during reads/idle
assign data = (wr_en && !o_en) ? wdata : {WIDTH{1'bz}};
sp_ram #(.WIDTH(WIDTH), .ADDR(ADDR)) dut(.*);
initial begin
reset = 1; #5 reset = 0;
// Write 0x55 to addr 0
wr_en = 1; o_en = 0; addr = 4'h0; wdata = 8'h55; #2;
wr_en = 0; #1;
// Read back and check
o_en = 1; #1;
if (data !== 8'h55) $error("mem[0] expected 0x55, got %h", data);
o_en = 0;
$display("Test passed");
$finish;
end
endmodule
This is the bare minimum self-check. For real verification, run a constrained-random sequence: pick random addresses, write random data, build a reference model, and compare on every read.
Next: Part 2 — Single Port RAM Testbench (full directed + random flow)
Comments (0)
Leave a Comment