The FPGA build process tends to be presented to newcomers as a single operation: you write RTL, you click a button, several hours pass, and a .bit file emerges. If everything works, great. If it doesn't, you read cryptic error messages from a tool you don't understand, make changes that may or may not be relevant, and click the button again.
This is a bad way to learn. The build is not one operation; it is a pipeline of at least six distinct phases, each with its own job, its own failure modes, and its own ways of telling you something is wrong. Engineers who understand these phases debug problems in minutes. Engineers who don't debug problems in weeks, because they're changing the wrong things at the wrong layer.
This article opens the black box. We'll walk through each phase in order: what it does, what it takes as input, what it produces as output, how it can fail, and why the phase exists at all. The goal is not to teach you every option of every tool (that's what vendor manuals are for), but to give you the mental model that lets you read a build log, recognize which phase is complaining, and know where to look for the cause.
The phases are largely the same across vendors (Xilinx/AMD Vivado, Intel/Altera Quartus, Lattice Diamond, Efinix Efinity), though each uses slightly different names and has its own implementation details. We'll use generic names throughout and call out vendor-specific terminology where it matters.
The shape of the pipeline
Before we dive into each phase, here's the overall flow. An FPGA build takes your RTL (written in Verilog, SystemVerilog, VHDL, or a mix) and produces a bitstream, a file that configures the FPGA to implement your design. Between the two, six major phases run in sequence:
- Elaboration: parsing and structural analysis of your RTL
- Synthesis: converting behavioral RTL into a gate-level netlist
- Technology mapping: rewriting the generic netlist in terms of FPGA-specific primitives (LUTs, flip-flops, block RAMs, DSP slices)
- Placement: deciding where each primitive physically sits on the chip
- Routing: deciding which wires connect which primitives
- Bitstream generation: encoding all of that into the binary configuration file
Timing analysis runs alongside and after several of these phases, as a cross-cutting concern rather than a standalone step. We'll treat it as its own topic below.
Each phase consumes the output of the previous phase and produces something more concrete. Elaboration works on source code; synthesis works on an abstract logic graph; technology mapping works on the specific primitives the target FPGA provides; placement works on physical coordinates; routing works on wires and switches; bitstream generation produces the final configuration bits. Each phase adds constraints and commitments that the previous phase didn't have to consider.
Think of it as a funnel. At the top, your RTL describes what you want. At the bottom, the bitstream specifies exactly where every signal goes and which transistors it traverses. Every phase in between is the tool progressively committing to specifics.
Phase 1: Elaboration
Input: your RTL source files, plus any parameters passed at the top level. Output: an elaborated design, a hierarchical structure of modules with their parameters resolved and their connections analyzed. Think of it as: compiling and linking your RTL into a single, fully-resolved design hierarchy.
Elaboration is the first thing that happens and the phase where most "stupid" errors get caught. The tool reads your source files, parses them, checks syntax, resolves parameters, elaborates generate blocks, flattens hierarchy where instructed to, and builds an in-memory representation of your design.
If you've ever seen an error like "module not found," "port width mismatch," "undeclared identifier," or "parameter value must be constant": that's elaboration complaining. These are the errors that feel like compile errors, because that's essentially what they are. Your RTL is being treated as a program in a hardware description language, and elaboration is checking it.
One subtle point: elaboration does not yet make your design into hardware. It's still a hierarchical, somewhat-abstract representation. An always_comb block with a case statement in it is still a case statement at this point, not a multiplexer. That conversion happens in the next phase.
A few things worth knowing about elaboration:
- Parameter propagation happens here. If you have a parameterized module instantiated with
WIDTH = 16, elaboration is whereWIDTHgets substituted throughout that instance. Two instances of the same module with different parameters are, after elaboration, effectively two different modules. - Generate blocks are unrolled here. A
generate for (i = 0; i < 8; i++)loop instantiating eight copies of a submodule produces, after elaboration, eight distinct submodules in the hierarchy. - Some "designs" become trivial after elaboration. If you parameterize a module with
WIDTH = 1and it has a loop that runsWIDTHtimes, elaboration turns that into a one-iteration loop, which may optimize away entirely. This can produce confusing warnings like "module has no logic" that trace back to parameter choices you forgot about.
Phase 2: Synthesis
Input: elaborated design. Output: a gate-level netlist, a graph of generic logic primitives (AND, OR, XOR, MUX, flip-flop, etc.) connected by wires. Think of it as: translating "what I want" into "gates that do it."
Synthesis is the phase where behavioral RTL (your if statements, your arithmetic operators, your case blocks) becomes actual logic. The tool takes each piece of behavioral description and converts it into a netlist of primitives that implements the same function.
Some of this is straightforward. a & b becomes an AND gate. a + b becomes an adder (often several styles of adder are available; the synthesizer picks one based on timing and area goals). always_ff @(posedge clk) q <= d; becomes a flip-flop.
Some of it is not straightforward at all. a * b where a is a 16-bit signal and b is a 32-bit signal has to become some multiplier circuit, and there are many options: array multipliers, Wallace trees, Booth-encoded multipliers, sequential multipliers that take multiple cycles, hardware multipliers built into the FPGA, and so on. The synthesizer makes a choice based on your constraints and settings.
A few things the synthesizer does that are worth knowing:
- Constant propagation. If a signal is tied to a constant (or becomes constant through a chain of simplifications), the logic that depends on it is simplified.
x & 1'b0just becomes1'b0, and anything downstream of that signal collapses. This is why it's important not to be surprised when a design "uses fewer resources than expected"; the synthesizer may have realized something was unreachable. - Dead code elimination. If a register is written but its output goes nowhere (no other logic reads it), the register and everything that feeds into it is removed. This often surprises beginners who expect every
regdeclaration to produce hardware. It only does if something observes the register's output. - Inference. Certain RTL patterns are inferred into specific FPGA primitives. A clocked always block that updates a RAM-like array with a registered address is inferred as block RAM. A multiply-accumulate written a certain way is inferred as a DSP slice. Getting this right matters enormously; the same function coded in two slightly different ways can produce a DSP-slice implementation (fast, compact) or a fabric-based implementation (slow, huge). Every vendor publishes coding style guides that document the exact patterns their synthesizer recognizes.
- Optimization. The synthesizer tries to produce a netlist that's fast, small, or balanced, depending on settings. It may share common subexpressions, retime logic across pipeline registers, rearrange conditions, and perform dozens of other transformations. Most of the time this is invisible and beneficial. Occasionally it does something that surprises you, usually when your RTL didn't clearly express what you wanted.
Synthesis is where most beginners' mental model breaks down. They expect the output to correspond line-by-line to their RTL, and it doesn't. The synthesizer's job is to produce a netlist that implements the same behavior as your RTL, not a netlist that textually mirrors your RTL. When people say "write synthesizable RTL," they mean RTL that the synthesizer can confidently and predictably translate, which is a narrower subset of the full language than simulators accept.
Phase 3: Technology mapping
Input: a generic gate-level netlist from synthesis. Output: a netlist in terms of FPGA-specific primitives: lookup tables (LUTs), flip-flops, block RAMs, DSP slices, I/O buffers, and so on. Think of it as: rewriting the design in the vocabulary of the specific chip you're targeting.
This is the phase where the design becomes specific to your FPGA family. An FPGA isn't a sea of generic AND and OR gates; it's a structured grid of very specific primitives. The main ones:
- Lookup tables (LUTs). The fundamental programmable logic element. A modern FPGA LUT typically has 4 to 6 inputs and one output, and it implements any Boolean function of those inputs. The function is stored as a truth table in the LUT's configuration memory. "Using a LUT" means programming it with the truth table for whatever small Boolean function you need.
- Flip-flops. Each LUT in the fabric is typically paired with one or two flip-flops. So the natural "unit" of fabric logic is "a LUT plus a register": a combinational function followed by a clocked register, which is exactly the shape of synchronous logic.
- Block RAMs (BRAMs). Dedicated memory blocks, typically 18 Kb or 36 Kb, with configurable port width and depth. Vastly faster and denser than LUT-based memory. Also usable as ROMs, as FIFOs with a little extra control logic, or as sample buffers for DSP.
- DSP slices. Hardened multiply-accumulate units, typically 18×18 or 18×27 or similar. Enormously faster and more power-efficient than fabric-based multipliers. Using them well is a big part of hitting DSP-heavy performance targets.
- Specialized blocks. High-speed transceivers (for 10G+ Ethernet, PCIe, etc.), clock management tiles (PLLs, MMCMs), I/O buffers with various electrical standards, and, on some devices, hardened cores like ARM processors or memory controllers.
Technology mapping's job is to take the generic netlist ("here's a 32-bit adder, here's a comparator, here's a register file") and express it in terms of these specific primitives. A 32-bit adder might map to a chain of carry-optimized LUTs, or to a DSP slice used in "add" mode, depending on context and constraints. A small ROM might map to distributed memory built from LUTs, or to a block RAM, depending on size and access pattern.
This is also where inference decisions that started in synthesis get committed. Synthesis may have inferred "this looks like block RAM"; technology mapping is where that inference becomes an actual block RAM instantiation in the netlist.
A few consequences worth knowing:
- Resource reports come out of this phase. When the tool tells you your design uses "5,432 LUTs, 9,821 flip-flops, 12 BRAMs, and 4 DSP slices," those numbers are the output of technology mapping.
- If your design doesn't fit, this is where you find out. The tool counts mapped resources against the device's available resources. If you exceed the LUT count of the target chip, you get an error here, before placement starts, because there's no point trying to place what won't fit.
- Architecture-specific optimization happens here. The same netlist mapped to a small low-end FPGA and a large high-end FPGA may use completely different strategies, because the available primitives and their characteristics differ. This is one reason portability across FPGA families is harder than it looks: the same RTL can map very differently to different targets.
Phase 4: Placement
Input: the technology-mapped netlist. Output: an assignment of each primitive to a specific physical location on the chip. Think of it as: deciding which LUT in which slice of which tile each piece of logic will live in.
An FPGA chip is a two-dimensional grid of tiles. Each tile contains some fixed set of primitives (some number of LUTs, some flip-flops, maybe a BRAM or DSP if it's that kind of tile) and is connected to its neighbors by the routing fabric. A typical modern FPGA has tens to hundreds of thousands of tiles.
Placement's job is to decide, for each primitive in your netlist, exactly which physical slot it will occupy. This is an enormous combinatorial optimization problem. You have N primitives and M slots (with M > N), and you need to choose an assignment that:
- Keeps logically-connected primitives physically close to each other (to minimize routing delay)
- Respects resource constraints (don't try to put two things in the same slot, don't put a BRAM-only primitive in a non-BRAM slot)
- Respects user-specified location constraints (I/O pins, pblocks, floorplan regions)
- Is routable, leaving enough wire resources in each region for the subsequent routing phase to succeed
The first requirement is the most important for performance. Remember that signal propagation delay through FPGA routing is on the order of 50 to 100 ps per hop, and a long route across the chip can easily take several nanoseconds. If the placer puts two closely-connected primitives far apart, the paths between them will be slow, and timing closure suffers.
Modern placers use a combination of techniques (simulated annealing, analytical placement, quadratic programming) and run in passes, progressively refining the placement. The placer is guided by the same timing constraints you specified for synthesis; it knows which paths are on your critical timing paths and tries hardest to keep those paths short.
A few things worth knowing about placement:
- Placement dominates timing closure. If placement is good, routing has enough slack to finish cleanly. If placement is bad, no amount of routing cleverness will save you.
- Floorplanning is manual placement. When you specify pblocks (physical regions of the chip to which certain logic must be assigned), you're manually constraining the placer. This can be essential for complex designs (high-speed paths may need to be forced into specific chip regions to work), but it can also hurt, because you're removing freedom the placer might have used well. Use floorplanning surgically.
- The placer's output is deterministic given the same inputs, but highly sensitive to them. Change a constraint, or even add a trivial piece of logic, and the placement may reshuffle entirely. This is why builds that used to close timing sometimes stop closing after small changes, not because the change is slow, but because it perturbed placement and the critical path moved.
Phase 5: Routing
Input: placed design. Output: a complete routing, where every wire between every pair of connected primitives is assigned to specific routing resources. Think of it as: drawing every wire in the design, using only the pre-fabricated routing tracks the FPGA provides.
Once primitives are placed, the tool must connect them using the FPGA's fixed routing resources. An FPGA has a rich interconnect fabric: horizontal and vertical wires of various lengths, programmable switch boxes at intersections, dedicated carry chains, dedicated clock nets, and so on. Routing decides which specific wire segments and switches to use for each signal.
Routing has two jobs:
- Connect everything. Every signal in the placed netlist has a driver (one source) and one or more receivers (loads). The router must find a path from the driver through the routing fabric to each receiver.
- Meet timing. The router is aware of propagation delay through each routing resource. For timing-critical signals, it tries to find paths that are fast; for non-critical signals, it can use slower, less-desirable routes to leave the fast paths for things that need them.
Routing is hard because the routing resources are finite. A high-utilization design (80%+ of the chip in use) may literally not have enough free wires to make all the required connections, especially in congested regions. This is called routing congestion, and it's one of the most common failure modes at high utilization.
A few things about routing:
- Routing time scales nonlinearly with utilization. A 50%-utilized design routes quickly; an 85%-utilized design may route in 10× the time; a 95%-utilized design may fail to route at all. There's a reason FPGA teams aim for utilization around 70 to 75% for production designs; it's where the tools are most predictable.
- Routing is often the dominant contributor to path delay. On a modern FPGA, the LUT-to-LUT delay of a single-stage combinational path is dominated by routing, not by the LUT itself. A rough rule of thumb: the LUT itself takes ~100 ps, but the routing between LUTs can easily add 300 to 500 ps per hop. This is why reducing logic levels (shorter paths) is so effective: each level removed is a routing hop removed.
- Unroutable designs are recoverable, usually. If the router fails, options include: reducing utilization (simplifying logic, reducing pipelining), adjusting constraints, changing floorplan, turning on more aggressive router effort settings, or in the worst case moving to a larger FPGA.
After routing, the tool knows exactly how long every path takes, because every wire, every switch box, every LUT delay is now committed. This is when the final timing analysis runs.
Phase 6: Bitstream generation
Input: fully placed and routed design. Output: the .bit (or .bin, or .sof, depending on vendor) configuration file. Think of it as: writing out the final configuration for every programmable element on the chip.
This is the most mechanical phase. By now, every decision is made: which LUT contains which truth table, which flip-flop has which initial value, which switch boxes are programmed which way, which routing wires are driven by which drivers, which I/O buffers are configured for which electrical standard, which PLL has which multiplier setting. Bitstream generation walks this fully-committed design and produces the exact sequence of bits that, when loaded into the FPGA's configuration memory, makes the chip implement your design.
There's not much to say about this phase for a learner; it almost always "just works," and when it fails, it's usually due to a licensing issue, a device-support issue, or a bug in the tool. You'll rarely need to understand bitstream generation internally.
A few points worth knowing:
- Bitstreams are device-specific. A bitstream for one FPGA part number will not work on a different part, even if they're closely related. The physical layout is different, and the bitstream encodes physical decisions.
- Bitstreams may be encrypted or authenticated. Many production FPGAs support encrypted bitstreams (to protect IP) and authenticated bitstreams (to prevent tampering). Configuring these adds steps at this phase.
- Bitstream size is roughly proportional to chip size, not design size. A 10% utilized bitstream is nearly the same size as a 95% utilized one, because the bitstream has to specify the configuration of every programmable element, including the many that aren't doing anything useful.
Timing analysis: the cross-cutting concern
Timing analysis isn't a single phase; it runs multiple times throughout the flow, with progressively more accurate information:
- After synthesis, an early estimate using wire-load models. Very rough; it doesn't know real placement or routing. Useful for catching gross problems only.
- After placement, a better estimate based on distance between placed primitives. Still uses estimated routing delays. Much more accurate than post-synthesis.
- After routing, the actual timing, using committed routing resources and their precisely-characterized delays. This is the real answer.
The question timing analysis answers is: for every path in the design, does the signal arrive at its destination flip-flop's D input before the setup window of that flip-flop's clock edge? If yes, the path has positive slack (margin to spare). If no, the path has negative slack (it fails).
The path with the least slack is the critical path. Closing timing means raising the worst negative slack (WNS) above zero, so that every path meets its timing requirement. The critical path is the single most important thing to understand about your design; optimizing anything else is wasted effort until the critical path has slack.
Timing analysis is the main feedback mechanism from the tools back to you. It tells you which paths are slow, which logic is on those paths, and how much slack each path has. Reading a timing report is a skill on its own (it's the subject of a dedicated article in this subject, FDP 104), but the basic loop is:
- Look at the worst failing path.
- Identify which RTL blocks the path traverses.
- Decide how to fix it (shorter combinational path, pipelining, floorplanning, better constraints, different synthesis options, sometimes different RTL entirely).
- Rebuild. Repeat until all paths pass.
This loop can be quick (minutes per iteration on a small design) or punishing (hours per iteration on a large one), which is why experienced FPGA engineers get very good at making informed changes each iteration rather than trial-and-error tweaks.
A worked example: the life of a simple module
Let's trace a small piece of logic through the entire pipeline. Consider this trivial Verilog:
module counter #( parameter WIDTH = 8) ( input wire clk, input wire rst_n, input wire enable, output reg [WIDTH-1:0] count); always @(posedge clk or negedge rst_n) begin if (!rst_n) count <= {WIDTH{1'b0}}; else if (enable) count <= count + 1'b1; endendmoduleInstantiated as counter #(.WIDTH(16)) my_counter (...), here's what each phase does:
Elaboration: Parses the module, substitutes WIDTH = 16, creates an elaborated instance with a 16-bit count register, 16-bit adder, and the reset/enable logic. The {WIDTH{1'b0}} replication becomes 16'b0. No errors.
Synthesis: Recognizes the always block as an edge-triggered flip-flop register bank with async reset. Creates 16 flip-flops, a 16-bit incrementer (probably implemented as a ripple-carry or fast-carry adder depending on the synthesizer), a mux selecting between count + 1 and the current count based on enable, and reset logic tying all 16 flip-flop async-reset pins to !rst_n. Output is a generic gate-level netlist.
Technology mapping: Maps the 16 flip-flops to 16 physical flip-flops in the fabric (likely distributed across two slices). Maps the 16-bit adder to the FPGA's dedicated carry-chain logic, which gives a very fast ripple carry. Maps the enable mux into the LUTs that drive the flip-flop D inputs; on a modern FPGA, the LUTs driving each flip-flop implement the "if enable, count+1, else count" logic, often combined with the adder carry output. Resource cost: ~16 LUTs, 16 FFs, some carry chain. Tiny.
Placement: Places the 16-bit counter in a contiguous column, typically, so that the carry chain can run vertically through dedicated fast carry resources. This matters; if the placer scattered the 16 flip-flops across the chip, the carry chain would have to snake through slow general-purpose routing, and the adder would be much slower.
Routing: Connects clk to all 16 flip-flops via the global clock network (a dedicated low-skew resource). Connects rst_n to the async reset inputs. Connects enable to the LUTs driving the D inputs. Connects the carry outputs through the dedicated carry resources. Routes the output count bus to wherever downstream logic consumes it.
Timing analysis: Reports the setup slack for the clock-to-clock paths through each of the 16 flip-flops. For a 16-bit counter using dedicated carry, even at high clock rates the carry chain is fast enough that slack is easily positive, and this design will close timing at hundreds of MHz without any trouble.
Bitstream: The configuration bits for the 16 flip-flops (initial value, reset polarity), the LUT truth tables, the carry-chain mode bits, the routing-switch settings, and all the clock-network configuration get written into the bitstream.
From RTL to bitstream, this tiny counter touches every phase. For a real design with tens of thousands of flip-flops and hundreds of thousands of LUTs, the same phases apply, just at vastly greater scale, with each phase running for minutes or hours rather than milliseconds.
Reading a build log like a grown-up
Once you understand the phases, build logs become readable. Each phase announces itself, reports what it did, warns about what concerned it, and, if it failed, tells you why. The structure is consistent across vendors:
- Elaboration messages are language-level: syntax, port widths, undeclared identifiers, missing modules. Early in the log.
- Synthesis messages are design-level: inferred latches (almost always a bug), inferred structures you didn't intend (e.g., "inferred a 256-entry ROM" when you expected combinational logic), optimized-away logic, truncated constants. Middle of the log.
- Technology-mapping messages are device-level: resource utilization, device capacity, hard-block inferences (BRAM, DSP), I/O buffer choices.
- Placement and routing messages are physical: unroutable nets, congestion warnings, placement constraint violations.
- Timing messages appear repeatedly, with more precision each time: failing paths, critical path reports, setup/hold slack summaries.
When a build fails, scroll back to find the first error. Everything after the first error is often noise; downstream phases are confused by bad input from upstream. The first error is usually the real one.
When a build succeeds but produces something that doesn't work, the order of suspicion is inverted: look first at timing, then at synthesis inferences (did it actually build the hardware you expected?), then at elaboration warnings (did parameters propagate correctly?). The closer to the bottom of the stack, the more likely it's a true physical issue; the closer to the top, the more likely it's a modeling mistake.
Taking it away
The FPGA build is not a black box. It is six clearly-separable phases, each with a specific job:
- Elaboration turns source files into a resolved design hierarchy.
- Synthesis turns behavioral RTL into a generic gate-level netlist.
- Technology mapping rewrites that netlist in terms of FPGA-specific primitives.
- Placement assigns each primitive to a physical location.
- Routing connects the placed primitives using the chip's interconnect.
- Bitstream generation encodes everything into the final configuration file.
Timing analysis runs throughout, with progressively better accuracy, and provides the feedback loop that drives iteration.
Most debugging becomes dramatically easier once you can identify which phase is complaining. An elaboration error isn't a timing problem; a timing problem isn't a synthesis issue; an unroutable net isn't solved by rewriting RTL. Matching the fix to the phase is the difference between productive debugging and spray-and-pray.
More importantly, understanding the pipeline reframes how you write RTL. You start to think about how synthesis will interpret your code, how technology mapping will choose primitives, how placement will lay out your design, how routing will connect it. You stop writing RTL in a vacuum and start writing RTL for a pipeline that will turn it into silicon. That shift (from "code that compiles" to "code that becomes a good chip") is the single most important transition in becoming an effective FPGA engineer.
The rest of the articles in this subject go deeper into specific phases: the development loop (FDP 102) makes iterating productive, constraints (FDP 103) and timing reports (FDP 104) teach you to command the toolchain's most important feedback mechanism, architecting pipelined designs (FDP 105) shapes your RTL to flow well through the pipeline you now understand, IP integration (FDP 106) and board bring-up (FDP 107) handle the real-world frictions, and the project lifecycle (FDP 108) zooms back out to the process as a whole.
But it all rests on this one mental shift: the build isn't one thing. It's six, and knowing which one is talking to you is the foundation of everything else.