Skip to content

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_rules.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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 loops

A 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

context_sets_timing.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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

synthesizable subprogram constructs versus simulation-only constructshardwareno hardwareSYNTHESIZABLEbounded loops, if/case,arithmetic, scratch varsinlines as logiccomb, or registered by callcontextSIMULATION ONLYwait, file I/O, real,dynamic alloc, unboundedrecursiontestbench / modelsno gate equivalent12
The synthesis boundary for subprograms. On the hardware side, a body that maps to a fixed amount of logic synthesizes: bounded loops (which unroll), if/case, arithmetic, conversions, and immediate scratch variables — all inlined as combinational logic at the call site, with the call context deciding whether the result is registered. On the simulation-only side are constructs with no gate equivalent: wait, file I/O and textio, real/math_real, dynamic allocation, and unbounded recursion or loops — usable in testbenches but not synthesizable. Knowing the boundary keeps RTL helpers buildable. This is a construct-classification structure, not a timing waveform.

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.

bounded_and_stateless.vhd
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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/real in RTL subprograms. Simulation-only; sequence in a clocked process and keep file I/O and real math 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.