True Dual-Port RAM in Verilog: Two R/W Ports with Code
A true dual-port (TDP) RAM is a memory block with two completely independent access ports. Each port has its own clock, address, data-in, write-enable and data-out, and each port can both read and write. This is the key difference from a simple dual-port RAM, where one port is read-only and the other write-only.
This page gives you working, simulator-verified true dual port RAM Verilog code: a parameterized TDP RAM with registered reads, plus a second version that lets you pick read-first or write-first behavior on the read port. Both come with self-checking testbenches so you can see exactly what each port reads back and how a same-address collision behaves.
A parameterized true dual-port RAM. Both ports (A and B) have their own clock, address, data and write-enable, both can read and write, and reads are registered (one-cycle latency, read-old-on-write). The testbench writes different data through each port, reads it back on both, and performs cross-reads where each port sees data written by the other.
module tdp_ram #(
parameter DW = 8,
parameter AW = 4
)(
input clka,
input [AW-1:0] addra,
input [DW-1:0] dina,
input wea,
output reg [DW-1:0] douta,
input clkb,
input [AW-1:0] addrb,
input [DW-1:0] dinb,
input web,
output reg [DW-1:0] doutb
);
reg [DW-1:0] mem [0:(1<<AW)-1];
// Port A: independent clock, read-first (registered read returns old data)
always @(posedge clka) begin
if (wea) mem[addra] <= dina;
douta <= mem[addra];
end
// Port B: fully independent clock, address, data and write-enable
always @(posedge clkb) begin
if (web) mem[addrb] <= dinb;
doutb <= mem[addrb];
end
endmodule
module tb;
localparam DW = 8;
localparam AW = 4;
reg clka, clkb;
reg [AW-1:0] addra, addrb;
reg [DW-1:0] dina, dinb;
reg wea, web;
wire [DW-1:0] douta, doutb;
integer errors;
tdp_ram #(.DW(DW), .AW(AW)) dut (
.clka(clka), .addra(addra), .dina(dina), .wea(wea), .douta(douta),
.clkb(clkb), .addrb(addrb), .dinb(dinb), .web(web), .doutb(doutb)
);
// Two independent (asynchronous) clocks
initial clka = 0; always #5 clka = ~clka; // 100 MHz
initial clkb = 0; always #7 clkb = ~clkb; // ~71 MHz
initial begin
errors = 0;
wea = 0; web = 0; addra = 0; addrb = 0; dina = 0; dinb = 0;
// --- Port A writes 0xA5 @ addr 3 (drive on negedge, hold across posedge) ---
@(negedge clka); addra = 4'd3; dina = 8'hA5; wea = 1;
@(negedge clka); wea = 0;
// --- Port B writes 0x3C @ addr 9 ---
@(negedge clkb); addrb = 4'd9; dinb = 8'h3C; web = 1;
@(negedge clkb); web = 0;
// --- Port A reads back addr 3 ---
@(negedge clka); addra = 4'd3; wea = 0;
@(posedge clka); #1;
if (douta !== 8'hA5) begin
errors = errors + 1; $display("FAIL portA read addr3 got %02h exp A5", douta);
end else $display("PASS portA read addr3 = %02h", douta);
// --- Port B reads back addr 9 ---
@(negedge clkb); addrb = 4'd9; web = 0;
@(posedge clkb); #1;
if (doutb !== 8'h3C) begin
errors = errors + 1; $display("FAIL portB read addr9 got %02h exp 3C", doutb);
end else $display("PASS portB read addr9 = %02h", doutb);
// --- Cross reads: each port sees data written by the OTHER port ---
@(negedge clkb); addrb = 4'd3; web = 0;
@(posedge clkb); #1;
if (doutb !== 8'hA5) begin
errors = errors + 1; $display("FAIL portB cross-read addr3 got %02h exp A5", doutb);
end else $display("PASS portB cross-read addr3 = %02h", doutb);
@(negedge clka); addra = 4'd9; wea = 0;
@(posedge clka); #1;
if (douta !== 8'h3C) begin
errors = errors + 1; $display("FAIL portA cross-read addr9 got %02h exp 3C", douta);
end else $display("PASS portA cross-read addr9 = %02h", douta);
if (errors == 0) $display("ALL TESTS PASSED");
else $display("TESTS FAILED: %0d", errors);
$finish;
end
endmodule
PASS portA read addr3 = a5 PASS portB read addr9 = 3c PASS portB cross-read addr3 = a5 PASS portA cross-read addr9 = 3c ALL TESTS PASSEDThe log shows port A reading back 0xA5 (which it wrote to address 3) and port B reading back 0x3C (which it wrote to address 9), then each port reading the other port's data at the shared memory array. Because the two clocks run at 5 ns and 7 ns half-periods, the ports are genuinely asynchronous, confirming the single
memarray is shared across two independent clock domains.The same TDP RAM with a
WRITE_FIRSTparameter that selects the read-during-write behavior. WithWRITE_FIRST=0the read port returns the old stored value (read-first); withWRITE_FIRST=1it returns the value just written (write-first). The testbench instantiates both modes side by side and drives an identical write-and-read-same-address sequence so the only difference in the outputs is the mode.module tdp_ram_mode #( parameter DW = 8, parameter AW = 4, parameter WRITE_FIRST = 0 // 0 = read-first (read-old), 1 = write-first (read-new) )( input clka, input [AW-1:0] addra, input [DW-1:0] dina, input wea, output reg [DW-1:0] douta, input clkb, input [AW-1:0] addrb, input [DW-1:0] dinb, input web, output reg [DW-1:0] doutb ); reg [DW-1:0] mem [0:(1<<AW)-1]; always @(posedge clka) begin if (WRITE_FIRST) begin if (wea) begin mem[addra] <= dina; douta <= dina; end // read-new else douta <= mem[addra]; end else begin if (wea) mem[addra] <= dina; douta <= mem[addra]; // read-old end end always @(posedge clkb) begin if (WRITE_FIRST) begin if (web) begin mem[addrb] <= dinb; doutb <= dinb; end else doutb <= mem[addrb]; end else begin if (web) mem[addrb] <= dinb; doutb <= mem[addrb]; end end endmodule module tb; localparam DW = 8; localparam AW = 4; reg clk; reg [AW-1:0] addra_r, addrb_r; reg [DW-1:0] dina_r, dinb_r; reg wea_r, web_r; wire [DW-1:0] douta_rf, doutb_rf; // read-first instance wire [DW-1:0] douta_wf, doutb_wf; // write-first instance integer errors; // Both instances driven by the same stimulus so outputs differ only by mode tdp_ram_mode #(.DW(DW), .AW(AW), .WRITE_FIRST(0)) rf ( .clka(clk), .addra(addra_r), .dina(dina_r), .wea(wea_r), .douta(douta_rf), .clkb(clk), .addrb(addrb_r), .dinb(dinb_r), .web(web_r), .doutb(doutb_rf) ); tdp_ram_mode #(.DW(DW), .AW(AW), .WRITE_FIRST(1)) wf ( .clka(clk), .addra(addra_r), .dina(dina_r), .wea(wea_r), .douta(douta_wf), .clkb(clk), .addrb(addrb_r), .dinb(dinb_r), .web(web_r), .doutb(doutb_wf) ); initial clk = 0; always #5 clk = ~clk; initial begin errors = 0; wea_r = 0; web_r = 0; addra_r = 0; addrb_r = 0; dina_r = 0; dinb_r = 0; // Preload addr 5 with 0x11 via port A @(negedge clk); addra_r = 4'd5; dina_r = 8'h11; wea_r = 1; @(negedge clk); wea_r = 0; // Collision cycle: write 0x22 to addr 5 AND read addr 5 on the same edge @(negedge clk); addra_r = 4'd5; dina_r = 8'h22; wea_r = 1; @(posedge clk); #1; // sample the output registered on THIS edge if (douta_rf !== 8'h11) begin errors = errors + 1; $display("FAIL read-first expected OLD 11 got %02h", douta_rf); end else $display("PASS read-first returned OLD data = %02h", douta_rf); if (douta_wf !== 8'h22) begin errors = errors + 1; $display("FAIL write-first expected NEW 22 got %02h", douta_wf); end else $display("PASS write-first returned NEW data = %02h", douta_wf); @(negedge clk); wea_r = 0; // Both memories must now hold 0x22 (clean registered read via port B) @(negedge clk); addrb_r = 4'd5; web_r = 0; @(posedge clk); #1; if (doutb_rf !== 8'h22 || doutb_wf !== 8'h22) begin errors = errors + 1; $display("FAIL post-write mem rf=%02h wf=%02h exp 22", doutb_rf, doutb_wf); end else $display("PASS memory holds updated value 22 on both instances"); if (errors == 0) $display("ALL TESTS PASSED"); else $display("TESTS FAILED: %0d", errors); $finish; end endmodulePASS read-first returned OLD data = 11 PASS write-first returned NEW data = 22 PASS memory holds updated value 22 on both instances ALL TESTS PASSEDOn the collision cycle the read-first instance outputs the old data 0x11 while the write-first instance outputs the freshly written 0x22, proving the two behaviors diverge exactly as intended. A following clean read on port B shows both memories now hold 0x22, confirming the write landed in storage in both modes and only the read-out timing differs.
Simple dual-port vs true dual-port RAM
These two are often confused, but they are different hardware primitives:
- Simple dual-port (SDP): one write-only port and one read-only port. Good for FIFOs, line buffers and streaming where producer and consumer are distinct.
- True dual-port (TDP): two full read/write ports. Either port may read or write any address on any cycle. Needed when two masters (two CPUs, a CPU and a DMA engine, two pipeline stages) both need to modify shared memory.
A TDP RAM costs more silicon than an SDP RAM because every bit cell needs two independent access paths, but it is strictly more flexible.
Independent (dual-clock) ports
In a true dual-port RAM each port has its own clock. Example 1 drives
clkaandclkbat different frequencies on purpose to show the ports are asynchronous. The singlememarray is shared, so a value written by one clock domain becomes readable from the other after it has been committed.When the two clocks are asynchronous, crossing data between them is a clock-domain-crossing (CDC) problem. The RAM array itself is safe to share, but you must not assume a value written on port A is visible on port B on a specific port-B edge unless you add your own synchronization/handshake. Treat the address/control buses of each port as belonging solely to that port's clock.
The same-address write collision hazard
The dangerous case in a TDP RAM is when both ports write the same address on the same (aligned) edge, or one writes while the other reads the same address. The outcomes:
- Write/write to the same address: the stored result is undefined. Hardware block RAMs give no guarantee which port wins; the location may even become corrupted (X). You must prevent this with arbitration or address partitioning.
- Write on one port, read on the other port, same address: the reading port may see the old data, the new data, or (on real block RAM) X, depending on the device and timing. This is the cross-port read-during-write hazard and is also generally undefined.
- Write and read on the same port, same address: this is well-defined and controlled by the read-first / write-first choice shown in Example 2.
Golden rule: never rely on the data result of a simultaneous same-address collision between the two ports. Design so it cannot happen.
Read-first vs write-first
Read-during-write on a single port has two standard, synthesizable behaviors:
Read-first (read-old)
if (we) mem[addr] <= din; dout <= mem[addr]; // sees the OLD contentsBecause both assignments are non-blocking, the read samples
mem[addr]before the write updates it, sodoutgets the previous value.Write-first (read-new / write-through)
if (we) begin mem[addr] <= din; dout <= din; end else dout <= mem[addr];Here the data being written is forwarded directly to the output, so the port reads back exactly what it just wrote. FPGA vendors expose this as a RAM attribute (for example WRITE_FIRST / READ_FIRST / NO_CHANGE). Match the HDL to the mode you want or the tool may infer extra bypass logic.
Mapping to FPGA block RAM
To get this inferred as a hardware block RAM (BRAM) rather than registers:
- Use a single shared 2-D
regarray indexed by both ports, exactly as shown.- Register the read outputs (the
dout <=inside the clocked block). Unregistered combinational reads usually prevent BRAM inference and force distributed RAM.- Keep each port's logic in its own
always @(posedge clk)block so the tool recognizes two true ports.- Avoid reset on the RAM contents and avoid byte-enable logic the target BRAM cannot support, or the array may fall back to LUTRAM/flip-flops.
On Xilinx a TDP RAM maps to a block RAM primitive in TDP mode; on Intel/Altera it maps to an M-block in bidirectional dual-port mode. Both require registered outputs and one write-enable per port, which is precisely the structure in the examples above.
FAQ
Can both ports write at the same time?
Yes, as long as they target different addresses. Simultaneous writes to the same address are undefined and must be avoided by design.
Why is the read registered?
Registering the read output is what lets the tool map the memory to a dedicated block RAM and meet timing. It adds one cycle of read latency, which the testbenches account for.
Do the two ports need the same clock?
No. A true dual-port RAM supports two independent clocks. If they are asynchronous, add proper clock-domain-crossing handling around the RAM; the array itself can be shared.
Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.▶ Try it in the ChipXpert LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
Learn Verilog with mentors, labs and placement support
This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.
Explore VLSI Courses Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres