Rule Writing

AI Rules for VHDL and Hardware Description Languages

VHDL describes hardware, not software. AI trained on software languages generates non-synthesizable code. Rules for synthesizable patterns, clock domains, and testbenches.

7 min read·May 14, 2025

VHDL describes hardware — code that simulates but doesn't synthesize is useless

Synthesizable patterns, clock domains, reset strategies, and testbench design

Why VHDL Needs AI Rules More Than Any Software Language

VHDL (and Verilog) describe hardware, not software. When you write VHDL, you're defining physical circuits that get manufactured into silicon or programmed into FPGAs. Every line of code corresponds to real gates, flip-flops, and wires. AI assistants trained on software languages generate VHDL that simulates correctly in a testbench but produces non-synthesizable, timing-violating, or functionally incorrect hardware.

The consequences are severe: a software bug causes a crash you can fix with a deploy. A hardware bug in an ASIC is permanent — the chip is manufactured incorrectly and can't be patched. Even in FPGAs (which are reprogrammable), debugging hardware timing issues can take weeks. AI-generated VHDL that 'works in simulation' is the most dangerous code the AI can produce.

These rules focus on synthesizable RTL (Register Transfer Level) VHDL — the subset that synthesis tools can convert to hardware. Behavioral VHDL for testbenches has looser rules and is noted separately.

Rule 1: Synthesizable Code Only in RTL

The rule: 'All RTL code must be synthesizable. No wait statements in synthesizable code (only in testbenches). No file I/O in synthesizable code. No initial values on signals (use reset instead). No after clauses for delays (only in testbenches). No shared variables. Every process must have a sensitivity list that matches all signals read in the process — or use process(all) in VHDL-2008.'

For combinational logic: 'Combinational processes must assign every output in every branch of every if/case statement. Missing assignments create latches — unintentional latches are the #1 FPGA synthesis bug. Use case ... is when others => to provide a default. Prefer concurrent assignments (y <= a and b;) over processes for simple combinational logic.'

For sequential logic: 'Sequential processes use only the clock (and optionally async reset) in the sensitivity list: process(clk, rst_n). Use rising_edge(clk) for clock edge detection — never clk'event and clk='1' (deprecated). One clock domain per process. Separate combinational and sequential logic into separate processes.'

  • No wait, file I/O, initial values, or after in synthesizable code
  • Sensitivity list matches all read signals — or process(all) in VHDL-2008
  • Combinational: assign every output in every branch — no unintentional latches
  • Sequential: rising_edge(clk) — one clock domain per process
  • Separate combinational and sequential processes — never mixed
⚠️ #1 FPGA Bug

Missing assignments in combinational processes create unintentional latches — the #1 FPGA synthesis bug. Every output must be assigned in every branch of every if/case. Use 'when others =>' as a safety net.

Rule 2: Clock Domain and Reset Strategy

The rule: 'Use a single clock domain wherever possible. When multiple clock domains are unavoidable, use proper clock domain crossing (CDC) techniques: two-stage synchronizers for single-bit signals, async FIFOs for multi-bit data, and gray-code counters for address crossings. Never sample a signal from one clock domain directly in another — it causes metastability.'

For resets: 'Use synchronous active-low reset (rst_n) as the default reset strategy. Assert reset asynchronously, deassert synchronously (async assert, sync deassert). Include a reset synchronizer at each clock domain entry point. Initialize all flip-flops in reset — never rely on power-on state.'

AI assistants generate multi-clock designs without CDC, sample signals across domains without synchronizers, and use asynchronous reset without proper deassertion. Each of these creates intermittent, difficult-to-debug hardware failures that only appear under specific timing conditions.

💡 Metastability Kills

Sampling a signal from one clock domain directly in another causes metastability — the flip-flop output is neither 0 nor 1 for an unpredictable duration. Always use two-stage synchronizers. This bug is intermittent and nearly impossible to debug.

