Block RAM, ROM, and FIFOs
FPGAs contain dedicated memory resources. Good designs use those resources deliberately instead of building every storage structure from flip-flops. Memory choice affects area, timing, latency, throughput, and CDC safety.
Learning Objectives
You will learn to:
- distinguish registers, distributed RAM, block RAM, ROM, and FIFOs;
- infer simple synchronous RAM and ROM in HDL;
- account for one-cycle memory read latency;
- choose FIFO depth and flags for rate matching;
- recognize when asynchronous FIFO design needs proven IP or careful review.
FPGA Memory Types
| Type | Built from | Best for | Main caution |
|---|---|---|---|
| Registers | Flip-flops | tiny state, counters, valid bits | expensive for large buffers |
| Distributed RAM | LUT fabric | small tables, shallow queues | consumes LUTs |
| Block RAM | dedicated RAM blocks | buffers, lookup tables, frame data | usually synchronous read |
| ROM | initialized LUT RAM or BRAM | constants, fonts, sine tables | initialization format is tool-specific |
| FIFO | RAM plus pointers and flags | streaming queues, rate matching, CDC | full/empty logic must be correct |
Use the smallest resource that meets capacity, speed, and portability requirements. A 16-byte table may fit well in LUT RAM. A 1024-byte buffer should usually use BRAM.
Capacity Calculations
Memory capacity is width times depth:
bits = data_width_bits x depth_words
bytes = bits / 8
address_bits = ceil(log2(depth_words))
Examples:
| Structure | Width | Depth | Capacity | Address bits |
|---|---|---|---|---|
| UART FIFO | 8 bits | 64 | 512 bits | 6 |
| line buffer | 16 bits | 1024 | 16,384 bits | 10 |
| sine ROM | 12 bits | 256 | 3,072 bits | 8 |
Synchronous RAM Model
Many FPGA memories read synchronously: the address is sampled on a clock edge and output data appears after that edge.
module simple_ram #(
parameter AW = 8,
parameter DW = 8
) (
input wire clk,
input wire we,
input wire [AW-1:0] addr,
input wire [DW-1:0] din,
output reg [DW-1:0] dout
);
reg [DW-1:0] mem [0:(1<<AW)-1];
always @(posedge clk) begin
if (we)
mem[addr] <= din;
dout <= mem[addr];
end
endmodule
The line dout <= mem[addr]; means the consumer should expect registered output data. Pipeline the valid signal along with the data.
ROM Example
Small ROMs can be written as a case statement:
always @* begin
case (addr)
4'd0: data = 8'h3f;
4'd1: data = 8'h06;
4'd2: data = 8'h5b;
4'd3: data = 8'h4f;
default: data = 8'h00;
endcase
end
For larger ROMs, use a vendor-supported initialization file or an HDL style known to infer initialized block RAM in your toolchain. Always verify the synthesis report to see whether the memory became LUTs, BRAM, or something unexpected.
FIFO Concept
A FIFO stores data in order. Write pushes data into the queue. Read pops the oldest data. Full and empty flags prevent overflow and underflow.
Useful FIFO signals:
| Signal | Meaning |
|---|---|
wr_en |
write one word when not full |
rd_en |
read one word when not empty |
full |
no more writes are safe |
empty |
no valid word is available |
almost_full |
backpressure should start soon |
almost_empty |
consumer should expect a gap soon |
Choosing FIFO Depth
FIFO depth depends on burst size and service latency.
minimum depth >= worst_case_burst_words - words_consumed_during_burst
For a UART receiver, if the processor may be busy for 2 ms and bytes arrive every 86.8 us at 115200 baud with 10 bits per byte:
bytes during busy time = 2 ms / 86.8 us = 23.0 bytes
A 32-byte FIFO gives margin. A 4-byte FIFO will overflow during that service delay.
Asynchronous FIFOs
An asynchronous FIFO has different write and read clocks. A safe implementation typically uses:
- separate write and read pointer counters;
- gray-coded pointers so only one bit changes at a time;
- two-flop synchronizers for pointers crossing domains;
- full logic in the write clock domain;
- empty logic in the read clock domain;
- reset release handled safely in both domains.
Do not casually invent an asynchronous FIFO for production. Use vendor IP or a well-reviewed open design until you can explain the pointer synchronization and flag equations.
Worked Example: UART Receive Buffer
A UART receiver writes one byte whenever a frame is decoded. A processor reads when firmware has time.
Design review:
- UART logic asserts
wr_enwithwr_datawhen a byte is valid. - FIFO ignores writes when
fullis true and records an overflow flag. - Processor reads only when
emptyis false. - The byte-valid indication is tied to FIFO state, not a raw UART pulse.
- Tests cover full, empty, almost full, overflow, and reset behavior.
Common Mistakes
- Expecting block RAM reads to be combinational.
- Forgetting to delay
validalong with synchronous memory data. - Using thousands of flip-flops for a buffer that should use BRAM.
- Ignoring
fullandemptybecause the test stream is short. - Crossing FIFO status flags into another clock domain without CDC review.
- Trusting inference without checking the synthesis utilization report.
Summary
Registers are for tiny state. LUT RAM is for small storage. Block RAM is for real buffers and tables. ROMs hold fixed data. FIFOs connect producer and consumer rates, and asynchronous FIFOs are the standard structure for data streams crossing clock domains. Always account for memory latency and verify the inferred resource.
Next: Building Peripherals.
Further Reading
- FPGA vendor block RAM and FIFO generator user guides
- AMD Vivado Synthesis Guide memory inference templates
- Intel Quartus HDL coding guidelines for inferred RAM
- Clifford Cummings: Simulation and Synthesis Techniques for Asynchronous FIFO Design