The most common RTL coding mistakes come from forgetting that Verilog and VHDL describe hardware, not a program. Unsynchronised clock domain crossings, software-style code, accidental latches, incomplete resets, tangled state machines, thin simulation, missing timing constraints, hard-coded widths, unreadable code and unchecked synthesis output account for most of the bugs found in junior engineers' designs. Each one is avoidable with a few habits and the right tool checks.
Engineers new to Register Transfer Level (RTL) design often make these mistakes because they are still getting used to hardware description languages (HDLs) such as Verilog or VHDL, and to the constraints of real hardware. Here are the top 10, based on industry experience and common best practice:
- Not understanding clock domains
- Writing software-like code
- Ignoring synchronous design principles
- Poor reset handling
- Overcomplicating state machines
- Not simulating enough
- Ignoring timing constraints
- Hard-coding parameters
- Not following coding guidelines
- Not verifying synthesis output
Below is a detailed breakdown of each one, with explanations, examples, consequences and best practices. Every Verilog example was compiled and, where it has behaviour worth checking, simulated.
Key takeaways
- Every signal that crosses a clock domain needs a deliberate synchroniser: two flops for a single bit, a handshake or asynchronous FIFO for buses.
- Read every always block as a circuit. If you cannot sketch the gates and flops, the synthesis tool may build something you did not expect.
- An if without an else in combinational logic makes a latch. Lint tools catch it; reviewers look for it first.
- Pick a reset strategy, document it, and reset every control register.
- Self-checking testbenches, coverage, complete SDC constraints and formal equivalence checking are what turn RTL that looks right into RTL that is right.
Quick reference
| # | Mistake | Typical symptom | Main fix | Tool that catches it |
|---|---|---|---|---|
| 1 | Unsynchronised clock domain crossings | Rare, random failures in silicon | Synchronisers, handshakes, async FIFOs | CDC checker |
| 2 | Software-like code | Unsynthesisable code or huge logic | Think in hardware blocks | Synthesis, lint |
| 3 | Ignoring synchronous design | Latches, glitches, timing failures | Clocked registers, complete assignments | Lint, STA |
| 4 | Poor reset handling | X states, sim and synthesis mismatch | One reset strategy, reset all state | Lint, reset checks |
| 5 | Overcomplicated FSMs | Lock-ups, unreachable states | Named states, two-block template | Lint, formal, coverage |
| 6 | Not simulating enough | Bugs found in the lab | Self-checking tests, coverage | Simulator, coverage tools |
| 7 | Ignoring timing constraints | Misses frequency, hold failures | Complete SDC | STA |
| 8 | Hard-coding parameters | Hard to reuse or resize | Parameters and localparams | Review |
| 9 | Not following coding guidelines | Slow reviews, maintenance bugs | Team style guide | Lint |
| 10 | Not verifying synthesis output | Netlist differs from RTL | Equivalence checking, gate-level sim | Formal equivalence |
1. Not understanding clock domains
Failing to manage signals that cross between clock domains can cause metastability, data corruption or unpredictable behaviour. Junior engineers may not notice when a signal moves from one domain (for example 100 MHz) to another (for example 50 MHz), or may assume a simple assignment is enough. A flop whose setup or hold time is violated can go metastable and resolve late or to either value, causing functional failures that are hard to reproduce and debug.
// Direct signal crossing between clock domains
module bad_cdc (
input clk_a, clk_b,
input data_in,
output reg data_out
);
always @(posedge clk_b) begin
data_out <= data_in; // data_in is from clk_a domain
end
endmodule
// Two-stage synchronizer (plus an output register)
module good_cdc (
input clk_b, rst_n,
input data_in, // From clk_a domain
output reg data_out
);
reg sync1, sync2;
always @(posedge clk_b or negedge rst_n) begin
if (!rst_n) begin
sync1 <= 0;
sync2 <= 0;
data_out <= 0;
end else begin
sync1 <= data_in; // First stage
sync2 <= sync1; // Second stage
data_out <= sync2;
end
end
endmodule
Why it matters: the chance of a synchroniser failing falls exponentially with the time the first flop is given to settle. Two flops in series give it roughly one full destination clock period, which makes the mean time between failures very long for typical designs; very fast clocks or high-reliability products may use three. This only works for a single bit, or for a multi-bit value that changes one bit at a time (Gray code). Putting each bit of a binary bus through its own synchroniser is a classic bug, because the bits can resolve on different cycles.
Best practices:
- Study CDC techniques: synchronisers, handshake protocols and asynchronous FIFOs.
- Use CDC analysis tools (for example Synopsys SpyGlass CDC) to find every crossing.
- Simulate with random clock phase differences to shake out CDC issues, but do not rely on simulation alone, since it cannot model metastability.
2. Writing software-like code
Treating HDL like a software program, using loops, waits or functions without thinking about what they become in hardware, can lead to serious problems. Engineers from C or C++ backgrounds may apply programming habits without realising that HDL describes physical circuits that all operate at once. The code may be unsynthesisable, generate far more hardware than expected, or behave differently from what was intended.
A loop is not wrong in itself. The original version of this post showed the two modules below as bad and good. Both are in fact synthesisable and equivalent: the loop has fixed bounds, so synthesis unrolls it into the same adder tree as the written-out sum. A simulation over all 256 inputs confirms they give identical results.
module bad_loop (
input [7:0] data_in,
output reg [7:0] data_out
);
integer i;
always @(*) begin
data_out = 0;
for (i = 0; i < 8; i = i + 1) begin
data_out = data_out + data_in[i]; // unrolled by synthesis
end
end
endmodule
module good_loop (
input [7:0] data_in,
output [7:0] data_out
);
assign data_out = data_in[0] + data_in[1] + data_in[2] + data_in[3] +
data_in[4] + data_in[5] + data_in[6] + data_in[7];
endmodule
The real software-style mistake is code that expects to execute step by step over time. The first module below simulates, but synthesis tools reject the event-driven wait. The second describes the same behaviour as hardware: a counter.
// Simulates, but synthesis tools reject it: a software-style "wait"
module bad_wait (input clk, start, output reg busy);
initial busy = 0;
always @(posedge start) begin
busy = 1;
repeat (10) @(posedge clk); // waits 10 clocks, like a sleep()
busy = 0;
end
endmodule
// The hardware way: a counter that holds busy high for 10 clocks
module good_wait (input clk, rst_n, start, output busy);
reg [3:0] cnt;
always @(posedge clk or negedge rst_n)
if (!rst_n) cnt <= 4'd0;
else if (start && !busy) cnt <= 4'd10;
else if (busy) cnt <= cnt - 4'd1;
assign busy = (cnt != 4'd0);
endmodule
Best practices:
- Learn the synthesis rules, for example that initial blocks, # delays and event waits inside procedural code are for testbenches only.
- Picture the hardware (adders, multiplexers, registers) each line of code implies.
- Use simulation to check behaviour matches intent before synthesis, and read the synthesis report to check the hardware size matches your sketch.
3. Ignoring synchronous design principles
Failing to use clocked logic for state changes, or neglecting to register module inputs and outputs, can lead to timing violations, glitches or race conditions. Junior engineers may assume combinational logic is simpler. But combinational logic in critical paths can glitch, fail timing analysis and make the design unreliable.
The bad example below has a second, subtler problem: when en is low, data_out is not assigned, so it must hold its old value, and synthesis infers a latch. Latches are transparent while enabled, are hard to time, and are rarely intended.
module bad_sync (
input clk, en, data_in,
output reg data_out
);
always @(*) begin
if (en)
data_out = data_in; // Combinational, no else: infers a latch
end
endmodule
module good_sync (
input clk, rst_n, en, data_in,
output reg data_out
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_out <= 0;
else if (en)
data_out <= data_in;
end
endmodule
Best practices:
- Register module outputs where you can, to simplify timing analysis between blocks.
- Avoid combinational feedback loops, which are prone to races and oscillation.
- In combinational always blocks, assign every output on every path, or set a default at the top.
- Use static timing analysis (STA) to verify setup and hold times.
4. Poor reset handling
Inconsistent or incomplete reset logic, such as mixing synchronous and asynchronous resets without a plan or failing to reset all state, can cause serious problems. Inexperienced engineers may not set a clear reset strategy or may miss registers that need initialising. Registers left unreset start as X in simulation and as random values in silicon, causing simulation and synthesis mismatches or functional bugs.
module bad_reset (
input clk, rst_n, data_in,
output reg data_out,
output reg [1:0] state
);
always @(posedge clk) begin
if (!rst_n)
data_out <= 0; // state not reset
else
state <= data_in ? 2'b01 : 2'b10;
end
endmodule
module good_reset (
input clk, rst_n, data_in,
output reg data_out,
output reg [1:0] state
);
always @(posedge clk) begin
if (!rst_n) begin
data_out <= 0;
state <= 2'b00;
end else begin
state <= data_in ? 2'b01 : 2'b10;
data_out <= data_in;
end
end
endmodule
If you use an asynchronous reset, assert it asynchronously but release it synchronously: pass the reset through a two-flop reset synchroniser in each clock domain, so every flop leaves reset on the same clock edge. Datapath registers that are always written before being read (pipeline data, for example) may be left unreset to save area, but only as a deliberate, documented choice.
Best practices:
- Choose one reset type (synchronous or asynchronous) for the design or block, and stick to it.
- Reset all control and state registers unless there is a documented reason not to.
- Verify reset behaviour in RTL and gate-level simulation, including reset asserted during operation.
5. Overcomplicating state machines
Complex or poorly structured finite state machines (FSMs) lead to hard-to-debug logic and inefficient hardware. Inexperienced engineers may not break an FSM into manageable states, or may ignore standard templates such as Moore and Mealy models. Overly complex FSMs raise the risk of bugs, make verification harder, and may synthesise into larger, slower logic.
In the bad example below, magic numbers hide the intent, states 5, 7 and 2 have no defined next state except through default, and the output is registered from the current state, so it lags the state by a cycle, which may or may not be what was meant.
module bad_fsm (
input clk, rst_n, in,
output reg out
);
reg [3:0] state;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= 4'h0;
else begin
case (state)
4'h0: state <= in ? 4'h5 : 4'h3;
4'h3: state <= in ? 4'h7 : 4'h2;
default: state <= 4'h0;
endcase
out <= (state == 4'h5 || state == 4'h7) ? 1 : 0;
end
end
endmodule
module good_fsm (
input clk, rst_n, in,
output reg out
);
localparam IDLE = 2'b00, S1 = 2'b01, S2 = 2'b10;
reg [1:0] state, next_state;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
always @(*) begin
next_state = state;
case (state)
IDLE: next_state = in ? S1 : IDLE;
S1: next_state = in ? S2 : IDLE;
S2: next_state = IDLE;
default: next_state = IDLE; // recover from the unused 2'b11
endcase
end
always @(*) begin
out = (state == S2);
end
endmodule
The good version uses named states, a separate next-state block, a Moore output, and a default branch so the unused encoding 2'b11 returns to IDLE (the original listing had no default; it is added here for safety).
Best practices:
- Use Moore or Mealy FSM templates for clarity.
- Document state transitions with diagrams or comments.
- Simulate all state transitions and edge cases, and check state coverage.
6. Not simulating enough
Writing RTL without thorough simulation lets corner cases, initialisation bugs and functional errors slip through. Inexperienced engineers may skip testbench development or rush to synthesis. Undetected bugs then surface in synthesis or in hardware, where they take far longer to debug.
The original testbench below drives the counter but never checks anything, so it only passes if someone reads the waveform correctly every time.
module counter (
input clk, rst_n, en,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
module counter_tb;
reg clk, rst_n, en;
wire [3:0] count;
counter uut (.clk(clk), .rst_n(rst_n), .en(en), .count(count));
initial begin
clk = 0;
forever #5 clk = ~clk; // 100 MHz clock
end
initial begin
rst_n = 0; en = 0;
#20 rst_n = 1; // Release reset
#10 en = 1; // Enable counter
#100 en = 0; // Disable counter
#20 $finish;
end
endmodule
A self-checking version adds a reference model and compares on every cycle. It also tests wrap-around past 15 and a reset in the middle of counting:
module counter_tb_check;
reg clk, rst_n, en;
wire [3:0] count;
reg [3:0] expected;
integer errors = 0;
counter uut (.clk(clk), .rst_n(rst_n), .en(en), .count(count));
initial begin clk = 0; forever #5 clk = ~clk; end
// reference model: what the counter should hold after each edge
always @(posedge clk or negedge rst_n)
if (!rst_n) expected <= 0; else if (en) expected <= expected + 1;
// compare on the falling edge, when both have settled
always @(negedge clk)
if (rst_n && count !== expected) begin
errors = errors + 1;
$display("ERROR at %0t: count=%0d expected=%0d", $time, count, expected);
end
initial begin
rst_n = 0; en = 0;
#20 rst_n = 1;
#10 en = 1;
#200; // 20 enabled clocks: wraps past 15 back to 0
#3 rst_n = 0; // reset in the middle of operation
#4 rst_n = 1;
#50 en = 0;
#20;
if (errors == 0) $display("PASS");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmodule
Best practices:
- Write self-checking testbenches with assertions or reference models.
- Use code and functional coverage (for example in Synopsys VCS) to make sure all paths are exercised.
- Simulate corner cases: reset during operation, maximum counter values, back-to-back requests.
7. Ignoring timing constraints
Not defining timing constraints for synthesis and place-and-route, or defining them wrongly, leads to timing violations. Inexperienced engineers may not yet understand STA, or may assume default constraints are enough. Without a clock definition, a tool has nothing to time against: paths go unconstrained, the design may miss its target frequency, or it may behave unpredictably in hardware.
# Missing clock definition: the input delay refers to a clock that does not exist
set_input_delay 2 -clock clk [get_ports data_in]
# Proper clock and input/output delays
create_clock -period 10 -name clk [get_ports clk]
set_input_delay 2 -clock clk [get_ports data_in]
set_output_delay 2 -clock clk [get_ports data_out]
Worked example: with a 10 ns clock and a 2 ns input delay, the logic from data_in to its first flop has 10 - 2 = 8 ns, less the flop's setup time and any clock uncertainty. If setup is 0.1 ns and uncertainty 0.2 ns (illustrative values), the logic must finish in 7.7 ns. Without the constraints, that path is simply not checked.
Best practices:
- Learn STA concepts: setup and hold, slack, clock uncertainty, false and multicycle paths.
- Use tools such as Synopsys PrimeTime to validate timing, and check the report of unconstrained paths.
- Ask senior engineers to review your SDC files; exceptions (false paths, multicycle paths) need special care.
Timing constraints are normally written in SDC files, which are independent of the HDL, so no Verilog, SystemVerilog or VHDL example is needed here.
8. Hard-coding parameters
Relying on fixed values such as bus widths or depths instead of parameters limits reuse and scalability. Inexperienced engineers may not anticipate future changes or may go for the quickest implementation. Hard-coded designs are difficult to modify or reuse, which raises maintenance effort.
module bad_param (
input [7:0] data_in,
output [7:0] data_out
);
assign data_out = data_in;
endmodule
module good_param #(
parameter WIDTH = 8
) (
input [WIDTH-1:0] data_in,
output [WIDTH-1:0] data_out
);
assign data_out = data_in;
endmodule
Best practices:
- Parameterise all configurable aspects (bit widths, FIFO depths, pipeline stages), and use localparam for values derived from them, such as $clog2(DEPTH).
- Document parameter meaning, legal ranges and default values.
- Test the design with several parameter values, including the smallest and largest supported.
9. Not following coding guidelines
Inconsistent, poorly commented or unstructured code is difficult to read, debug and maintain. Inexperienced engineers may not know the team or industry standards, or may put function ahead of readability. Poorly structured code slows reviews, extends debug time and makes mistakes during maintenance more likely.
Both modules below compile and do the same thing. Only one can be reviewed.
module m1 (input c,r,i,output o);reg [3:0] s;always@(posedge c or negedge r)begin if(!r)s<=0;else if(i)s<=s+1;end assign o=s[0];endmodule
module counter (
input clk, // System clock
input rst_n, // Active-low reset
input enable, // Counter enable
output state // LSB of counter
);
reg [3:0] count; // 4-bit counter state
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0000; // Reset to 0
else if (enable)
count <= count + 1; // Increment on enable
end
assign state = count[0]; // Output LSB
endmodule
Best practices:
- Adopt team or industry standards, for example consistent names such as clk for clocks and an _n suffix for active-low signals.
- Use comments to explain intent, especially for complex logic.
- Run lint tools (for example Verilator in lint mode, or SpyGlass) to enforce coding rules.
10. Not verifying synthesis output
Skipping the check that the synthesised netlist matches the RTL can let optimisation-related bugs through. Inexperienced engineers may skip gate-level simulation or formal equivalence checking because of time pressure or unfamiliarity, risking differences caused by synthesis, for example logic that was pruned because a signal was never used, or behaviour that differs because of X handling or a missing sensitivity list entry. Those differences can become hardware failures.
Recommendation:
- Use tools such as Synopsys Formality or Cadence Conformal to prove RTL-to-netlist equivalence.
- Run gate-level simulations with the same testbench used for RTL.
Best practices:
- Always compare RTL and gate-level simulation results.
- Review synthesis logs for warnings, such as inferred latches, unmapped logic or removed registers.
- Include timing information (SDF back-annotation) in gate-level simulation to find timing-related issues.
How reviewers and interviewers probe these topics
- "Where does this code infer a latch?" They show an always @(*) block with a missing else or an incomplete case.
- "How would you pass a 4-bit counter value to another clock domain?" They expect Gray code or a handshake, not four two-flop synchronisers.
- "What does this loop synthesise to?" They want you to describe the unrolled hardware.
- "Why does your simulation pass but the netlist fail?" Good answers mention X optimism, sensitivity lists, unreset flops and missing constraints.
- "What is in your SDC?" At minimum: clocks, generated clocks, I/O delays, clock uncertainty and justified exceptions.
General advice for junior engineers
Master the tools: learn simulation (for example ModelSim or Questa, VCS), synthesis (for example Synopsys Design Compiler) and STA tools (for example PrimeTime). Understand their outputs and warnings.
Seek code reviews: share your RTL with senior engineers to catch mistakes early and learn good practice.
Study real designs: read open-source RTL (for example OpenCores projects or RISC-V cores) or your company's IP to see practical implementations.
Practise debugging: spend time on simulation failures to build intuition for hardware behaviour.
Learn from synthesis reports: review them to understand the area, timing and power impact of your code.
Document everything: keep clear documentation for your design, including block diagrams, state machines and interface specifications.
Additional resources
- Verilog HDL by Samir Palnitkar
- SystemVerilog for Design by Stuart Sutherland (with Simon Davidmann and Peter Flake)
- VHDL for Logic Synthesis by Andrew Rushton
- Online platforms such as Coursera and Udemy offer FPGA and HDL courses.
- Open-source tools such as Verilator (simulation and lint) and Yosys (synthesis) are good for experiments.
FAQ
Are for loops allowed in synthesisable Verilog?
Yes, when the number of iterations is fixed at compile time. The tool unrolls the loop into parallel hardware. Loops whose bounds depend on run-time signals, or that wait for clock edges, are not synthesisable.
Is a three-flop synchroniser better than two?
It gives more settling time and a longer mean time between failures. Use it when clock rates are very high or when reliability targets demand it; check the MTBF calculation for your process.
Synchronous or asynchronous reset?
Both work. Asynchronous reset works without a running clock; synchronous reset is easier to time. Whichever you choose, release an asynchronous reset synchronously and use the same approach across the block.
Is gate-level simulation still needed if equivalence checking passes?
Equivalence checking proves logical equality; gate-level simulation with timing still helps check reset sequences, X propagation, and timing exceptions. Many teams run a small set of gate-level tests for this reason.

.jpg)
Your comments will be moderated before it appears here.