Skip to content

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.

the-need.v — four results, one bus, one must win
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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 three sel bits one rank at a time. The tree depth is log2(N), so the propagation delay grows logarithmically with N. This is what a good synthesis tool infers from a well-written case.
  • 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. sel is an encoded index (log2(N) bits): 2'b10 means "input 2." The mux must decode that index internally to steer the switch. This is the case (sel) form — compact select wiring, a decoder hidden inside.
  • One-hot-select mux. sel is 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 of log2(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 when sel genuinely picks one source.
  • Priority mux — an if / else if / else chain 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 flow
The mux family — one atom, two axes, one control-style axis2:1 atomy = sel ? b : a (AND-OR)N:1 as 2:1 treecase → balanced, delay ~ log2(N)binary selectsel = encoded index, decode insideone-hot selectAND-OR, sel already decodedparallel (case)mutually exclusive, no prioritypriority(if-chain)first match wins, delay ~ N
Everything is the 2:1 atom composed. Compose it as a balanced tree from a case and you get a parallel, log2(N)-delay selector; compose it as a chain from an if/else-if and you get a priority, N-delay cascade. Feed it a binary index and a decoder hides inside; feed it a one-hot select and the switch is a bare AND-OR. Same selection, different hardware — the RTL you write chooses which. This is language-independent: the same two axes exist in SystemVerilog, Verilog, and VHDL.

Default 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.

mux2.sv — the 2:1 atom (dataflow, latch-proof by construction)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
   endmodule

The 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.

mux2_tb.sv — self-checking: exhaustive over sel, assert y === expected
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

mux2.v — the 2:1 atom in Verilog (assign is total → no latch)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
mux2_tb.v — self-checking Verilog testbench (compare, print PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

mux2.vhd — the 2:1 atom in VHDL (when/else, both paths driven)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

mux2_tb.vhd — self-checking VHDL testbench (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

mux_case.sv — parameterized N:1, binary-select (case form)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

mux_case_tb.sv — exhaustive sel sweep; dout === din[sel] every time
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

mux_case.v — parameterized N:1 in Verilog (flattened bus + part-select)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
mux_case_tb.v — self-checking Verilog N:1 testbench (exhaustive sel)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

VHDL 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.

mux_case.vhd — parameterized N:1 in VHDL (array-of-vector + integer index)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
mux_case_tb.vhd — self-checking VHDL N:1 testbench (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

mux_onehot.sv — parameterized N:1, one-hot select (AND-OR)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

If 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.

mux_onehot_tb.sv — check each one-hot select; assume $onehot(onehot)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

mux_onehot.v — one-hot AND-OR in Verilog (flattened bus, replicated mask)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
mux_onehot_tb.v — self-checking; one-hotness via (v & (v-1))==0
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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]}}.

mux_onehot.vhd — one-hot AND-OR in VHDL (replicate select over the word)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
mux_onehot_tb.vhd — self-checking; assert exactly-one-bit via count
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

mux_bug.sv — BUGGY (latch) vs FIXED (default-first)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
mux_bug_tb.sv — sweep ALL FOUR op_sel; the uncovered value exposes the latch
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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.

mux_bug.v — BUGGY vs FIXED in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
mux_bug_tb.v — self-checking; exhaustive op_sel catches the uncovered value
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

mux_bug.vhd — BUGGY (no others) vs FIXED (when others => default)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
mux_bug_tb.vhd — self-checking; sweep all four op_sel (assert report severity)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 sel coverage (self-checking). For a binary-select N:1, drive every value of sel from 0 to N-1 with random data on all din, and assert dout === din[sel] each time. Because sel has only N legal values, this is complete — a for loop over sel with a reference model (expected = din[sel]) proves the whole table, not a sampled corner. The three mux_case testbenches in §4b do exactly this: SV assert (dout === din[s]), Verilog if (dout !== exp) $display("FAIL…"), VHDL assert 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, "if onehot is one-hot then dout equals 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 $onehot assumption/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 uses assert ($onehot(onehot)); Verilog uses the equivalent (onehot & (onehot-1)) == 0 && onehot != 0; VHDL counts the set bits. Drive onehot = 0 (expect the defined all-zeros default) and, in a directed check, onehot with 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 sel out of range or to X and confirm the intended behaviour — with unique case (SV) or when 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 fourth op_sel value 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 sel sweep for N = 2, 4, 8 (and a non-power-of-two like N = 3, where the default-assignment discipline earns its keep) and for WIDTH = 1, 8, 32. A form that passes at N=4 can still be wrong at N=3 if the default path was sloppy — the leading dout='0 / (others => '0') / default-first is what makes it survive the odd N.
  • Expected waveform. Because a mux has no clock, "correct" on a waveform is immediate: as sel steps 0 → 1 → 2 → 3, dout tracks din[0] → din[1] → din[2] → din[3] combinationally (within the propagation delay), never showing a value that is not one of the current inputs. A dout that holds its previous value when sel changes 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.)

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/default and 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 its always @(*), 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 case statement. sel steers exactly one input, verbatim, onto a shared output; if/case/? : (and VHDL's when/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 a case, delay ~log2(N), parallel) or a cascade (from an if-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 of N select wires instead of log2(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 an if-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 incomplete case/if infers a latch that remembers the old value — the history-dependent bug in §7. Verify by exhaustively sweeping the select and asserting dout == din[sel] (and one-hotness via $onehot for the one-hot form) — self-checking testbenches shown here in SystemVerilog, Verilog, and VHDL.