When Synthesis Goes Wrong: Debugging and Solutions

Synthesis is powerful, but it's not magic. The tool faithfully translates your RTL into gates, which means it will also faithfully translate your mistakes into hardware problems. Unlike software bugs that crash at runtime, hardware synthesis issues can create chips that behave unpredictably, fail timing closure, or simply don't work at all. The good news? Most synthesis problems fall into predictable categories with well-known solutions.

This module explores the most common synthesis issues you'll encounter: unintended latches, unmapped cells, combinational loops, unconnected ports, and constant tie cells. You'll learn to recognize these problems in synthesis reports and warnings, understand why they occur, and most importantly, fix them in your RTL. These debugging skills will save you countless hours and prevent expensive tape-out mistakes.

Latch Inference: The Accidental State

What Are Latches?

A latch is a level-sensitive storage element—it's transparent (passes input to output) when the enable is high, and holds its value when enable is low. Unlike flip-flops (edge-triggered), latches are level-triggered.

D-Latch Behavior

When EN = 1: Q follows D (transparent)
When EN = 0: Q holds previous value (latched)

Why Latches Are Usually Bad

Problems with Latches:

  1. Timing analysis nightmare: Level-sensitive timing is complex
  2. Race conditions: Glitches can cause incorrect latching
  3. Testability issues: Difficult to control and observe in testing
  4. Clock domain crossing: Complications with multiple clocks
  5. Technology mapping: Not all libraries have efficient latch cells

When Latches Are Intentional:

  • Low-power designs (less switching than flip-flops)
  • Asynchronous interfaces
  • Specific architectural requirements
  • But these are rare—99% of the time, latch inference is a bug

How Latches Get Inferred

Classic Mistake: Incomplete Combinational Logic

// BAD: Infers latch
always @(*) begin
    if (select)
        out = a;
    // Missing else - what happens when select = 0?
    // Synthesis infers latch to hold previous value
end

Why a Latch?
The always @(*) block describes combinational logic, but the output isn't defined for all input conditions. The synthesis tool assumes you want to retain the previous value, creating a latch.

More Examples of Latch Inference

Incomplete Case Statement

// BAD: Infers latch
always @(*) begin
    case (opcode)
        2'b00: result = a + b;
        2'b01: result = a - b;
        2'b10: result = a & b;
        // Missing 2'b11 case - latch inferred for result
    endcase
end

Conditional Assignment

// BAD: Infers latch
always @(*) begin
    if (condition)
        data_out = data_in;
    // No else - latch inferred
end

Partially Assigned Multi-Bit Signal

// BAD: Infers latches for upper bits
always @(*) begin
    case (sel)
        2'b00: out[1:0] = 2'b00;  // What about out[7:2]?
        2'b01: out[3:0] = 4'b1111;
        2'b10: out = 8'hFF;
        2'b11: out[5:0] = 6'h3F;
    endcase
end

Detecting Latch Inference

Yosys Warning

Warning: Latch inferred for signal `\out' on process `\my_module.$proc$design.v:15$1'.

Synthesis Statistics

yosys> stat

Number of cells:
  $_DLATCH_P_        1    # Latch detected!
  $_DFF_P_           8

Any cell with "LATCH" in the name indicates latch inference.

Visual Detection

yosys> show

Look for feedback loops with enable signals—characteristic of latches.

Fixing Latch Inference

Solution 1: Add Default Case

// GOOD: No latch
always @(*) begin
    case (opcode)
        2'b00: result = a + b;
        2'b01: result = a - b;
        2'b10: result = a & b;
        2'b11: result = a ^ b;  // Now all cases covered
    endcase
end

Or use default:

// GOOD: Default handles all other cases
always @(*) begin
    case (opcode)
        2'b00: result = a + b;
        2'b01: result = a - b;
        2'b10: result = a & b;
        default: result = 8'h00;  // Explicit default
    endcase
end

Solution 2: Add Else Clause

// GOOD: Complete specification
always @(*) begin
    if (select)
        out = a;
    else
        out = b;  // Explicit else
end

Solution 3: Initialize Before Conditionals

// GOOD: Default value for all paths
always @(*) begin
    out = 8'h00;  // Default assignment
    case (opcode)
        2'b00: out = a + b;
        2'b01: out = a - b;
        2'b10: out = a & b;
        // 2'b11 uses default value
    endcase
end

This is the most robust approach—guarantees no latches.

Best Practice Pattern

always @(*) begin
    // Default assignments first
    result = 0;
    valid = 0;
    error = 0;
    
    // Then conditional logic
    case (state)
        IDLE: begin
            valid = 1;
        end
        ACTIVE: begin
            result = data;
            valid = 1;
        end
        ERROR: begin
            error = 1;
        end
        // No default needed - all signals initialized
    endcase
end

Unmapped Cells: Lost in Translation

What Are Unmapped Cells?

Unmapped cells are generic cells (like $add, $mux, $dff) that didn't get converted to technology library cells during technology mapping.

Example

yosys> stat

Number of cells:
  $add                    1    # Generic adder - NOT MAPPED!
  AND2_X1                 5    # Technology cell - mapped
  DFF_X1                  8    # Technology cell - mapped

The $add cell should have been mapped to technology-specific adder cells.

Why Cells Don't Map

Cause 1: Missing Library Cells

Your Liberty file doesn't contain cells needed for the operation:

assign result = a * b;  // 8-bit multiply

If your library has no multiplier cells, the generic $mul cell can't map.

Cause 2: Unsupported Constructs

Some RTL constructs don't have direct technology mapping:

assign out = $signed(a) >>> 3;  // Arithmetic shift

Not all libraries have arithmetic shift cells.

Cause 3: Technology Mapping Not Run

You forgot to run abc:

read_verilog design.v
hierarchy -check
proc
opt
# Oops - no 'abc' command!
write_verilog output.v

Cause 4: Mapping Failed

ABC couldn't find a suitable mapping (rare, but possible with complex logic).

Detecting Unmapped Cells

Check Cell Types

yosys> select t:$*
yosys> select -assert-none t:$*

If the assertion fails, you have unmapped cells.

Statistics Show Generic Cells

Number of cells:
  $_DFF_P_                8    # Generic flip-flop
  $_AND_                  5    # Generic AND

Cell names starting with $ or $_ are generic (unmapped).

Fixing Unmapped Cells

Solution 1: Run Technology Mapping

read_liberty -lib cells.lib
abc -liberty cells.lib

Most common fix—just ensure abc runs.

Solution 2: Add Missing Cells to Library

If library truly lacks required cells:

  • Use a more complete library
  • Request library vendor to add cells
  • Implement the function differently in RTL

Solution 3: Manual Mapping

For special cases, manually instantiate technology cells:

// Instead of:
// assign out = a * b;

// Use library multiplier directly:
MULT8_X1 u_mult (
    .A(a),
    .B(b),
    .Y(out)
);

Solution 4: Change RTL

Rewrite to use available primitives:

// Instead of shift right arithmetic (>>>)
// Use arithmetic operations
assign out = $signed(a) / 8;  // Division by power of 2

Solution 5: Use FPGA Synthesis

If targeting FPGA, use FPGA-specific synthesis:

synth_ice40  # For iCE40 FPGAs
synth_ecp5   # For ECP5 FPGAs

These map to FPGA primitives instead of ASIC standard cells.

Combinational Loops: The Feedback Problem

What Are Combinational Loops?

A combinational loop is a path through combinational logic that feeds back to itself—creating a cycle with no registers to break it.

Example

assign a = b & c;
assign b = a | d;  // Loop! a depends on b, b depends on a

Why Combinational Loops Are Bad

Problems:

  1. Unstable behavior: Output oscillates or settles unpredictably
  2. Timing analysis impossible: No well-defined delay
  3. Simulation-hardware mismatch: Simulators may not catch the issue
  4. Synthesis tool confusion: May produce warnings or errors

Intentional Combinational Loops: Very rare—only in specialized circuits like:

  • Arbiters
  • Metastability resolvers
  • Analog-style circuits

How Combinational Loops Occur

Mistake 1: Unintentional Feedback

// BAD: Combinational loop
always @(*) begin
    temp = a & b;
    out = temp | out;  // out depends on itself!
end

Mistake 2: Circular Dependencies

// BAD: Module A uses Module B, Module B uses Module A output
module A (input x, output y);
    assign y = x & b_out;
endmodule

module B (input p, output q);
    assign q = p | a_out;  // Circular!
endmodule

Mistake 3: Incorrect Latch Modeling

// BAD: Trying to model latch, created loop
assign q = enable ? d : q;  // q depends on q combinationally