Rule 3: Naming Conventions and Structure

The rule: 'Use consistent naming: _i suffix for inputs, _o suffix for outputs, _n suffix for active-low signals, _reg suffix for registered signals, _next suffix for combinational next-state signals. Entity names match the file name: counter.vhd contains entity counter. One entity per file. Architecture name is rtl for synthesizable, tb for testbench, behavioral for simulation-only.'

For hierarchy: 'Use component instantiation with port map for structural composition. Use generics for parameterizable modules: generic (WIDTH : positive := 8). Use generate statements for repetitive structures. Keep hierarchy flat — deep nesting makes timing analysis harder.'

For packages: 'Define project-wide types, constants, and functions in a package: package my_project_pkg is. Use custom types for data buses: type data_bus_t is array (natural range <>) of std_logic_vector(7 downto 0). Never use magic numbers — define constants in the package.'

  • _i for inputs, _o for outputs, _n for active-low, _reg for registered
  • One entity per file — entity name matches filename
  • Architecture rtl for synthesis, tb for testbench, behavioral for sim
  • Generics for parameterization — generate for repetitive structures
  • Package for shared types, constants, functions — no magic numbers

Rule 4: Testbench Design

The rule: 'Every RTL module has a corresponding testbench in tb/ directory. Testbenches use behavioral VHDL (wait, file I/O, initial values are allowed). Use a clock generation process: clk <= not clk after CLK_PERIOD/2. Use procedures for common stimulus patterns: reset, write, read, wait_for_n_cycles. Assert outputs with assert/report severity failure — never just visually inspect waveforms.'

For self-checking testbenches: 'Testbenches must be self-checking — they verify correct behavior programmatically, not by waveform inspection. Compare DUT outputs against expected values. Use file-based test vectors for complex patterns. Report pass/fail at the end of simulation: assert false report "Test PASSED" severity note.'

For coverage: 'Use functional coverage to verify that all important scenarios are exercised. Cover: all state machine states, all branch conditions, boundary values (0, max, overflow), and error conditions. Coverage-driven verification catches corner cases that directed tests miss.'

ℹ️ Self-Checking Tests

Never verify hardware by inspecting waveforms alone. Self-checking testbenches assert outputs against expected values programmatically. If the simulation ends without assertion failures, it passed. Reproducible, automatable, trustworthy.

Rule 5: FPGA-Specific Patterns

The rule: 'For FPGA targets: use vendor primitives for RAMs, DSPs, and PLLs — don't infer them from behavioral code unless you've verified the inference result. Use attribute syntax to guide synthesis: attribute ram_style, attribute use_dsp. Constrain timing with SDC/XDC files — never rely on unconstrained synthesis. Use registered outputs on every module for timing closure.'

For resource usage: 'Monitor LUT, FF, BRAM, and DSP utilization after synthesis. Target <80% utilization for timing closure margin. Use pipelining to meet timing on long combinational paths. Use resource sharing (time-multiplexing) when multiple operations share the same hardware unit.'

For timing: 'Run timing analysis after place-and-route, not just after synthesis. Fix all timing violations before testing on hardware. Use false_path and multicycle_path constraints only when you can prove the paths are genuinely non-critical — never to suppress real violations.'

Complete VHDL Rules Template

Consolidated rules for VHDL RTL and testbench design.

  • Synthesizable RTL only: no wait, no file I/O, no initial values, no after
  • Assign every output in every branch — no unintentional latches
  • rising_edge(clk) — one clock per process — separate comb and seq
  • CDC: two-stage synchronizers for bits, async FIFOs for data, gray-code for addresses
  • Synchronous active-low reset — async assert, sync deassert — initialize all FFs
  • Naming: _i/_o/_n/_reg/_next — one entity per file — rtl/tb/behavioral architectures
  • Self-checking testbenches — assert/report — functional coverage
  • FPGA: vendor primitives for RAM/DSP, SDC timing constraints, <80% utilization target