Block RAM, DSP Slices and Hard IP
The LUT fabric
can be anything — that is its glory and its curse. Try to build a 64-kilobit buffer out of LUTs and
you will burn tens of thousands of them on a structure that a memory designer could lay out in a
speck of dense SRAM. Try to build one 18×18 multiplier and hundreds of LUTs vanish into partial
products, running at half the speed a custom multiplier would manage. Universality has a price, and
for the handful of structures every design needs — memories, multipliers, clock circuits —
the price is absurd. So FPGA architects made a deal: sprinkle the fabric with small islands of
hardened, fixed-function silicon for exactly those structures. This lesson is
about those islands, and about the quiet art of writing RTL so the tools actually use them.
The islands in the sea
- Block RAM (BRAM). Columns of dense SRAM blocks — classically
36 kilobits each — threaded through the fabric. Each block is
dual-ported (two independent ports can read or write in the same cycle, perfect
for a FIFO with a writer on one side and a reader on the other) and reshapeable: one 36 Kb block
can present itself as 1K × 36, 2K × 18, 4K × 9, and so on. A big buffer stitches several blocks
together.
- Distributed LUT-RAM. The other memory option: a LUT's 64-bit table is itself
a tiny RAM, and some slices let you write it at runtime. Below roughly a few hundred bits —
register files, small FIFOs — LUT-RAM sits right next to the logic that uses it and wastes no
36 Kb block; above that, BRAM wins overwhelmingly. Knowing where the crossover lies is part of
the craft.
- DSP slices. Hardened multiply-accumulate datapaths (the classic is Xilinx's
DSP48: an 18×18 → 36-bit multiplier feeding a 48-bit accumulator, with optional
pre-adder and pipeline registers). One slice does at full clock rate what ~300 LUTs do slowly.
A signal-processing or machine-learning design lives and dies by its DSP count.
- Clock managers (PLLs/MMCMs). Analogue-flavoured circuits that multiply,
divide and phase-align clocks — you cannot synthesise a PLL from LUTs at all.
- SerDes transceivers. Serialiser/deserialiser pairs driving multi-gigabit
serial links (PCIe, Ethernet, displays). Again physically impossible in fabric: this is
high-speed analogue design, hardened at the die edge.
- Hard processor systems. The biggest island of all — entire ARM clusters baked
in beside the fabric — big enough to get its own lesson
next.
Where the islands sit
The floorplan makes the deal visible: a vast regular sea of CLBs, interrupted by skinny columns of
BRAM and DSP (columns, so the regular routing pattern survives), with the exotic analogue blocks
pushed to the edges:
Inference: writing RTL the tools recognise
How do you use a DSP slice or a BRAM? Mostly, you don't ask — you describe a
pattern the synthesis tool recognises, and it maps your RTL onto the hard block for you.
This is called inference, and it keeps your RTL portable across vendors. The two
patterns that matter most:
// A registered multiply: the tool infers a DSP slice.
always_ff @(posedge clk)
p <= a * b; // 18x18-ish operands + a register = DSP48 shape
// A synchronous-read memory: the tool infers Block RAM.
logic [7:0] mem [0:4095]; // 4K x 8 = 32 Kb -> fits one 36 Kb block
always_ff @(posedge clk) begin
if (we) mem[waddr] <= wdata;
rdata <= mem[raddr]; // the READ is clocked: that register is the key
end
The alternative is explicit instantiation: placing the vendor's primitive
(DSP48E2, RAMB36E2…) in your netlist by name, port by port. You get exact
control and zero portability; teams reserve it for the cases where inference cannot express what
the block can do. The professional default is: infer, check the report, instantiate only when
forced.
"Check the report" is the load-bearing phrase. Every FPGA build prints a utilization
report — a table of how much of each resource the design consumed:
| Resource | Used | Available | Util% |
| LUT | 48 213 | 274 080 | 17.6% |
| FF | 61 402 | 548 160 | 11.2% |
| BRAM (36 Kb) | 112 | 912 | 12.3% |
| DSP | 2 496 | 2 520 | 99.0% |
Read it like a doctor reads bloodwork. This design is LUT-light but has all-but-exhausted its DSPs
— one more multiplier and the tools start building multipliers from LUTs, and speed falls off a
cliff. A surprise in the report is also your best bug detector: if you meant to infer one BRAM and
the report shows zero BRAMs and five thousand extra LUTs, the tool did not recognise your pattern —
which brings us to the classic way that happens (see the Watch out below).
Counting the cost: DSPs versus LUT multipliers
Make the economics concrete. Suppose a matrix-multiply engine needs P
parallel multiply-accumulate units, on a mid-range part with 274 000 LUTs and 2 520 DSP slices.
Each MAC is either one DSP slice or roughly 300 LUTs:
// One MAC = 1 DSP slice, or ~300 LUTs built from fabric.
const LUTS_PER_MAC = 300;
const device = { luts: 274000, dsps: 2520 };
console.log("parallel MACs | as DSPs (util%) | as LUTs (util%)");
console.log("--------------+-------------------+------------------");
for (let p = 32; p <= 4096; p *= 2) {
const dspPct = (100 * p / device.dsps).toFixed(1);
const luts = p * LUTS_PER_MAC;
const lutPct = (100 * luts / device.luts).toFixed(1);
const dspNote = p > device.dsps ? " FULL" : "";
const lutNote = luts > device.luts ? " FULL" : "";
console.log(
` ${String(p).padStart(6)} | ${String(p).padStart(6)} (${dspPct}%)${dspNote}`.padEnd(42) +
`| ${String(luts).padStart(7)} (${lutPct}%)${lutNote}`,
);
}
console.log("\nThe LUT version runs out of chip ~3x earlier - and each");
console.log("LUT-built MAC is slower too. Hard blocks are how FPGAs stay usable.");
The fabric version hits the wall around 900 MACs while the DSP version sails to 2 520 — and every
one of those LUT-built MACs would clock slower besides. Same die, wildly different capability,
purely because someone hardened the right islands.
- BRAM: dual-port ~36 Kb SRAM blocks in columns; use for memories beyond a few
hundred bits — below that, distributed LUT-RAM wins;
- DSP slices: hardened multiply-accumulate (18×18 + 48-bit accumulate); one
slice ≈ hundreds of LUTs at higher speed;
- PLLs, SerDes, hard CPUs: analogue or hyper-optimised functions the fabric
cannot express at all;
- inference maps recognised RTL patterns (registered multiply → DSP;
synchronous-read array → BRAM) onto hard blocks; instantiation is the explicit fallback;
- the utilization report (LUT/FF/BRAM/DSP %) is where you verify the mapping
actually happened.
It looks arbitrary until you factor it: 36 = 4 \times 9, and
9 = 8 + 1. Memory in the computing world loves widths of 8-plus-a-spare:
the ninth bit per byte carries a parity or ECC check bit, so a 36 Kb block presents
clean power-of-two depths at widths of 9, 18 or 36 — that is 4K × 9, 2K × 18, 1K × 36 — with error
checking riding along for free. The same 9-flavoured arithmetic shows up in the DSP slice's
18×18 multiplier (two 9s) and its 48-bit accumulator, which is no coincidence: the blocks are sized
to feed each other. Vendors' marketing sheets quote memory in these units to this day — when a
datasheet brags of "4.5 Mb of Block RAM", you are meant to divide by 36 and picture the 128 columns
of little SRAM islands.
BRAM has a non-negotiable physical property: its reads are clocked. Address in at
the edge, data out after the edge. So this innocent-looking line —
assign rdata = mem[raddr]; — an asynchronous read, describes something a
36 Kb block physically cannot do. The tool will not error; it will politely build your entire
memory out of LUT-RAM and multiplexers instead. For a 32 Kb buffer that is
thousands of LUTs plus a monstrous read mux: one missing register, and your one-BRAM design becomes
a LUT bonfire that fails timing to boot. The fix costs a single line — register the read
(rdata <= mem[raddr]; inside always_ff) and accept the one-cycle read
latency, which is the honest price of dense memory. The habit to build: after any change near a
memory, open the utilization report and confirm the BRAM count is what you expect.
It is the cheapest bug check in FPGA design.