Detecting Combinational Loops

Yosys Warning

Warning: Found combinational loop in circuit:
  wire $auto$simplify.cc:99:simplify$123

Visual Detection

yosys> show

Look for cycles in the graph that don't pass through flip-flops.

Check Command

yosys> check
Error: Found combinational loop

Fixing Combinational Loops

Solution 1: Add Registers

// GOOD: Break loop with register
always @(posedge clk) begin
    temp <= a & b;
    out <= temp | out_reg;
end

Solution 2: Restructure Logic

// BAD: Loop
always @(*) begin
    temp = a & b;
    out = temp | out;
end

// GOOD: No feedback
always @(*) begin
    temp = a & b;
    out = temp | c;  // Use different signal
end

Solution 3: Use Proper Latch Syntax

// Instead of combinational feedback for latch
// Use proper level-sensitive always block
always @(enable or d) begin
    if (enable)
        q <= d;
    // Synthesizes to latch correctly
end

Solution 4: Fix Module Interconnections

Break circular module dependencies:

// Redesign so data flows in one direction
// Use registers at module boundaries if needed

Unconnected Ports: The Floating Signal

What Are Unconnected Ports?

Unconnected ports are module inputs that aren't driven or outputs that aren't used.

Example

module counter (
    input clk,
    input rst,
    input enable,     // Never connected
    output [7:0] count
);
    // ...
endmodule

// Instantiation
counter u_cnt (
    .clk(clk),
    .rst(rst),
    // .enable missing!
    .count(count)
);

Why Unconnected Ports Are Problematic

Unconnected Inputs:

  • May synthesize to undefined values (X in simulation)
  • Could float in actual hardware (unpredictable behavior)
  • Synthesis tool may tie to 0 or 1 (but which?)

Unconnected Outputs:

  • Wasted logic (synthesis may not remove it)
  • Confusion about design intent
  • Could indicate a bug (forgot to use the signal?)

Detecting Unconnected Ports

Yosys Warnings

Warning: Ignoring unconnected input port 'enable' on cell 'u_cnt'
Warning: Ignoring unconnected output port 'overflow' on cell 'u_adder'

Elaboration Check

yosys> hierarchy -check

Reports unconnected ports.

Fixing Unconnected Ports

Solution 1: Connect the Port

// If port should be used, connect it
counter u_cnt (
    .clk(clk),
    .rst(rst),
    .enable(1'b1),  // Connect to constant or signal
    .count(count)
);

Solution 2: Remove Unused Ports

// If truly unused, remove from module definition
module counter (
    input clk,
    input rst,
    // enable removed
    output [7:0] count
);

Solution 3: Explicit Unused Indication

// Document that port is intentionally unused
counter u_cnt (
    .clk(clk),
    .rst(rst),
    .enable(),      // Explicitly left unconnected
    .count(count)
);

// Or in Verilog-2001
counter u_cnt (
    .clk(clk),
    .rst(rst),
    // .enable not mentioned - implicitly unconnected
    .count(count)
);

Solution 4: Default Port Values

// In module definition, provide defaults
module counter (
    input clk,
    input rst,
    input enable = 1'b1,  // Default value
    output [7:0] count
);

Now if enable isn't connected, it defaults to 1.

Tie-High/Tie-Low Cells: Constant Connections

What Are Tie Cells?

Tie cells are special library cells that generate constant logic levels (0 or 1).

Common Names:

  • LOGIC0_X1, TIE_LO_X1 (generates 0)
  • LOGIC1_X1, TIE_HI_X1 (generates 1)

Why Not Just Connect to Vdd/Gnd?

Problems with Direct Power/Ground Connections:

  1. ESD damage: Direct connection to power rail can cause ESD issues
  2. IR drop: Large fanout to many gates creates current spikes
  3. Reliability: Better to control constant sources explicitly

Benefits of Tie Cells:

  • Controlled drive strength
  • Proper buffering for fanout
  • ESD protection
  • Better for DFT (Design for Test)

When Tie Cells Appear

Scenario 1: Constant Inputs

module design (
    input [7:0] data,
    output [7:0] result
);
    assign result = data & 8'hF0;  // Lower bits always 0
endmodule

Synthesis may create tie-low cells for the constant 0 bits.

Scenario 2: Unused Enable Signals

DFF_EN u_ff (
    .D(d),
    .CK(clk),
    .EN(1'b1),  // Always enabled
    .Q(q)
);

The constant 1 on EN uses a tie-high cell.

Scenario 3: Partially Used Buses

assign result[7:4] = 4'b0000;  // Constant upper nibble
assign result[3:0] = data[3:0];

Detecting Tie Cells

Statistics

yosys> stat

Number of cells:
  LOGIC0_X1               4
  LOGIC1_X1               2
  AND2_X1                12
  DFF_X1                  8

Check Constants

yosys> select t:LOGIC0_X1
yosys> select t:LOGIC1_X1

Managing Tie Cells

Normal Situation

A few tie cells are normal and expected:

LOGIC0_X1: 2-5 cells
LOGIC1_X1: 2-5 cells

Red Flag: Excessive Tie Cells

LOGIC0_X1: 100 cells
LOGIC1_X1: 50 cells

This suggests:

  • Large constant values in RTL
  • Many unused inputs
  • Potential design issue

Solution 1: Review Constants in RTL

// BAD: Creating many tie cells
assign wide_bus = {16'h0000, data[15:0]};

// BETTER: Is the wide bus really needed?
// Consider redesigning to avoid constants

Solution 2: Optimize Away Constants

# During synthesis
opt_expr   # Constant propagation
opt_clean  # Remove unused logic

Solution 3: Use Fewer Tie Instances

Some libraries provide shared tie cells:

// One tie-low cell, buffered to drive many loads
TIELO_BUF u_tielow (
    .Y(const_zero)
);

// Many gates use the buffered constant

Solution 4: Accept It

If your design genuinely needs constants, tie cells are the correct solution—don't try to eliminate them artificially.

Debugging Workflow for Synthesis Issues

Systematic Approach

Step 1: Check Synthesis Log

$ yosys -s synthesis.ys 2>&1 | tee synthesis.log
$ grep -i "warning\|error" synthesis.log

Step 2: Examine Statistics

yosys> stat

Look for:

  • Generic cells (unmapped)
  • Latch cells
  • Excessive tie cells

Step 3: Run Checks

yosys> check
yosys> select -assert-none t:$*  # No unmapped cells

Step 4: Visual Inspection

yosys> show

Look for:

  • Feedback loops (combinational loops)
  • Latch structures (unintended state)
  • Dangling connections

Step 5: Fix RTL

Address issues in source Verilog, not synthesized netlist.

Step 6: Re-synthesize and Verify

$ yosys -s synthesis.ys
$ # Check that issues are resolved

Prevention Checklist

Before Synthesis:

  • [ ] All combinational blocks have complete assignments
  • [ ] All case statements have default cases
  • [ ] All if statements have else clauses
  • [ ] No circular module dependencies
  • [ ] All instantiated ports connected or explicitly left unconnected

During Synthesis:

  • [ ] Read synthesis warnings carefully
  • [ ] Check statistics for generic cells
  • [ ] Verify no latches (unless intentional)
  • [ ] Run check command

After Synthesis:

  • [ ] Review cell type distribution
  • [ ] Verify no combinational loops
  • [ ] Check for excessive tie cells
  • [ ] Compare against expected results

Summary

Common synthesis issues have predictable causes and solutions:

  • Latch inference: Incomplete combinational logic → Add defaults, else clauses, or full case coverage
  • Unmapped cells: Missing technology mapping or library cells → Run abc, use complete library, or change RTL
  • Combinational loops: Unintentional feedback paths → Add registers or restructure logic
  • Unconnected ports: Missing connections → Connect ports, remove unused ports, or document intent
  • Tie-high/tie-low cells: Constant generation → Normal in moderation; review if excessive

Key debugging skills you've developed:

  • Recognizing warning patterns in synthesis logs
  • Using stat, check, and show to diagnose issues
  • Tracing problems back to RTL source
  • Applying systematic fixes
  • Preventing issues with good coding practices

These debugging skills are invaluable throughout your career. Every designer encounters these issues, but experienced designers fix them quickly because they recognize the patterns. With practice, you'll spot potential problems in RTL before synthesis, write cleaner code, and spend less time debugging synthesis issues. The patterns you've learned here apply across all synthesis tools—commercial or open-source—making you a more effective hardware designer.

Did you find what you were looking for?

Quiz

No quiz questions available for this article.