VHDL · Chapter 6.2 · Combinational Logic Design
Combinational Processes
Concurrent assignments are perfect for a single combinational function, but real blocks often have several outputs, a case decode, or a shared intermediate value, and there a combinational process is cleaner. The recipe is precise: make the process sensitive to every input, use no clock-edge guard, and write the body as default-then-override, assigning every output a safe default at the top and then conditionally overriding it. That structure scales to multi-output logic and, crucially, is latch-free by construction. This lesson develops the combinational-process pattern, shows when it beats a concurrent assignment, and reinforces the golden rules of combinational design from the module opener.
Foundation14 min readVHDLCombinational Processprocess(all)Default OverrideLatch-freeRTL
1. Engineering intuition — a process that is pure logic
A combinational process looks like a clocked one but with two deliberate differences: it has no
clock-edge guard, and it reacts to all its inputs. That makes it describe pure logic — outputs
that settle to a function of the inputs, no state. The reason to use it instead of concurrent
assignments is organisational: when a block computes several outputs, or decodes a value with case,
or shares an intermediate result, a single process reads far better than a scatter of concurrent lines
— and the default-then-override structure keeps it correct.
2. Formal explanation — the three ingredients
proc : process (all) -- 1) complete sensitivity (every read signal)
begin
y <= b; -- 3) default-then-override: default first ...
z <= '0';
if sel = '1' then -- (no clock guard anywhere — 2)
y <= a; -- ... then conditional overrides
z <= '1';
end if;
end process;A combinational process has three defining properties: (1) complete sensitivity — process(all)
(VHDL-2008) or every read signal listed, so simulation matches synthesis (lesson 5.2); (2) no clock
guard — no rising_edge(clk), so nothing registers; (3) default-then-override — every output is
assigned a default before any conditional, so every path drives every output and no latch is inferred.
Miss any one and you get a mismatch, a register, or a latch instead of clean logic.
3. When a process beats a concurrent assignment
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
-- Concurrent is fine for ONE simple function:
y <= a when sel = '1' else b;
-- A PROCESS is clearer for MULTI-OUTPUT + case + shared intermediate:
ctrl : process (all)
variable s : unsigned(8 downto 0); -- shared intermediate (immediate)
begin
flags <= "00"; -- defaults
result <= (others => '0');
s := resize(unsigned(a), 9) + resize(unsigned(b), 9);
case mode is
when "00" => result <= std_logic_vector(s(7 downto 0));
flags(0) <= s(8); -- carry
when "01" => result <= a and b;
when others => result <= a or b;
end case;
if result = x"00" then flags(1) <= '1'; end if; -- zero flag
end process;What hardware does this become? A combinational block driving result and flags from a shared
adder intermediate s and a case decode — readable as one unit. The same logic as scattered
concurrent assignments, but the process keeps the related outputs, the shared computation, and the
decode together, which is why a process is preferred here.
4. Hardware interpretation — one cone, multiple outputs
5. Simulation interpretation — default first, then override
A combinational process drives two outputs as pure functions of its inputs
8 cycles6. Debugging example — the process that registered or latched
Expected: combinational logic. Observed: either an inferred latch, or — surprisingly — a
register. Root cause: for the latch, an output was not defaulted and some path left it unassigned
(rule 1). For the accidental register, the body contained a rising_edge(clk) guard, turning the
"combinational" process into clocked logic. Fix: default every output at the top and remove any
clock guard from a combinational process (keep clocked behaviour in a separate process). Engineering
takeaway: a combinational process must have no clock guard and default every output; a stray
edge-guard makes registers, an unassigned path makes latches.
-- BUG A: no default for 'z' → unassigned when sel='0' → latch.
process (all) begin
if sel = '1' then y <= a; z <= '1'; else y <= b; end if; -- z not assigned on else → latch
end process;
-- FIX A: default both outputs, then override.
process (all) begin
y <= b; z <= '0'; -- defaults
if sel = '1' then y <= a; z <= '1'; end if;
end process;7. Common mistakes & what to watch for
- Forgetting to default an output. Any output not assigned on every path infers a latch — default all outputs at the top.
- A clock guard in a combinational process.
rising_edge(clk)makes registers; keep it out of combinational processes. - Incomplete sensitivity. Use
process(all); an omitted input causes the sim/synth mismatch. - Reading an output back as an intermediate. Use a variable for shared intermediates (immediate), not a signal you read after assigning.
- One giant process. Split unrelated combinational blocks into separate processes for readability and easier review.
8. Engineering insight & continuity
The combinational process is concurrent assignment's big sibling: same hardware, but organised for
multi-output, case-driven, and intermediate-sharing logic. Its correctness reduces to a mechanical
habit — process(all), no clock guard, default-then-override — that makes latches and mismatches
structurally impossible. Adopt that template for every combinational block of any size and the logic is
clean by construction. The next lesson examines the failure this template prevents in depth:
Unintended Latches — what they are, exactly how incomplete assignment creates them, and how to spot
and eliminate them — followed by Default Assignments as the disciplined cure.