How to Make RTL Code Synthesis-Friendly and Technology-Agnostic

Writing RTL that produces the expected simulation result is only one part of digital design.

For an RTL design to be useful in an ASIC or SoC development flow, it must also describe hardware clearly enough for synthesis tools to convert it into the intended gate-level implementation. At the same time, reusable RTL should avoid unnecessary dependence on a particular standard-cell library, technology node, or implementation tool.

This is where synthesis-friendly and technology-agnostic RTL becomes important.

A synthesis-friendly design uses coding constructs and structures that map predictably to hardware. A technology-agnostic design keeps the functional RTL independent of implementation-specific cells and libraries wherever possible.

These principles are closely related, but they are not exactly the same.

For example, code can be synthesizable but still contain technology-specific primitives. Conversely, code can look portable but accidentally infer a latch or create an unintended combinational path.

This guide explains how to write RTL that is synthesizable, portable, readable, reusable, and easier to verify, with practical Verilog and SystemVerilog examples.

What Does Synthesis-Friendly RTL Mean?

Synthesis-friendly RTL is RTL written so that synthesis tools can reliably infer the intended hardware structure.

The RTL should make it reasonably clear whether the designer intends to create:

  • Combinational logic
  • Flip-flops
  • Registers
  • Multiplexers
  • Counters
  • Memories
  • FSMs
  • Arithmetic logic
  • Clock enables
  • Other supported hardware structures

For example, this sequential block clearly describes register behavior:

always_ff @(posedge clk) begin

    if (en)

        data_q <= data_d;

end

 

The intended hardware is straightforward:

            +———+

data_d —–>|         |

             |   FF    |—-> data_q

clk ——–>|         |

en ———>|         |

             +———+

 

The goal is not to write RTL that forces a particular gate implementation. The goal is to describe the required hardware behavior clearly and let synthesis perform the technology-specific optimization.

What Does Technology-Agnostic RTL Mean?

Technology-agnostic RTL is functional RTL that does not unnecessarily depend on a specific semiconductor technology or standard-cell library.

For example, generic RTL should normally describe:

AND behavior

OR behavior

multiplexing

registers

arithmetic

state machines

 

rather than directly instantiating a library-specific cell everywhere.

Instead of writing technology-dependent logic such as:

MY_7NM_SPECIAL_MUX u_mux (…);

 

a reusable RTL block can describe the intended behavior:

assign y = sel ? b : a;

 

The synthesis flow can then map that behavior to cells available in the target technology.

Technology-independent RTL is particularly useful when the same functional IP may be synthesized for different technologies, libraries or implementation targets. Industry RTL guidance also recommends separating technology-dependent elements from reusable core logic.

Synthesis-Friendly vs Technology-Agnostic RTL

These terms are related but should not be treated as synonyms.

Synthesis-Friendly RTL

Technology-Agnostic RTL

Focuses on predictable synthesis

Focuses on portability

Avoids unintended hardware

Avoids unnecessary technology-specific implementation

Prevents latch and inference problems

Allows reuse across technologies

Uses synthesis-supported constructs

Avoids unnecessary library dependencies

Considers timing, area and power implications

Separates functional RTL from implementation-specific cells

Good RTL should ideally satisfy both goals.

1. Clearly Separate Combinational and Sequential Logic

One of the simplest ways to make RTL easier to understand and synthesize is to clearly distinguish combinational logic from sequential logic.

For sequential logic:

always_ff @(posedge clk) begin

    q <= d;

end

 

For combinational logic:

always_comb begin

    y = a & b;

end

 

SystemVerilog’s always_ff and always_comb also provide stronger intent checking in supporting tools. For example, established RTL style guides recommend always_ff for sequential logic and always_comb for combinational logic.

If a project uses Verilog rather than SystemVerilog, the equivalent conventional forms are:

always @(posedge clk)

 

and:

always @(*)

 

The important principle is not the keyword itself.

It is making the hardware intent unambiguous.

2. Avoid Unintended Latch Inference

One of the most common RTL mistakes is accidentally describing a latch.

Consider:

always_comb begin

    if (enable)

        y = data;

end

 

What happens when enable is 0?

There is no assignment to y.

The synthesis tool therefore needs some storage behavior to preserve the previous value, potentially resulting in latch inference.

A safer combinational structure is:

always_comb begin

    y = ‘0;

 

    if (enable)

        y = data;

end

 

Or:

always_comb begin

    if (enable)

        y = data;

    else

        y = ‘0;

end

 

The general rule is:

Every combinational output should receive a defined value for every possible execution path.

Unintended latches can create timing and verification complications, which is why avoiding incomplete combinational assignments is a common synthesis guideline.

3. Use Non-Blocking Assignments for Clocked Logic

For clocked sequential logic, use non-blocking assignments:

always_ff @(posedge clk) begin

    q1 <= d;

    q2 <= q1;

end

 

This represents two separate registers.

Using blocking assignments in sequential logic can produce simulation behavior that does not reflect the intended register-to-register relationship.

A simple rule for RTL coding is:

Sequential logic     → <=

Combinational logic  → =

 

The exact coding standard may vary between organizations, but consistency is essential.

4. Do Not Mix Combinational and Sequential Intent Carelessly

Consider:

always_ff @(posedge clk) begin

    if (enable)

        q <= d;

end

 

always_comb begin

    y = q & mask;

end

 

The structure is easy to understand:

d

 |

 v

[Register]

 |

 q

 |

 v

Combinational Logic

 |

 y

 

Compare this with a large procedural block containing registers, combinational calculations, temporary variables and multiple control conditions.

Large mixed blocks can become harder to review and debug.

Good RTL organization should make the hardware structure visible from the code.

5. Avoid Simulation-Only Constructs in Synthesizable RTL

Not everything that can execute in a Verilog/SystemVerilog simulator represents hardware.

Common examples that generally belong in testbench or verification code rather than generic synthesizable RTL include:

#10

 

simulation delays,

$display(…)

 

and:

initial begin

    …

end

 

depending on the target technology and synthesis flow.

File I/O, simulation control and timing delays are typically verification constructs rather than portable RTL.

The distinction is important:

RTL

 ↓

Hardware description

 ↓

Synthesis

 ↓

Gate-level implementation

 

versus:

Testbench

 ↓

Simulation control

 ↓

Stimulus / checking

 

Do not confuse what a simulator can execute with what a synthesis flow can implement as hardware. Current synthesis guidance continues to distinguish synthesis-supported constructs from simulation-only constructs such as delays and system tasks.

6. Make Widths Explicit

Width mismatches can produce subtle RTL bugs.

Consider:

logic [7:0] a;

logic [7:0] b;

logic [7:0] result;

 

assign result = a + b;

 

Now consider an expression involving different widths:

logic [7:0]  a;

logic [15:0] b;

logic [7:0]  result;

 

The result may be truncated depending on the expression and assignment context.

A synthesis-friendly coding style should make widths intentional.

For example:

logic [15:0] sum;

 

assign sum = {8’b0, a} + b;

 

Explicit widths make the designer’s intention easier to review and reduce surprises from implicit sizing and signedness rules.

7. Be Careful With Signed and Unsigned Arithmetic

Signedness can change arithmetic and comparison behavior.

For example:

logic [7:0] a;

logic [7:0] b;

 

assign greater = (a > b);

 

Both signals are unsigned unless declared otherwise.

If signed arithmetic is required, state that intention explicitly:

logic signed [7:0] a;

logic signed [7:0] b;

 

Do not assume that the synthesis tool will interpret arithmetic based on what the designer intended.

The RTL should express the intended data type clearly.

8. Use Parameters Instead of Hardcoding Repeated Design Values

Technology-agnostic RTL should also be reusable.

Consider a fixed-width counter:

logic [7:0] count;

 

If the design later needs a 16-bit counter, the module may need to be modified.

A parameterized implementation is more reusable:

module counter #(

    parameter int WIDTH = 8

) (

    input  logic             clk,

    input  logic             rst_n,

    output logic [WIDTH-1:0] count

);

 

Now the same functional RTL can support different widths.

Parameters are particularly useful for:

  • Data width
  • Address width
  • FIFO depth
  • Counter width
  • Number of channels
  • Buffer size

