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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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:
Parameterized RTL is also a commonly used industry pattern because it reduces duplicated modules and supports reusable IP.
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:
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.
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.
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.
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.
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.
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.
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.
Synthesis tools perform extensive optimization, but they cannot infer design intent that was never correctly expressed.
For example, if RTL accidentally creates:
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.
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:
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.
Technology-agnostic does not mean that the RTL will produce exactly the same hardware in every technology.
Different libraries have different:
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.
Lint is one of the most useful ways to catch RTL problems early.
A lint tool can identify issues such as:
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.
A design can behave differently in simulation and after synthesis if the RTL contains ambiguous or unsupported behavior.
Common causes include:
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?”
Synthesis-friendly RTL should also be verification-friendly.
Clear interfaces and predictable behavior make it easier to create:
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.
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:
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:
But optimization should be driven by measurable design requirements rather than assumptions about what the synthesis tool will produce.
Consider this simple combinational block.
always @(*) begin
if (enable)
y = a + b;
end
The problem is that y is not assigned when enable is false.
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.
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.
Before submitting an RTL block for synthesis, ask:
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:
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.