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

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:

ResourceUsedAvailableUtil%
LUT48 213274 08017.6%
FF61 402548 16011.2%
BRAM (36 Kb)11291212.3%
DSP2 4962 52099.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.

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.