← All Articles

VHDL DESIGN & VERIFICATION

10 Common RTL Design Mistakes and How to Avoid Them

Practical lessons for FPGA designers, illustrated in VHDL.

Concept illustration of VHDL design review and self-checking verification
Concept illustration of VHDL design and verification.

VHDL can compile successfully and still describe the wrong circuit. Some mistakes change simulated behavior; others create unwanted storage, lose arithmetic information, or break an interface on the board. This article focuses on ten mistakes worth checking early, with a concrete way to avoid each one.

1. Reading VHDL as a software program

The statements inside a process execute in order, but separate processes and concurrent assignments describe hardware that operates together. Moving a statement lower in the source file does not make a different circuit wait for it.

How to avoid it: sketch the registers, combinational paths, and clock boundaries before writing the code. For a pipeline, say which clock edge accepts an input and which edge produces its result. A loop with constant bounds can describe replicated hardware; it does not automatically spread work over several clock cycles. If an operation needs several cycles, describe the state and scheduling explicitly.

A useful review question is: “Which registers hold this information between clock edges?” If that answer is unclear, the architecture probably needs more work.

2. Expecting signal assignments to take effect immediately

A signal assignment schedules an update. A later statement in the same process activation still reads the signal’s current value. Consider two eight-bit unsigned registers:

process(clk)
begin
  if rising_edge(clk) then
    count <= count + 1;
    mirrored <= count;
  end if;
end process;

If count was 5 before the edge, the scheduled results are count = 6 and mirrored = 5. This is a valid pipeline when that is the intended behavior.

How to avoid it: decide whether the second register needs the old value or the newly calculated value. For the new value, calculate it once in a variable:

process(clk)
  variable next_count : unsigned(7 downto 0);
begin
  if rising_edge(clk) then
    next_count := count + 1;
    count <= next_count;
    mirrored <= next_count;
  end if;
end process;

Here the variable is assigned before it is read on every active edge. Variables are not inherently “software-only”; the surrounding code determines the inferred hardware. See AMD’s signal and variable example.

3. Leaving combinational outputs unassigned

If a combinational process assigns an output only when an enable is high, what should happen when the enable is low? Without another assignment, the description asks the output to retain its value, which can infer a latch.

How to avoid it: give each combinational output a default, then override it for the required cases. This example explicitly outputs zero when disabled:

process(all)
begin
  result <= (others => '0');
  if enable = '1' then
    result <= data;
  end if;
end process;

The default must match the required behavior. Do not choose zero simply to silence a warning if the specification needs something else. In a clocked process, leaving a register unassigned on a clock edge normally means hold its value; that is different from an accidental combinational latch. Review inferred storage in the synthesis report. See AMD’s latch guidance.

4. Writing an incomplete sensitivity list

A combinational process may read a and b while listing only a. Simulation then fails to rerun the process when only b changes. Synthesis may still build a circuit that depends on both inputs, creating a simulation-versus-hardware mismatch.

How to avoid it: use VHDL-2008 process(all) for combinational logic, or list every signal read if the project uses an older language version:

process(all)
begin
  y <= a and b;
end process;

Enable VHDL-2008 in both simulation and synthesis. process(all) fixes sensitivity, not missing output assignments: mistake 3 still needs its own review. For a synchronous register process, use its clock, plus any genuinely asynchronous reset, in the sensitivity list. See AMD’s sensitivity-list documentation.

5. Ignoring arithmetic type, width, and overflow

A vector’s bits do not tell the reader whether the value is signed or unsigned. With numeric_std, use unsigned or signed for arithmetic and make conversions explicit at interfaces.

Width matters before the calculation. Adding two eight-bit unsigned values produces an eight-bit result with this overload; assigning or resizing it afterward does not restore a discarded carry.

-- a and b are unsigned(7 downto 0)
-- sum9 is unsigned(8 downto 0)

