Beginner · Lesson 9 of 10

Memories, block RAM and FIFOs

Distributed versus block RAM, how to write RAMs and ROMs so synthesis infers the right primitive, read-before-write against write-first, and a synchronous FIFO with full and empty flags.

Beginner40 minUpdated 13 Nov 2026

You will learn

  • Describe the difference between distributed RAM and block RAM and when each is inferred
  • Write single-port and simple dual-port RAMs in Verilog and VHDL that map to block RAM
  • Explain read-before-write and write-first behaviour and the one-cycle read latency of block RAM
  • Initialise a ROM from a hex file and build a synchronous FIFO with correct full and empty flags

Before this: counters timers and debouncing

Registers hold a few bits each. When a design needs to hold a few thousand bytes, a frame of pixels, a lookup table or a queue of packets, it needs memory, and every FPGA family has two kinds built in. This lesson explains what they are, how to write HDL that the synthesiser turns into the right one, and the behaviour you must design around: block RAM reads take a clock cycle. It finishes with the most useful memory-based component there is, the FIFO. You should have lesson 03 and lesson 04 behind you.

Two kinds of memory

Distributed RAM is made from the LUTs themselves. A LUT is a tiny memory already, 64 bits in a 6-input LUT, with the inputs acting as the address. Most families let the fabric write to those bits at run time, so a handful of LUTs becomes a small RAM. Its read path is combinational: present an address, and the data appears after a LUT delay with no clock involved. Distributed RAM is cheap for small, wide structures such as register files and short shift registers, and scarce for anything larger, because every bit you store is a LUT you cannot use for logic.

Block RAM (BRAM) is a dedicated hard block, typically 18 kb or 36 kb per block on AMD 7 Series parts, 9 kb or 20 kb on Altera parts depending on the family, 18 kb on Gowin GW1N, with dozens to thousands of them per device. Each block has one or two ports, each with its own address, data and enable, and each port is synchronous: the address is captured on a clock edge and the data comes out after that edge. There is no combinational read. This is the single most important fact about block RAM and the source of most beginner confusion.

Property Distributed RAM Block RAM
Built from LUTs Dedicated SRAM blocks
Typical size Tens to hundreds of bits per LUT group 9 kb to 36 kb per block
Read Asynchronous (combinational) or registered Synchronous only, one-cycle latency
Ports One write, one or more reads True dual-port on most families
Good for Register files, small FIFOs, shift registers Buffers, tables, line stores, large FIFOs
Cost Steals logic resources Fixed, separate budget

The synthesiser decides which to use from the way you write the HDL. A memory array with a registered read, above some size threshold, becomes block RAM. A memory with a combinational read cannot be block RAM and becomes distributed RAM, and if it is large you will get a warning and a very slow design. Write the registered read and the tool does the rest.

Inferring a single-port RAM

This is the canonical pattern. One port, one clock, a write enable, and a read that is registered on the same edge.

// Single-port RAM, synchronous read, read-before-write (the default
// behaviour most tools infer).
module ram_sp #(
  parameter integer DW = 8,
  parameter integer AW = 10            // 1024 words
) (
  input  wire          clk,
  input  wire          we,
  input  wire [AW-1:0] addr,
  input  wire [DW-1:0] wdata,
  output reg  [DW-1:0] rdata
);
  reg [DW-1:0] mem [0:(1 << AW) - 1];

  always @(posedge clk) begin
    if (we)
      mem[addr] <= wdata;
    rdata <= mem[addr];               // old contents on a write cycle
  end
endmodule

The declaration reg [DW-1:0] mem [0:(1 << AW) - 1]; is an array of 1,024 eight-bit words. The write is a non-blocking assignment into one element under we. The read is a non-blocking assignment from one element into a register, every cycle, with no condition. That unconditional registered read is what the tool looks for.

Block RAM read latency. The address presented before edge 2 produces its data after edge 2, available for use in cycle 3.clkaddr0x100x110x12werdatamem[10]mem[11]mem[12]
Block RAM read latency. The address presented before edge 2 produces its data after edge 2, available for use in cycle 3.