Parameterized RTL is also a commonly used industry pattern because it reduces duplicated modules and supports reusable IP.

9. Avoid Hardcoding Technology-Specific Cells in Generic RTL

Suppose an RTL block requires a multiplexer.

A technology-independent implementation might be:

assign y = sel ? b : a;

 

The synthesis tool can map this function to the appropriate cells available in the target library.

Direct library-cell instantiation may instead look like:

MYTECH_MUX2 u_mux (

    .A(a),

    .B(b),

    .S(sel),

    .Y(y)

);

 

The second approach may be necessary in some specialized situations, but it is no longer technology-agnostic.

If a specific library cell is required for:

  • Clocking
  • Memory
  • I/O
  • Analog interaction
  • Specialized timing
  • Low-power implementation

keep that technology-specific logic isolated from the reusable functional RTL wherever possible.

This creates a useful separation:

            Generic RTL

                 |

          —————–

          |               |

     Functional        Technology-

        Logic          Specific Layer

 

This approach improves portability and makes later technology migration easier.

10. Do Not Manually Build Clock Gating Without Understanding the Flow

Clock logic requires special care.

A beginner might write:

assign gated_clk = clk & enable;

 

This appears simple, but it creates a derived clock and can introduce clock skew, glitches and implementation problems.

For ASIC flows, dedicated integrated clock-gating cells are commonly used rather than treating an ordinary combinational AND gate as a generic clock-gating solution.

The exact methodology depends on the technology and implementation flow.

For reusable RTL, a clock-enable structure is often preferable when the requirement is simply to prevent unnecessary register updates:

always_ff @(posedge clk) begin

    if (enable)

        q <= d;

end

 

This keeps the clock itself unchanged while controlling when the register captures new data.

Inskill’s existing RTL coding-pattern material also discusses clock-enable structures as a common RTL pattern.

11. Keep Reset Intent Clear

Reset logic affects synthesis, timing, verification and physical implementation.

For example:

always_ff @(posedge clk or negedge rst_n) begin

    if (!rst_n)

        q <= ‘0;

    else

        q <= d;

end

 

This describes an asynchronously reset register.

A synchronous reset would instead be:

always_ff @(posedge clk) begin

    if (!rst_n)

        q <= ‘0;

    else

        q <= d;

end

 

Neither approach should automatically be treated as universally better.

The correct choice depends on the design architecture and project requirements.

The important principle is:

Do not hide reset behavior in complicated logic. Make the reset strategy obvious and consistent.

12. Write FSMs With Clear State and Transition Logic

Finite-state machines are common in RTL.

A clean structure separates state storage from next-state logic.

For example:

typedef enum logic [1:0] {

    IDLE,

    START,

    ACTIVE,

    DONE

} state_t;

 

state_t state, next_state;

 

State register:

always_ff @(posedge clk or negedge rst_n) begin

    if (!rst_n)

        state <= IDLE;

    else

        state <= next_state;

end

 

Next-state logic:

always_comb begin

    next_state = state;

 

    case (state)

        IDLE: begin

            if (start)

                next_state = START;

        end

 

        START: begin

            next_state = ACTIVE;

        end

 

        ACTIVE: begin

            if (done)

                next_state = DONE;

        end

 

        DONE: begin

            next_state = IDLE;

        end

 

        default:

            next_state = IDLE;

    endcase

end

 

This structure makes the state machine easier to review and verify.

It also helps prevent incomplete assignments and unintended latch inference.

13. Use case Carefully

Incomplete or ambiguous case logic can cause problems.

For example:

always_comb begin

    case (sel)

        2’b00: y = a;

        2’b01: y = b;

    endcase

end

 

What happens when sel is 2’b10 or 2’b11?

If y has no defined value for those conditions, the intended hardware may not be what the designer expects.

A safer pattern is:

always_comb begin

    y = ‘0;

 

    case (sel)

        2’b00: y = a;

        2’b01: y = b;

        2’b10: y = c;

        2’b11: y = d;

        default: y = ‘0;

    endcase

end

 

The appropriate use of unique, priority, or other SystemVerilog constructs should follow the project’s coding and verification methodology rather than being added automatically.

14. Avoid Multiple Drivers for the Same Signal

