VHDL · Chapter 11.2 · Functions and Procedures
Functions
A function is the workhorse subprogram for combinational computation. It takes input-only parameters, runs a body of sequential statements over local variables, and returns exactly one value. Because it has no signal side effects and cannot suspend, a synthesizable function is pure combinational logic, and because it is used inside an expression it reads naturally right where the value is needed. The most powerful pattern is the unconstrained array parameter, a function whose input is a vector with no fixed width that adapts to any width through the range and length attributes. That gives you width-generic helpers like parity, reductions, and Gray-to-binary conversion. This lesson covers writing functions, the unconstrained-parameter idiom, and how each call inlines directly into combinational logic at the point where you use it.
Foundation14 min readVHDLFunctionsCombinationalSubprogramsReuseSynthesis
1. Engineering intuition — a value computed by name
A function answers one question: given these inputs, what is the value? It computes that value from in
parameters only, returns it, and changes nothing else — no signals driven, no time elapsed. That purity is
exactly what makes it combinational: the output depends only on the present inputs, like a block of gates. You
reach for a function whenever a single result must be computed in more than one place — count leading zeros,
compute a parity bit, convert an encoding — and you want one definition that drops into any expression that needs
the value.
2. Formal explanation — anatomy of a function
-- in-only parameters; returns exactly ONE value; no signals, no wait → combinational.
function clog2 (n : positive) return natural is
variable r : natural := 0; -- local variables (immediate, like any process variable)
variable v : positive := 1;
begin
while v < n loop -- sequential statements in the body
v := v * 2;
r := r + 1;
end loop;
return r; -- the single returned value (every path must return)
end function;
-- UNCONSTRAINED array parameter → one function works for ANY width.
function parity (v : std_logic_vector) return std_logic is
variable p : std_logic := '0';
begin
for i in v'range loop p := p xor v(i); end loop; -- 'range adapts to the actual argument
return p;
end function;A function declares in parameters (mode in is the default and the only useful one for a pure function), a
return type, and a body of variables and sequential statements ending in return. Leaving an array
parameter unconstrained (std_logic_vector with no range) lets the body use 'range/'length so the same
function serves every width — the function equivalent of a generic.
3. Production usage — functions in expressions
-- Declared in a package (shared) or a local declarative part; called inside expressions.
signal par : std_logic;
signal depth : natural;
par <= parity(data); -- works whether data is 8, 16, or 32 bits wide
depth <= clog2(FIFO_DEPTH); -- compile-time address width from a parameter
-- Reduction / select helpers read cleanly at the call site:
all_ones <= and_reduce(flags); -- '1' only if every flag bit is '1'
sel_out <= mux4(a, b, c, d, sel); -- one returned value chosen by selWhat hardware does this become? Each call inlines at its site: parity(data) becomes an XOR tree across
data, and_reduce(flags) becomes a wide AND, mux4(...) becomes a 4:1 multiplexer. The function is purely a
source-level name for that combinational logic — there is no call or return in the netlist, just the expanded
gates. clog2(FIFO_DEPTH) with a constant argument folds to a constant at elaboration.
4. Structural interpretation — a function inlines into combinational logic
5. Why this is structural, not timing
A synthesizable function is pure combinational logic with no state and no time, so its result depends only on the present inputs and a gate-level structure (above), not a waveform, is the right picture — its behaviour is simply that of the logic it inlines to, which the combinational-logic module already covered in waveform terms. The local variables inside the body are immediate (computed in zero time during evaluation), not registers; they are scratch values used to build the returned result, not state that persists between calls.
6. Debugging example — the path that did not return (or the latch-shaped function)
Expected: a function returns a value on every call. Observed: a compile error that not all paths return a
value, or a synthesized result that depends on more than the inputs. Root cause: a control path through the
body reached the end without a return (e.g. an if with no else and no trailing return), so the function is
incomplete — the functional analogue of an incompletely-assigned combinational output. Fix: ensure every
path returns; the simplest discipline is a default return at the end or an if/else (or case with
when others) where every branch returns. Engineering takeaway: a function must return on all paths — treat a
missing return like a missing default assignment in combinational logic.
-- BUG: no return on the else path → not all paths return a value.
-- function f(x : std_logic) return std_logic is begin if x='1' then return '0'; end if; end function;
-- FIX: every path returns.
function f(x : std_logic) return std_logic is begin
if x = '1' then return '0'; else return '1'; end if;
end function;7. Common mistakes & what to watch for
- Not returning on every path. Every control path must
return; add a default or make branches exhaustive. - Driving signals or using
waitin a function. Functions are pure: in-only, no signal assignment, no suspension. Use a procedure or process otherwise. - Fixing the array width unnecessarily. Leave array parameters unconstrained and use
'range/'lengthfor width-generic helpers. - Expecting variables to be registers. Function variables are immediate scratch values, not state — a function holds no memory between calls.
- Hiding heavy logic in an innocent-looking call. A function inlines; a costly function called many times replicates that logic each time.
8. Engineering insight & continuity
A function is reusable combinational computation: in-only inputs, one returned value, no side effects, inlined
into the expression that calls it — and made width-generic by unconstrained array parameters with 'range. It is
the cleanest way to share a single-value calculation across a design. When the operation produces more than one
result, or must perform a sequence of actions, you need the other subprogram kind — which is exactly the next
lesson, Procedures: how out/inout parameters let a subprogram return several values and act as a
statement.