Procedures vs functions in VHDL
VHDL has two kinds of subprogram, and the language reference explains them in terms of expressions and statements. The hardware view is simpler and more useful: a function is a named piece of combinational logic with exactly one output. A procedure is a named block of statements that can drive as many outputs as you give it. Everything else about them follows from that.
Functions: one result, usable anywhere an expression goes
A function takes inputs (only inputs), returns exactly one value, and can be called inside any expression: an assignment, a condition, a port map. Here’s a three-input majority vote, live. Note what you’re looking at: this is the whole source, no entity, no architecture, and the ports come from the function’s own signature:
function majority (a, b, c : std_logic) return std_logic is
begin
return (a and b) or (b and c) or (a and c);
end function;
Interactive schematic by RapidRTL
The schematic is the point: a function is that cloud of gates. Calling
majority(x, y, z) in three places doesn’t create a “call” in hardware,
because hardware has no call stack; each call site gets its own copy of
the logic (which the optimizer may then share if the inputs match).
Functions are how VHDL names a computation: type conversions, reductions,
decoders, address calculations. If you’ve written the same expression
twice, it wants to be a function.
Two rules keep functions honest:
- Parameters are inputs, full stop. A function can’t drive anything except its return value.
- Pure by default. An ordinary function may read only its parameters,
so the same inputs always give the same result, which is exactly the
property combinational logic has. (VHDL-2008’s
impurefunctions may read outside signals; treat them as a testbench tool.)
Procedures: many outputs, called as a statement
A procedure call is a statement, like an assignment, and its parameters have directions. That makes it the natural home for an operation with more than one result. The classic small example: sort two values, low and high out, in one call.
Interactive schematic by RapidRTL
One comparator, two muxes: again, the procedure dissolved into the logic
it described. Nothing about “being a procedure” survives synthesis; it’s
an organizational tool for you, not a structure the hardware keeps. A
min_max used by an 8-value sorting network would be written once and
instantiated as gates seven times.
The parameter-class trap
Procedure parameters have a direction (in, out, inout) and also a
class: constant, variable, or signal. The class must match what
you pass. The one that bites: to assign a signal (like a port) from
inside a procedure, the formal parameter must be declared
signal ... : out, as in the example above. Leave the class off an out
parameter and it defaults to variable, at which point passing a port
into it is an error that reads like the compiler is being obtuse. It
isn’t; it’s asking whether you want
variable or signal assignment semantics
inside the body, and those genuinely differ.
Choosing between them
- Computing one value? Function. You get expression-position calls and purity checking for free.
- Producing several values, or wrapping a reusable chunk of process code? Procedure.
- Testbench sequences (drive these signals, wait, check that): a
procedure, because procedures may contain
waitstatements and functions may not. That same fact means a procedure containingwaitis unsynthesizable; in code destined for hardware, procedures stay purely combinational-or-clocked like everything else. - Unconstrained array parameters (
std_logic_vectorwith no range) make subprograms generic over width, and synthesis sizes them at each call site. (The live function embed above has fixed-width inputs; if you visualize one with unconstrained ports, RapidRTL picks an 8-bit default and says so in a note.)
The Verilog aside
Verilog’s split is the same idea with different names: a Verilog
function is likewise a single-return combinational calculation, and a
task is the procedure analogue, multiple outputs allowed, timing
controls allowed in simulation, the same “no wait reaches hardware”
rule in practice. If you’ve internalized one language’s pair, you already
understand the other’s.
The pitfalls checklist
outparameter withoutsignalclass, then passing a port: the most common procedure compile error. Declare the class.waitinside a procedure that later gets dragged into synthesis: fine in a testbench, fatal in hardware. Keep testbench-only procedures in testbench-only packages.- Expecting the subprogram to exist in the netlist: it won’t. If you need a reusable hardware block with its own identity, that’s an entity, not a subprogram.
- Impure functions in synthesizable code: a function that reads signals it wasn’t passed hides inputs from every reader and some tools. Pass what you read.