RTL Design Patterns · Chapter 1 · Combinational Building Blocks
Multiplexers (2:1, N:1, one-hot)
Every datapath eventually converges: many sources, one destination that carries exactly one value at a time. The multiplexer resolves that convergence, and it is the first pattern you meet because almost every later pattern contains one. A register with a load enable is a mux feeding a flop, an FSM's next-state logic is a mux over the current state, and a bus is a mux over its masters. This page treats the mux as physical selection, hardware that steers one chosen input onto a shared output, and shows the forms it takes: a 2-to-1 atom, an N-to-1 tree versus a flat case, one-hot versus binary select, and priority versus parallel. You learn to write each so it synthesizes to the hardware you intend, and the most common way it silently goes wrong, an incomplete description that infers a latch, all in SystemVerilog, Verilog, and VHDL.
Foundation14 min readRTL Design PatternsMultiplexerDatapathOne-Hot SelectLatch InferenceCombinational Logic
Chapter 1 · Section 1.1 · Combinational Building Blocks
1. The Engineering Problem
You are building a simple ALU output stage. Four functional units — an adder, a subtractor, a bitwise-AND unit, and a shifter — each produce a 32-bit result every cycle. But the ALU has one result port. The instruction decoder has already worked out which operation this cycle wants; your job is to get that unit's result, and only that one, onto the shared alu_result bus. Three of the four computed results must go nowhere.
This is not a made-up puzzle. It is the shape of nearly every datapath junction in a chip: many sources produce values in parallel, a shared wire (a bus, a register input, a port) can hold exactly one value at a time, and something must select which source drives it this cycle. A register-file read port selects one of thirty-two registers. A memory data bus selects one of several slaves. A pipeline forwarding path selects between the register value and a bypassed result. In every case the need is identical: choose one of N inputs, route it to a single output, under the control of a select signal.
You cannot just wire all four results to alu_result — tying four drivers to one wire is a short, not a selection; in real hardware it is contention that resolves to X. You need a structure that lets exactly one source through and blocks the rest. That structure is the multiplexer, and the reason it is Chapter 1 is that it is the atom of the datapath — the primitive almost every later pattern is built out of.
// Four units compute IN PARALLEL, every cycle:
wire [31:0] add_res, sub_res, and_res, shift_res; // all valid at once
// The bus can carry ONE value. Which one? The decoder already decided:
wire [1:0] op_sel; // 00=add 01=sub 10=and 11=shift
// WRONG — four drivers on one net is contention, not selection:
// assign alu_result = add_res;
// assign alu_result = sub_res; // multiple continuous drivers => X
// RIGHT — a MULTIPLEXER: op_sel steers exactly one result onto the bus.
// (the pattern this page builds — 2:1 atom, N:1 tree, one-hot form)2. Mental Model
3. Pattern Anatomy
The whole family is built from one atom and two axes of variation.
The 2:1 atom. The smallest mux has two data inputs a, b, one select bit sel, and an output y: when sel==0, y=a; when sel==1, y=b. In gates it is y = (sel & b) | (~sel & a) — an AND-OR selector. Every larger mux is built from this atom, so if you understand the 2:1, you understand all of them.
Axis 1 — width of the selection (2:1 vs N:1), and how you build the N:1. To select one of N inputs you can compose 2:1 atoms two ways:
- A balanced tree of 2:1s. An 8:1 is three ranks of 2:1 muxes (
4 → 2 → 1), controlled by the threeselbits one rank at a time. The tree depth islog2(N), so the propagation delay grows logarithmically with N. This is what a good synthesis tool infers from a well-writtencase. - A flat / cascaded selector. Written as a long
if / else if / else if …chain, an N:1 can instead become a cascade — a chain of 2:1s where each stage's output feeds the next, giving a delay that grows linearly with N and a built-in priority. Same truth table for a full, mutually-exclusive select; very different hardware and delay.
Axis 2 — encoding of the select (binary vs one-hot).
- Binary-select mux.
selis an encoded index (log2(N)bits):2'b10means "input 2." The mux must decode that index internally to steer the switch. This is thecase (sel)form — compact select wiring, a decoder hidden inside. - One-hot-select mux.
selis already decoded: exactly one of N select lines is high (onehot[2]means "input 2"). The mux is then a pure AND-OR: AND each input with its own select line, OR the results — no internal decoder. It costs N select wires instead oflog2(N), but removes the decode and is the natural companion to structures whose select is already one-hot (an FSM in one-hot encoding, a round-robin grant vector).
The orthogonal third distinction — priority vs parallel (which the two select encodings tend to produce):
- Parallel mux — a
case(or a one-hot AND-OR) says "these alternatives are mutually exclusive; pick the matching one." The tool builds a balanced, delay-log2(N)selector with no priority. This is what you want whenselgenuinely picks one source. - Priority mux — an
if / else if / elsechain says "check condition 1 first, then 2, then 3…" The tool builds a priority cascade: earlier conditions win, delay grows with chain length. This is correct when the selects can overlap and you mean a priority (this is a priority encoder feeding a mux); it is a bug when you meant a parallel selection and paid for priority you did not want.
The mux family — one atom, two axes, one control-style axis
data flowDefault handling and X/unknown-select reasoning close the anatomy. A combinational mux must define its output for every select value; the moment one is left undefined, the description stops being a mux and becomes a mux-plus-latch (§7). The universal discipline — in every HDL — is assign a default first, then override, so no select value can escape unassigned. And when sel is X/unknown (binary form) or not one-hot (one-hot form), a correct design decides what happens rather than leaving it to chance: propagate X to expose the bug in simulation, or pin a defined default and assert the illegal case never occurs.
4. Real RTL Implementation
This is the core of the page. We build four patterns — (a) the 2:1 atom, (b) a parameterized N:1, (c) a one-hot-select N:1, and (d) the incomplete-case latch bug (buggy vs fixed) — and each is shown in SystemVerilog, Verilog, and VHDL, with both an RTL block and a self-checking testbench. The idea is identical in all three; the syntax differs, and seeing them side by side is what makes the pattern language-independent in your head.
One discipline runs through every RTL block: every output bit is assigned on every path. In SystemVerilog and Verilog that means a leading default assignment (or a total ? : / case with default); in VHDL it means the case/when is exhaustive with an others => arm. Miss it and you get pattern (d)'s latch.
4a. The 2:1 atom — the smallest mux, three ways
Start with the atom. A 2:1 mux is y = sel ? b : a. It is latch-proof by construction because the conditional (or when/else) assigns y on both branches — there is no third path. This is why the dataflow form is the safest way to write a small mux.
module mux2 #(
parameter int WIDTH = 8 // one source, any width
)(
input logic [WIDTH-1:0] a, // selected when sel == 0
input logic [WIDTH-1:0] b, // selected when sel == 1
input logic sel, // the lever
output logic [WIDTH-1:0] y // exactly one of a, b — verbatim
);
// A continuous conditional is TOTAL: y is driven on sel==0 AND sel==1,
// so there is no unassigned path and no latch can be inferred.
assign y = sel ? b : a;
endmoduleThe SystemVerilog testbench drives every value of sel with random data and checks the output against a one-line reference model — the whole 2:1 truth table is only two rows, so this is exhaustive, not sampled.
module mux2_tb;
localparam int WIDTH = 8;
logic [WIDTH-1:0] a, b, y;
logic sel;
int errors = 0;
mux2 #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .sel(sel), .y(y));
initial begin
for (int t = 0; t < 20; t++) begin
a = $urandom;
b = $urandom;
sel = t[0]; // sweep BOTH values of sel
#1; // let combinational logic settle
// Reference model: the output must be the selected input, verbatim.
assert (y === (sel ? b : a))
else begin $error("sel=%b a=%h b=%h y=%h", sel, a, b, y); errors++; end
end
if (errors == 0) $display("PASS: mux2 output == selected input on all sel");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog form is identical in intent; the only differences are wire/reg typing and $random instead of $urandom. The assign is just as total, so it is equally latch-proof.
module mux2 #(
parameter WIDTH = 8
)(
input wire [WIDTH-1:0] a,
input wire [WIDTH-1:0] b,
input wire sel,
output wire [WIDTH-1:0] y
);
// Continuous assignment, both branches driven — latch-proof by construction.
assign y = sel ? b : a;
endmodule module mux2_tb;
parameter WIDTH = 8;
reg [WIDTH-1:0] a, b;
reg sel;
wire [WIDTH-1:0] y;
reg [WIDTH-1:0] exp;
integer t, errors;
mux2 #(.WIDTH(WIDTH)) dut (.a(a), .b(b), .sel(sel), .y(y));
initial begin
errors = 0;
for (t = 0; t < 20; t = t + 1) begin
a = $random;
b = $random;
sel = t[0]; // exhaustive: both sel values
#1;
exp = sel ? b : a; // reference model
if (y !== exp) begin
$display("FAIL: sel=%b a=%h b=%h y=%h exp=%h", sel, a, b, y, exp);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: mux2 matches reference on all sel");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the same atom is a when/else concurrent assignment. Because when/else covers both the '1' and the else case, y is driven on every path — the VHDL equivalent of a total conditional, so again no latch.
library ieee;
use ieee.std_logic_1164.all;
entity mux2 is
generic ( WIDTH : positive := 8 ); -- generic == Verilog parameter
port (
a : in std_logic_vector(WIDTH-1 downto 0); -- selected when sel = '0'
b : in std_logic_vector(WIDTH-1 downto 0); -- selected when sel = '1'
sel : in std_logic; -- the lever
y : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of mux2 is
begin
-- Concurrent conditional signal assignment: y is driven for sel='1' and
-- for every other value, so there is no unassigned path (no latch).
y <= b when sel = '1' else a;
end architecture;The VHDL testbench uses assert … report … severity error as its self-check — the VHDL idiom that plays the role SystemVerilog's assert/$error and Verilog's if (…) $display("FAIL") play.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity mux2_tb is
end entity;
architecture sim of mux2_tb is
constant WIDTH : positive := 8;
signal a, b, y : std_logic_vector(WIDTH-1 downto 0);
signal sel : std_logic;
begin
dut : entity work.mux2 generic map (WIDTH => WIDTH)
port map (a => a, b => b, sel => sel, y => y);
stim : process
variable exp : std_logic_vector(WIDTH-1 downto 0);
begin
for t in 0 to 19 loop
a <= std_logic_vector(to_unsigned((t*7) mod 256, WIDTH));
b <= std_logic_vector(to_unsigned((t*13) mod 256, WIDTH));
sel <= '1' when (t mod 2 = 1) else '0'; -- sweep both sel values
wait for 1 ns; -- let it settle
if sel = '1' then exp := b; else exp := a; end if;
assert y = exp
report "mux2 mismatch: y /= selected input" severity error;
end loop;
report "mux2 self-check complete" severity note;
wait;
end process;
end architecture;4b. Parameterized N:1 — binary-select (the everyday default)
Now the ALU mux from §1, generalized: one source serves any width and any number of inputs. The select is a binary index, so the mux decodes it internally — this is the case (sel) form, and the default/others arm is what keeps it latch-free when N is not a power of two.
module mux_case #(
parameter int WIDTH = 32, // datapath width
parameter int N = 4, // number of inputs
localparam int SELW = (N > 1) ? $clog2(N) : 1 // sel is an ENCODED index
)(
input logic [WIDTH-1:0] din [N], // N sources, valid in parallel
input logic [SELW-1:0] sel, // binary index of the winner
output logic [WIDTH-1:0] dout // the shared output bus
);
// Combinational SELECTION — no clock, no state. unique case documents the
// INTENT that the alternatives are mutually exclusive (parallel mux, no
// priority) and lets lint/sim flag overlap or an out-of-range sel.
always_comb begin
dout = '0; // DEFAULT-ASSIGN FIRST:
// every path drives dout =>
// provably latch-free even if
// N is not a power of two.
unique case (sel)
default: dout = din[sel]; // indexing din[sel] IS the tree
endcase
end
endmoduleThe default: dout = din[sel] is worth one line of reasoning: indexing din[sel] with the variable sel is the balanced 2:1 tree — the tool builds a log2(N)-deep parallel mux from it. unique asserts mutual exclusivity, so the tool builds a parallel selector (no priority) and flags an out-of-range sel instead of silently holding. The leading dout = '0 is the default-assignment discipline. The testbench sweeps every legal sel (exhaustive, because sel has only N values) and re-runs the sweep for several N and WIDTH values — a parameterized mux must be verified generic, not assumed generic.
module mux_case_tb;
localparam int WIDTH = 32;
localparam int N = 4;
localparam int SELW = (N > 1) ? $clog2(N) : 1;
logic [WIDTH-1:0] din [N];
logic [SELW-1:0] sel;
logic [WIDTH-1:0] dout;
int errors = 0;
mux_case #(.WIDTH(WIDTH), .N(N)) dut (.din(din), .sel(sel), .dout(dout));
initial begin
foreach (din[i]) din[i] = $urandom; // random data on all sources
for (int s = 0; s < N; s++) begin // EXHAUSTIVE over legal sel
sel = s[SELW-1:0];
#1;
// Invariant: output is the selected input, verbatim.
assert (dout === din[s])
else begin $error("sel=%0d dout=%h exp=%h", s, dout, din[s]); errors++; end
end
if (errors == 0) $display("PASS: mux_case dout==din[sel] for all N=%0d", N);
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog N:1 cannot take an unpacked port array in the classic style, so the idiomatic form flattens the N sources into one N*WIDTH bus and slices it with din[sel*WIDTH +: WIDTH] (an indexed part-select). The engineering content is identical: a case with a default that indexes the selected slice, default-assigned first.
module mux_case #(
parameter WIDTH = 32,
parameter N = 4,
parameter SELW = 2 // = clog2(N); set by the instantiator
)(
input wire [N*WIDTH-1:0] din_flat, // N sources packed into one bus
input wire [SELW-1:0] sel, // binary index
output reg [WIDTH-1:0] dout
);
// Combinational block. Default-assign, then a case whose default indexes the
// selected WIDTH-wide slice: dout = din_flat[sel*WIDTH +: WIDTH].
always @(*) begin
dout = {WIDTH{1'b0}}; // DEFAULT FIRST → no latch even for N=3
case (sel)
default: dout = din_flat[sel*WIDTH +: WIDTH]; // parallel indexed mux
endcase
end
endmodule module mux_case_tb;
parameter WIDTH = 32;
parameter N = 4;
parameter SELW = 2;
reg [N*WIDTH-1:0] din_flat;
reg [SELW-1:0] sel;
wire [WIDTH-1:0] dout;
reg [WIDTH-1:0] exp;
integer s, errors;
mux_case #(.WIDTH(WIDTH), .N(N), .SELW(SELW)) dut
(.din_flat(din_flat), .sel(sel), .dout(dout));
initial begin
errors = 0;
din_flat = {$random, $random, $random, $random}; // fill all N slices
for (s = 0; s < N; s = s + 1) begin // EXHAUSTIVE over sel
sel = s[SELW-1:0];
#1;
exp = din_flat[sel*WIDTH +: WIDTH]; // reference slice
if (dout !== exp) begin
$display("FAIL: sel=%0d dout=%h exp=%h", s, dout, exp);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: mux_case dout==din[sel] for all sel");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleVHDL expresses the N:1 most naturally: din is an unconstrained array of vectors, sel is a std_logic_vector converted to an integer index with to_integer(unsigned(sel)), and the selection is dout <= din(idx). The others => arm of a case (or the guard on the index) is VHDL's default — the exhaustiveness that keeps it latch-free.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package mux_pkg is
type slv_array is array (natural range <>) of std_logic_vector; -- N sources
end package;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.mux_pkg.all;
entity mux_case is
generic (
WIDTH : positive := 32;
N : positive := 4
);
port (
din : in slv_array(0 to N-1)(WIDTH-1 downto 0); -- all valid in parallel
sel : in std_logic_vector(natural(ceil(log2(real(N))))-1 downto 0);
dout : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of mux_case is
begin
-- Combinational selection. Converting sel to an integer index and reading
-- din(idx) IS the balanced parallel mux. The bounded index plus a guarded
-- default make the assignment total (VHDL's answer to default-assign).
process (all)
variable idx : integer;
begin
dout <= (others => '0'); -- DEFAULT FIRST → no latch
idx := to_integer(unsigned(sel));
if idx <= N-1 then
dout <= din(idx); -- steer selected source
end if; -- else: defined default
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.mux_pkg.all;
entity mux_case_tb is
end entity;
architecture sim of mux_case_tb is
constant WIDTH : positive := 32;
constant N : positive := 4;
constant SELW : positive := 2; -- = clog2(N)
signal din : slv_array(0 to N-1)(WIDTH-1 downto 0);
signal sel : std_logic_vector(SELW-1 downto 0);
signal dout : std_logic_vector(WIDTH-1 downto 0);
begin
dut : entity work.mux_case
generic map (WIDTH => WIDTH, N => N)
port map (din => din, sel => sel, dout => dout);
stim : process
begin
for i in 0 to N-1 loop -- fill all sources
din(i) <= std_logic_vector(to_unsigned((i+1)*305419896 mod 2**WIDTH, WIDTH));
end loop;
for s in 0 to N-1 loop -- EXHAUSTIVE over sel
sel <= std_logic_vector(to_unsigned(s, SELW));
wait for 1 ns;
-- Invariant: dout is the selected source, verbatim.
assert dout = din(s)
report "mux_case mismatch: dout /= din(sel)" severity error;
end loop;
report "mux_case self-check complete" severity note;
wait;
end process;
end architecture;4c. One-hot-select N:1 — the AND-OR form
When the select arrives already decoded (an FSM in one-hot state, an arbiter's grant vector, a decoder's output), you skip the internal decode and build a pure AND-OR: AND each source with its own select bit, OR the lot. Two decisions are load-bearing — replicate the 1-bit select across the whole word so the mask covers all WIDTH bits, and remember the correctness rests on an invariant the module cannot enforce alone: onehot must have exactly one bit set.
module mux_onehot #(
parameter int WIDTH = 32,
parameter int N = 4 // #inputs == #select lines
)(
input logic [WIDTH-1:0] din [N], // N sources, valid in parallel
input logic [N-1:0] onehot, // ALREADY decoded: exactly one bit high
output logic [WIDTH-1:0] dout
);
always_comb begin
dout = '0; // DEFAULT: if (illegally) onehot==0, dout=0
for (int i = 0; i < N; i++) // elaboration-unrolled: N masks + OR
dout |= din[i] & {WIDTH{onehot[i]}}; // replicate select across the WORD
end
endmoduleIf two onehot bits are set the |= ORs two sources together and the output is neither input — silent corruption. The default dout = '0 defines the zero-hot case (output all-zeros); the two-hot case is an invariant violation the driver must prevent and the testbench must assert. The SystemVerilog TB checks the legal one-hot cases against a reference and adds an explicit $onehot assumption on the stimulus.
module mux_onehot_tb;
localparam int WIDTH = 32;
localparam int N = 4;
logic [WIDTH-1:0] din [N];
logic [N-1:0] onehot;
logic [WIDTH-1:0] dout;
int errors = 0;
mux_onehot #(.WIDTH(WIDTH), .N(N)) dut (.din(din), .onehot(onehot), .dout(dout));
initial begin
foreach (din[i]) din[i] = $urandom;
for (int i = 0; i < N; i++) begin // drive each legal one-hot code
onehot = '0;
onehot[i] = 1'b1;
#1;
assert ($onehot(onehot)); // stimulus is genuinely one-hot
// Reference: output equals the source whose select bit is set.
assert (dout === din[i])
else begin $error("onehot bit %0d: dout=%h exp=%h", i, dout, din[i]); errors++; end
end
onehot = '0; #1; // zero-hot corner → defined default
assert (dout === '0) else begin $error("zero-hot: dout=%h exp=0", dout); errors++; end
if (errors == 0) $display("PASS: mux_onehot AND-OR selects correctly");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog one-hot RTL uses the same flattened-bus trick as 4b and a for loop inside an always @(*) to build the AND-OR. Verilog has no $onehot, so the testbench asserts one-hotness with the classic (onehot & (onehot-1)) == 0 && onehot != 0 reduction.
module mux_onehot #(
parameter WIDTH = 32,
parameter N = 4
)(
input wire [N*WIDTH-1:0] din_flat,
input wire [N-1:0] onehot,
output reg [WIDTH-1:0] dout
);
integer i;
always @(*) begin
dout = {WIDTH{1'b0}}; // DEFAULT: zero-hot → 0
for (i = 0; i < N; i = i + 1) // N AND-masks OR'd together
dout = dout | (din_flat[i*WIDTH +: WIDTH] & {WIDTH{onehot[i]}});
end
endmodule module mux_onehot_tb;
parameter WIDTH = 32;
parameter N = 4;
reg [N*WIDTH-1:0] din_flat;
reg [N-1:0] onehot;
wire [WIDTH-1:0] dout;
reg [WIDTH-1:0] exp;
integer i, errors;
mux_onehot #(.WIDTH(WIDTH), .N(N)) dut
(.din_flat(din_flat), .onehot(onehot), .dout(dout));
initial begin
errors = 0;
din_flat = {$random, $random, $random, $random};
for (i = 0; i < N; i = i + 1) begin
onehot = {N{1'b0}};
onehot[i] = 1'b1; // legal one-hot code
#1;
// assert one-hotness of the stimulus, then check the selection
if (!((onehot & (onehot - 1)) == 0 && onehot != 0))
$display("FAIL: stimulus not one-hot: %b", onehot);
exp = din_flat[i*WIDTH +: WIDTH];
if (dout !== exp) begin
$display("FAIL: bit %0d dout=%h exp=%h", i, dout, exp);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: mux_onehot selects the hot source");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the AND-OR loops over the array, masking each source with a full-width replication of its select bit. (others => onehot(i)) is VHDL's aggregate way of replicating one bit across the word — the counterpart to SystemVerilog's {WIDTH{onehot[i]}}.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.mux_pkg.all;
entity mux_onehot is
generic ( WIDTH : positive := 32; N : positive := 4 );
port (
din : in slv_array(0 to N-1)(WIDTH-1 downto 0);
onehot : in std_logic_vector(N-1 downto 0); -- exactly one bit high
dout : out std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture rtl of mux_onehot is
begin
process (all)
variable acc : std_logic_vector(WIDTH-1 downto 0);
variable mask : std_logic_vector(WIDTH-1 downto 0);
begin
acc := (others => '0'); -- DEFAULT: zero-hot → 0
for i in 0 to N-1 loop
mask := (others => onehot(i)); -- replicate bit over word
acc := acc or (din(i) and mask); -- AND-mask, then OR
end loop;
dout <= acc;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.mux_pkg.all;
entity mux_onehot_tb is
end entity;
architecture sim of mux_onehot_tb is
constant WIDTH : positive := 32;
constant N : positive := 4;
signal din : slv_array(0 to N-1)(WIDTH-1 downto 0);
signal onehot : std_logic_vector(N-1 downto 0);
signal dout : std_logic_vector(WIDTH-1 downto 0);
begin
dut : entity work.mux_onehot
generic map (WIDTH => WIDTH, N => N)
port map (din => din, onehot => onehot, dout => dout);
stim : process
begin
for i in 0 to N-1 loop
din(i) <= std_logic_vector(to_unsigned((i+3)*286331153 mod 2**WIDTH, WIDTH));
end loop;
for i in 0 to N-1 loop
onehot <= (others => '0');
onehot(i) <= '1'; -- legal one-hot code
wait for 1 ns;
-- Invariant: output equals the source whose select bit is set.
assert dout = din(i)
report "mux_onehot mismatch: dout /= hot source" severity error;
end loop;
report "mux_onehot self-check complete" severity note;
wait;
end process;
end architecture;4d. The incomplete-case latch bug — buggy vs fixed, in all three HDLs
Pattern (d) is the signature failure, shown here as buggy vs fixed RTL in each language, then dramatized narratively in §7. The bug is the same everywhere: a combinational case (or VHDL case) omits a select value and has no default/others, so on that value the block assigns nothing — and "assign nothing in combinational logic" means "hold the old value," which is a latch.
// BUGGY: op_sel==2'b11 has no branch and there is no default → alu_result is
// unassigned on that value → the tool infers a LATCH that HOLDS.
module alu_out_mux_bad (
input logic [1:0] op_sel,
input logic [31:0] add_res, sub_res, and_res, shift_res,
output logic [31:0] alu_result
);
always_comb begin
case (op_sel)
2'b00: alu_result = add_res;
2'b01: alu_result = sub_res;
2'b10: alu_result = and_res;
// 2'b11 (shift) missing, no default → LATCH on alu_result
endcase
end
endmodule
// FIXED: default-assign first (or list all cases). Every path drives the
// output → pure combinational mux, no latch.
module alu_out_mux_good (
input logic [1:0] op_sel,
input logic [31:0] add_res, sub_res, and_res, shift_res,
output logic [31:0] alu_result
);
always_comb begin
alu_result = 32'b0; // DEFAULT FIRST → total assignment
unique case (op_sel)
2'b00: alu_result = add_res;
2'b01: alu_result = sub_res;
2'b10: alu_result = and_res;
2'b11: alu_result = shift_res; // the branch that was missing
endcase
end
endmodule module mux_bug_tb;
logic [1:0] op_sel;
logic [31:0] add_res, sub_res, and_res, shift_res, y;
logic [31:0] exp;
int errors = 0;
// Point the DUT at alu_out_mux_good to PASS; at _bad to see the latch fail.
alu_out_mux_good dut (.op_sel(op_sel), .add_res(add_res), .sub_res(sub_res),
.and_res(and_res), .shift_res(shift_res), .alu_result(y));
initial begin
{add_res, sub_res, and_res, shift_res} = {32'hA, 32'hB, 32'hC, 32'hD};
for (int s = 0; s < 4; s++) begin // EXHAUSTIVE — includes 2'b11
op_sel = s[1:0];
#1;
exp = (s==0)?add_res : (s==1)?sub_res : (s==2)?and_res : shift_res;
assert (y === exp)
else begin $error("op_sel=%b y=%h exp=%h", op_sel, y, exp); errors++; end
end
if (errors == 0) $display("PASS: all four op_sel drive the correct result");
else $display("FAIL: %0d mismatches (latch on the uncovered value)", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair is the same story with reg outputs and always @(*). Sweeping all four op_sel values in the testbench is what exposes the latch — the fourth value is the whole bug.
// BUGGY: no 2'b11 branch, no default → latch on alu_result.
module alu_out_mux_bad (
input wire [1:0] op_sel,
input wire [31:0] add_res, sub_res, and_res, shift_res,
output reg [31:0] alu_result
);
always @(*) begin
case (op_sel)
2'b00: alu_result = add_res;
2'b01: alu_result = sub_res;
2'b10: alu_result = and_res;
// 2'b11 missing, no default → LATCH
endcase
end
endmodule
// FIXED: default-assign first → every path drives alu_result, no latch.
module alu_out_mux_good (
input wire [1:0] op_sel,
input wire [31:0] add_res, sub_res, and_res, shift_res,
output reg [31:0] alu_result
);
always @(*) begin
alu_result = 32'b0; // DEFAULT FIRST
case (op_sel)
2'b00: alu_result = add_res;
2'b01: alu_result = sub_res;
2'b10: alu_result = and_res;
2'b11: alu_result = shift_res;
default: alu_result = 32'bx; // X flags an impossible sel in sim
endcase
end
endmodule module mux_bug_tb;
reg [1:0] op_sel;
reg [31:0] add_res, sub_res, and_res, shift_res;
wire [31:0] y;
reg [31:0] exp;
integer s, errors;
// Bind to _good to PASS; to _bad to observe the latch (y holds on op_sel==3).
alu_out_mux_good dut (.op_sel(op_sel), .add_res(add_res), .sub_res(sub_res),
.and_res(and_res), .shift_res(shift_res), .alu_result(y));
initial begin
errors = 0;
add_res = 32'hAAAA0000; sub_res = 32'hBBBB0000;
and_res = 32'hCCCC0000; shift_res = 32'hDDDD0000;
for (s = 0; s < 4; s = s + 1) begin // EXHAUSTIVE, includes 2'b11
op_sel = s[1:0];
#1;
exp = (s==0)?add_res : (s==1)?sub_res : (s==2)?and_res : shift_res;
if (y !== exp) begin
$display("FAIL: op_sel=%b y=%h exp=%h", op_sel, y, exp);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: all four op_sel correct (no latch)");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the same bug appears when a case omits when others => (or when a when/else chain has no final else). The others => arm is exactly VHDL's default; omit it on an incompletely-listed std_logic_vector case and the synthesis tool infers a latch to hold the unlisted values.
library ieee;
use ieee.std_logic_1164.all;
-- BUGGY: the case omits "11" and has NO "when others" → std_logic_vector has
-- many meta-values (X, Z, ...) plus "11" left unassigned → LATCH.
entity alu_out_mux_bad is
port (
op_sel : in std_logic_vector(1 downto 0);
add_res, sub_res, and_res, shift_res : in std_logic_vector(31 downto 0);
alu_result : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of alu_out_mux_bad is
begin
process (all)
begin
case op_sel is
when "00" => alu_result <= add_res;
when "01" => alu_result <= sub_res;
when "10" => alu_result <= and_res;
-- no "11", no "when others" → alu_result unassigned here → LATCH
end case;
end process;
end architecture;
-- FIXED: exhaustive case with "when others" (VHDL's default) → total assignment.
entity alu_out_mux_good is
port (
op_sel : in std_logic_vector(1 downto 0);
add_res, sub_res, and_res, shift_res : in std_logic_vector(31 downto 0);
alu_result : out std_logic_vector(31 downto 0)
);
end entity;
architecture rtl of alu_out_mux_good is
begin
process (all)
begin
case op_sel is
when "00" => alu_result <= add_res;
when "01" => alu_result <= sub_res;
when "10" => alu_result <= and_res;
when "11" => alu_result <= shift_res; -- the missing branch
when others => alu_result <= (others => 'X'); -- flags illegal sel; total
end case;
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity mux_bug_tb is
end entity;
architecture sim of mux_bug_tb is
signal op_sel : std_logic_vector(1 downto 0);
signal add_res, sub_res, and_res, shift_res, y : std_logic_vector(31 downto 0);
begin
-- Bind to alu_out_mux_good to PASS; to _bad to observe the latch hold.
dut : entity work.alu_out_mux_good
port map (op_sel => op_sel, add_res => add_res, sub_res => sub_res,
and_res => and_res, shift_res => shift_res, alu_result => y);
stim : process
variable exp : std_logic_vector(31 downto 0);
begin
add_res <= x"AAAA0000"; sub_res <= x"BBBB0000";
and_res <= x"CCCC0000"; shift_res <= x"DDDD0000";
wait for 1 ns;
for s in 0 to 3 loop -- EXHAUSTIVE, includes "11"
op_sel <= std_logic_vector(to_unsigned(s, 2));
wait for 1 ns;
case s is
when 0 => exp := add_res; when 1 => exp := sub_res;
when 2 => exp := and_res; when others => exp := shift_res;
end case;
assert y = exp
report "latch/mismatch on op_sel value" severity error;
end loop;
report "mux_bug self-check complete" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: assign the output on every path — a leading default in SV/Verilog, a when others => in VHDL — or the combinational block becomes a latch.
5. Verification Strategy
A mux is combinational, so its correctness is a truth table, and the good news is the table is small enough to check exhaustively. The single invariant that captures a correct mux is:
The output is always equal to the selected input — verbatim, never a blend, never a value that is not one of the inputs.
Everything below is that one invariant, made checkable, and it maps directly onto the testbenches in §4.
- Exhaustive
selcoverage (self-checking). For a binary-select N:1, drive every value ofselfrom0toN-1with random data on alldin, and assertdout === din[sel]each time. Becauseselhas onlyNlegal values, this is complete — aforloop overselwith a reference model (expected = din[sel]) proves the whole table, not a sampled corner. The threemux_casetestbenches in §4b do exactly this: SVassert (dout === din[s]), Verilogif (dout !== exp) $display("FAIL…"), VHDLassert dout = din(s) report … severity error. - The output==selected-input invariant. Stated as a property that holds every evaluation:
dout == din[sel]for the binary form; for the one-hot form, "ifonehotis one-hot thendoutequals the source whose bit is set." This is the property the whole page is about; if it ever fails, the mux is broken. - One-hot-select
$onehotassumption/assertion. The one-hot AND-OR form is correct only under the one-hot invariant, so verification must both assume it on the stimulus and assert it holds. The SV testbench usesassert ($onehot(onehot)); Verilog uses the equivalent(onehot & (onehot-1)) == 0 && onehot != 0; VHDL counts the set bits. Driveonehot = 0(expect the defined all-zeros default) and, in a directed check,onehotwith two bits set (this must be flagged as an invariant violation — it is the overlapping-select bug of §6). - X-on-undefined-select behaviour. Decide, then test the decision. For the binary form, drive
selout of range or toXand confirm the intended behaviour — withunique case(SV) orwhen others => 'X'(VHDL) the illegal value is exposed rather than silently held; the buggy variant of §4d holds the previous value, and sweeping all select values is what surfaces that latch. This is why the §4d testbenches sweep the fourthop_selvalue the naive sim skipped. - Parameter-generic verification (N=2,4,8). A parameterized mux must be verified generic, not assumed generic. Re-run the exhaustive
selsweep forN = 2, 4, 8(and a non-power-of-two likeN = 3, where the default-assignment discipline earns its keep) and forWIDTH = 1, 8, 32. A form that passes atN=4can still be wrong atN=3if the default path was sloppy — the leadingdout='0/(others => '0')/ default-first is what makes it survive the oddN. - Expected waveform. Because a mux has no clock, "correct" on a waveform is immediate: as
selsteps0 → 1 → 2 → 3,douttracksdin[0] → din[1] → din[2] → din[3]combinationally (within the propagation delay), never showing a value that is not one of the current inputs. Adoutthat holds its previous value whenselchanges to an uncovered case is the visual signature of the inferred-latch bug from §7.
6. Common Mistakes
Incomplete case / if → inferred latch. The signature mux bug. In a combinational block (SV always_comb, Verilog always @(*), VHDL a combinational process), if some path does not assign the output, the tool must "remember" the old value to honour it — and memory in combinational logic is a latch. An if with no else, a case with no default, or a VHDL case with no when others, that omits the output on some path infers a latch on that output. The fix is the default-assignment discipline: assign a default to every output at the top of the block, so every path assigns it. (Full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4d.)
Missing default / others (even when the case "covers" all values). For a power-of-two N a case over an M-bit sel might list all 2^M values, but the moment N is not a power of two — N=3, sel is 2 bits, value 2'b11 unlisted — the missing default re-opens the latch. In VHDL it is worse: a std_logic_vector has nine meta-values per bit, so a case over it always needs when others => even when you listed every binary value. Always include the default; never rely on "I listed them all."
Unintended priority when you meant parallel. Writing if (sel==0) … else if (sel==1) … else if (sel==2) … when the selects are mutually exclusive gets the right answer but the wrong hardware: a priority cascade with delay growing linearly in the number of branches, instead of the balanced log2(N) tree a case builds. It simulates identically and synthesizes slower and larger. Use case for parallel selection; reserve the if-chain for genuine priority.
Overlapping one-hot selects. The one-hot AND-OR form is correct only if exactly one select line is high. If two are high, the OR combines two sources and the output is a bitwise-OR of two inputs — a value that is neither input, silently corrupt. Whatever drives the one-hot vector must guarantee one-hotness, and verification must assert it ($onehot, or the (v & (v-1))==0 reduction, or a bit-count in VHDL).
Masking with a 1-bit select in the one-hot form. din[i] & onehot[i] (without replication) ANDs a WIDTH-bit word with a 1-bit value, clearing every bit except bit 0 — the word is silently truncated to one bit. Replicate: {WIDTH{onehot[i]}} in SV/Verilog, (others => onehot(i)) in VHDL.
A giant flat mux as a timing/area problem. A very wide flat mux — 48:1 or 128:1 over wide data — is not a one-liner, it is an architecture decision. Even as a tree it is log2(N) deep, every input must be routed to the junction (congestion, not just gates), and a binary select needs a log2(N)-to-N decode. Written as an if-chain it is far worse (linear depth). Beyond a modest N, pipeline the tree, decompose it hierarchically, or rethink whether all N truly converge in one cycle.
7. DebugLab
The ALU result that froze — an incomplete case infers a latch
The engineering lesson: a combinational block that fails to assign an output on some path does not compute "nothing" — it infers a latch that remembers the old value, turning a stateless mux into accidental state. The tell is a bug that depends on history ("shift returns the previous instruction's result"): history-dependence in something you designed to be combinational means you built storage you did not intend. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: default-assign every output at the top of every combinational block (a leading assignment, or a when others => arm), and exhaustively sweep the select in verification so no select value goes untested.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "what switch am I building?" habit.
Exercise 1 — Pick the mux for the source
For each scenario, state whether you'd use a binary-select or a one-hot-select mux, and why in one line: (a) a register-file read port selecting 1 of 32 registers from a 5-bit register address; (b) selecting the output of the currently-active state in a one-hot-encoded FSM; (c) a bus granting one of 4 masters where the arbiter already emits a 4-bit grant vector; (d) a 2:1 bypass in a pipeline selecting between the register value and a forwarded result. (Hint: match the mux to the encoding of the select you already have.)
Exercise 2 — Predict the hardware, then the bug
Two engineers implement the same 4:1 datapath mux. Engineer A writes a unique case (sel). Engineer B writes if (sel==0) … else if (sel==1) … else if (sel==2) … else …. Both simulate identically. Describe (i) how the synthesized hardware differs (structure and delay scaling), and (ii) which one is at risk of an inferred latch and under exactly what omission — and why the other is safe by construction. Then say how the VHDL equivalents of A and B would look and where VHDL's when others fits.
Exercise 3 — Break the one-hot mux on paper
The one-hot AND-OR mux is correct only under one invariant. State that invariant precisely. Then describe the two distinct ways the driver can violate it (too few / too many active lines), what the output becomes in each case, and what single check you would add to each of a SystemVerilog, a Verilog, and a VHDL testbench to catch it. Explain why a plain case-based binary mux is not exposed to this particular failure.
Exercise 4 — Size the convergence
You must route one of 48 128-bit results onto a single result bus, and static timing shows the combinational mux is now on the critical path. Without changing the number of sources, give two architectural changes that reduce the mux's contribution to the critical path, and name the cost each one introduces (latency, area, or control complexity). (Hint: think tree depth, pipelining, and whether all 48 truly need to converge in one cycle.)
10. Related Tutorials
Continue in RTL Design Patterns (forward links unlock as they ship):
- Decoders — Chapter 1.2; the inverse of a mux — turning a binary index into the one-hot select this page's AND-OR mux consumes.
- The Register Pattern (D-FF) — Chapter 2.1; a register with a load-enable is a 2:1 mux feeding a flop — where selection meets state.
- FSM Fundamentals — Chapter 4.1; next-state logic and one-hot state encoding, both of which are muxes in disguise.
- ALU Construction — Chapter 5; the ALU output mux from §1, built out in full.
Backward / method:
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this page applied to the mux (a mux is pure datapath, no state).
- What Is an RTL Design Pattern? — Chapter 0.1; why the mux earns the name "pattern" — a recurring problem, a reusable form, a synthesis reality, and a signature failure.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Case Statements — the construct behind the binary-select mux, and where
unique/defaultand latch inference live. - Continuous Assignments — the
assign/ dataflow form of the ternary mux (sel ? b : a), latch-proof by construction. - The Conditional (
? :) Operator — the ternary that expresses a 2:1 mux directly and, chained, an N:1. - Blocking and Non-Blocking Assignments — why a combinational mux uses blocking (
=) in itsalways @(*), and how that differs from the clocked patterns ahead. - Generate Blocks — the elaboration-time replication used to parameterize the N-input one-hot AND-OR mux.
11. Summary
- A mux is physical selection, not a
casestatement.selsteers exactly one input, verbatim, onto a shared output;if/case/? :(and VHDL'swhen/else) are notations for the same switch. The output is always one of the inputs — never a blend. - One atom, composed. The 2:1 (
y = sel ? b : a, an AND-OR) is the atom; every N:1 is 2:1s composed — as a balanced tree (from acase, delay~log2(N), parallel) or a cascade (from anif-chain, delay~N, priority). - Two select encodings. Binary-select carries an encoded index and hides a decoder inside (
case (sel)); one-hot-select arrives already decoded and needs only a bare AND-OR — the right choice when the select is naturally one-hot (FSM state, grant vector), at the cost ofNselect wires instead oflog2(N). - Parallel vs priority is a decision, not an accident.
case= mutually-exclusive, no-priority, balanced hardware;if-chain = ordered, first-match-wins, linear-delay hardware. Writing anif-chain when you meant parallel silently buys priority you did not want. - The default-assignment discipline keeps it combinational — in every HDL. Assign every output on every path (a leading default in SystemVerilog/Verilog, a
when others =>in VHDL); an incompletecase/ifinfers a latch that remembers the old value — the history-dependent bug in §7. Verify by exhaustively sweeping the select and assertingdout == din[sel](and one-hotness via$onehotfor the one-hot form) — self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL.