Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • RTL Foundations36
  • Combinational Design25
  • Sequential Design27
  • FSM & Control27
  • Timing & Clocking39
  • Reset & CDC34
  • Memory & Buses57
  • Advanced RTL28
    • Parameterization and Generate Hub
    • Top-Level Integration Hub
    • Synthesis Intent Hub
      • Inference-focused coding style
      • Inferring flip-flops and RAM
      • Latch avoidance and case semantics
      • Reading a synthesis report
    • Style and Static Checks Hub
    • Assertions in RTL Hub
    • Formal Verification Basics Hub
    • Reusable SystemVerilog for RTL
  • Senior & Interview76
  • Glossary and Notation
Loading lesson

‹Synthesis Intent Hub
  1. Topics
  2. RTL Design
  3. Advanced RTL
  4. Synthesis Intent Hub
  5. Reading a synthesis report

Part 8 · Advanced · Advanced

Reading a synthesis report

The synthesis log and the stat report are a free code review: five things to look for, in order, on a block seeded with five real bugs.

On this page

    Why this matters

    A block can synthesise with exit status 0 and a clean final check while it loses a status bit, an error flag and an interrupt register. The log and the stat table record every one of those losses if you read them in order.

    After this lesson you can: read a Yosys log for latches, driver conflicts, undriven outputs, constant and removed flops, and check the flop count in stat against a count made from the RTL.

    The log is a code review you get for free

    The 58-line block below synthesises with exit status 0, and the last line of the check pass says Found and reported 0 problems. Yet the netlist has 18 flip-flops where the RTL describes 21, four latches nobody asked for, and a status bit tied to 0.

    Every one of those facts is in the log. Synthesis has to understand each line of RTL to build it, so it notices things a quick simulation does not. Read the log and the stat table before you commit, in the same order every time.

    A top-down flow. Step 1, the warning count at the end of the log. Step 2, latch warnings from the proc step. Step 3, the check pass for conflicting or missing drivers. Step 4, optimiser messages about D equals Q, constant bits and resolved conflicts. Step 5, flop and latch counts in stat compared with your own count. If everything is as predicted, commit; otherwise fix the RTL and rerun.
    Five places to look in a Yosys log, in the order they appear, before the design is committed

    Steps 1 to 3 are warnings and are easy to find with grep. Step 4 is not a warning at all, and step 5 needs a number you worked out yourself. The seeded block shows why both are needed.

    A status unit with five seeded bugs

    An event counter with four registers. Software enables it, sets a threshold, reads the count and reads a status register with an overflow bit and an error bit. Each bug below is one that reaches real code reviews.

    evt_unit.v
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    // yosys: latch
    // Event counter with a threshold interrupt and four registers.
    // addr 0: ctrl {irq_en, enable}   addr 1: thresh   addr 2: count (read only)
    // addr 3: status {err, ovf}, a write clears it   addr 4-7: unmapped, a write sets err
    module evt_unit (
      input  wire       clk,
      input  wire       rst_n,
      input  wire       wr_en,
      input  wire [1:0] addr,
      input  wire [7:0] wdata,
      input  wire       evt,
      output reg  [7:0] rdata,
      output wire       irq,
      output reg  [3:0] level,
      output wire       busy
    );
      reg [1:0] ctrl;
      reg [7:0] thresh, count;
      reg       ovf, err, hit_q;
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          ctrl <= 2'b00; thresh <= 8'd0;
        end else if (wr_en) begin
          if (addr == 2'd0) ctrl   <= wdata[1:0];
          if (addr == 2'd1) thresh <= wdata;
          if (addr == 2'd3) ovf    <= 1'b0;
        end
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          count <= 8'd0; ovf <= 1'b0;
        end else if (ctrl[0] && evt) begin
          count <= count + 8'd1;
          if (count == 8'hff) ovf <= 1'b1;
        end
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n)                   err <= 1'b0;
        else if (wr_en && addr == 4) err <= 1'b1;
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n) hit_q <= 1'b0;
        else        hit_q <= (count >= thresh);
    
      always @(*)
        case (addr)
          2'd0:    rdata = {6'd0, ctrl};
          2'd1:    rdata = thresh;
          2'd2:    rdata = count;
          default: rdata = {6'd0, err, ovf};
        endcase
    
      always @(*)
        if (ctrl[0]) level = count[7:4];
    
      assign irq = ctrl[1] & (count >= thresh);
    endmodule

    Synthesise it and keep the log, then pull out every warning:

    Diagram
    yosys -p "read_verilog evt_unit.v; synth -top evt_unit; check" > before.log
    grep -i warning before.log
    Diagram
    Warning: Latch inferred for signal `\evt_unit.\level' from process `\evt_unit.$proc$evt_unit.v:54$19': $auto$proc_dlatch.cc:547:proc_dlatch$60
    Warning: multiple conflicting drivers for evt_unit.\ovf:
    Warning: Wire evt_unit.\busy is used but has no driver.
    Warning: Driver-driver conflict for \ovf between cell $auto$ff.cc:337:slice$99.Q and constant 1'0 in evt_unit: Resolved using constant.
    ABC: Warning: The network is combinational (run "fraig" or "fraig_sweep").
    Warnings: 4 unique messages, 4 total

    The last line is the summary: four unique warnings. The ABC line is the internal logic optimiser talking about its own script; it appears for every design, including the clean one later, and says nothing about your RTL. Now take the findings one by one.

    1. A latch on level

    The first warning names the signal, level, and line 54, the second always @(*). When ctrl[0] is 0 nothing assigns level, so it must hold its value: four latches, one per bit, which stat lists as $_DLATCH_P_. Fix: assign level on every path.

    2. Two blocks drive ovf

    The check pass inside synth lists both flops that drive ovf: one from the register-write block (clear on a write to address 3) and one from the counter block (set on wrap).

    Diagram
    2.5. Executing CHECK pass (checking for obvious problems).
    Checking module evt_unit...
    Warning: multiple conflicting drivers for evt_unit.\ovf:
        port Q[0] of cell $procdff$97 ($dff)
        port Q[0] of cell $procdff$80 ($adff)
    Warning: Wire evt_unit.\busy is used but has no driver.
    Found and reported 2 problems.
    ...
    3. Executing CHECK pass (checking for obvious problems).
    Checking module evt_unit...
    Found and reported 0 problems.

    Real hardware cannot have two flops driving one wire, and simulation lets the last writer win, so the two disagree. Later the optimiser settles it on its own: the fourth warning says the conflict was Resolved using constant. The status bit now always reads 0. Notice the final check: by then the conflict is gone from the netlist, so it reports 0 problems. Fix: one block owns each register; put set and clear in the same block with a stated priority.

    3. busy has no driver

    Wire evt_unit.\busy is used but has no driver. An output port counts as used. Someone declared the port and never wrote the logic. The netlist drives it with a constant, so the chip is never busy. Fix: drive it, or delete the port.

    4. err can never be set

    No warning mentions err. The clue is an optimiser message about a cell name, which the proc step ties back to a signal:

    Diagram
    Creating register for signal `\evt_unit.\err' using process `\evt_unit.$proc$evt_unit.v:38$11'.
      created $adff cell `$procdff$70' with positive edge clock and positive level reset.
    ...
    Handling D = Q on $procdff$70 ($adff) from module evt_unit (removing D path).
    Setting constant 0-bit at position 0 on $procdff$70 ($dlatch) from module evt_unit.

    Handling D = Q means the flop's next value is always its current value, so it can never change and is replaced by its reset value, 0. The cause is width: addr is 2 bits, so addr == 4 is never true. Yosys does not warn about the comparison; a lint run does. Fix: make addr 3 bits, as the address map says.

    5. hit_q is optimised away

    hit_q is computed every cycle but nothing reads it: irq uses the combinational compare instead. Its only trace in the log is a count:

    Diagram
    Creating register for signal `\evt_unit.\hit_q' using process `\evt_unit.$proc$evt_unit.v:42$15'.
      created $adff cell `$procdff$65' with positive edge clock and positive level reset.
    ...
    2.4. Executing OPT_CLEAN pass (remove unused cells and wires).
    Finding unused cells or wires in module \evt_unit..
    Removed 14 unused cells and 53 unused wires.

    There is no warning, because removing unused logic is the optimiser's job. The bug is that irq comes straight from a comparator instead of from a flop, a glitchy, timing-hungry output. Only the flop count in stat gives it away.

    Read stat by counting the flops yourself

    Count the state bits from the declarations before you look: ctrl 2, thresh 8, count 8, then ovf, err and hit_q 1 each. That is 21 flops and 0 latches.

    Cell in stat

    Before

    After the fixes

    What the change tells you

    $_DFFE_PN0P_ (flop, async reset, enable)

    18

    20

    Before: only ctrl, thresh and count survived. ovf and err are back after the fixes.

    $_DFF_PN0_ (flop, async reset)

    0

    1

    hit_q: no enable, because it updates every cycle

    $_DLATCH_P_ (latch)

    4

    0

    The four bits of level

    All cells

    102

    120

    More logic after the fixes, because more logic is now used

    A flop count lower than yours means logic was removed: something is unread, constant or driven twice. A higher count means storage you did not intend, such as a combinational block that became a latch or an array built from flops instead of a RAM. Any latch in a design without latches in its specification is a bug.

    Do not stop at the last line of the log. Run check -assert straight after proc, before the optimiser has had a chance to tidy problems away, and the run fails instead of passing:

    Diagram
    yosys -p "read_verilog evt_unit.v; hierarchy -top evt_unit; proc; check -assert"
    ...
    Warning: multiple conflicting drivers for evt_unit.\ovf:
        port Q[0] of cell $procdff$80 ($adff)
        port Q[0] of cell $procdff$97 ($dff)
    Warning: Wire evt_unit.\busy is used but has no driver.
    ERROR: Found 2 problems in 'check -assert'.

    That command exits with status 1, so it can gate a commit or a CI job.

    The fixed block and its clean log

    The comments mark the five fixes. The status bits are now set and cleared in one block each, and irq comes from the registered hit_q.

    evt_unit.v
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    // yosys: ok
    // Event counter with a threshold interrupt and four registers.
    // addr 0: ctrl {irq_en, enable}   addr 1: thresh   addr 2: count (read only)
    // addr 3: status {err, ovf}, a write clears it   addr 4-7: unmapped, a write sets err
    module evt_unit (
      input  wire       clk,
      input  wire       rst_n,
      input  wire       wr_en,
      input  wire [2:0] addr,                        // fix 4: three bits reach 4-7
      input  wire [7:0] wdata,
      input  wire       evt,
      output reg  [7:0] rdata,
      output wire       irq,
      output reg  [3:0] level,
      output wire       busy
    );
      reg [1:0] ctrl;
      reg [7:0] thresh, count;
      reg       ovf, err, hit_q;
      wire      clr_status = wr_en && addr == 3'd3;
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          ctrl <= 2'b00; thresh <= 8'd0;
        end else if (wr_en) begin
          if (addr == 3'd0) ctrl   <= wdata[1:0];
          if (addr == 3'd1) thresh <= wdata;
        end
    
      always @(posedge clk or negedge rst_n)          // fix 2: one block owns ovf
        if (!rst_n) begin
          count <= 8'd0; ovf <= 1'b0;
        end else begin
          if (ctrl[0] && evt) count <= count + 8'd1;
          if (ctrl[0] && evt && count == 8'hff) ovf <= 1'b1;
          else if (clr_status)                  ovf <= 1'b0;
        end
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n)                   err <= 1'b0;
        else if (wr_en && addr[2])    err <= 1'b1;
        else if (clr_status)          err <= 1'b0;
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n) hit_q <= 1'b0;
        else        hit_q <= (count >= thresh);
    
      always @(*)
        case (addr)
          3'd0:    rdata = {6'd0, ctrl};
          3'd1:    rdata = thresh;
          3'd2:    rdata = count;
          3'd3:    rdata = {6'd0, err, ovf};
          default: rdata = 8'd0;
        endcase
    
      always @(*) level = ctrl[0] ? count[7:4] : 4'd0;  // fix 1: every path assigns
      assign busy = ctrl[0] & ~ovf;                      // fix 3: the output is driven
      assign irq  = ctrl[1] & hit_q;                     // fix 5: registered interrupt
    endmodule
    Diagram
    yosys -p "read_verilog evt_unit_fixed.v; synth -top evt_unit; check" > after.log
    grep -i "warning\|latch inferred" after.log
    No latch inferred for signal `\evt_unit.\level' from process `\evt_unit.$proc$evt_unit_fixed.v:57$21'.
    No latch inferred for signal `\evt_unit.\rdata' from process `\evt_unit.$proc$evt_unit_fixed.v:48$20'.
    ABC: Warning: The network is combinational (run "fraig" or "fraig_sweep").
    Diagram
    === evt_unit ===
    ...
          120 cells
            5   $_ANDNOT_
           30   $_AND_
           20   $_DFFE_PN0P_
            1   $_DFF_PN0_
           38   $_NAND_
            1   $_NOR_
            2   $_NOT_
           16   $_ORNOT_
            7   $_XOR_

    No warnings from Yosys, both combinational processes confirmed latch-free, 21 flops as counted, and the same check -assert command now exits with status 0.

    Warning

    Limits: Yosys maps to generic gates with no area or delay, so these cell counts are a structural check, not an area or timing report. A production flow maps to a characterised standard-cell library and adds area per cell, timing against the clock constraints, and power. Read those reports too, but the structural findings above come first: no library fixes a lost flop.

    Common mistakes

    Common pitfall

    Treating warnings as noise. A design that synthesises is not a design that synthesised what you wrote. Here the exit status was 0, the final check reported 0 problems, and the netlist had lost the overflow bit, the error bit and the interrupt register. Keep the warning count at zero, so a new warning stands out, and compare the flop count with your own every time.

    Check yourself After a refactor of the fixed evt_unit, the warning count is still zero but $_DFFE_PN0P_ drops from 20 to 12. What do you suspect, and where do you look?

    Eight flops are gone, the width of one 8-bit register: thresh or count. A register vanishes without a warning when nothing reads it (removed as unused) or when its next value can never change (D = Q, then constant). Search the log for Handling D = Q and Setting constant, then check which reads of thresh and count the refactor changed. In this block thresh is read by the compare and by rdata, so look for a dropped read path.

    Check yourself Why is the final check in the before log not enough, even though it ran after synthesis?

    Because the optimiser had already resolved the double driver on ovf by tying it to a constant. The problem was gone from the netlist, so the last check had nothing to report. The check that ran right after proc found both problems; run check -assert there to make them fatal.

    Testbench

    The fixed block with a self-checking bench: register writes, the one-cycle delay of the registered interrupt, level, busy, err on an unmapped write, overflow after 256 events, and clearing the status register.

    evt_unit.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    // Event counter with a threshold interrupt and four registers.
    // addr 0: ctrl {irq_en, enable}   addr 1: thresh   addr 2: count (read only)
    // addr 3: status {err, ovf}, a write clears it   addr 4-7: unmapped, a write sets err
    module evt_unit (
      input  wire       clk,
      input  wire       rst_n,
      input  wire       wr_en,
      input  wire [2:0] addr,                        // fix 4: three bits reach 4-7
      input  wire [7:0] wdata,
      input  wire       evt,
      output reg  [7:0] rdata,
      output wire       irq,
      output reg  [3:0] level,
      output wire       busy
    );
      reg [1:0] ctrl;
      reg [7:0] thresh, count;
      reg       ovf, err, hit_q;
      wire      clr_status = wr_en && addr == 3'd3;
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
          ctrl <= 2'b00; thresh <= 8'd0;
        end else if (wr_en) begin
          if (addr == 3'd0) ctrl   <= wdata[1:0];
          if (addr == 3'd1) thresh <= wdata;
        end
    
      always @(posedge clk or negedge rst_n)          // fix 2: one block owns ovf
        if (!rst_n) begin
          count <= 8'd0; ovf <= 1'b0;
        end else begin
          if (ctrl[0] && evt) count <= count + 8'd1;
          if (ctrl[0] && evt && count == 8'hff) ovf <= 1'b1;
          else if (clr_status)                  ovf <= 1'b0;
        end
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n)                   err <= 1'b0;
        else if (wr_en && addr[2])    err <= 1'b1;
        else if (clr_status)          err <= 1'b0;
    
      always @(posedge clk or negedge rst_n)
        if (!rst_n) hit_q <= 1'b0;
        else        hit_q <= (count >= thresh);
    
      always @(*)
        case (addr)
          3'd0:    rdata = {6'd0, ctrl};
          3'd1:    rdata = thresh;
          3'd2:    rdata = count;
          3'd3:    rdata = {6'd0, err, ovf};
          default: rdata = 8'd0;
        endcase
    
      always @(*) level = ctrl[0] ? count[7:4] : 4'd0;  // fix 1: every path assigns
      assign busy = ctrl[0] & ~ovf;                      // fix 3: the output is driven
      assign irq  = ctrl[1] & hit_q;                     // fix 5: registered interrupt
    endmodule
    

    Remember

    • Latch and driver warnings come early, in proc and check. The final check can report 0 problems after the optimiser has hidden them.

    • A removed or constant flop gives no warning: look for D = Q and constant-bit messages, and compare the flop count with your own.

    • Run check -assert straight after proc so driver problems fail the run instead of scrolling past.

    Rule of thumb: count the flops in the RTL before you open stat; a different number is a bug until it is explained.

    How this was checked

    Tool: Yosys 0.69+post (git sha1 143eb14f9), run on 2026-10-17. Commands: yosys -p "read_verilog evt_unit.v; synth -top evt_unit; check" > before.log; the same for evt_unit_fixed.v > after.log; grep -i warning on both; yosys -p "read_verilog <file>; hierarchy -top evt_unit; proc; check -assert" on both (exit 1 seeded, exit 0 fixed).

    Result: every log and stat line on the page is copied from those runs, trimmed with ... marks. Seeded: 4 unique warnings, 18 $_DFFE_PN0P_ and 4 $_DLATCH_P_ in 102 cells, final check 0 problems. Fixed: no Yosys warnings (only the ABC note), 20 $_DFFE_PN0P_ and 1 $_DFF_PN0_ in 120 cells. That hit_q ($procdff$65) is removed by the first OPT_CLEAN was confirmed with proc; opt_clean; select -list t:$adff t:$dff.

    The statement that lint flags the addr == 4 comparison was checked with Verilator 5.048 --lint-only -Wall (WIDTHEXPAND at line 40); that run did not flag the double driver on ovf. The bench for the fixed block was run with Icarus Verilog 13.0 and prints PASS; it is a directed check of the listed behaviours, not a proof.

    Scope and limits: generic gates with no library, so cell counts are structural, not area or timing; other synthesis tools word and order their messages differently, and may resolve a double driver differently or refuse it.

    Keep going

    • Practice: Level-sensitive latch: when it is risky

    • Practice: Incomplete case and unique0: simulation versus synthesis

    • Interview: run a mock interview on this topic

    Related topics

    • Latch avoidance and case semantics

    • From code to hardware

    • Lint, RDC, and waiver policy

    Previous

    255 / 350

    Next: Style and Static Checks Hub

    Up next

    Up nextStyle and Static Checks Hub

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Inference-focused coding style
    • Inferring flip-flops and RAM