RTL Design Patterns · Chapter 1 · Combinational Building Blocks
Decoders
A decoder is the inverse of a multiplexer. It takes a compact n-bit binary code and lights exactly one of 2 to the n output lines, gated by an enable. This is the primitive behind every address decode, chip select, and register write-enable, and it produces the very one-hot select signal a mux consumes. This lesson builds the enable-gated decoder, compares the shift-based idiom with the case form, and covers the enable as the thing that makes an all-quiet deselected state legal. It also studies the two signature failures: a forgotten enable that spuriously selects during idle cycles, and an out-of-range code that aliases onto a real line and corrupts a register. Everything is built and self-check-verified in SystemVerilog, Verilog, and VHDL.
Foundation12 min readRTL Design PatternsDecoderOne-HotAddress DecodeEnable GatingCombinational Logic
Chapter 1 · Section 1.2 · Combinational Building Blocks
1. The Engineering Problem
You are building the write-back stage of a small register file. Every cycle, the datapath produces a result and a 5-bit destination index waddr that says which of the 32 registers this result belongs to. Thirty-two registers sit in a row, each with its own load-enable input. Your job is to turn that 5-bit number into a single control action: raise the load-enable of register waddr, and only that register, so exactly one flop bank captures the result and the other thirty-one hold their value untouched.
This is not a puzzle invented for a lesson. It is the shape of nearly every control junction in a chip, and it is the mirror image of the datapath junction the multiplexer solved. A memory map takes a bus address and must assert the select of exactly one peripheral — the UART, the timer, the GPIO block — while every other slave stays silent. A cache set-decode takes an index and activates one word-line. An instruction decoder takes an opcode field and lights the one control line for the operation this cycle wants. In every case the need is identical: take an n-bit binary code and activate exactly one of 2^n output lines — a one-hot signal — under the control of an enable.
You cannot skip the decode. If you leave the index encoded and try to compare it inline at each register (if (waddr == 0) ... if (waddr == 1) ...), you have simply reinvented the decoder as thirty-two scattered comparators — the same hardware, spread out and unnamed, and now impossible to gate cleanly. And you cannot omit the enable: without it, the decoder always asserts one output, which means that even on a cycle where the register file is doing a read and should write nothing, some register's load-enable is high — a spurious write into whatever register the stale waddr happens to point at. The structure that does this correctly, in one place, gated by one enable, is the decoder, and the reason it is Section 1.2 is that it is the control-side atom the way the mux is the datapath-side atom — the primitive almost every later control path is built out of.
// The write-back stage every cycle produces:
wire [4:0] waddr; // destination register index (0..31)
wire reg_wen; // is this cycle actually a write? (read cycles: 0)
wire [31:0] result; // the value to store
// 32 registers, each with its own load-enable. We must raise EXACTLY ONE,
// and ONLY when reg_wen is asserted:
wire [31:0] reg_load_en; // one-hot: reg_load_en[waddr] high on a write, else all 0
// WRONG — no enable: some load_en is ALWAYS high, so a READ cycle still
// writes whatever register the stale waddr points at (silent corruption):
// assign reg_load_en = (32'b1 << waddr); // one-hot, but never quiet
// RIGHT — a DECODER gated by the enable: one-hot when writing, ALL-ZERO when not.
// (the pattern this page builds — enable-gated n→2^n one-hot generation)2. Mental Model
3. Pattern Anatomy
The whole family is built from one core translation and two forms of writing it, wrapped in one non-negotiable control input.
The core translation. A decoder has n binary inputs (addr, ADDR_W bits), 2^n one-hot outputs (y / dec_out, OUT_W = 2^n bits), and it drives y[k] high exactly when addr == k. The whole truth table is: for each of the 2^n input codes, the one matching output is 1 and the rest are 0. A 3→8 decoder takes addr[2:0] and drives y[7:0] so that addr = 3'd5 gives y = 8'b0010_0000. That is the entire behaviour before the enable.
The enable — the input that makes the pattern usable. Real decoders have an enable en. Its rule is a single sentence: when en is high the decoder behaves as above (one-hot); when en is low, all outputs are forced to zero (zero-hot), regardless of addr. The enable is what a chip-select decoder uses to stay quiet while another decoder owns the bus, what a write-decoder uses to stay quiet on a read cycle, and what lets you build a hierarchical decode (§6, §8) by enabling only the sub-decoder whose range is being addressed. A decoder without an enable is a decoder that can never be silent.
The one-hot invariant — the property that defines "correct." A correct enable-gated decoder obeys one invariant on its output, and it has two faces:
- When maybe-disabled: at most one output bit is high — the SystemVerilog
$onehot0property (one-hot-or-zero). This is the general invariant, becauseenlow legitimately makes it zero-hot. - When always-enabled (
entied high): exactly one output bit is high — the$onehotproperty. This is the special case of a decoder that is never allowed to be silent.
Never — under any legal input — may two output bits be high at once. Two-hot is not a weaker form of correct; it is the aliasing/overlap bug of §6, and it is the single thing verification (§5) must most aggressively assert.
Decoder ↔ demultiplexer — the same structure, one extra port. A demultiplexer (demux) is a decoder with a data input: instead of driving the selected output to a constant 1, it routes a data bit d to the selected output and holds the rest at 0. Set the demux's data input to constant 1 and it is a decoder; the enable of a decoder is exactly the data input of a demux. So the mux (many→one, §1.1) and the demux/decoder (one→many) are the two halves of routing: a mux collects, a demux/decoder distributes, and a decoder is the special demux whose payload is a bare enable. This is why the decoder's outputs are precisely the one-hot select vector a §1.1 mux consumes.
The decoder family — one translation, two forms, one control input
data flowDefault handling and out-of-range reasoning close the anatomy. A combinational decoder must define its full one-hot output for every input code and enable value. Two hazards appear the moment the output width and the input space stop matching perfectly. First, the disabled state: the block must force all outputs low when en is low — the discipline is default the output to all-zeros first, then set the one selected bit only if enabled, so no path can leave a stale bit high. Second, the out-of-range code: when 2^ADDR_W exceeds the number of legal targets (a 5-bit index but only 24 real registers, or a case that lists only the valid codes), the unlisted codes must map to all-zeros via a default / when others, not to a real line — otherwise an illegal address aliases onto a valid select and asserts something that must never be asserted (§6). The universal discipline, in every HDL, is the same as the mux's: assign the safe default first, then override exactly one bit under the enable and range guard.
4. Real RTL Implementation
This is the core of the page. We build three patterns — (a) the basic enable-gated n→2^n decoder, (b) a parameterized/generic decoder shown in both the 1 << addr idiom and the case / with-select / unique case form, and (c) the ungated / out-of-range 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: default the whole output to all-zeros, then assert exactly one bit only when enabled and in range. In SystemVerilog and Verilog that means a leading y = '0 before the shift or case; in VHDL it means a (others => '0') default (or a when others => (others => '0') arm). Miss it and you get pattern (c)'s spurious select.
4a. The basic enable-gated decoder — the atom, three ways
Start with a fixed 3→8 enable-gated decoder — small enough to read whole, general enough to show the enable and the one-hot output. The behaviour is: default y = 0; if en, set the one bit at position addr. Because the default assigns all eight bits first, the disabled and out-of-range states are safe by construction — there is no path that leaves a stale bit set.
module decoder3to8 (
input logic [2:0] addr, // the 3-bit code to decode
input logic en, // master enable: low => all outputs 0
output logic [7:0] y // one-hot when en, all-zero when !en
);
// Combinational one-hot GENERATION — no clock, no state.
always_comb begin
y = 8'b0; // DEFAULT ALL-ZERO FIRST:
// guarantees the disabled state (en=0) is
// fully quiet and no bit can be left stale.
if (en)
y[addr] = 1'b1; // assert exactly ONE bit (the selected line)
end
endmoduleThe y[addr] = 1'b1 after a full-width default zero is the one-hot generator: indexing the output with the variable addr synthesizes to a 3-to-8 fan-out of AND gates, each gated by en. The SystemVerilog testbench sweeps every addr with en high (exhaustive — only eight codes) and re-checks with en low, asserting the $onehot/$onehot0 invariant on every step, so it proves the whole truth table plus the enable behaviour, not a sampled corner.
module decoder3to8_tb;
logic [2:0] addr;
logic en;
logic [7:0] y;
int errors = 0;
decoder3to8 dut (.addr(addr), .en(en), .y(y));
initial begin
// ENABLED: every code must light exactly the matching bit (one-hot).
en = 1'b1;
for (int a = 0; a < 8; a++) begin
addr = a[2:0];
#1;
assert (y === (8'b1 << a)) // reference: 1 shifted to addr
else begin $error("en=1 addr=%0d y=%b exp=%b", a, y, (8'b1 << a)); errors++; end
assert ($onehot(y)) // invariant: exactly one bit hot
else begin $error("en=1 addr=%0d not one-hot: y=%b", a, y); errors++; end
end
// DISABLED: output must be all-zero regardless of addr (zero-hot).
en = 1'b0;
for (int a = 0; a < 8; a++) begin
addr = a[2:0];
#1;
assert (y === 8'b0)
else begin $error("en=0 addr=%0d y=%b exp=0", a, y); errors++; end
assert ($onehot0(y)); // at-most-one holds when disabled
end
if (errors == 0) $display("PASS: decoder3to8 one-hot when en, all-zero when !en");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog form is identical in intent; the only differences are reg typing on the output and always @(*). The leading y = 8'b0 is just as total, so the disabled and out-of-range states are equally safe. Verilog has no $onehot, so the testbench checks one-hotness with the classic (y & (y-1)) == 0 && y != 0 reduction.
module decoder3to8 (
input wire [2:0] addr,
input wire en,
output reg [7:0] y
);
// Combinational block; default all-zero then set one bit under the enable.
always @(*) begin
y = 8'b0; // DEFAULT ALL-ZERO FIRST → quiet when !en
if (en)
y[addr] = 1'b1; // exactly one selected line
end
endmodule module decoder3to8_tb;
reg [2:0] addr;
reg en;
wire [7:0] y;
reg [7:0] exp;
integer a, errors;
decoder3to8 dut (.addr(addr), .en(en), .y(y));
initial begin
errors = 0;
en = 1'b1; // ENABLED: one-hot per code
for (a = 0; a < 8; a = a + 1) begin
addr = a[2:0];
#1;
exp = 8'b1 << a; // reference one-hot
if (y !== exp) begin
$display("FAIL: en=1 addr=%0d y=%b exp=%b", a, y, exp);
errors = errors + 1;
end
// exactly-one-bit check: power of two and non-zero
if (!((y & (y - 1)) == 0 && y != 0)) begin
$display("FAIL: en=1 addr=%0d not one-hot: y=%b", a, y);
errors = errors + 1;
end
end
en = 1'b0; // DISABLED: all-zero per code
for (a = 0; a < 8; a = a + 1) begin
addr = a[2:0];
#1;
if (y !== 8'b0) begin
$display("FAIL: en=0 addr=%0d y=%b exp=0", a, y);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: decoder3to8 one-hot when en, all-zero when !en");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleIn VHDL the atom is written most naturally with with … select on the address, guarded by the enable, or as a case. Here the enable is applied as an outer gate: the with-select builds the raw one-hot, and it is ANDed to zero when en is low. The when others => arm is VHDL's default — the exhaustiveness that keeps the output defined for the meta-values a std_logic_vector can carry.
library ieee;
use ieee.std_logic_1164.all;
entity decoder3to8 is
port (
addr : in std_logic_vector(2 downto 0); -- the 3-bit code
en : in std_logic; -- master enable
y : out std_logic_vector(7 downto 0) -- one-hot when en, else all-zero
);
end entity;
architecture rtl of decoder3to8 is
signal raw : std_logic_vector(7 downto 0); -- ungated one-hot
begin
-- with-select is exhaustive: every listed code plus "when others" gives a
-- fully defined raw one-hot (no unassigned path, no latch).
with addr select
raw <= "00000001" when "000",
"00000010" when "001",
"00000100" when "010",
"00001000" when "011",
"00010000" when "100",
"00100000" when "101",
"01000000" when "110",
"10000000" when "111",
"00000000" when others; -- meta-values => quiet default
-- The enable is the master switch: low forces the whole word to zero.
y <= raw when en = '1' else (others => '0');
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity decoder3to8_tb is
end entity;
architecture sim of decoder3to8_tb is
signal addr : std_logic_vector(2 downto 0);
signal en : std_logic;
signal y : std_logic_vector(7 downto 0);
begin
dut : entity work.decoder3to8 port map (addr => addr, en => en, y => y);
stim : process
variable exp : std_logic_vector(7 downto 0);
begin
en <= '1'; -- ENABLED: one-hot per code
for a in 0 to 7 loop
addr <= std_logic_vector(to_unsigned(a, 3));
wait for 1 ns;
exp := (others => '0');
exp(a) := '1'; -- reference one-hot
assert y = exp
report "en=1: y /= one-hot for this addr" severity error;
end loop;
en <= '0'; -- DISABLED: all-zero per code
for a in 0 to 7 loop
addr <= std_logic_vector(to_unsigned(a, 3));
wait for 1 ns;
assert y = "00000000"
report "en=0: y not all-zero" severity error;
end loop;
report "decoder3to8 self-check complete" severity note;
wait;
end process;
end architecture;4b. Parameterized / generic decoder — the 1 << addr idiom and the case form
Now the register-select decoder from §1, generalized: one source serves any address width. ADDR_W sets the input width, OUT_W = 2^ADDR_W sets the one-hot output width, and the enable and range guard keep it safe when the number of real targets is not a full power of two. Two forms are worth showing because engineers meet both, and they synthesize to the same fan-out.
The shift form is the one-liner: en ? (1 << addr) : 0. It is compact, obviously one-hot when in range, and the enable multiplexes cleanly to all-zeros.
module decoder_param #(
parameter int ADDR_W = 5, // input code width
localparam int OUT_W = 1 << ADDR_W // 2^ADDR_W one-hot outputs
)(
input logic [ADDR_W-1:0] addr, // the code to decode
input logic en, // master enable
output logic [OUT_W-1:0] dec_out // one-hot when en, else all-zero
);
// The shift IS the one-hot generator: a single 1 shifted left by addr, gated
// by en. When en is low the whole word is zero => fully quiet, no stale bit.
assign dec_out = en ? (OUT_W'(1) << addr) : '0;
endmoduleThe case / unique case form is the explicit one: default all-zero, then a unique case (addr) whose default sets dec_out[addr] only when enabled. unique documents that the codes are mutually exclusive (so the tool builds a parallel decode and flags an impossible overlap), and the leading dec_out = '0 is the default-assignment discipline that survives a non-power-of-two target count. Both forms below are the same decoder; pick whichever reads clearer for the range logic you need.
module decoder_param_case #(
parameter int ADDR_W = 5,
localparam int OUT_W = 1 << ADDR_W
)(
input logic [ADDR_W-1:0] addr,
input logic en,
output logic [OUT_W-1:0] dec_out
);
always_comb begin
dec_out = '0; // DEFAULT ALL-ZERO FIRST
unique case (en)
1'b1: dec_out[addr] = 1'b1; // enabled: exactly one bit
default: dec_out = '0; // disabled: fully quiet
endcase
end
endmoduleThe testbench sweeps every addr (exhaustive, because addr has only OUT_W codes) with en high and low, asserts $onehot when enabled and $onehot0 when disabled, and re-runs for several ADDR_W values — a parameterized decoder must be verified generic, not assumed generic.
module decoder_param_tb;
localparam int ADDR_W = 5;
localparam int OUT_W = 1 << ADDR_W;
logic [ADDR_W-1:0] addr;
logic en;
logic [OUT_W-1:0] dec_out;
int errors = 0;
decoder_param #(.ADDR_W(ADDR_W)) dut (.addr(addr), .en(en), .dec_out(dec_out));
initial begin
en = 1'b1; // ENABLED: one-hot per code
for (int a = 0; a < OUT_W; a++) begin
addr = a[ADDR_W-1:0];
#1;
assert (dec_out === (OUT_W'(1) << a)) // reference one-hot
else begin $error("en=1 addr=%0d out=%b", a, dec_out); errors++; end
assert ($onehot(dec_out)) // exactly-one invariant
else begin $error("en=1 addr=%0d not one-hot", a); errors++; end
end
en = 1'b0; // DISABLED: all-zero per code
for (int a = 0; a < OUT_W; a++) begin
addr = a[ADDR_W-1:0];
#1;
assert (dec_out === '0)
else begin $error("en=0 addr=%0d out=%b exp=0", a, dec_out); errors++; end
assert ($onehot0(dec_out)); // at-most-one when disabled
end
if (errors == 0) $display("PASS: decoder_param one-hot/all-zero, ADDR_W=%0d", ADDR_W);
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleThe Verilog parameterized decoder uses the same two forms. The shift form is a one-line continuous assignment; the case form flags the range with a default that keeps illegal codes quiet. parameter sets ADDR_W; the instantiator passes OUT_W = 1 << ADDR_W. (Cross-link: the elaboration-time width math is exactly the parameter / generate machinery from Verilog v1.)
module decoder_param #(
parameter ADDR_W = 5,
parameter OUT_W = 32 // = 1 << ADDR_W; set by the instantiator
)(
input wire [ADDR_W-1:0] addr,
input wire en,
output wire [OUT_W-1:0] dec_out
);
// en gates a single 1 shifted to position addr; low en => all-zero (quiet).
assign dec_out = en ? ({{(OUT_W-1){1'b0}}, 1'b1} << addr) : {OUT_W{1'b0}};
endmodule module decoder_param_tb;
parameter ADDR_W = 5;
parameter OUT_W = 32;
reg [ADDR_W-1:0] addr;
reg en;
wire [OUT_W-1:0] dec_out;
reg [OUT_W-1:0] exp;
integer a, errors;
decoder_param #(.ADDR_W(ADDR_W), .OUT_W(OUT_W)) dut
(.addr(addr), .en(en), .dec_out(dec_out));
initial begin
errors = 0;
en = 1'b1; // ENABLED: one-hot per code
for (a = 0; a < OUT_W; a = a + 1) begin
addr = a[ADDR_W-1:0];
#1;
exp = {{(OUT_W-1){1'b0}}, 1'b1} << a; // reference one-hot
if (dec_out !== exp) begin
$display("FAIL: en=1 addr=%0d out=%b exp=%b", a, dec_out, exp);
errors = errors + 1;
end
// exactly-one-bit check
if (!((dec_out & (dec_out - 1)) == 0 && dec_out != 0)) begin
$display("FAIL: en=1 addr=%0d not one-hot", a);
errors = errors + 1;
end
end
en = 1'b0; // DISABLED: all-zero per code
for (a = 0; a < OUT_W; a = a + 1) begin
addr = a[ADDR_W-1:0];
#1;
if (dec_out !== {OUT_W{1'b0}}) begin
$display("FAIL: en=0 addr=%0d out=%b exp=0", a, dec_out);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: decoder_param one-hot/all-zero for all addr");
else $display("FAIL: %0d mismatches", errors);
$finish;
end
endmoduleVHDL expresses the generic decoder with a generic for the address width and the to_integer(unsigned(addr)) index into the output. The shift form uses a bounded index and a (others => '0') default; the others guard is VHDL's answer to keeping unlisted / out-of-range codes quiet. (generic is VHDL's parameter.)
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity decoder_param is
generic ( ADDR_W : positive := 5 ); -- generic == Verilog parameter
port (
addr : in std_logic_vector(ADDR_W-1 downto 0); -- the code to decode
en : in std_logic; -- master enable
dec_out : out std_logic_vector((2**ADDR_W)-1 downto 0)
);
end entity;
architecture rtl of decoder_param is
begin
-- Combinational one-hot generation. Default all-zero, then set exactly one
-- bit at the decoded index, but ONLY when enabled. The bounded index plus
-- the all-zero default make every path defined (VHDL's default-assign).
process (all)
variable idx : integer;
begin
dec_out <= (others => '0'); -- DEFAULT: quiet by default
if en = '1' then
idx := to_integer(unsigned(addr)); -- decode the code to a position
dec_out(idx) <= '1'; -- assert exactly one line
end if; -- en=0 => stays all-zero
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity decoder_param_tb is
end entity;
architecture sim of decoder_param_tb is
constant ADDR_W : positive := 5;
constant OUT_W : positive := 2**ADDR_W;
signal addr : std_logic_vector(ADDR_W-1 downto 0);
signal en : std_logic;
signal dec_out : std_logic_vector(OUT_W-1 downto 0);
begin
dut : entity work.decoder_param
generic map (ADDR_W => ADDR_W)
port map (addr => addr, en => en, dec_out => dec_out);
stim : process
variable exp : std_logic_vector(OUT_W-1 downto 0);
begin
en <= '1'; -- ENABLED: one-hot per code
for a in 0 to OUT_W-1 loop
addr <= std_logic_vector(to_unsigned(a, ADDR_W));
wait for 1 ns;
exp := (others => '0');
exp(a) := '1'; -- reference one-hot
assert dec_out = exp
report "en=1: dec_out /= one-hot for this addr" severity error;
end loop;
en <= '0'; -- DISABLED: all-zero per code
for a in 0 to OUT_W-1 loop
addr <= std_logic_vector(to_unsigned(a, ADDR_W));
wait for 1 ns;
assert dec_out = (dec_out'range => '0')
report "en=0: dec_out not all-zero" severity error;
end loop;
report "decoder_param self-check complete" severity note;
wait;
end process;
end architecture;4c. The ungated / out-of-range bug — buggy vs fixed, in all three HDLs
Pattern (c) is the signature failure, shown here as buggy vs fixed RTL in each language, then dramatized narratively in §7. There are two closely-related versions of the same structural mistake, and both are shown: an ungated decoder (no enable, so a select line is always high — spurious write on an idle/read cycle), and an out-of-range decode (an illegal address maps onto a real line — aliasing). The fix in both cases is the same discipline: gate with the enable, default to all-zero, and force out-of-range codes quiet.
// Register-file write-select decoder. 24 real registers, 5-bit index (0..31).
// BUGGY: no enable, and codes 24..31 fall through to a live line.
module reg_wsel_bad (
input logic [4:0] waddr,
output logic [23:0] wsel // one write-enable per real register
);
// No en input at all: SOME wsel bit is high EVERY cycle, even on a read.
// And waddr in 24..31 shifts a 1 off the top OR (with a raw index) aliases.
assign wsel = (24'b1 << waddr); // waddr>=24 => wrong/undefined bit set
endmodule
// FIXED: add the enable AND guard the range. Quiet on read; illegal codes quiet.
module reg_wsel_good (
input logic [4:0] waddr,
input logic reg_wen, // is this cycle actually a write?
output logic [23:0] wsel
);
always_comb begin
wsel = '0; // DEFAULT ALL-ZERO: quiet on reads
if (reg_wen && (waddr < 24)) // enable AND in-range guard
wsel[waddr] = 1'b1; // exactly one real register selected
end // waddr>=24 or !reg_wen => stays quiet
endmodule module regsel_bug_tb;
logic [4:0] waddr;
logic reg_wen;
logic [23:0] wsel;
int errors = 0;
// Bind to reg_wsel_good to PASS; to reg_wsel_bad to see the spurious selects.
reg_wsel_good dut (.waddr(waddr), .reg_wen(reg_wen), .wsel(wsel));
initial begin
// 1) READ cycle (reg_wen=0): NO write-select may be high, any waddr.
reg_wen = 1'b0;
for (int a = 0; a < 32; a++) begin
waddr = a[4:0];
#1;
assert (wsel === '0)
else begin $error("READ cycle asserted wsel=%b at waddr=%0d", wsel, a); errors++; end
end
// 2) WRITE cycle (reg_wen=1): in-range => one-hot; out-of-range => quiet.
reg_wen = 1'b1;
for (int a = 0; a < 32; a++) begin
waddr = a[4:0];
#1;
if (a < 24) begin
assert (wsel === (24'b1 << a) && $onehot(wsel))
else begin $error("write in-range addr=%0d wsel=%b", a, wsel); errors++; end
end else begin
assert (wsel === '0) // illegal address must NOT alias
else begin $error("out-of-range addr=%0d aliased wsel=%b", a, wsel); errors++; end
end
end
if (errors == 0) $display("PASS: quiet on reads, one-hot in range, quiet out-of-range");
else $display("FAIL: %0d spurious/aliased selects", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair is the same story with reg outputs and always @(*). Sweeping a read cycle (enable low) across all addresses and the out-of-range addresses is what exposes the bug — the ungated decoder asserts a select on both.
// BUGGY: no enable; some wsel bit is high every cycle; codes 24..31 misbehave.
module reg_wsel_bad (
input wire [4:0] waddr,
output wire [23:0] wsel
);
assign wsel = (24'b1 << waddr); // always asserts; out-of-range aliases
endmodule
// FIXED: default all-zero, then set one bit only when enabled AND in range.
module reg_wsel_good (
input wire [4:0] waddr,
input wire reg_wen,
output reg [23:0] wsel
);
always @(*) begin
wsel = 24'b0; // DEFAULT ALL-ZERO → quiet on reads
if (reg_wen && (waddr < 24)) // enable AND in-range guard
wsel[waddr] = 1'b1; // exactly one real register
end
endmodule module regsel_bug_tb;
reg [4:0] waddr;
reg reg_wen;
wire [23:0] wsel;
reg [23:0] exp;
integer a, errors;
// Bind to reg_wsel_good to PASS; to reg_wsel_bad to observe spurious selects.
reg_wsel_good dut (.waddr(waddr), .reg_wen(reg_wen), .wsel(wsel));
initial begin
errors = 0;
reg_wen = 1'b0; // READ cycle: must be quiet
for (a = 0; a < 32; a = a + 1) begin
waddr = a[4:0];
#1;
if (wsel !== 24'b0) begin
$display("FAIL: READ asserted wsel=%b at waddr=%0d", wsel, a);
errors = errors + 1;
end
end
reg_wen = 1'b1; // WRITE cycle
for (a = 0; a < 32; a = a + 1) begin
waddr = a[4:0];
#1;
if (a < 24) exp = 24'b1 << a; // in-range: one-hot
else exp = 24'b0; // out-of-range: quiet
if (wsel !== exp) begin
$display("FAIL: waddr=%0d wsel=%b exp=%b", a, wsel, exp);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: quiet on reads and out-of-range, one-hot in range");
else $display("FAIL: %0d spurious/aliased selects", errors);
$finish;
end
endmoduleIn VHDL the same two failures appear when the decode has no enable gate and no when others / range guard: some output is always high, and an out-of-range integer index either aliases or raises a bounds error. The fixed version defaults to (others => '0'), gates on the enable, and guards the index — the when others => / range check is exactly VHDL's quiet default.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- BUGGY: no enable, no range guard. A wsel bit is asserted every cycle, and an
-- out-of-range index reaches a live line (or a bounds error).
entity reg_wsel_bad is
port (
waddr : in std_logic_vector(4 downto 0);
wsel : out std_logic_vector(23 downto 0)
);
end entity;
architecture rtl of reg_wsel_bad is
begin
process (all)
begin
wsel <= (others => '0');
wsel(to_integer(unsigned(waddr))) <= '1'; -- no en, no range check => bug
end process;
end architecture;
-- FIXED: default all-zero, enable gate, in-range guard => quiet unless writing.
entity reg_wsel_good is
port (
waddr : in std_logic_vector(4 downto 0);
reg_wen : in std_logic; -- master enable
wsel : out std_logic_vector(23 downto 0)
);
end entity;
architecture rtl of reg_wsel_good is
begin
process (all)
variable idx : integer;
begin
wsel <= (others => '0'); -- DEFAULT: quiet on reads
idx := to_integer(unsigned(waddr));
if reg_wen = '1' and idx <= 23 then -- enable AND in-range
wsel(idx) <= '1'; -- exactly one real register
end if; -- else: stays all-zero
end process;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity regsel_bug_tb is
end entity;
architecture sim of regsel_bug_tb is
signal waddr : std_logic_vector(4 downto 0);
signal reg_wen : std_logic;
signal wsel : std_logic_vector(23 downto 0);
begin
-- Bind to reg_wsel_good to PASS; to reg_wsel_bad to observe spurious selects.
dut : entity work.reg_wsel_good
port map (waddr => waddr, reg_wen => reg_wen, wsel => wsel);
stim : process
variable exp : std_logic_vector(23 downto 0);
begin
reg_wen <= '0'; -- READ cycle: must be quiet
for a in 0 to 31 loop
waddr <= std_logic_vector(to_unsigned(a, 5));
wait for 1 ns;
assert wsel = (wsel'range => '0')
report "READ cycle asserted a write-select" severity error;
end loop;
reg_wen <= '1'; -- WRITE cycle
for a in 0 to 31 loop
waddr <= std_logic_vector(to_unsigned(a, 5));
wait for 1 ns;
if a <= 23 then -- in-range: one-hot
exp := (others => '0');
exp(a) := '1';
else -- out-of-range: quiet
exp := (others => '0');
end if;
assert wsel = exp
report "wsel mismatch (spurious or aliased select)" severity error;
end loop;
report "regsel_bug self-check complete" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: default the decoder to all-zero, then assert exactly one bit only under the enable and an in-range guard — a leading zero-default plus an if (en && in_range) in SV/Verilog, a (others => '0') plus a guarded index in VHDL — or the decoder selects something on a cycle where it must select nothing.
5. Verification Strategy
A decoder is combinational, so its correctness is a truth table, and the good news is the table is small enough to check exhaustively — an ADDR_W-bit input has only 2^ADDR_W codes. The single invariant that captures a correct enable-gated decoder is:
When enabled, exactly one output is high and it is the one whose index equals the input code; when disabled, all outputs are low. At no legal input may two outputs be high.
Everything below is that one invariant, made checkable, and it maps directly onto the testbenches in §4.
- The one-hot output invariant (
$onehot/$onehot0). This is the property the whole page is about, and it has the two faces of §3. Whenenis high, assert$onehot(dec_out)— exactly one bit set — on every code; whenenis low, assert$onehot0(dec_out)— at most one, which for a correct decoder is zero. Never accept two bits high. The SV testbenches useassert ($onehot(...))andassert ($onehot0(...)); Verilog uses the equivalent(v & (v-1))==0 && v!=0(exactly-one) and(v & (v-1))==0(at-most-one) reductions; VHDL counts the set bits or compares against the reference one-hot. This is the check that catches the overlapping/aliasing failure of §6 the moment it appears. - Exhaustive address sweep (self-checking). For an ADDR_W-bit decoder, drive every code from
0to2^ADDR_W - 1withenhigh and assertdec_out === (1 << addr)each time — aforloop overaddrwith a reference model (expected = 1 << addr) proves the whole truth table, not a sampled corner. The threedecoder_paramtestbenches in §4b do exactly this: SVassert (dec_out === (OUT_W'(1) << a)), Verilogif (dec_out !== exp) $display("FAIL…"), VHDLassert dec_out = exp report … severity error. - Enable behaviour (the deselected state). Re-run the same exhaustive address sweep with
enlow and assert the output is all-zero for every code. This is the check the §4c bug fails: an ungated decoder asserts a select even on a read cycle, and only a sweep with the enable off surfaces it. The invariant in plain terms — "a disabled decoder is silent for any address" — must be a first-class assertion, not an afterthought. - Out-of-range / illegal-code handling. When
2^ADDR_Wexceeds the number of legal targets (24 real registers behind a 5-bit index), drive the illegal codes (24..31) and assert the output stays all-zero — an illegal address must not alias onto a real line. The §4c testbenches do this explicitly: the out-of-range branch assertswsel === '0. Decide the illegal-code behaviour deliberately (quiet, or anX/flagged error) and test that decision rather than letting an unlisted code alias silently. - Parameter-generic verification (ADDR_W = 2, 3, 5). A parameterized decoder must be verified generic, not assumed generic. Re-run the exhaustive address sweep for
ADDR_W = 2, 3, 5(and a case where the legal-target count is not a power of two, e.g. 24 of 32, where the range guard earns its keep). A form that passes atADDR_W = 3can still alias at a non-power-of-two target count if the range guard was sloppy — the leadingdec_out = '0plus thein_rangeguard is what makes it survive. - Expected waveform. Because a decoder has no clock, "correct" on a waveform is immediate: with
enhigh, asaddrsteps0 → 1 → 2 → …, the single hot bit ofdec_outwalksbit0 → bit1 → bit2 → …combinationally (within the propagation delay), and only one bit is ever high. Pullenlow and the whole word drops to zero at once. A waveform where two bits are simultaneously high, or where a bit stays high afterengoes low, is the visual signature of the aliasing / ungated bug from §7.
6. Common Mistakes
Forgotten enable → spurious, always-on select. The signature decoder bug. A pure n→2^n decoder must assert one output for every input code — there is no "select nothing" state built into it. If you build the decoder without an enable (assign wsel = 1 << waddr), then some output is high on every cycle, including cycles where the block should do nothing at all — a read cycle in a register file, an idle cycle on a bus. That stray one-hot bit is a write-enable or a chip-select firing when it must be silent, and because it depends on whatever stale value addr holds, the corruption is intermittent and history-dependent. The fix is the enable-gating discipline: default the output to all-zero and assert the selected bit only when the enable is high. (Full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4c.)
Out-of-range / illegal address decoded onto a real line. When the input space (2^ADDR_W codes) is bigger than the number of legal targets — a 5-bit index but only 24 registers, or a case that lists only the valid codes with no default — an illegal code must map to all-zeros, never to a live output. Omit the range guard and code 25 aliases onto some real register's select, or a raw 1 << 25 sets a bit that overlaps a valid line, and you get a write to a register the instruction never named. Always guard the range (if (addr < N_LEGAL)) and give a default / when others => that stays quiet; in VHDL an out-of-range integer index can also raise a bounds error, so the guard is doubly required.
Overlapping / aliased ranges breaking one-hot (address-decode). In a memory-map decoder you assign address ranges to slaves. If two ranges overlap — a copy-paste that leaves the UART's range and the timer's range both matching address 0x4000_0010 — then two chip-selects assert for one address, and the one-hot invariant is broken: two slaves drive the bus at once (contention / X), or two registers capture one write. This is the multi-hot failure $onehot exists to catch. Verify that the decode of every address produces at-most-one select, and that adjacent ranges are contiguous and non-overlapping.
A flat decoder where a hierarchical decode is needed. Decoding a wide address (say a 32-bit bus address) with a single flat 2^32-output decoder is absurd — you never build 4G output lines. Real address decoding is sparse and hierarchical: decode the high bits to pick a region (which peripheral), then let that region's local decoder decode the low bits to pick a register within it, enabling only the selected sub-decoder. Trying to flatten a sparse, wide decode into one monster combinational block is the decoder equivalent of the mux's "giant flat select" — a timing, area, and routing problem, and usually a sign the decode should be staged. Match the decoder's depth to the structure of the address space, not to its raw width.
7. DebugLab
The register that got corrupted on read cycles — an ungated decoder that never sleeps
The engineering lesson: a decoder without an enable can never be silent — it asserts one select on every cycle, so any cycle the design intended to be quiet becomes a spurious write or a stray chip-select. The tell is corruption that depends on instruction history ("a read after a write to R3 clobbers whatever waddr points at"): a control line firing when nothing asked for it means you built a selector with no off-state. Two habits make it impossible, and they are identical across SystemVerilog, Verilog, and VHDL: gate every decoder with an enable and default its output to all-zero (assert a bit only inside if (en && in_range)), and verify the deselected state by sweeping the address with the enable off and asserting the output is fully quiet — the cycle where the decoder must do nothing is the cycle the naive test skips.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "one-hot generation, gated by an enable" habit.
Exercise 1 — Give the decoder its off-state
An engineer writes a chip-select decoder as assign cs = (8'b1 << region); for an 8-peripheral bus and reports that "every peripheral responds even when the bus is idle." (a) State precisely why the idle bus still asserts a chip-select. (b) Rewrite the decode so it is quiet on an idle cycle, naming the one input you must add and where the default-assignment goes. (c) Explain, in one line each, how the same fix looks in a SystemVerilog always_comb, a Verilog always @(*), and a VHDL combinational process.
Exercise 2 — Predict the aliasing
A register file has 24 real registers but a 5-bit write index (waddr, 0..31). Engineer A writes wsel = 32'b1 << waddr (no range guard); Engineer B writes a case that lists only codes 0..23 and omits the default. Both pass a directed test that only ever drives waddr in 0..23. Describe (i) what each design does when waddr is 27 — for A and for B specifically, and how they differ — and (ii) the single guard that fixes both. Then say what invariant a testbench would assert to catch the aliasing automatically, and why sweeping only the legal codes never sees the bug.
Exercise 3 — Break the one-hot on paper
A memory-map decoder assigns address ranges to four slaves. State the one-hot invariant the decoder must satisfy across the whole address space. Then describe the two distinct ways the range assignment can violate it — too much coverage and too little — what the bus does in each case (two selects high vs. no select high), and what single check you would add to a testbench to catch each. Explain why an address decoder is exposed to this multi-hot failure in a way a simple 1 << addr register-select decoder (with a range guard) is not.
Exercise 4 — Size the decode
You must select one of the 4096 registers spread across 16 peripherals on a bus with a 32-bit address. Explain why a single flat 32-bit decoder is the wrong structure, then sketch a two-level hierarchical decode: which bits pick the peripheral, which bits pick the register inside it, and exactly where the enable of each local decoder comes from. Name one cost the hierarchy introduces (latency, control complexity, or decode-tree area) and one property it preserves that the flat version could not scale to.
10. Related Tutorials
Continue in RTL Design Patterns (sibling links ship in this batch; forward links unlock as they ship):
- Encoders — Chapter 1.3; the inverse of this page — collapsing a one-hot (or general) input back into a compact binary code, undoing the decode.
- Priority Encoders — Chapter 1.3; when more than one input is active, which one wins — the ordered cousin of the plain encoder, and the front-half of an arbiter.
- The Register Pattern (D-FF) — Chapter 2.1; where the decoder's one-hot output goes — a register's load-enable is one bit of a write-decode.
- Register Files — Chapter 2; the §1 write-select decoder built out in full, with read-port muxes consuming the same index.
- FSM Fundamentals — Chapter 4.1; one-hot state encoding is a decoder's output used as the state register — decode meets control.
Backward / method:
- Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the decoder's mirror image — the one-hot a decoder generates is exactly the select a one-hot mux consumes; together they are the two halves of routing.
- The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this page applied to the decoder (a decoder is pure control, no state).
- What Is an RTL Design Pattern? — Chapter 0.1; why the decoder 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 explicit decode form, and where
unique/defaultand one-hot completeness live. - Parameters — the
parameter/localparammachinery that makes the decoder generic (ADDR_W,OUT_W = 1 << ADDR_W). - Generate Blocks — the elaboration-time replication used to build a parameterized decode fan-out.
- Continuous Assignments — the
assign/ dataflow form of the shift-based decoder (en ? (1 << addr) : 0). - Blocking and Non-Blocking Assignments — why a combinational decoder uses blocking (
=) in itsalways @(*), and how that differs from the clocked patterns ahead.
11. Summary
- A decoder is one-hot generation, not a
casestatement. An n-bit code lights exactly one of2^noutputs;case,1 << addr, and VHDL'swith … selectare three notations for the same lighting board. When enabled the output is one-hot; when disabled it is all-zero — never two bits high. - It is the inverse of selection. A decoder expands a dense code into a sparse one-hot position; a mux collapses many sources onto one under an encoded select. The one-hot a decoder generates is exactly the select a one-hot mux consumes, and a decoder is a demultiplexer whose data input is a constant
1— the one-to-many mirror of the mux's many-to-one. - The enable makes silence possible. A pure decoder must assert one output for every code; the enable gives it an off-state, so a read cycle, an idle bus, or a deselected region can select nothing. Forgetting it is the signature failure of §7 — an always-on select that fires a spurious write on the cycle the design meant to be quiet.
- The one-hot invariant is the verification. Assert
$onehotwhen enabled,$onehot0when maybe-disabled, and never two bits high — this is what catches an overlapping address range or an out-of-range code aliasing onto a real line. Verify by exhaustively sweeping the address (enabled → one-hot, disabled → all-zero, out-of-range → quiet) with self-checking testbenches — shown here in SystemVerilog, Verilog, and VHDL. - The default-and-guard discipline keeps it correct — in every HDL. Default the output to all-zero, then assert exactly one bit only under the enable and an in-range guard (a leading zero-default in SystemVerilog/Verilog, a
(others => '0')in VHDL). Miss the enable and you get a spurious select; miss the range guard and an illegal address aliases onto a real line — the two bugs of §4c and §7. And when the address space is wide and sparse, decode hierarchically, matching the decoder's depth to the structure of the address map, not its raw width.