A design that presents an address and expects the data in the same cycle will read the previous address’s data instead. Nothing warns you. If you are converting a mental model from a microcontroller, where a load returns the value in the same instruction, this takes a little getting used to. The usual pattern is to pipeline: whatever is going to consume the data is itself registered a cycle later, so that the latency is absorbed.

Read-before-write and write-first

What does rdata show on a cycle when we is high and the address being written is the one being read? In the module above, the answer is the old contents, because both non-blocking assignments read mem[addr] as it was before the edge. This is called read-before-write, or read-first, and it is what most tools infer from this code and what the hardware does by default.

Write-first means rdata shows the data being written. It is coded like this:

// Single-port RAM, write-first: on a write cycle rdata shows the new data.
module ram_sp_wf #(
  parameter integer DW = 8,
  parameter integer AW = 10
) (
  input  wire          clk,
  input  wire          we,
  input  wire [AW-1:0] addr,
  input  wire [DW-1:0] wdata,
  output reg  [DW-1:0] rdata
);
  reg [DW-1:0] mem [0:(1 << AW) - 1];

  always @(posedge clk) begin
    if (we) begin
      mem[addr] <= wdata;
      rdata     <= wdata;
    end else
      rdata <= mem[addr];
  end
endmodule

There is a third mode, no-change, where rdata holds its previous value during a write. All three exist as hardware modes on AMD block RAM, and the synthesiser picks the one that matches your code. The thing to avoid is writing code whose behaviour matches none of them, for example a write-first read on one port of a dual-port memory where the other port is writing the same address. The tools cannot guarantee that case and will say so.

A simple dual-port RAM

Most real uses need to write from one place and read from another at the same time. A simple dual-port RAM has one write port and one read port, each with its own address. This is the memory a FIFO is built on, and it is also the common case for a frame buffer or a sample buffer.

// Simple dual-port RAM: one write port, one independent read port.
module ram_sdp #(
  parameter integer DW = 8,
  parameter integer AW = 10
) (
  input  wire          clk,
  input  wire          we,
  input  wire [AW-1:0] waddr,
  input  wire [DW-1:0] wdata,
  input  wire [AW-1:0] raddr,
  output reg  [DW-1:0] rdata
);
  reg [DW-1:0] mem [0:(1 << AW) - 1];

  always @(posedge clk) begin
    if (we)
      mem[waddr] <= wdata;
    rdata <= mem[raddr];
  end
endmodule

And the same thing in VHDL, where the array type is declared explicitly and the address is converted to an integer index:

-- Simple dual-port RAM that maps to block RAM.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity ram_sdp is
  generic (
    DW : positive := 8;
    AW : positive := 10
  );
  port (
    clk   : in  std_logic;
    we    : in  std_logic;
    waddr : in  std_logic_vector(AW - 1 downto 0);
    wdata : in  std_logic_vector(DW - 1 downto 0);
    raddr : in  std_logic_vector(AW - 1 downto 0);
    rdata : out std_logic_vector(DW - 1 downto 0)
  );
end entity;

architecture rtl of ram_sdp is
  type mem_t is array (0 to 2 ** AW - 1) of std_logic_vector(DW - 1 downto 0);
  signal mem : mem_t := (others => (others => '0'));
begin
  process (clk)
  begin
    if rising_edge(clk) then
      if we = '1' then
        mem(to_integer(unsigned(waddr))) <= wdata;
      end if;
      rdata <= mem(to_integer(unsigned(raddr)));
    end if;
  end process;
end architecture;

Both versions have one write port and one read port on the same clock. A true dual-port RAM, with two ports that can each read and write, is coded as two such processes on the same array, and the tools infer it in the same way. Giving the two ports different clocks is also allowed, and is how data crosses clock domains in an asynchronous FIFO, a subject for the intermediate track.

