HDLbits

Reference · Good coding practices

HDL Coding Guidelines

Practical rules, examples and design patterns for writing clear RTL, avoiding common hardware pitfalls, and building designs that are easier to verify and maintain.

VHDL · Coding guide

VHDL Good Coding Practices

13 chapters

Chapter index (13)

Rules and templates for VHDL that is readable, synthesizable, safely clocked and verifiable.

1. Purpose and scope

This guide collects good practices for writing clear, synthesizable and testable VHDL. Advice is given as short rules; comparisons, naming conventions, resets and common mistakes are in tables; examples are in labeled code panels.

Topic What it covers
Layout and naming Files, formatting, association style, naming conventions
Combinational logic Completeness, defaults, sensitivity
Sequential logic Clock idiom, resets
State machines Structure and safe coding
Arithmetic and types numeric_std, conversions, widths
Hierarchy and reuse Generics, packages, instantiation
Clocks and timing Clock domains, pipelining
Memory Inferring RAM
Verification Self-checking testbenches
Tool hygiene Reports, lint, version control

2. Source layout and naming

Files and formatting

  • Keep one entity and its architecture per file, and name the file after the entity.
  • Indent with spaces and set your editor to convert the Tab key, so the file looks the same everywhere.
  • Use named association in port maps and generic maps, and label every process, generate and instance.
  • Keep lines short enough to read side by side in a diff or review tool.
  • Delete abandoned code. Version control keeps the history, so commented-out blocks only add noise.
  • Write comments that explain intent and assumptions, not what the syntax already says.

A naming convention

The exact convention matters less than using one consistently. A common scheme is shown below.

Item Convention Example
Active-low signal Suffix _n rst_n, cs_n
Register output Suffix _reg count_reg
Next value of a register Suffix _next count_next
Constants and generics Upper case DATA_WIDTH
Types Suffix _t link_state_t
Instances Prefix u_ u_fifo
Clocks and resets Include the domain clk_sys, rst_sys

Choose names that describe purpose. A reader should be able to tell what a signal is for without reading the logic around it.

3. Combinational logic

Combinational logic has no memory, so every output must have a value for every input combination. If a process leaves an output unassigned on some path, synthesis inserts a latch to hold the old value. Latches are rarely intended and complicate timing analysis.

Techniques

  • Assign a default value to every output at the top of the process, then override it in branches.
  • Cover every case with when others.
  • Use process(all) (VHDL-2008) so the sensitivity list is always complete. With older tools, list every signal that is read.
  • Use a concurrent assignment (when ... else, with ... select) for simple logic, and a process for anything with several outputs.

Default first, complete case

decode : process (all)
begin
    grant <= "0000";                       -- default first
    case req is
        when "00"   => grant <= "0001";
        when "01"   => grant <= "0010";
        when "10"   => grant <= "0100";
        when others => grant <= "1000";
    end case;
end process decode;

Priority versus parallel

An if / elsif chain expresses priority: earlier conditions win. A case statement expresses mutually exclusive choices; the synthesized structure and timing depend on the synthesis tool, target device, constraints and coding details, so do not assume that case always produces shallower logic. Pick the form that matches the behavior you actually want.

4. Sequential logic

The clocked process

  • Use rising_edge(clk) inside a process whose sensitivity list is the clock (plus an asynchronous reset if used).
  • Use one clock per process, and one process per group of related registers.
  • Drive each signal from exactly one process.
  • Model enables with an if inside the clocked branch rather than by gating the clock.

Register with enable

capture : process (clk)
begin
    if rising_edge(clk) then
        if load_en = '1' then
            data_reg <= data_in;
        end if;
    end if;
end process capture;

Choosing a reset

Synchronous reset Asynchronous reset
Sampled Only on a clock edge Immediately, independent of the clock
Hardware Extra logic in the data path; some primitives have dedicated sync-reset pins Uses dedicated reset pins of registers
Risk Needs a running clock during reset Release near a clock edge can cause metastability
Common practice Simple, timing-friendly on many FPGAs Assert asynchronously, release synchronously