A signal should normally have one clear source of procedural assignment.

Avoid structures such as:

always_ff @(posedge clk)

    q <= a;

 

always_ff @(posedge clk)

    q <= b;

 

This creates multiple procedural drivers for q and is not the intended description of an ordinary register.

Instead, combine the control:

always_ff @(posedge clk) begin

    if (sel)

        q <= a;

    else

        q <= b;

end

 

Clear ownership makes RTL easier for synthesis, lint and human reviewers to understand.

15. Be Careful With Combinational Depth

Synthesis-friendly RTL is not just about whether the code synthesizes.

The resulting hardware also needs to meet timing.

Consider a long chain of combinational operations:

Input

  |

Logic

  |

Logic

  |

Logic

  |

Logic

  |

Register

 

A long combinational path can make timing closure difficult.

One solution is pipelining:

Input

  |

Logic

  |

Register

  |

Logic

  |

Register

  |

Output

 

Pipelining changes latency, so it cannot simply be inserted without considering the functional specification.

The important point is that RTL structure influences the eventual timing architecture.

Current RTL guidance similarly emphasizes coding structures that help synthesis produce efficient implementations and meet timing objectives.

16. Do Not Assume Synthesis Will Fix Poor RTL Structure

Synthesis tools perform extensive optimization, but they cannot infer design intent that was never correctly expressed.

For example, if RTL accidentally creates:

  • A latch
  • An unintended priority chain
  • A very deep combinational path
  • A wrong-width arithmetic operation
  • A combinational feedback loop
  • An incorrect clock relationship

the designer should not rely on synthesis to automatically repair the problem.

A better principle is:

Write correct hardware intent first; use synthesis to optimize the intended hardware.

17. Separate Functional RTL From Technology-Specific Implementation

A scalable SoC project often has multiple abstraction levels.

For example:

Reusable IP

   |

   v

Technology-Independent RTL

   |

   v

Synthesis

   |

   v

Technology Mapping

   |

   v

Standard Cells / Memories / Macros

 

Technology-specific elements can then be handled separately.

Examples include:

  • Standard-cell libraries
  • Memory macros
  • I/O cells
  • Clock cells
  • Physical-only cells
  • Technology-specific power cells

This separation makes the functional RTL easier to reuse.

It also allows the implementation team to change the target library without rewriting the entire functional design.

18. Understand What “Technology-Agnostic” Does Not Mean

Technology-agnostic does not mean that the RTL will produce exactly the same hardware in every technology.

Different libraries have different:

  • Cell architectures
  • Drive strengths
  • Threshold-voltage options
  • Memory macros
  • Timing characteristics
  • Power characteristics

The synthesis tool will map the same functional RTL differently depending on the target technology and constraints.

Technology independence means the functional description does not unnecessarily depend on a particular implementation technology.

19. Use Lint Before Synthesis

Lint is one of the most useful ways to catch RTL problems early.

A lint tool can identify issues such as:

  • Inferred latches
  • Width mismatches
  • Undriven signals
  • Multiple drivers
  • Unreachable logic
  • Suspicious case statements
  • Clock/reset issues
  • Coding-style violations

The earlier these issues are found, the cheaper they are to fix.

A practical RTL flow is:

RTL Coding

    ↓

Lint

    ↓

Simulation

    ↓

Synthesis

    ↓

STA

    ↓

Implementation

 

Do not wait for synthesis to discover basic coding problems.

20. Compare Simulation and Synthesis Results

A design can behave differently in simulation and after synthesis if the RTL contains ambiguous or unsupported behavior.

Common causes include:

  • Incomplete combinational assignments
  • Incorrect sensitivity lists
  • Blocking/non-blocking misuse
  • Unsized constants
  • Signedness errors
  • Initialization assumptions
  • Simulation-only constructs
  • Race conditions
  • Incorrect reset assumptions

The goal is to minimize the possibility of simulation-synthesis mismatches.

A strong RTL engineer therefore thinks about both:

“What does the simulator do?”

 

and:

“What hardware will this code actually infer?”

 

21. Write RTL With Verification in Mind

Synthesis-friendly RTL should also be verification-friendly.

