Skip to content

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-need.v — a 5-bit index must become ONE write-enable, and only when writing
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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 $onehot0 property (one-hot-or-zero). This is the general invariant, because en low legitimately makes it zero-hot.
  • When always-enabled (en tied high): exactly one output bit is high — the $onehot property. 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 flow
The decoder family — one translation, two forms, one control inputn-bit code (addr)dense: a number, ADDR_W bitsenable (en)master switch: low forces all-zero2^n one-hot (y)sparse: one position, OUT_W bits1<<addr formy = en ? (1 << addr) : 0case /with-selectexplicit per-code arm + defaultdemux (add datad)route d instead of 1 → distributor
A decoder converts a dense n-bit code into a sparse 2^n one-hot, but only when the enable permits it — pull en low and the whole output goes zero-hot. The same behaviour has two spellings: the arithmetic shift form (one line: en gating a 1 shifted left by addr) and the explicit case / with-select form (one arm per code plus a default). Add a data input and route it in place of the constant 1 and the decoder becomes a demultiplexer — the one-to-many mirror of the mux's many-to-one. This is language-independent: the same code, enable, one-hot output, and two forms exist in SystemVerilog, Verilog, and VHDL.

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

decoder3to8.sv — the enable-gated 3→8 atom (default-zero, then one bit)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

decoder3to8_tb.sv — exhaustive addr sweep; assert $onehot when en, all-zero when !en
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

decoder3to8.v — the enable-gated 3→8 atom in Verilog (default-zero, then one bit)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
decoder3to8_tb.v — self-checking; one-hotness via (y & (y-1))==0
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

decoder3to8.vhd — the enable-gated 3→8 atom in VHDL (with-select + enable gate)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
decoder3to8_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 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.

decoder_param.sv — parameterized n→2^n, the 1<<addr shift form
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
   endmodule

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

decoder_param_case.sv — the same decoder, explicit case/unique-case form
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

decoder_param_tb.sv — exhaustive addr sweep; $onehot when en, all-zero when !en
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

decoder_param.v — parameterized n→2^n in Verilog (shift form, enable-gated)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
decoder_param_tb.v — self-checking Verilog decoder testbench (exhaustive addr)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

decoder_param.vhd — parameterized n→2^n in VHDL (generic width + integer index)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
decoder_param_tb.vhd — self-checking VHDL decoder 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 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.

regsel_bug.sv — BUGGY (ungated + aliasing) vs FIXED (enable + range guard)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
regsel_bug_tb.sv — read cycle must be quiet; out-of-range must be quiet
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

regsel_bug.v — BUGGY (ungated) vs FIXED (enable + range guard) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
regsel_bug_tb.v — self-checking; read cycle and out-of-range must stay quiet
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

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

regsel_bug.vhd — BUGGY (ungated) vs FIXED (enable + range guard) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
regsel_bug_tb.vhd — self-checking; assert quiet on reads and out-of-range
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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. When en is high, assert $onehot(dec_out)exactly one bit set — on every code; when en is low, assert $onehot0(dec_out)at most one, which for a correct decoder is zero. Never accept two bits high. The SV testbenches use assert ($onehot(...)) and assert ($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 0 to 2^ADDR_W - 1 with en high and assert dec_out === (1 << addr) each time — a for loop over addr with a reference model (expected = 1 << addr) proves the whole truth table, not a sampled corner. The three decoder_param testbenches in §4b do exactly this: SV assert (dec_out === (OUT_W'(1) << a)), Verilog if (dec_out !== exp) $display("FAIL…"), VHDL assert dec_out = exp report … severity error.
  • Enable behaviour (the deselected state). Re-run the same exhaustive address sweep with en low 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_W exceeds 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 asserts wsel === '0. Decide the illegal-code behaviour deliberately (quiet, or an X/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 at ADDR_W = 3 can still alias at a non-power-of-two target count if the range guard was sloppy — the leading dec_out = '0 plus the in_range guard is what makes it survive.
  • Expected waveform. Because a decoder has no clock, "correct" on a waveform is immediate: with en high, as addr steps 0 → 1 → 2 → …, the single hot bit of dec_out walks bit0 → bit1 → bit2 → … combinationally (within the propagation delay), and only one bit is ever high. Pull en low and the whole word drops to zero at once. A waveform where two bits are simultaneously high, or where a bit stays high after en goes 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.

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/default and one-hot completeness live.
  • Parameters — the parameter / localparam machinery 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 its always @(*), and how that differs from the clocked patterns ahead.

11. Summary

  • A decoder is one-hot generation, not a case statement. An n-bit code lights exactly one of 2^n outputs; case, 1 << addr, and VHDL's with … select are 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 $onehot when enabled, $onehot0 when 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.