Reset only the registers that need a known value, such as control and state. Data-path registers often need no reset, and leaving them out reduces routing and fanout. Follow your device vendor guidance on reset and initial values.

Reset synchronizer

-- Reset synchronizer: asserts at once, releases after two clock edges
reset_sync : process (clk, arst_n)
begin
    if arst_n = '0' then
        rst_pipe <= (others => '1');       -- rst_pipe : std_logic_vector(1 downto 0)
    elsif rising_edge(clk) then
        rst_pipe <= rst_pipe(0) & '0';
    end if;
end process reset_sync;

rst <= rst_pipe(1);                        -- use rst as a synchronous reset

5. State machines

  • Declare states with an enumerated type so tools show names and can re-encode them.
  • Separate the state register from the next-state and output logic, or use a single clocked process. Both work; pick one style per project.
  • Give next-state and outputs defaults, then override in each state.
  • Reset into a defined idle state.
  • Consider whether outputs should be registered so they are glitch-free and easy to time.
  • Document the states and transitions in a diagram alongside the code.

Two-process state machine

type   link_state_t is (WAIT_REQ, SEND, WAIT_ACK);
signal state_reg, state_next : link_state_t := WAIT_REQ;
...
state_update : process (clk)
begin
    if rising_edge(clk) then
        if rst = '1' then
            state_reg <= WAIT_REQ;
        else
            state_reg <= state_next;
        end if;
    end if;
end process state_update;

state_decode : process (all)
begin
    state_next <= state_reg;               -- defaults
    tx_valid   <= '0';
    case state_reg is
        when WAIT_REQ =>
            if req = '1' then state_next <= SEND; end if;
        when SEND =>
            tx_valid   <= '1';
            state_next <= WAIT_ACK;
        when WAIT_ACK =>
            if ack = '1' then state_next <= WAIT_REQ; end if;
    end case;
end process state_decode;

6. Arithmetic and types

  • Use ieee.numeric_std with unsigned and signed. Avoid std_logic_arith, std_logic_unsigned and std_logic_signed, which are not IEEE standards.
  • Keep vectors as std_logic_vector at interfaces and convert to unsigned or signed inside.
  • Choose result widths deliberately. Use resize to keep a carry or to widen before an operation.
  • Compare against zero with unsigned(x) /= 0, or use the VHDL-2008 reduction operators such as or x.
  • Limit integer signals with a range so synthesis uses the smallest width.
  • Do not mix signed and unsigned values without an explicit conversion.
signal a_u, b_u : unsigned(7 downto 0);
signal sum_u    : unsigned(8 downto 0);     -- one extra bit for the carry
...
sum_u   <= resize(a_u, 9) + resize(b_u, 9);
nonzero <= '1' when a_u /= 0 else '0';

7. Hierarchy and reuse

  • Prefer several small, single-purpose modules over one large module. They are easier to test and reuse.
  • Use generics for widths and depths instead of hard-coded numbers.
  • Put shared types, constants and functions in packages.
  • Instantiate with entity work.name so no separate component declaration is needed.
  • Give each module a documented interface: clock, reset, protocol, latency.

8. Clocks and timing

Crossing clock domains

A signal that moves between two unrelated clocks can be sampled while changing and become metastable. A single control bit is usually passed through two or more flip-flops in the destination domain. Multi-bit values need a handshake, a Gray-coded pointer scheme or an asynchronous FIFO.

Two-flop synchronizer

sync_proc : process (dst_clk)
begin
    if rising_edge(dst_clk) then
        meta_ff <= async_in;               -- may go metastable
        sync_ff <= meta_ff;                -- safe to use
    end if;
end process sync_proc;
-- Many FPGA tools have an attribute (for example ASYNC_REG) to keep these
-- two registers together; check your vendor documentation.

