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 / elsifchain expresses priority: earlier conditions win. Acasestatement expresses mutually exclusive choices; the synthesized structure and timing depend on the synthesis tool, target device, constraints and coding details, so do not assume thatcasealways 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
ifinside 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_stdwithunsignedandsigned. Avoidstd_logic_arith,std_logic_unsignedandstd_logic_signed, which are not IEEE standards. - Keep vectors as
std_logic_vectorat interfaces and convert tounsignedorsignedinside. - Choose result widths deliberately. Use
resizeto keep a carry or to widen before an operation. - Compare against zero with
unsigned(x) /= 0, or use the VHDL-2008 reduction operators such asor 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.nameso 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
assertso 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.