← All guides

What synthesis does to Verilog, pass by pass

Synthesis turns HDL into a netlist: a graph whose nodes are cells, such as adders, multiplexers and flip-flops, and whose edges are the wires between them. A synthesis tool builds this graph from the code, then rewrites it in a series of passes until every cell is a primitive the target device provides. The graph between passes is the tool’s intermediate representation (IR).

This page follows a 4-bit counter through each pass of Yosys, an open-source synthesis tool that can write its IR, called RTLIL, as text after any pass, and compares the result with Vivado’s elaborated and synthesized designs.

The counter

module counter (
  input  wire       clk,
  input  wire       rst,
  input  wire       en,
  output reg  [3:0] count
);
  always @(posedge clk) begin
    if (rst)
      count <= 4'd0;
    else if (en)
      count <= count + 4'd1;
  end
endmodule

On each rising edge of clk, count is cleared when rst is high and increases by one when en is high.

The passes

StepPassWhat changesCells
1read_verilogThe text becomes a syntax tree, then RTLIL. The always block is kept as a process.1, and a process
2procThe process becomes multiplexers and a flip-flop.4
3optThe reset and the enable are folded into the flip-flop.2
4techmapEvery cell becomes one-bit gates.47
5synth_xilinxThe logic is minimised and mapped to Xilinx 7-series primitives.6

1. Parsing: the syntax tree and the process

The parser first reads the text into a syntax tree. Each construct in the code is a node, and the parts it is made of are its children: an assignment has the signal it writes and the expression it writes, an addition has its two operands. Names and numbers are the leaves.

AST_MODULEmodule counterAST_WIRE clkinput clkAST_WIRE rstinput rstAST_WIRE eninput enAST_WIRE countoutput reg [3:0] countAST_ALWAYSalways @(posedge clk)AST_POSEDGEposedge clkclkAST_CASEif (rst)rstAST_COND 1'b1thenAST_ASSIGN_LEcount <= 4'd0count4'b0000AST_COND defaultelseAST_CASEif (en)enAST_COND 1'b1thenAST_ASSIGN_LEcount <= count + 4'd1countAST_ADDcount + 4'd1count4'b0001
The counter's syntax tree, from Yosys's AST dump. Each if is stored as a case on its condition (highlighted). Wrapper blocks, and the step that reduces each condition to one bit, are left out.

The tree is then converted to RTLIL. An expression becomes a cell at once: the AST_ADD node is an $add cell. The always block is not converted to hardware yet. It is stored as a process: the decision tree of the if statements as switch rules, and a sync rule that says when the result is stored.

process $proc$counter.v:7$1
  assign $0\count[3:0] \count
  switch \rst
    case 1'1
      assign $0\count[3:0] 4'0000
    case
      switch \en
        case 1'1
          assign $0\count[3:0] $add$counter.v:11$2_Y
        case
      end
  end
  sync posedge \clk
    update \count $0\count[3:0]
end

$0\count[3:0] is the value count will take. It starts as count, becomes 0 when rst is 1, becomes the adder’s output when en is 1, and is stored in count on the rising edge of clk. A process has no schematic, because it describes behaviour rather than cells.

2. proc: from process to cells

proc converts each process into cells. Each switch becomes a multiplexer and the sync rule becomes a flip-flop.

clk rst en count 1 0x0 1234
The counter after proc: an adder, two multiplexers and a flip-flop.
  1. Adder. count + 1.
  2. Enable. Passes the adder's output when en is 1, and count when it is 0.
  3. Reset. Passes 0 when rst is 1.
  4. Register. A 4-bit flip-flop ($dff) clocked by clk.

Each cell keeps a src attribute: the lines and columns of the code it came from. The reset multiplexer in RTLIL:

attribute \src "counter.v:8.9-8.12|counter.v:8.5-11.29"
cell $mux $procmux$6
  parameter \WIDTH 4
  connect \A $procmux$3_Y
  connect \B 4'0000
  connect \S \rst
  connect \Y $0\count[3:0]
end

A is selected when S is 0 and B when S is 1. Latches are created in this pass: in a combinational process, a path through the decision tree that assigns nothing becomes a $dlatch. See inferred latches in Verilog.

This is the As-Written view in the RapidRTL web editor.

3. opt: optimisation

opt runs a set of passes that fold constants, remove unused cells and merge identical ones. On this design the pass that acts is opt_dff. A multiplexer in front of a flip-flop that selects a constant when a signal is high is a synchronous reset. A multiplexer that selects the flip-flop’s own output is an enable. Both are folded into the flip-flop.

$sdffe CLK D EN SRST Q clk rst en count 1 12
The counter after opt: a flip-flop with reset and enable, and the adder.
  1. Register with reset and enable. $sdffe: a flip-flop with a synchronous reset (SRST) and an enable (EN).
  2. Adder. Unchanged.
cell $sdffe $auto$ff.cc:337:slice$10
  parameter \SRST_VALUE 4'0000
  parameter \WIDTH 4
  connect \CLK \clk
  connect \EN \en
  connect \SRST \rst
  connect \D $add$counter.v:11$2_Y
  connect \Q \count
end

The folded form matches the device: an FPGA flip-flop has its own clock-enable and reset pins, so the reset and the enable cost no logic. This is the High-Level view.

4. techmap: generic gates

