Digital Design / Module 7: Memories and FIFOs
40. Using SRAM in RTL
Work with synchronous memory correctly.
See it move
The idea
Large storage uses SRAM macros, generated for a specific size and process rather than written in RTL. An SRAM is far denser than flops, but it behaves differently, and your control logic must allow for that.
The key difference: read latency
A typical synchronous SRAM captures the address on a clock edge, and the data appears after that edge. If you request address A in cycle 1, you get the data in cycle 2. In a flop-based memory with a combinational read, you'd get it in cycle 1.
Any valid signal travelling with the request must be delayed by the same amount, or the control logic will pair data with the wrong request.
always_ff @(posedge clk) begin
rd_valid <= rd_en; // aligned with the SRAM output
end
sram_1p u_mem (
.clk(clk), .cs(rd_en | wr_en), .we(wr_en),
.addr(addr), .wdata(wdata), .rdata(rdata)
);Port types
| Type | Per cycle | Cost |
|---|---|---|
| Single-port | One read or one write | Smallest |
| Two-port | One read and one write | Larger |
| True dual-port | Two accesses, each read or write | Largest |
If a single-port memory is enough, or if you can organise accesses into banks, you can save considerable area.
Wrap your macros
Never instantiate a memory macro directly throughout a design. Put it in a small wrapper with your own interface. Then it can be replaced by a behavioural model in simulation, an FPGA block RAM, or a different size, without touching the rest of the design. The wrapper is also where test (BIST) and power pins connect.
Common mistake
Changing a flop array to SRAM late in a project and assuming only the storage changes. The added read cycle moves through all the control logic. Decide on your memory style early, and always design control logic with the read in mind.
Key ideas
- SRAM reads usually have one cycle of latency.
- Single-port and dual-port differ in cost.
- Macros are generated, so wrap them.
Your exercise
Find the bug
This code has one deliberate bug. Read the symptom, then click the line you think is wrong.
The fix
reg v = 1'b0; always @(posedge clk) v <= rd_en; assign rd_valid = v;
Why it failed
A synchronous memory's output appears a cycle after the address is captured. A valid signal has to travel with its data, so it needs the same one-cycle delay. This is the most common bug when moving from a flop array to an SRAM.
Interview questions
coreWhat changes moving from flops to SRAM?
Reads become registered, so control logic must account for added latency.
basicWhy are large on-chip memories built as SRAM macros rather than arrays of flops?
Density. An SRAM bit is a small cell, typically six transistors, while a flop is many more transistors plus its own clock load. A flop array also needs a read mux per port, which grows with depth. So beyond a few hundred bits, SRAM is far smaller and usually lower in power. The price is a different interface, with a clocked read and fixed port types.
basicWhat is the difference between a single-port, a two-port and a true dual-port SRAM?
A single-port SRAM does one read or one write per cycle, through one address. A two-port SRAM has one read port and one write port, so it can read and write in the same cycle. A true dual-port SRAM has two ports that can each read or write. Area grows in that order, so choose the smallest type that meets the access pattern.
coreA controller reads from a synchronous SRAM and sets rd_valid directly from rd_en. What goes wrong?
assign rd_valid = rd_en; // rdata comes from the SRAM sram_1p u_mem (.clk(clk), .cs(rd_en | wr_en), .we(wr_en), .addr(addr), .wdata(wdata), .rdata(rdata));
The SRAM captures the address at a clock edge and the data appears after it, one cycle later. With rd_valid = rd_en, valid arrives one cycle before its data, so the consumer captures the previous request's data, and the first capture is stale or x. In simulation, a three-read burst was captured as one stale value plus the first two reads, and the third read was missed. The fix is a flop: rd_valid <= rd_en.
coreA block issues 64 back-to-back reads, one address per cycle, to an SRAM with one cycle of read latency. How many cycles from the first address to the last data?
65 cycles. Addresses go in at cycles 1 to 64, and each data word comes out one cycle after its address, at cycles 2 to 65. Because the reads are pipelined, the latency is paid once, not 64 times, and throughput stays at one word per cycle. That is why control logic should keep requests flowing rather than wait for each answer.
coreThis behavioural single-port SRAM model is written to the same address it is read from in one cycle. What does rdata show afterwards, and can you trust that the real macro does the same?
always @(posedge clk) begin if (we) mem[addr] <= wdata; rdata <= mem[addr]; end
With non-blocking assignments, the read uses the value before the edge, so rdata shows the old data. Simulation confirms it: writing 99 to an address that held 22 returned 22 in that cycle, and 99 on the next read. Real macros differ: some return old data, some return new data, and some leave the output undefined on a collision. Check the macro's datasheet, and make the model match it.
coreWhy should every SRAM macro sit inside a wrapper module of your own?
The wrapper gives the rest of the design one stable interface, with your port names and your latency. Behind it, you can swap in a behavioural model for simulation, an FPGA block RAM, or a different-size macro, without touching the rest of the design. It is also the place to connect test logic (BIST), power pins and any output registers. Without it, a memory change spreads through the whole design.
advancedYou need one read and one write every cycle on average, but only single-port SRAM is available. What can you do?
Bank the memory, for example even and odd addresses in two single-port banks, so a read and a write that land in different banks proceed together. When both hit the same bank, one must wait, so add a small write buffer and a rule for the conflict. Another option is a memory twice as wide, accessed every other cycle, reading or writing two words at a time. Either way, the extra control logic is the price of avoiding a costlier two-port macro.
advancedYour SRAM's rdata still shows the last word read, during cycles with no read. Is it safe for downstream logic to use rdata in those cycles?
It is not a good idea. Whether a macro holds its output between reads is a property of that macro, and a different macro, or an FPGA RAM, may behave differently. The safe pattern is to use rdata only in the cycle marked by rd_valid, and to capture it into your own register if it is needed later. Then the design depends only on the one-cycle latency promise.