Clear interfaces and predictable behavior make it easier to create:

  • Assertions
  • Functional coverage
  • Scoreboards
  • Monitors
  • Formal properties
  • Debug waveforms

For example, if a valid-ready interface is clearly defined:

assign transfer = valid && ready;

 

the verification environment has an obvious transaction condition to check.

Inskill’s existing RTL coding guidance similarly emphasizes verification-friendly RTL as part of practical industry design.

22. Avoid Over-Optimization at the RTL Stage

A common mistake is trying to manually optimize every gate.

For example, a designer may rewrite simple Boolean logic specifically to force a particular gate arrangement.

That can make the RTL:

  • Harder to understand
  • Less reusable
  • More technology-dependent
  • More difficult to verify

If there is no architectural reason to control the exact implementation, describe the intended behavior clearly and allow synthesis to optimize it.

RTL optimization is valuable when it addresses a real requirement such as:

  • Timing
  • Area
  • Power
  • Resource sharing
  • Pipeline structure

But optimization should be driven by measurable design requirements rather than assumptions about what the synthesis tool will produce.

A Practical Example

Consider this simple combinational block.

Less robust version

always @(*) begin

    if (enable)

        y = a + b;

end

 

The problem is that y is not assigned when enable is false.

Better version

always_comb begin

    y = ‘0;

 

    if (enable)

        y = a + b;

end

 

Now the intended behavior is clear:

enable = 1 → y = a + b

enable = 0 → y = 0

 

And the combinational block has a defined output for both conditions.

Another Example: Reusable RTL

Instead of:

logic [7:0] counter;

 

use:

module counter #(

    parameter int WIDTH = 8

) (

    input  logic             clk,

    input  logic             rst_n,

    input  logic             enable,

    output logic [WIDTH-1:0] count

);

 

always_ff @(posedge clk or negedge rst_n) begin

    if (!rst_n)

        count <= ‘0;

    else if (enable)

        count <= count + 1’b1;

end

 

endmodule

 

The same module can support:

WIDTH = 8

WIDTH = 16

WIDTH = 32

WIDTH = 64

 

without rewriting the functional design.

That is an example of both synthesis-friendly and reusable RTL.

Technology-Agnostic RTL Checklist

Before submitting an RTL block for synthesis, ask:

Functional correctness
  • Does the RTL implement the specification?
  • Are all state transitions defined?
  • Are reset conditions clear?
Synthesis
  • Are all constructs supported by the intended synthesis flow?
  • Are there unintended latches?
  • Are there multiple drivers?
  • Are combinational paths complete?
  • Are clocked assignments using the project’s required style?
Portability
  • Does the RTL unnecessarily instantiate technology-specific cells?
  • Are technology-specific elements isolated?
  • Are widths and types explicitly defined?
  • Are parameters used where reuse is expected?
Timing
  • Are there unnecessarily deep combinational paths?
  • Are pipeline stages required?
  • Are clock relationships clearly defined?
Verification
  • Is the RTL easy to observe and debug?
  • Can important behavior be asserted?
  • Are interfaces and transaction boundaries clear?
Maintainability
  • Are names meaningful?
  • Is the module reasonably sized?
  • Are repeated structures parameterized?
  • Can another engineer understand the hardware intent quickly?

 

Final Takeaway

Making RTL synthesis-friendly does not mean memorizing a list of forbidden Verilog statements.

It means developing the habit of asking:

What hardware does this RTL describe?

And for technology independence:

Does this RTL describe the required function, or am I unnecessarily describing a particular implementation technology?

Good RTL should:

  • Clearly express hardware intent
  • Avoid unintended latches
  • Separate combinational and sequential behavior
  • Use appropriate assignment styles
  • Make widths and signedness explicit
  • Use parameters for reusable structures
  • Keep clock and reset behavior clear
  • Avoid unnecessary technology-specific cells
  • Consider timing and power implications
  • Work well with lint, simulation and synthesis
  • Remain understandable to verification and implementation engineers

The objective is not to force the synthesis tool to produce a particular set of gates. The objective is to provide a correct, clear and reusable hardware description that synthesis can map efficiently to the target technology.

That is the foundation of professional RTL design.

Leave a Reply

Your email address will not be published. Required fields are marked *