Loading header...

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

flowchart LR A["Write side"] --> B["Memory array"] B --> C["Read side"] D["Write pointer"] --> B E["Read pointer"] --> B D --> F["Full logic"] E --> G["Empty logic"]

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:

  1. UART logic asserts wr_en with wr_data when a byte is valid.
  2. FIFO ignores writes when full is true and records an overflow flag.
  3. Processor reads only when empty is false.
  4. The byte-valid indication is tied to FIFO state, not a raw UART pulse.
  5. Tests cover full, empty, almost full, overflow, and reset behavior.

Common Mistakes

  • Expecting block RAM reads to be combinational.
  • Forgetting to delay valid along with synchronous memory data.
  • Using thousands of flip-flops for a buffer that should use BRAM.
  • Ignoring full and empty because 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

Mind Map

mindmap root((BRAM ROM FIFO)) Core concept Dedicated memory Sync read latency FIFO preserves order Applications UART buffers Video line store Lookup tables CDC streams Formulas Bits equal width times depth Addr bits ceil log2 depth FIFO depth covers burst Design rules Use BRAM for buffers Pipeline valid Check full empty Gray pointers for async Practical checks Synthesis report Read latency test Overflow flag Reset both domains Common mistakes Async read assumption Lost valid timing Ignored full flag Bit-sync status