Meeting timing

  • Break long combinational paths by adding pipeline registers, and account for the added latency.
  • A high-fanout signal can limit timing; registering it can give the implementation tool an opportunity to replicate or otherwise optimize the fanout, but the result is tool- and constraint-dependent.
  • Do not gate clocks with logic in the fabric. Use clock enables or the device clock primitives.
  • Register module outputs where practical, so timing at module boundaries is predictable.
  • Write down the reason for every constraint exception, such as a false path or multicycle path.
  • Read the timing report after every significant change instead of only at the end.

9. Memory

Synthesis tools infer RAM from a recognizable coding pattern. Use the template your tool documents, including the read behavior (read-first, write-first or no-change), because the choice affects which resources the tool can use.

Inferred RAM

type   ram_t is array (0 to 1023) of std_logic_vector(15 downto 0);
signal ram : ram_t;
...
ram_proc : process (clk)
begin
    if rising_edge(clk) then
        if wr_en = '1' then
            ram(to_integer(wr_addr)) <= wr_data;    -- wr_addr : unsigned
        end if;
        rd_data <= ram(to_integer(rd_addr));        -- synchronous read
    end if;
end process ram_proc;

Small or wide, shallow memories may map better to distributed (LUT) RAM, while large memories map to block RAM. The best choice depends on the device, so check the synthesis report.

10. Verification

  • Give every module its own testbench and run it before integrating.
  • Make testbenches self-checking with assert so a run reports pass or fail without inspecting waveforms.
  • Test corner cases: reset during activity, back-to-back transfers, empty and full conditions, minimum and maximum values.
  • Use waveforms to diagnose a failure, not to prove correctness.
  • Wrap repeated checks in a procedure so failures give consistent, readable messages.
  • Re-run the whole regression after every change.
  • For larger projects consider open-source libraries such as OSVVM or UVVM for randomization, scoreboards and coverage.

Check procedure

check : process
    procedure check_equal (actual, expected : std_logic_vector; label_text : string) is
    begin
        assert actual = expected
            report label_text & ": expected " & to_string(expected) & ", got " & to_string(actual)
            severity error;
    end procedure check_equal;
begin
    wait until rising_edge(clk);
    wait for 1 ns;
    check_equal(dout, x"3C", "after first word");
    wait;
end process check;

to_string is available from VHDL-2008.

11. Tool hygiene

  • Read the synthesis log for inferred latches, unconnected ports, width mismatches and trimmed logic. Treat each warning as something to understand.
  • Run the linter your tools provide, and keep the design free of warnings you cannot explain.
  • Check clock-domain crossing and timing reports as well as functional results.
  • Commit to version control often, with messages that say why a change was made.
  • Review code in small pieces and keep a short log of design decisions.

12. Common mistakes

Mistake Effect Remedy
Output not assigned on every path Inferred latch Default assignments, when others
Incomplete sensitivity list Simulation differs from hardware process(all)
Two processes drive one signal Multiple-driver error or X Single driver per signal
Mixing arithmetic packages Ambiguous operators or wrong results Use numeric_std only
Unsynchronized signal between clocks Metastability, intermittent failures Synchronizer or handshake
Width mismatch ignored Truncation, lost carry Explicit resize and checks
Reading simulation only Hardware behaves differently Also read synthesis and timing reports
No self-checking test Regressions go unnoticed assert in every testbench

13. Standards and further reading

  • IEEE Std 1076 (VHDL Language Reference Manual), including VHDL-2008 and 2019 revisions.
  • FPGA vendor synthesis guides, for example AMD Vivado UG901 (Vivado Design Suite User Guide: Synthesis).
  • Papers by Clifford Cummings (Sunburst Design) on coding style, resets and clock-domain crossing.
  • OSVVM and UVVM open-source VHDL verification libraries.

Licence. To the extent possible under law, the author has dedicated all copyright and related rights in this document, including its text, tables and code examples, to the public domain under the CC0 1.0 Universal Public Domain Dedication (https://creativecommons.org/publicdomain/zero/1.0/). You may copy, modify and share it for any purpose without asking permission. It is provided as is, without warranty. IEEE, Verilog, SystemVerilog, VHDL, UVM and other names are used only to identify the languages and standards they refer to.