What synthesis does with your code

Tell which lines of a design become gates and which only the simulator sees, and read the cells, flops and levels of logic the Synthesize button reports.

Lesson
R1.1
Time
20 min
Runs on
Your browser
Check
5 questions

A FIFO designer added a check inside the RTL: when the count passed 16, an always_comb block printed ERROR. Every simulation either ran clean or printed the error, so the team called the block self-checking. On the board an overflow came and went without a trace. The synthesizer had turned the check into a cell with inputs and no output.

The check as a print

always_comb begin
  err = 1'b0;
  if (count > 5'd16)
    $display("ERROR: count=%0d", count);
end

Yosys 0.70 builds four gates for count > 16 and feeds them into one $print cell, which has no output. The err pin is wired to 0.

The check as a pin

assign err = (count > 5'd16);

The same four gates drive err. The board can read it, count it or raise an interrupt with it.

One file, two readers

Your design file has two readers. The simulator runs it as a program: every statement executes, in order and in time, and every $display prints. The synthesizer reads the same text as a description of hardware. It keeps what gates and flip-flops can build: assign statements, always_comb and always_ff blocks, module instances and parameters. Everything else it drops, keeps as a marker, or refuses.

Run and Synthesize read different filesArrows carry source text into a tool and results out of it. Run reads the design and the bench; Synthesize reads the design alone.
Run and Synthesize read different filesArrows carry source text into a tool and results out of it. Run reads the design and the bench; Synthesize reads the design alone. design mux2 design what you edit bench tb bench read-only stimulus component Elaboration parameters,hierarchy, top component Simulator runs both files in time component Synthesis generic cells andwires component Run log printed lines component Gates tab cells, flops, latches,levels sourcesourceprintssourceone modulenetlist
  1. Run

    The simulator compiles the design and the bench together and runs them. The initial in the design prints mux ready before the first edge.

    Follow the calls, values and waits
    1. data mux ready: Simulator → Run log
  2. Elaborate

    Synthesize starts from the design alone. Elaboration picks the top module, resolves its parameters and connects its instances into one hierarchy. The bench never enters.

  3. Synthesize

    Synthesis turns statements into a netlist: cells and the wires between them. For this design: eight 2:1 mux cells and one $print cell.

    Follow the calls, values and waits
    1. data 8 MUX + 1 $print: Synthesis → Gates tab
Read the diagram as text
  • mux2 design: what you edit
  • tb bench: read-only stimulus
  • Elaboration: parameters, hierarchy, top
  • Simulator: runs both files in time
  • Synthesis: generic cells and wires
  • Run log: printed lines
  • Gates tab: cells, flops, latches, levels
  • mux2 design → Simulator: source
  • tb bench → Simulator: source
  • Simulator → Run log: prints
  • mux2 design → Elaboration: source
  • Elaboration → Synthesis: one module
  • Synthesis → Gates tab: netlist

The bench reaches the simulator only. A line that needs time or prints text has nothing to become in the netlist.

A real flow adds a third step, technology mapping: it replaces each generic cell by a cell from a vendor's library. This site stops before that step. The Gates tab reports generic cells such as $_MUX_, $_AND_ and $_DFF_P_.

What the synthesizer keeps, marks and refuses

Line in the designWhat Yosys 0.70 does on this flow
assign y = s ? b : a; (8 bits)builds 8 $_MUX_ cells, 1 level of logic
always_ff that registers yadds 8 flip-flops: 16 cells, still 1 level
initial $display("mux ready");keeps one $print cell with no output: 9 cells
initial q = 8'h5a; on a registerbuilds the 8 flops and keeps 01011010 only as an init attribute on q
initial #5 $display(...) or assign #5 y = ...refuses: unsynthesizable timing control
#2 inside always_combrefuses: statements that pass time are not allowed in this context
$finish or $urandom in a clocked blockrefuses: unsupported system task

Measured on the site's flow, one small design per row. Lint (Verilator 5.052) says nothing about the delays or the prints.

Three outcomes, then. A line that describes logic or state becomes cells. A print becomes a $print cell, kept so that a simulation of the netlist can still print. That cell has three inputs and no output, so nothing on the chip can read it. A delay or a wait is refused, because a gate cannot be told to wait 5 ns.

The initial value on a register is the odd case. Some FPGAs load such a value when they power up, so Yosys keeps it as an attribute. An ASIC flop has no such load. Give ASIC registers their first value with a reset (R2.2).

Three buttons and the numbers they report

Every lab from here on has three buttons. Run simulates the design with its bench. Lint asks Verilator to read the design alone and warn about likely mistakes: widths that do not match, unused signals, latches. Synthesize asks Yosys to build the design and reports what it built. A design is done when all three agree with what the design is meant to do.

NumberWhat it countsPlain muxRegistered mux
Cellsevery gate and flop in the netlist816
Flopsflip-flops: state that changes on a clock edge08
Latchesstate that follows its input while an enable is high (R1.3)00
Levels of logicthe longest chain of cells between flops and ports11

Measured with Yosys 0.70 on this flow, with the initial removed. The registered mux puts a flop after each mux.

The registered mux still reports 1 level. The longest chain runs from an input pin through one mux to a flop, and the count stops at the flop. That is the meaning of levels of logic on this flow: a count of cells in a row, never a delay in nanoseconds. Fewer levels allow a faster clock (R4.1).

  1. Run and Synthesize as shipped

    Run prints mux ready and four lines. Synthesize reports 9 cells: eight $_MUX_ and one $print, 1 level of logic.

  2. Add a delay

    Write initial #5 $display("mux ready");. Run still prints the greeting. Synthesize refuses with unsynthesizable timing control.

  3. Delete the initial

    Run loses one line. The Gates tab drops to 8 cells, and nothing else changes.

  4. Register the output

    Add clk and rst_n ports and write y in an always_ff block. Flops go from 0 to 8, cells to 16, levels stay at 1.

Asked in interviews
  1. What does synthesis do with an initial block?

    Most synthesizers ignore or reject it, because it is a verification construct. Yosys refuses a delay inside it, keeps a $display as a cell with no output, and keeps a register value only as an FPGA power-up attribute.

  2. Why can a testbench never be synthesized?

    It is built from time: delays, waits for events, $finish, random stimulus. None of these has a gate. It also has no ports, so it describes no block to build.

  3. What is the difference between RTL and gate level?

    RTL describes registers and the transfers between them with operators and statements. A gate-level netlist lists cells and the wires between them. Synthesis turns the first into the second.

Try it

The first line comes from the design's initial, before the first clock edge. Each later line shows the inputs the bench applied before edge k and the y they produced: a when s is 0, b when s is 1.

Synthesize reports 9 cells. Delete the initial and the count drops to 8. Register y with an always_ff and the bench prints the same four lines, because it prints after each edge. The Gates tab tells the two apart: 8 flops, 16 cells, 1 level.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: mux ready e2 s=0 a=3c b=a5 y=3c e3 s=1 a=3c b=a5 y=a5 e4 s=0 a=0f b=f0 y=0f e5 s=1 a=0f b=f0 y=f0

Original design · 9 cells · 0 flip-flops · 0 latches · 1 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic [7:0] a = 0, b = 0;
  logic       s = 0;
  logic [7:0] y;
  mux2 dut (.*);                          // ports connect to the signals of the same name
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    if (rst_n) $display("e%0d s=%b a=%h b=%h y=%h", edge_n, s, a, b, y);
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    a = 8'h3c; b = 8'ha5; s = 0; tick();
    s = 1;                       tick();
    a = 8'h0f; b = 8'hf0; s = 0; tick();
    s = 1;                       tick();
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe design waits 5 time units in an initial and prints. The bench flips s at time 3. What is the output line?
module mux2 (input logic [7:0] a, b, input logic s, output logic [7:0] y);
  assign y = s ? b : a;
  initial #5 $display("y=%0d at %0t", y, $time);
endmodule
module tb;
  logic [7:0] a = 10, b = 20;
  logic       s = 0;
  logic [7:0] y;
  mux2 dut (.a(a), .b(b), .s(s), .y(y));
  initial begin
    #3 s = 1;
    #10 $finish(0);
  end
endmodule
Spot the bugThe overflow flag err must be 1 whenever count is above 16, so the chip can raise an interrupt. Each option is the body of the checking module. Which one fails the bench?
ConceptYosys 0.70 on this site reads four versions of the mux. Which one does it build?
ConceptThe registered mux reports 16 cells, 8 flops and 1 level of logic. What does the 1 count?
ConceptA register is declared with initial q = 8'h5a; and written in an always_ff block. What does Yosys 0.70 build?

Remember

  • The simulator runs every line of a design; the synthesizer keeps only what gates and flops can build.
  • On this flow a $display becomes a $print cell with no output, and any delay or wait is refused with an error.
  • A check the board must see needs an output port; a print checks only simulations.
  • Synthesize reports cells, flops, latches and levels of logic: the longest chain of cells between flops and ports, a count and never a delay.