Every real design needs to hold data, and where it holds it decides how much fits and how fast it runs. An FPGA offers two kinds of on-chip memory. Small amounts can be built from the lookup tables themselves, called distributed memory, which is handy for a few words of storage near the logic that uses them. Anything larger belongs in block RAM, the dedicated memory tiles the fabric provides. The trick with both is that you do not instantiate them directly. You write a memory in a particular shape and the tools recognize the pattern and map it onto the right resource. This page covers those shapes and builds the buffer that memory most often becomes.
Inferring a block RAM
Block RAM (BRAM) is a dedicated memory tile, roughly 18 or 36 kilobits each, with two independent ports so one part of a design can read while another writes. To get one, you describe an array and read it through a register, because the register is the part the tools use to decide this is a block RAM rather than a field of flip-flops. The registered read costs one cycle of latency, and accepting that latency is the price of the dense, fast memory. Here is the pattern for a simple dual-port memory with one write port and one read port.
dual_port_ram.svmodule dual_port_ram #(
parameter int DATA_W = 32,
parameter int DEPTH = 512,
parameter int ADDR_W = $clog2(DEPTH)
)(
input logic clk,
input logic we,
input logic [ADDR_W-1:0] waddr,
input logic [DATA_W-1:0] wdata,
input logic [ADDR_W-1:0] raddr,
output logic [DATA_W-1:0] rdata
);
logic [DATA_W-1:0] mem [DEPTH];
always_ff @(posedge clk) begin
if (we) mem[waddr] <= wdata;
rdata <= mem[raddr]; // registered read, one cycle of latency
end
endmoduleThe $clog2 function computes the address width from the
depth so the interface stays honest when the depth changes. The read
and the write are in the same clocked block and on the same clock,
which is the shape a block RAM wants. If you instead read the array
combinationally, without the register, the tools will usually give you
distributed memory built from lookup tables, which is fine for a tiny
array and wasteful for a large one.
The FIFO, memory as a buffer
The most common thing a memory becomes is a first-in first-out buffer, a FIFO, which decouples a producer from a consumer so that short bursts and brief stalls do not force the two to run in lockstep. A FIFO is a circular buffer in memory with a write pointer, a read pointer, and a way to know how full it is. The subtlety people trip on is telling full from empty, because both leave the two pointers equal. The clean fix is to track the count of items directly rather than inferring it from the pointers, which also means no entry is wasted.
sync_fifo.svmodule sync_fifo #(
parameter int DATA_W = 8,
parameter int DEPTH = 16,
parameter int ADDR_W = $clog2(DEPTH)
)(
input logic clk,
input logic rst,
input logic wr, // push wdata when not full
input logic [DATA_W-1:0] wdata,
input logic rd, // pop when not empty
output logic [DATA_W-1:0] rdata,
output logic full,
output logic empty,
output logic [ADDR_W:0] count
);
logic [DATA_W-1:0] mem [DEPTH];
logic [ADDR_W-1:0] wptr, rptr;
assign empty = (count == 0);
assign full = (count == DEPTH);
assign rdata = mem[rptr]; // first word available to read
wire do_wr = wr && !full;
wire do_rd = rd && !empty;
always_ff @(posedge clk) begin
if (rst) begin
wptr <= '0; rptr <= '0; count <= '0;
end else begin
if (do_wr) begin mem[wptr] <= wdata; wptr <= wptr + 1'b1; end
if (do_rd) rptr <= rptr + 1'b1;
case ({do_wr, do_rd})
2'b10: count <= count + 1'b1; // write only
2'b01: count <= count - 1'b1; // read only
default: count <= count; // both or neither, count holds
endcase
end
end
endmoduleThe guards do_wr and do_rd make the FIFO
ignore a push when full and a pop when empty, so it can never corrupt
itself no matter how it is driven. When a push and a pop land on the
same cycle the count stays put, which is correct, because one entry
came in as another left. The testbench in the
repository
fills the buffer, drains it, and checks that the data comes out in the
order it went in with the full and empty flags behaving.
This FIFO lives in one clock domain. When a buffer has to bridge two different clocks the pointers can no longer be compared directly, and the design needs the careful Gray-code crossing covered on the clock domain crossing page. Before that, the next two pages make the datapath fast, with pipelining and then timing closure.