VHDL · Chapter 11.7 · Functions and Procedures
Synthesizable Subprograms
Subprograms are a source abstraction, so the key question for RTL is what actually becomes hardware. The governing fact is that a synthesizable subprogram is inlined: its body is substituted as combinational logic at every call site. Two consequences follow directly. It holds no state between calls, since its variables are immediate scratch rather than registers, and each call replicates the logic. From that the rules fall out: statically bounded loops are fine, but timed waits, file I/O, real and textio, dynamic allocation, and unbounded recursion are simulation-only. And because the body is just logic, the call context decides timing, staying combinational in a combinational context and becoming registered when called inside a clocked process. This lesson sets the boundary between hardware and simulation subprograms.
Foundation14 min readVHDLSynthesisSubprogramsInliningRTLSimulation
1. Engineering intuition — a subprogram is logic substituted in
The synthesizer does not build a "callable block" with a return address; it pastes the body in at each call site and turns it into gates. Once you hold that picture, everything about synthesizable subprograms becomes predictable. There is no memory carried from one call to the next, because there is no persistent block — just fresh logic each time. Anything that only makes sense over time or with unbounded resources — waiting for an edge, reading a file, recursing without a fixed depth — has no gate equivalent and is therefore simulation-only. The mental test is simple: could this body be drawn as a fixed block of combinational logic? If yes, it synthesizes.
2. Formal explanation — the synthesis rules
-- SYNTHESIZABLE subprogram: inlines to combinational logic.
-- ✓ bounded loops (range known at elaboration) ✓ variables as scratch (immediate)
-- ✓ if/case, arithmetic, conversions ✓ recursion ONLY if depth is statically bounded
function popcount (v : std_logic_vector) return natural is
variable n : natural := 0;
begin
for i in v'range loop -- bounded loop (v'range is fixed) → unrolls to an adder tree
if v(i) = '1' then n := n + 1; end if;
end loop;
return n; -- pure combinational result
end function;
-- NOT SYNTHESIZABLE (simulation / testbench only):
-- ✗ wait ✗ file I/O (textio) ✗ real / math_real
-- ✗ dynamic allocation (access types) ✗ unbounded recursion / unbounded loopsA subprogram synthesizes when its body maps to a fixed amount of logic: bounded loops (they unroll), ordinary
sequential statements, immediate variables. It does not synthesize when it relies on time (wait), the host
(file/textio), unbounded resources (dynamic allocation, unbounded recursion), or non-hardware types (real) —
those belong to simulation and testbenches.
3. Production usage — same subprogram, timing set by context
-- COMBINATIONAL context: the inlined logic is pure combinational.
y <= std_logic_vector(to_unsigned(popcount(data), 4)); -- popcount inlines as an adder tree
-- CLOCKED context: the SAME function inlines, but its result is captured in a register.
process (clk) begin
if rising_edge(clk) then
ones_r <= to_unsigned(popcount(data), 4); -- now registered (one cycle latency)
end if;
end process;What hardware does this become? popcount inlines to an adder tree either way — the function itself is
combinational logic. What differs is the call context: used in a concurrent assignment it is pure
combinational; used inside a clocked process, the same logic feeds a flip-flop and the result is registered.
The subprogram does not decide whether it is combinational or sequential — where you call it does. This is why
one well-written synthesizable helper serves both pipelined and combinational uses unchanged.
4. Structural interpretation — synthesizable vs simulation-only
5. Why this is structural, not timing
This lesson classifies which constructs become hardware, a structural property of the source, so the
synthesizable/simulation-only map (above) is the right picture rather than a waveform. The run-time behaviour of a
synthesizable subprogram is just the behaviour of the logic it inlines to — combinational, or registered when the
enclosing context is clocked — which earlier modules already covered in waveform form. The non-synthesizable
constructs do have timed behaviour (a wait-based testbench procedure sequences stimulus), but that timing
lives in the verification environment, not in synthesizable RTL.
6. Debugging example — the helper that would not synthesize
Expected: a reusable RTL helper builds. Observed: the synthesizer rejects the subprogram — "cannot
synthesize wait / file operation / non-static loop", or it infers unexpected hardware. Root cause: the body
used a simulation-only construct (a wait, textio, a real computation, dynamic allocation, or a loop
whose bound is not statically known), or relied on persistent state that a subprogram cannot hold. Fix: keep
synthesizable subprograms to bounded loops and ordinary logic; move timing/sequencing to a clocked process, file
I/O and real math to the testbench, and any required state into signals/registers outside the subprogram.
Engineering takeaway: if a subprogram cannot be drawn as a fixed block of logic — wait, file I/O, real,
unbounded loops/recursion — it is testbench-only; RTL helpers must be bounded, stateless, inlinable logic.
-- BUG: non-static loop bound and a wait → not synthesizable.
-- function f(n : integer) return ... is begin for i in 0 to n loop ... wait; end loop; ... end;
-- FIX: bound the loop by a fixed range and remove wait (sequence in a clocked process instead).
function f(v : std_logic_vector) return natural is variable c:natural:=0; begin
for i in v'range loop if v(i)='1' then c:=c+1; end if; end loop; return c; end function;7. Common mistakes & what to watch for
wait/file I/O/realin RTL subprograms. Simulation-only; sequence in a clocked process and keep file I/O andrealmath in the testbench.- Non-static loop bounds. Synthesizable loops must have statically-determinable ranges (they unroll); data-dependent bounds do not synthesize.
- Expecting state to persist between calls. Subprograms are stateless when inlined; hold state in signals/registers outside them.
- Unbounded recursion. Only statically-bounded recursion synthesizes; unbounded recursion is simulation-only.
- Assuming the subprogram fixes the timing. Combinational vs registered is set by the call context, not the subprogram — call it in a clocked process to register the result.
8. Engineering insight & continuity
A synthesizable subprogram is inlined, stateless, fixed-size logic: bounded loops and ordinary statements map to
gates, while wait, file I/O, real, dynamic allocation, and unbounded recursion are simulation-only — and the
call context, not the subprogram, decides combinational versus registered. Keep that boundary clear and your
helpers drop cleanly into RTL. One subtlety remains about subprogram state and side effects — the difference
between a function that depends only on its inputs and one that does not — which is the module's final lesson,
Pure vs Impure Functions.