After synthesis, look at the utilisation report. If the memory landed in block RAM you will see it counted there. If it became a few hundred LUTs of distributed RAM instead, something in your code broke the pattern, most often a reset on the output register, an asynchronous read, or a write with a byte mask the family does not support.

A ROM from a hex file

A ROM is a RAM you never write. Fill it at elaboration time and the synthesiser puts the contents into the block RAM initialisation, which the bitstream loads at configuration. Verilog’s $readmemh reads a text file of hex values, one per line.

// 256-entry ROM loaded from a hex file at elaboration time.
module rom_sin (
  input  wire       clk,
  input  wire [7:0] addr,
  output reg  [7:0] data
);
  reg [7:0] mem [0:255];

  initial $readmemh("sin_table.hex", mem);

  always @(posedge clk)
    data <= mem[addr];
endmodule

The file is plain text, so the first lines of a sine table might be:

80
83
86
89

$readmemh is a simulation construct that Vivado, Quartus Prime and Yosys all honour in synthesis, and Gowin EDA does too at the time of writing, which makes it the most portable way to initialise a memory. Two cautions. The path is resolved relative to the tool’s working directory, not the source file, so builds that work on one machine can fail on another. And an initial block used this way is the one exception to the rule that initial has no hardware meaning. The same trick, an initial assigning a constant, is how registers get their power-up values on most FPGA families.

In VHDL the equivalent is a constant array built by a function that reads the file with textio, or a constant with the values written out in the source.

A synchronous FIFO

A FIFO is a queue. One side writes words in, the other reads them out in the same order, and the two sides need not agree on when. It is how a UART receiver hands bytes to a processor, how a pixel pipeline tolerates a slow stage, and how a DMA engine buffers a burst. A synchronous FIFO has both sides on the same clock. An asynchronous one does not, and is harder.

ram_sdpDEPTH x DWwptr [AW:0]rptr [AW:0]waddrraddrwdatardatacompare: full / emptywr_en and not full: write, wptr + 1rd_en and not empty: rptr + 1
A synchronous FIFO: a simple dual-port RAM addressed by a write pointer and a read pointer. The extra pointer bit distinguishes full from empty.

The design is a dual-port RAM with two counters. The write pointer says where the next word goes, the read pointer says where the next word comes from, and both wrap around the end of the memory. Empty means the pointers are equal. Full also means the pointers are equal, once the write pointer has lapped the read pointer, and that ambiguity is the one design problem in a FIFO. The clean solution is to make each pointer one bit wider than the address. The low bits address the RAM. When all the bits match, the FIFO is empty. When the low bits match but the top bits differ, the write side has gone round exactly once more than the read side, and the FIFO is full.

// Synchronous FIFO built on a simple dual-port RAM.
// DEPTH must be a power of two.
module fifo_sync #(
  parameter integer DW = 8,
  parameter integer DEPTH = 16
) (
  input  wire          clk,
  input  wire          rst,
  input  wire          wr_en,
  input  wire [DW-1:0] wdata,
  input  wire          rd_en,
  output wire [DW-1:0] rdata,
  output wire          full,
  output wire          empty
);
  localparam integer AW = $clog2(DEPTH);

  // Pointers carry one extra bit so full and empty can be told apart.
  reg [AW:0] wptr, rptr;

  wire do_write = wr_en && !full;
  wire do_read  = rd_en && !empty;

  ram_sdp #(.DW(DW), .AW(AW)) u_mem (
    .clk(clk), .we(do_write),
    .waddr(wptr[AW-1:0]), .wdata(wdata),
    .raddr(rptr[AW-1:0]), .rdata(rdata)
  );

  always @(posedge clk) begin
    if (rst) begin
      wptr <= 0;
      rptr <= 0;
    end else begin
      if (do_write) wptr <= wptr + 1'b1;
      if (do_read)  rptr <= rptr + 1'b1;
    end
  end

  assign empty = (wptr == rptr);
  assign full  = (wptr[AW] != rptr[AW]) && (wptr[AW-1:0] == rptr[AW-1:0]);