techmap replaces each multi-bit cell with one-bit gates from Yosys’s internal library: $_AND_, $_OR_, $_XOR_, $_NOT_, $_MUX_ and $_DFF_P_. In this view the reset and the enable are first split out of the register again, so every cell is a basic gate.

clk rst en count 0 1 0 1 2 3
The counter as 47 generic gates, before any logic minimisation.
Came fromGates
the adder12 $_XOR_, 9 $_AND_, 6 $_OR_, 4 $_NOT_, 4 $_MUX_
the reset and the enable8 $_MUX_
the register4 $_DFF_P_

The adder is a general 4-bit adder: one input is still the constant 1, and it keeps the logic that would invert an input for a subtraction. Nothing has been simplified yet. This is the Gate-Level view.

5. Mapping to the device

An FPGA builds logic from two kinds of block. A LUT (look-up table) is a small memory holding the truth table of any function of up to six inputs on 7-series devices. A carry chain is dedicated logic that passes the carry of an addition from bit to bit, faster than LUTs could.

synth_xilinx runs the full flow for Xilinx 7-series: passes like the ones above, then abc, which minimises the logic and maps it to LUTs, and passes that map flip-flops and carry chains to the device’s primitives.

INV I O CARRY4 CI CYINIT DI S CO O FDRE C CE D R Q FDRE C CE D R Q FDRE C CE D R Q FDRE C CE D R Q clk rst en count 0 0x1 0 1:3 0 1 2 0 1:3 0 1 2 3 123
The counter on Xilinx 7-series: 4 FDRE, 1 INV and 1 CARRY4.
  1. Register. Four FDRE flip-flops, one per bit, with en on CE and rst on R: the $sdffe from step 3.
  2. Inverter. Bit 0 of count, inverted.
  3. Adder. One CARRY4: four bits of carry chain.

Adding 1 to a binary number inverts bit 0, and inverts every higher bit whose lower bits are all 1. With c = count and n = count + 1:

n0=¬c0 ni=ci⊕(ci−1∧⋯∧c0)

The AND of the lower bits is the carry into bit i. The CARRY4 computes it: its S inputs are count[3:1] and the inverted bit 0, and each output bit is S XOR the carry into that bit. The 47 gates of step 4 become 6 primitives.

RapidRTL draws this mapping when an FPGA family is set as the target (free in the web editor, part of Pro in the VS Code extension).

Comparison with Vivado

The same file synthesized with Vivado 2025.1 for an Artix-7 (xc7a35t).

Elaborated design

Vivado's elaborated schematic of counter: an RTL_ADD adder whose I0 input is count and whose I1 input is tied to 1, feeding the D input of a 4-bit RTL_REG_SYNC register named count_reg[3:0], drawn as a stack. The register has clk on C, en on CE and rst on RST, and its output is count.
Vivado's elaborated design: one adder (RTL_ADD) and one 4-bit register (RTL_REG_SYNC), drawn as a stack.

The register has en on CE and rst on RST, and the constant 1 is the tie-high on the adder’s I1 input. There are no multiplexers: Vivado recognises the reset and the enable during elaboration, so its elaborated design corresponds to step 3, not step 2.

Synthesized design

Vivado's synthesized schematic of counter: input buffers on clk, en and rst, a BUFG on the clock, four FDRE flip-flops with en on CE and rst on R, one LUT1, LUT2, LUT3 and LUT4 feeding the flip-flops' D inputs, and four output buffers driving count.
Vivado's synthesized design: four FDRE, one each of LUT1 to LUT4, and the pin buffers. Click it for the full-size image.
PrimitiveYosysVivadoFunction
FDRE44the register, en on CE and rst on R
CARRY410count + 1 on the carry chain
INV10bit 0 of count + 1
LUT1 to LUT404count + 1, one LUT per bit
IBUF, OBUF, BUFGnot drawn3, 4, 1a buffer on every pin and on the clock

The register is the same in both netlists. The adder is not: Vivado puts each bit of the equation above in its own LUT. Bit i depends on i + 1 bits of count, so bit 0 is a LUT1, bit 1 a LUT2, and bit 3 a LUT4. Their INIT values are the truth tables: LUT2 has INIT = 6, binary 0110, which is XOR. Vivado packs these four into two physical LUTs, each with two outputs, so its utilisation report counts 2 LUTs. Both netlists compute the same function.

The same stages in other tools

Every synthesis tool works on a netlist graph and refines it in steps. The tools differ in which steps they show.

ToolGeneric netlistMapped netlist
YosysRTLIL after any pass (write_rtlil)device primitives, after synth_xilinx, synth_ice40 and others
Vivadoelaborated design (RTL_ADD, RTL_MUX, RTL_REG)synthesized design (LUT, FDRE, CARRY4)
QuartusRTL Viewer, after Analysis & ElaborationTechnology Map Viewer, after mapping and after fitting
Design Compiler (ASIC)GTECH generic gatescells of the target standard-cell library

Yosys writes its whole IR as text after any pass. Vivado and Quartus show their netlists as schematics at fixed points, and Vivado’s Tcl console can query them with get_cells and get_nets. Their internal formats are not published.

Synthesis ends with mapped primitives. Place and route then assigns each primitive a location on the device and each net a path through the routing. In Vivado this is implementation. For Yosys netlists, nextpnr does it for several FPGA families.

Next steps