55 / 63 · Concept
Synchronous memory and read/write collision contracts
Distinguish registered-read response timing and read-first, write-first, and no-change behavior, and define the separate roles of valid and address initialization.
Lessons are free to read. Enroll to save your learning progress.
Decide the response timing before the address
Connecting a memory requires a contract for read latency and collisions as well as capacity. Here we build a synchronous memory with 16 addresses, each storing 8 bits, that can accept one read and one write at a rising edge. Both requests share a clock but have independent addresses. Capacity is 16×8=128 bits, and the address width is bits.
If the read address and rd_en are stable before a rising edge, the registered rd_data and rd_valid appear after that edge. This is a single registered-read stage of latency. Another always_ff reading these outputs at the same edge sees their values before the update, so a response-consuming registerRegister A circuit that stores multiple bits of state. The synchronous registers in this course store their specified inputs at a clock edge. Learn more captures them at the next rising edge. Mark request timing and response observation separately on the waveform. A new request can be accepted every clock, so one stage of latency does not imply a throughputThroughput The number of operations accepted or completed per unit time. A pipeline can increase throughput without reducing the latency of an individual operation. Learn more of 1/2.
Reading and writing the same address
Suppose M[3]=8'h25 just before an edge, and we read address 3 while writing 8'h9A to it. Read-first responds with 25 and changes the stored value to 9A. Write-first responds with the new value, 9A. No-change holds the output register, so there is no guarantee that its contents are the old value at address 3. These are three different contracts for normal operation. AMD's RAM synchronization mode documentation also distinguishes these modes.
This example uses read-first. Let denote the array immediately before the edge and the array after it. At a non-reset edge, for any address :
Even on a collision, the read uses , not . With no read request, rd_data holds its value.
module sync_mem16x8 (
input logic clk, rst,
input logic wr_en, rd_en,
input logic [3:0] wr_addr, rd_addr,
input logic [7:0] wr_data,
output logic [7:0] rd_data,
output logic rd_valid
);
timeunit 1ns;
timeprecision 1ps;
logic [7:0] mem [0:15];
always_ff @(posedge clk) begin
if (rst) begin
rd_valid <= 1'b0;
end else begin
rd_valid <= rd_en;
if (wr_en)
mem[wr_addr] <= wr_data;
if (rd_en)
rd_data <= mem[rd_addr];
end
end
endmoduleThe right-hand sides of both assignments use values before the update, and the nonblockingNonblocking assignment An assignment written as <= in sequential RTL. It evaluates the right-hand side and schedules the update, allowing registers at the same edge to compute from the previous state. Learn more assignments take effect afterward. Writing the write statement before the read statement therefore does not change the behavior to reading the new value. To request write-first explicitly, replace the read expression with (wr_en && wr_addr==rd_addr) ? wr_data : mem[rd_addr]. The read output register remains in this bypass path.
Valid and initialization make different promises
rst is an active-high synchronous reset. If rst=1 at a rising edge, both requests are ignored and only rd_valid is cleared to 0. The array and rd_data are not filled with zeros. This contract does not define the contents of an address that has never been written since power-up. A simulator may show X, and real hardware cannot be assumed to contain a particular number either.
rd_valid=1 means “this is the response cycle for a read request,” not “the address has been initialized and its value is known.” The user must read only previously written addresses or maintain a separate initialization table for each address. Even if an address is read using read-first on the same edge as its first write, its previous contents remain undefined. A valid guard is also needed to prevent calculations from using rd_data when valid=0. Restricting reads to initialized addresses and guarding data with valid are separate requirements; neither replaces the other.
Because reset does not clear the storage, previously written values remain in the RTL. However, if the system discards previous contents after reset, its external initialization table must be managed accordingly. Erasing sensitive information is a separate design requirement from resetting control state.
Each column records one rising edgeRising edge The instant at which the clock changes from 0 to 1. Distinguish it from a level, which refers to the entire interval during which CLK=1. Learn more. Inputs are sampled just before the edge; states whose names include “after” are shown immediately after the update. Column alignment indicates sample order, not physical propagation delayPropagation delay The time from an input change until the output settles to the correct value. Logical equivalence and timing behavior are separate properties. Learn more. All read and write addresses are 3. E0 first writes 25; E1 overwrites it with 9A while reading. A dash in data means an undefined value, and a bus with valid=0 is not counted as a response. Data values are hexadecimal.
View waveform data
| Signal | Wave | Bus values |
|---|---|---|
| edge | 2345 | E0 → E1 → E2 → E3 |
| wr_en | 1.0. | |
| wr_data | 23.. | 25 → 9A |
| rd_en | 01.0 | |
| M[3] after | 23.. | 25 → 9A |
| rd_data after | 234. | - → 25 → 9A |
| rd_valid after | 01.0 |
Trace four edges by hand
At E0, write 25 to address 3 with rd_en=0. At E1, write 9A to address 3 and read the same address: after E1, valid=1 and data=25. At E2, read address 3 again without writing: data=9A. With rd_en=0 at E3, valid=0 and data remains 9A. Counting a new response just because the E3 output bus contains 9A would process the same data twice.
When addresses differ, reading and writing are independent. For example, reading address 7 while writing address 3 returns the existing value at address 7. Check the synthesis report and device documentation to determine which FPGA RAM resource this RTL maps to and how that resource handles same-address collisions. Defining RTL semantics and verifying the implementation on a specific device are separate steps.
Try it yourself
M[6]=8'h10 is already stored. At E0, read address 6 while writing 8'h20 to it; at E1, read it again without a write; at E2, set rd_en=0. Find data/valid after each edge for read-first. What changes with write-first? Does a read-first collision on the first write to an address return a defined value?
Read the explanation
After E0, data=10 and valid=1, and the array has been updated to 20. After E1, data=20 and valid=1. After E2, data stays 20, but valid=0, so it is not counted as a response. With write-first, only the E0 data changes, to 20. A read-first read coinciding with the first write returns the uninitialized contents from before the write, so no particular value is a valid expected answer. valid=1 does not guarantee initialization.