endmodule

Writes when full and reads when empty are ignored rather than corrupting the pointers, which is the behaviour a user of the FIFO can rely on. Because the RAM read is registered, rdata is valid the cycle after rd_en was accepted. Many FIFO users prefer a first-word-fall-through interface, where the head of the queue is always visible on rdata and rd_en acts as an acknowledge. That is a small wrapper around this one, and it is the exercise.

The testbench fills a four-deep FIFO with five writes, checks that full rose and that the fifth write was refused, then drains it and checks that the four words come back in order and that empty returns.

`timescale 1ns / 1ps
module tb_fifo_sync;
  reg clk = 0, rst = 1, wr_en = 0, rd_en = 0;
  reg  [7:0] wdata = 0;
  wire [7:0] rdata;
  wire full, empty;

  fifo_sync #(.DW(8), .DEPTH(4)) dut (
    .clk(clk), .rst(rst), .wr_en(wr_en), .wdata(wdata),
    .rd_en(rd_en), .rdata(rdata), .full(full), .empty(empty));

  always #5 clk = ~clk;

  integer i, errors = 0;
  initial begin
    $timeformat(-9, 0, " ns", 6);
    $dumpfile("fifo_sync.vcd");
    $dumpvars(0, tb_fifo_sync);
    repeat (2) @(posedge clk);
    rst <= 0;

    // Fill it. The fifth write must be refused.
    for (i = 0; i < 5; i = i + 1) begin
      @(posedge clk);
      wr_en <= 1; wdata <= 8'hA0 + i;
    end
    @(posedge clk); wr_en <= 0;
    @(negedge clk);
    if (!full) begin errors = errors + 1; $display("FAIL: not full"); end

    // Drain it. rdata is valid the cycle after rd_en, because the RAM
    // read is registered.
    for (i = 0; i < 4; i = i + 1) begin
      @(posedge clk); rd_en <= 1;
      @(posedge clk); rd_en <= 0;
      @(negedge clk);
      if (rdata !== 8'hA0 + i) begin
        errors = errors + 1;
        $display("FAIL: read %h, expected %h", rdata, 8'hA0 + i);
      end else
        $display("t=%0t read %h", $time, rdata);
    end
    @(negedge clk);
    if (!empty) begin errors = errors + 1; $display("FAIL: not empty"); end

    if (errors == 0) $display("PASS"); else $display("FAIL: %0d errors", errors);
    $finish;
  end
endmodule
iverilog -g2012 -o fifo.vvp ram_sdp.v fifo_sync.v tb_fifo_sync.v
vvp fifo.vvp

The checks happen on the falling edge, half a cycle after the outputs settle, so that the testbench never races the design. Expect four read lines showing a0 to a3 and then PASS.

Exercise

Wrap fifo_sync in a first-word-fall-through FIFO. The wrapper should present the oldest word on rdata whenever empty is low, and treat rd_en as “I have taken it”. The simplest approach is a one-word output register that is reloaded from the inner FIFO whenever it is empty or has been taken. Extend the testbench so that it reads with rd_en held high continuously and checks that one word arrives per cycle with no gaps. Then synthesise ram_sdp on its own with AW set to 4 and again set to 12, and compare where the tool put the memory in each case.

Summary

  • Distributed RAM is built from LUTs and can be read combinationally. Block RAM is dedicated, larger, and always has a one-cycle registered read.
  • Write memories as an array with a non-blocking write under an enable and an unconditional registered read. That pattern infers block RAM in every mainstream tool.
  • Read-before-write returns the old data on a write cycle, write-first returns the new data. Code the one you mean and avoid port collisions on dual-port memories.
  • $readmemh in an initial block initialises a ROM or RAM portably across simulators and synthesisers.
  • A FIFO is a dual-port RAM plus two pointers. Make the pointers one bit wider than the address to tell full from empty.

Further reading