In short: Block RAM is dedicated on-chip memory inside an FPGA, organized as hardened dual-port SRAM columns that you either let the synthesis tool infer from HDL or instantiate directly. It delivers single-cycle, deterministic access for buffers, FIFOs and caches without burning general-purpose logic.
Block RAM (BRAM) is a column of dedicated, hardened static-RAM blocks embedded in the FPGA fabric alongside the configurable logic. Unlike distributed RAM, which repurposes the lookup tables (LUTs) of the logic slices to store small amounts of data, block RAM is a purpose-built memory primitive. A single Xilinx block is typically 18 Kbit or 36 Kbit; an Intel (Altera) M20K block holds 20 Kbit. Dozens to thousands of these blocks are laid out in vertical columns across the die, and the router stitches them into whatever width and depth your design needs.
Because the storage array, the address decoder, the sense amplifiers and the output registers are all fabricated as fixed silicon, block RAM is dense and fast. You spend none of your LUTs on the bit cells, and you get a synchronous memory with a guaranteed access time rather than a timing path that depends on how the tool happened to pack your logic.
Most non-trivial FPGA designs need to store data between clock cycles: a line buffer for an image pipeline, a FIFO crossing two clock domains, a packet queue in a network block, coefficient tables for a filter, or a small instruction/data memory for a soft processor. Building those out of flip-flops or distributed RAM is wasteful and slow once you pass a few hundred bits.
Block RAM solves three problems at once:
Every block RAM primitive exposes a clock, an address bus, a data-in bus, a data-out bus, a write-enable and usually a clock enable per port. The defining characteristic is how many independent access ports it has.
One port shared for reads and writes. In any given cycle you either write the addressed location or read it. This is the simplest and smallest configuration, suitable for a scratchpad or a coefficient store accessed by one agent.
Two ports, but asymmetric: one port is write-only and the other is read-only. This maps perfectly onto FIFOs, where a producer writes and a consumer reads, and it is the most area-efficient dual-port style because the hardware does not need full read/write logic on both sides.
Two fully independent ports, each with its own clock, address and write-enable, and each able to read or write any location. True dual-port is what lets two clock domains or two processing engines share one memory. The cost is a design decision you cannot avoid: what happens on a write-write or read-write collision to the same address. FPGA vendors expose this as a configurable write mode per port — WRITE_FIRST (new data appears on the output), READ_FIRST (old data appears, giving read-before-write), or NO_CHANGE (the output holds). Picking the wrong one is a classic source of silent data corruption.
There are two ways to get block RAM into your design, and knowing when to use each separates a hobby project from production RTL.
You write ordinary, portable HDL that describes a memory, and the synthesis tool recognizes the pattern and maps it onto block RAM automatically. The canonical pattern is a 2-D array (a Verilog reg [W-1:0] mem [0:D-1] or VHDL array) with a synchronous read. The tool only infers block RAM when the read is registered; an asynchronous read forces distributed RAM or flip-flops instead. Inference keeps your code vendor-neutral and readable, and modern tools are very good at it, so it should be your default.
You directly instantiate the vendor primitive or a generated memory IP core (Xilinx Block Memory Generator / XPM macros, Intel's altsyncram or the IP Catalog). You do this when you need a feature the inference template cannot express cleanly — a specific collision mode, built-in error-correcting code (ECC), byte-write enables, asymmetric port widths, or a cascaded deep memory. The trade-off is portability: an instantiated primitive is tied to that vendor's architecture.
A good rule of thumb: infer first, instantiate only when inference cannot give you the behavior you need, and always check the synthesis report to confirm the tool actually mapped your array to BRAM rather than silently exploding it into registers.
Real projects and industry tool flows, with certification.
Explore CourseTron Courses →Xilinx/AMD Vivado reports inferred memories in the synthesis log as RAM_STYLE and lets you force the choice with a (* ram_style = "block" *) attribute; the Block Memory Generator and XPM_MEMORY macros cover the instantiation route. Intel Quartus uses the ramstyle attribute ("M20K", "MLAB") and the IP Catalog / altsyncram for explicit cores. Both toolchains will also implement small memories in distributed/LUT RAM when that is more efficient, and both let you pack initial contents from a .mem, .hex or .mif file so the block comes up pre-loaded at configuration — invaluable for coefficient tables and boot ROMs.
The optional output register inside each block is the lever you pull for timing closure: enabling it adds a cycle of read latency but dramatically improves the clock-to-out path, often the difference between meeting and missing your target frequency on a memory-heavy design.
The block is hardened SRAM with a registered address path built into the silicon; the read is inherently synchronous to the port clock. If your HDL describes an asynchronous (combinational) read, the tool cannot map it to BRAM and will fall back to distributed LUT RAM or flip-flops instead. Always register the read if you want block RAM.
Distributed RAM reuses the logic slice LUTs for small, shallow memories and supports asynchronous reads; it is cheap for a few hundred bits. Block RAM is dedicated, dense SRAM (18/20/36 Kbit per block) with synchronous access, far better for buffers, FIFOs and anything over roughly 1 Kbit.
It is when both ports access the same address in the same cycle. You control the result with the per-port write mode: WRITE_FIRST passes the new data to the output, READ_FIRST returns the old (pre-write) data, and NO_CHANGE freezes the output. Choose deliberately and document it — mismatched assumptions between designer and tool cause corrupt reads.
When you need features the portable inference template cannot express: built-in ECC, byte-write enables, asymmetric read/write port widths, a specific collision mode, or cascaded deep memories. For everything else, inference is cleaner and vendor-portable.
Related: a related topic · the VLSI Glossary · all courses.