-- Carry can already be lost inside a + b:
sum9 <= resize(a + b, sum9'length);

-- Extend the operands before adding:
sum9 <= resize(a, sum9'length) + resize(b, sum9'length);

For a = 255 and b = 1, the first expression gives 0; the second gives 256.

How to avoid it: specify whether each operation wraps, saturates, or preserves a carry. Check multiplication result widths, signed extension, and narrowing conversions too. Test zero, maximum values, negative values where applicable, and transitions across the sign boundary.

6. Giving one register multiple owners

Writing to the same signal from two processes can feel like splitting responsibilities: one process handles reset and another handles normal updates. It actually creates separate drivers. A resolved type such as std_logic can show conflicts as X in simulation; it does not supply arbitration for a normal FPGA register.

How to avoid it: give each register one owning process. Put reset, enable, and competing operations in that process with an explicit priority. Other blocks can provide requests, but the owner decides the next value.

When reviewing a signal, find every assignment to it. Deliberate shared buses and physical tri-state interfaces need their own supported implementation; they are not a reason to let unrelated processes drive the same internal register.

7. Building a clock with ordinary logic

An expression such as gated_clk <= clk and enable; puts ordinary logic in the clock path. An enable transition can create an unwanted edge, and the resulting network may lack the routing and timing treatment expected for a clock.

How to avoid it: keep the clock on supported clock resources and use a clock enable for ordinary conditional updates:

process(clk)
begin
  if rising_edge(clk) then
    if rst = '1' then
      count <= (others => '0');
    elsif tick = '1' then
      count <= count + 1;
    end if;
  end if;
end process;

For slower activity, a one-cycle tick often avoids creating another clock domain. If the design genuinely needs a separate or stoppable clock, use the device’s clock-management and clock-buffer resources, document their behavior, and constrain the generated clock. See AMD’s gated-clock recommendations.

8. Treating a clock-domain crossing as ordinary wiring

An unrelated destination clock can sample a signal near a transition. A suitable synchronizer reduces the risk of metastability propagating, but crossing a complete transaction needs more than individually synchronizing its bits.

How to avoid it: choose the crossing method from the information being transferred:

  • A single-bit level: use an appropriate synchronizer chain and placement attributes.
  • A brief event: use a pulse-transfer or toggle/acknowledgment protocol suited to the event rate; a short pulse can be missed.
  • A word or stream: use a stable-data handshake or an asynchronous FIFO, with its required operating contract.

Independent bit synchronizers do not guarantee that a destination observes one coherent word. Define acknowledgment, data-hold time, reset behavior, and what happens when one side stops. Review CDC reports alongside the protocol. See AMD’s multi-bit CDC guidance and my CDC coherency article.

9. Leaving startup and reset release undefined

A testbench may start every signal at a convenient value while the actual design has a different initialization contract. Conversely, some FPGA tools and devices support register initial values, so claiming that initializers “never become hardware” is also misleading.

How to avoid it: separate configuration-time initialization from a runtime reset. Check the target’s supported initialization behavior, decide which control registers must reset, and invalidate stored data when appropriate instead of assuming every memory location must be cleared.

If reset is asynchronous, release it safely in each relevant clock domain. Consider stopped clocks, clock-lock loss, pending transactions, and when downstream blocks may accept data again. Adding a reset everywhere can affect memory inference, fanout, and timing; the reset plan should follow the circuit’s required behavior. See AMD’s initialization guidance and reset design across clock domains.

10. Stopping after a waveform looks correct

A few plausible waveforms can hide overflow, reset priority, invalid inputs, full/empty boundaries, missed pulses, or failures under backpressure. A functional simulation also does not demonstrate that a routed circuit meets setup and hold timing.

How to avoid it: turn written requirements into self-checking tests. Check accepted transactions rather than attempted transfers, account for pipeline latency, and include stalls, reset during operation, boundary cases, and a watchdog timeout.

Drive inputs at deliberate times and sample after the DUT has settled. A checker that reads immediately on the clock edge can see an older signal value. The simple examples in this article were checked with GHDL; that does not validate an unrelated application or physical FPGA implementation.

Then inspect synthesis warnings, inferred resources, CDC findings, and routed setup/hold results. Verify clock definitions and the required I/O constraints; an unconstrained path is not evidence of timing success. Follow with staged board testing. My articles on useful VHDL testbenches and setup and hold timing develop those steps.

A short review before the next build

I work through the same practical questions: Are register ownership and priorities clear? Are combinational assignments complete? Are arithmetic widths intentional? Do clock and reset boundaries have a defined contract? Does the testbench check the behavior at the correct time?

Those questions make a code review useful before a tool reports the problem. When a test finds a mistake, keep the reproducing case and rerun it after the fix.

The snippets use ieee.std_logic_1164 and ieee.numeric_std; signal declarations are abbreviated where the surrounding text states their types. They illustrate specific behavior and are not complete board projects.

Build a self-checking testbench →