The internal memories of an FPGA are resources for storing data using registers, configurable LUTs, or dedicated RAM blocks.
So far, when we needed to store a piece of data (like a counter or a state), we used a Register built from Flip-Flops. It’s fast, simple, and available everywhere.
But what if we want to store an image to display on a monitor? Or a 5-second audio buffer? Or the code for a softcore processor?
If you try to create a giant array of registers like reg [7:0] image [0:10000], your synthesis tool will laugh (or cry). You’ll run out of Flip-Flops instantly, and routing will be impossible.
For mass storage, FPGAs have a trick up their sleeve: Internal Memories. Today, we’ll look at the two main ways to store large amounts of data: Distributed RAM and Block RAM (BRAM).
Registers: Memory with Flip-Flops
We already know these. They are scattered across the chip, right next to each LUT.
- Capacity: 1 bit per unit.
- Speed: Extremely fast.
- Access: Fully parallel (you can read 1000 registers at once if you have enough wires).
- Cost: Very high. Using Flip-Flops to store data arrays is a waste of silicon.
Rule: Only use Flip-Flops for control variables, state machines, or pipelines. Never to store data tables or buffers.
Distributed RAM (LUT RAM)
Here we start getting clever. Remember what a LUT (Look-Up Table) was? We said it was a small memory that stores the truth table of a logic function.
Normally, that memory is read-only (ROM) during circuit operation. But in many FPGAs (like Xilinx or Lattice), we can configure the LUTs to be writable dynamically.
This turns LUTs, which normally do logic, into small RAM memories. We call this Distributed RAM.
- Ideal use: Small arrays (e.g., a bank of 16 or 32 registers, small FIFOs).
- Advantage: They are “distributed” across the chip, so you can place them right next to the logic that needs them. They allow asynchronous (combinational) reads.
- Disadvantage: If you try to make a large memory, you’ll consume all the FPGA logic and leave no room for processing.
Block RAM (BRAM)
When we need to store KiloBytes or MegaBytes of information, we bring in the “heavy hitters.”
Block RAM (BRAM) are dedicated SRAM memory blocks physically embedded in the FPGA silicon. They are high-density islands surrounded by a sea of programmable logic.
Depending on the manufacturer, these blocks have fixed sizes (for example, in Lattice iCE40, they are 4 Kbit blocks; in Xilinx Series 7, they are 36 Kbit blocks). The synthesizer can combine several blocks to form the memory size you need.
Features of BRAM
- Typically Synchronous Read: In most families, BRAM is inferred with a registered read: you present the address and get the data after a clock edge. Check your device’s primitive, as available modes vary.
- High Density: You can store a lot of data using very little chip space.
- Dual Port: Many BRAMs have two independent access ports (A and B).
- You can be writing data through Port A (from a sensor).
- And reading different data through Port B (to a VGA monitor).
- Depending on the primitive, both ports can operate simultaneously and even use different clocks.
Technology Comparison
| Feature | Registers (FF) | Distributed RAM (LUT) | Block RAM (BRAM) |
|---|---|---|---|
| Base Resource | Flip-Flops | LUTs (Logic) | Dedicated Blocks |
| Capacity | Very Low (Bits) | Low (Hundreds of Bits) | High (KiloBits/MegaBits) |
| Read | Async / Sync | Async Possible | Strictly Synchronous |
| Area Cost | Very High | Medium | Very Low (Efficient) |
| Typical Use | States, Flags, Counters | Register Files, Small FIFOs | Video Buffers, Audio, CPU RAM |
Memory Inference in Verilog
How do we tell the tool “I want to use a BRAM”?
We could instantiate the manufacturer-specific primitive (e.g., SB_RAM40_4K in Lattice), but that would make our code non-functional in Xilinx.
The best approach is to use Inference. We write a generic Verilog array, and the tool is smart enough to say: “Hmm, this array is too large; I’d better put it in a BRAM.”
Code to Infer a Single-Port BRAM
module memory_ram #(
parameter DATA_WIDTH = 8, // 8 bits per data (one byte)
parameter ADDR_WIDTH = 10 // 10-bit address (2^10 = 1024 locations)
)(
input wire clk,
input wire write_enable,
input wire [ADDR_WIDTH-1:0] addr, // Address (0 to 1023)
input wire [DATA_WIDTH-1:0] data_in,
output reg [DATA_WIDTH-1:0] data_out
);
// 1. Array Declaration (The Memory)
// "An array of 1024 slots, each 8 bits wide"
reg [DATA_WIDTH-1:0] memory [0:(2**ADDR_WIDTH)-1];
always @(posedge clk) begin
// Write (if enabled)
if (write_enable) begin
memory[addr] <= data_in;
end
// Synchronous read (required to infer BRAM)
// If this were outside the always block, distributed RAM might be inferred
data_out <= memory[addr];
end
endmoduleNotice that the read data_out <= memory[addr] is inside the always @(posedge clk) block. This implies one cycle of latency and matches the typical inference template. Consult your tool’s synthesis guide for exact templates and read-during-write behavior.
Initialization ($readmemh)
A great advantage of FPGAs is the ability to initialize these memories with preloaded data when programming the chip. This is essential if we are making a ROM (Read-Only Memory) containing:
- A sine/cosine table for calculations.
- Character fonts for a VGA text generator.
- The compiled program (firmware) for a RISC-V processor.
Verilog provides system functions for this: $readmemh (hexadecimal) and $readmemb (binary).
// ... inside the memory module ...
initial begin
// Load the file "my_firmware.hex" into the 'memory' array
// The file must be in the project folder
$readmemh("my_firmware.hex", memory);
endThe my_firmware.hex file would be a simple text file with values:
FF
A0
00
1B
...