VHDL · Chapter 16.2 · Synthesis and RTL Implementation
The Synthesizable Subset of VHDL
VHDL is a large language built for both simulation and hardware, and only part of it maps to gates. The synthesizable subset is what a synthesizer can turn into a circuit: entities and architectures, clocked and combinational processes, if and case, bounded for loops, signals and variables, the standard types and operators, rising-edge detection, generics and generate, and bounded pure subprograms. Everything else is simulation-only and is either ignored or rejected by synthesis: timed waits and after-delays, file I/O and textio, access and protected types, real, initial values used as reset, and unbounded loops or recursion. Knowing the boundary is what keeps RTL buildable and keeps simulation and hardware in agreement. This lesson catalogs what is in the subset, what is not, and how to code within it.
Foundation14 min readVHDLSynthesisSynthesizable SubsetRTLSimulationCoding Style
1. Engineering intuition — write only what can be a circuit
A synthesizer can only build what corresponds to gates, flip-flops, and wires that exist before the circuit
runs. So the test for any construct is simple: could this be a fixed piece of hardware? A clocked process —
yes, flip-flops. A bounded loop — yes, it unrolls into fixed logic. But "wait 5 ns," "read a file," "allocate a
node," "compute in real" have no gate equivalent; they only make sense to a simulator. The synthesizable
subset is exactly the constructs that pass this test, and staying inside it is not a limitation so much as a
discipline: write hardware-describing code, keep the simulation-only constructs in testbenches, and your RTL
builds — and matches simulation.
2. Formal explanation — the subset, in and out
-- SYNTHESIZABLE (maps to hardware):
-- entities / architectures / components / generics / generate
-- processes: clocked (rising_edge/falling_edge) and combinational (process(all))
-- if / elsif / else, case/when (with full coverage), BOUNDED for-loops
-- signals, variables; types: std_logic, std_logic_vector, unsigned, signed,
-- RANGED integer, enumerations, records, constrained/bounded arrays
-- operators: and/or/not/xor, +, -, *, comparisons, &, slices, aggregates
-- conversions (numeric_std), rising_edge/falling_edge
-- BOUNDED, PURE functions/procedures (inline to logic)
-- NOT SYNTHESIZABLE (simulation-only — ignored or rejected by synthesis):
-- wait for <time>, signal <= x after <time> (timing/delays)
-- file / textio, report-as-IO (host I/O)
-- access types (pointers), protected types, 'new' (dynamic memory)
-- real / math_real (non-hardware types)
-- initial values used AS reset (HW powers up unknown)
-- unbounded loops / unbounded recursion (no fixed hardware)
-- division/modulo by a NON-constant (tool-dependent / costly)The synthesizable subset covers structural/behavioral RTL: entities, processes, control flow with bounded
loops, the standard types and operators, edge functions, generics/generate, and bounded pure subprograms.
Outside it: timed wait/after, file I/O, access/protected, real, initial-as-reset, unbounded
loops/recursion, and non-constant division — all simulation-only.
3. Production usage — keeping RTL inside the subset
-- SYNTHESIZABLE RTL: clocked process, bounded loop, standard types/ops, reset (not initial value).
process (clk) begin
if rising_edge(clk) then
if rst = '1' then acc <= (others => '0'); -- RESET, not an initializer
else
acc <= (others => '0');
for i in data'range loop -- BOUNDED loop (data'range fixed) → unrolls
if data(i) = '1' then acc <= acc + 1; end if;
end loop;
end if;
end if;
end process;
-- KEEP simulation-only constructs in the TESTBENCH, never in RTL:
-- wait for 10 ns; -- TB timing
-- readline(f, l); -- TB file I/O
-- shared variable sb : scoreboard_t; -- TB protected typeWhat hardware does this become? The process above is fully synthesizable — a registered accumulator with a
reset and an unrolled population-count loop — because every construct maps to fixed logic. The discipline is a
clean split: RTL uses only subset constructs (reset every register, bounded loops, standard types); the
testbench is where wait, textio, access/protected, and real live. This is the same boundary that
prevents sim/synth mismatches (15.5): stay in the subset and the simulator and synthesizer read your code the same
way.
4. Structural interpretation — the subset boundary
5. Why this is structural, not timing
The synthesizable subset is a classification of constructs — which language features become hardware and which do not — so the boundary diagram above is the right picture, not a waveform. It is a static property of the code, not a behavior over time; the synthesizable constructs' run-time behavior was covered in their own modules (combinational, sequential, memory), and the simulation-only ones have meaning only in a testbench timeline. Knowing the subset is structural knowledge: where each feature falls, so your RTL stays buildable.
6. Debugging example — a simulation-only construct in RTL
Expected: the RTL synthesizes cleanly and matches simulation. Observed: synthesis rejects the code
(unsupported construct), or silently ignores part of it so the hardware differs from simulation — an after
delay that vanishes, an initial value that "resets" in sim but not in silicon, a wait for or real the tool
drops. Root cause: a simulation-only construct leaked into RTL — timed wait/after, file I/O,
access/protected, real, an initial value used as reset, or an unbounded loop — none of which is in
the synthesizable subset. Fix: keep RTL strictly inside the subset — use a reset instead of an
initializer, bounded loops, no timed delays/file I/O/real — and move all simulation-only constructs into the
testbench. Engineering takeaway: if a construct has no gate equivalent (timed waits, file I/O, dynamic
memory, real, initial-as-reset, unbounded loops), it does not belong in RTL — keep it in the testbench so
synthesis and simulation agree.
-- BUG: relying on an initial value as reset + an 'after' delay — both sim-only, ignored by synth.
-- signal cnt : unsigned(7 downto 0) := (others=>'0'); q <= d after 2 ns;
-- FIX: explicit reset; no delay in RTL.
if rst='1' then cnt <= (others=>'0'); else cnt <= cnt + 1; end if; q <= d;7. Common mistakes & what to watch for
- Initial value as reset. Hardware powers up unknown; reset registers explicitly rather than relying on signal initializers.
- Timed
wait/afterin RTL. Delays are simulation-only and ignored by synthesis; sequence with clocks, not time. - Unbounded loops/recursion. Synthesizable loops must have static bounds (they unroll); unbounded forms are rejected.
- File I/O,
access/protected,realin RTL. All simulation-only; keep them in testbenches/models. - Non-constant division/modulo. Often unsupported or very costly; restructure (constant divisors, shifts, or dedicated dividers).
8. Engineering insight & continuity
The synthesizable subset is the set of constructs that become hardware — processes, if/case, bounded loops,
the standard types and operators, rising_edge, generics/generate, and bounded pure subprograms — while timed
waits, file I/O, dynamic memory, real, initial-as-reset, and unbounded loops are simulation-only. Coding
inside the subset is what makes RTL buildable and keeps it matching simulation. Within the subset, the next
question is what specific hardware each pattern produces — the inference rules: the next lesson, Inferring
Flip-Flops, Latches, and Logic, covers exactly which RTL patterns become which primitives.