VHDL · Chapter 6.4 · Combinational Logic Design
Default Assignments and Latch Avoidance
The previous lesson showed how an unassigned path infers a latch. This lesson is the cure, turned into a habit you apply to every combinational process: the default-then-override pattern. At the very top of the process, assign every output a safe default value, then let your if and case logic conditionally override it. Because the default runs on every path, every output is always driven, so a latch is impossible by construction no matter how complex the conditional logic gets. This scales to multi-output blocks, case decodes, and FSM output logic far better than trying to assign every output in every branch. The one nuance to keep straight is that in a clocked process the no-assignment behaviour is hold, which is a correct register and not something to default away.
Foundation13 min readVHDLDefault AssignmentLatch AvoidanceCombinationalFSMRTL
1. Engineering intuition — drive everything, then override
The root cause of a latch is "some path left this output unassigned." The cleanest fix is to make that impossible: at the top of the process, assign every output a default, so every path has already driven every output before any condition is tested. Your conditional logic then only needs to override the cases that differ from the default. You stop thinking "did I cover every branch for every output?" and instead rely on a single, always-true safety net — the default — that guarantees completeness.
2. Formal explanation — two cures, one preferred
-- CURE A (preferred): default-then-override. Default every output FIRST, then override.
process (all)
begin
y <= b; z <= '0'; -- defaults drive every output on every path
if sel = '1' then y <= a; z <= '1'; end if;
end process;
-- CURE B: assign every output in every branch (full coverage). Correct but verbose and fragile.
process (all)
begin
if sel = '1' then y <= a; z <= '1';
else y <= b; z <= '0'; -- must remember EVERY output in EVERY branch
end if;
end process;Both eliminate the latch, because both guarantee every output is assigned on every path. Cure A (default-then-override) is preferred: it requires you to remember each output's default once, then only the exceptions, so it scales to many outputs and many branches without the combinatorial burden of Cure B, where forgetting one output in one branch reintroduces a latch.
3. Production RTL — defaults across a multi-output block
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all;
-- A control decoder with several outputs — defaults make it bulletproof.
dec : process (all)
begin
-- defaults: the "inactive / safe" value of every output
rd_en <= '0';
wr_en <= '0';
addr <= (others => '0');
state <= IDLE;
case cmd is
when "01" => rd_en <= '1'; addr <= raddr; -- only override what differs
when "10" => wr_en <= '1'; addr <= waddr;
when "11" => state <= BUSY;
when others => null; -- defaults stand
end case;
end process;What hardware does this become? A combinational decoder driving four outputs; each branch overrides only the outputs it changes, and the top-of-process defaults drive the rest. No output is ever unassigned, so synthesis builds pure gates with no latch — even as the block grows.
4. Hardware interpretation — comb default vs clocked hold
5. Simulation interpretation — defaults always run
Default-then-override keeps outputs latch-free: they track inputs, never hold
8 cycles6. Debugging example — the new output that latched
Expected: adding a new output to a working combinational block stays latch-free. Observed: synthesis now warns of a latch on the new output. Root cause: the new output was assigned in some branches but not defaulted at the top, so the branches that do not set it leave it unassigned → a latch — even though the rest of the block was fine. Fix: add the new output to the top-of-process defaults. Engineering takeaway: every output needs a default; when you add an output to a combinational process, add its default first — that one habit prevents the most common "I only changed one thing" latch.
-- BUG: 'busy' added, set only in one branch, not defaulted → latch.
process (all) begin
y <= b; -- y defaulted (fine)
if sel = '1' then y <= a; busy <= '1'; end if; -- busy unassigned when sel='0' → latch
end process;
-- FIX: default 'busy' too.
process (all) begin
y <= b; busy <= '0'; -- default EVERY output
if sel = '1' then y <= a; busy <= '1'; end if;
end process;7. Common mistakes & what to watch for
- Defaulting some outputs but not all. Every output of a combinational process needs a default; one un-defaulted output latches.
- Defaulting registered outputs in a clocked process. A clocked output should hold when not assigned (that is the register); do not default it to a value every cycle unless you mean to.
- Choosing a default that masks bugs. Pick the safe/inactive default (disabled, zero, idle) so an unexpected path fails safe and is visible.
- Relying on full-branch coverage (Cure B) as the block grows. It is fragile; one missed output in one branch reintroduces a latch. Prefer default-then-override.
- Putting defaults after some logic. Defaults must be at the top, before any conditional, so they truly cover every path.
8. Engineering insight & continuity
Default-then-override is the single most valuable habit in combinational RTL: one safe default per output at the top of the process, then override the exceptions. It makes latches structurally impossible and keeps large multi-output decoders and FSM output logic clean and reviewable. The complementary discipline is knowing when not to default — in a clocked process, "hold" is the register doing its job. With latch avoidance now a reflex, the module turns to the combinational building blocks themselves: the next lesson, Multiplexers, applies these patterns to the most fundamental selection element, followed by Decoders and Encoders and Arithmetic Circuits.