Skip to content

RTL Design Patterns · Chapter 1 · Combinational Building Blocks

Encoders

An encoder is a decoder run backwards. It takes a bundle of one-hot lines, where exactly one is meant to be active, and compresses them into a compact binary index that names which line fired. You need it whenever one-active signals must become a small number, such as an interrupt controller reporting which source raised a request. The catch is that an encoder is only well defined when exactly one input is hot. Zero hot lines look identical to line zero being hot, and two hot lines silently OR their positions into a code that names neither. This lesson builds the encoder, explains why a valid output is essential, and shows the multi-hot bug a one-hot-only test never catches, in SystemVerilog, Verilog, and VHDL.

Foundation12 min readRTL Design PatternsEncoderOne-HotBinary IndexValid BitCombinational Logic

Chapter 1 · Section 1.3 · Combinational Building Blocks

1. The Engineering Problem

You are building the front end of a simple interrupt controller. Eight peripherals — a timer, a UART, two DMA channels, a GPIO block, and three others — each drive one request line into your module. When a peripheral needs service it raises its line, and (by the contract of this stage) at most one line is raised at a time. The CPU does not want eight wires; it wants a small number. Its interrupt-service routine reads a 3-bit register that says which source fired — 3'd0 for the timer, 3'd5 for the GPIO, and so on — and jumps to the matching handler.

So your job is a compression: take the 8-bit one-hot request bundle and produce the 3-bit binary index of the single line that is high. This is the inverse of the decoder from Section 1.2. A decoder took 3'd5 and lit up bit 5 of an 8-bit output; you now have bit 5 high and must recover 3'd5. The same shape recurs all over a chip: an arbiter emits a one-hot grant vector and you must log which master won as an ID; a one-hot FSM state must be written to a debug register as a state number; a priority resolver hands you a one-hot "winner" and you must name it. In every case the need is identical — many one-hot lines in, one compact binary index out.

There is a catch that the decoder never had, and it is the whole subtlety of this page. The decoder's input — a 3-bit number — was always meaningful; every one of its eight values named a real output. The encoder's input is an 8-bit vector, and only eight of its 256 possible values (the one-hot ones) are meaningful. The all-zeros case (no line high) has no index to return, and any multi-hot case (two or more lines high) has no single index either. A plain encoder that ignores this returns a confident, wrong number — and says nothing about the fact that it is wrong.

the-need.v — eight one-hot request lines, one 3-bit index out
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Eight peripherals, one request line each. By this stage's contract,
   // at MOST one is high at a time (a one-hot, or all-zero, bundle):
   wire [7:0] irq;                     // e.g. 8'b0010_0000 => source 5 is requesting
 
   // The CPU wants the NUMBER of the requesting source, not eight wires:
   wire [2:0] irq_id;                  // must become 3'd5 for the vector above
 
   // WRONG intuition — "just wire the bits": there is no single bit that IS
   //   the index; the index is a FUNCTION of WHICH bit is set. And what does
   //   irq_id mean when irq == 0 (nobody asked)? Or when TWO lines are high?
 
   // RIGHT — an ENCODER: compress the one-hot bundle to its binary index,
   //   AND a `valid` bit that says "some line really was high" (this page).

2. Mental Model

3. Pattern Anatomy

The encoder is built from one idea — an OR-tree over bit positions — wrapped in the one-hot contract and a valid output that makes the contract observable.

The OR-tree core. For a one-hot input, each output bit of the index is the OR of all input positions whose number has that bit set. Concretely, for an 8-to-3 encoder: output bit 0 is high when the hot line is at an odd position (in[1] | in[3] | in[5] | in[7]); output bit 1 when the hot line is at position 2, 3, 6, or 7; output bit 2 when it is at 4, 5, 6, or 7. Under the one-hot assumption exactly one of those terms is ever true per bit, so the OR simply "reads off" the binary index of the single set bit. That is the whole encoder: IDX_W OR-gates, each fed by half the input lines. It is combinational, stateless, and — crucially — contains no check that its input is legal.

The one-hot assumption — the load-bearing precondition. The OR-tree is a correct inverse of the decoder only when exactly one input bit is set. That assumption is not decoration; it is the definition of the encoder's domain, and it is why the same OR-tree that is perfectly correct on legal inputs produces confident nonsense on illegal ones.

Failure mode 1 — zero-hot (the ambiguous zero). When no input line is high, every OR term is 0, so the index is all-zeros. But all-zeros is also the legitimate index for "line 0 is hot." The bare encoder cannot tell these apart — the output 3'd0 is genuinely ambiguous. This is not a corner case you can wave away in an interrupt controller: "no interrupt pending" and "interrupt from source 0 pending" must not look identical.

Failure mode 2 — multi-hot (the OR-of-positions smear). When two lines are high — say positions 2 and 5 — the OR-tree ORs both their bit patterns together: 3'd2 | 3'd5 = 3'b010 | 3'b101 = 3'b111 = 3'd7. The output is 3'd7, an index that names input 7 — a line that was not even high. The encoder does not error, does not saturate, does not pick one: it silently smears two positions into a third, and returns a confident wrong number. This is the failure this page's DebugLab is built around, and it is exactly what a priority encoder (1.4) exists to remove.

Why the valid / any-hit output is essential. The fix for the ambiguous zero is a second output, valid = |in — the OR-reduction of the whole input. valid is high whenever any line is hot and low only for the all-zeros input. It converts the ambiguous 3'd0 into two distinguishable states: {valid=0, idx=0} means "nobody asked" and {valid=1, idx=0} means "line 0 asked." The index is only meaningful when valid is high — that is the encoder's output contract, and every consumer must honour it. (valid does not, by itself, fix multi-hot; two hot lines still make valid=1 with a smeared index. Distinguishing multi-hot is what the one-hot assumption or a priority encoder is for.)

The encoder — OR-tree inverse of the decoder, guarded by a valid bit

data flow
The encoder — OR-tree inverse of the decoder, guarded by a valid bitone-hot input2^n lines, (contract) exactly one hotOR-tree peroutput bitidx[k] = OR of positions with bit k setbinary indexn bits — WHICH line was hotvalid =OR-reduce(in)any-hit: 1 if some line hot, else 0zero-hotidx=0 AMBIGUOUS with line-0 → valid resolves itmulti-hotidx = OR of positions → names NEITHER input
The encoder inverts the decoder: an OR-tree reads the binary index of the single hot line off the one-hot input, and a parallel OR-reduction produces valid (any line hot). The two illegal domains hang off the index: zero-hot makes idx=0 ambiguous with line-0 (valid disambiguates it), and multi-hot ORs two positions into a code that names neither input (only the one-hot contract, or a priority encoder, resolves that). This is language-independent — the same OR-tree and OR-reduce exist in SystemVerilog, Verilog, and VHDL.

The anatomy closes on one point: an encoder is a conditionally-correct structure. Unlike the mux and decoder — total functions correct on every input — the encoder is a partial function made total only by (a) the one-hot input contract that the driver must honour, and (b) the valid output contract that the consumer must honour. Everything in §4 and §5 is about making both contracts explicit, checkable, and impossible to forget.

4. Real RTL Implementation

This is the core of the page. We build two patterns — (a) a parameterized binary encoder from a one-hot input, with a valid output, and (b) the multi-hot silent-smear 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: valid is a first-class output, computed as the OR-reduction of the input (valid = |one_hot_in), and the index is only meaningful when valid is high. The one-hot contract on the input is stated in a comment and asserted in the testbench — because the RTL cannot enforce it, verification must.

4a. Parameterized binary encoder from one-hot — the OR-tree, with a valid output

The everyday encoder: a WIDTH-line one-hot input compressed to an IDX_W-bit index, plus a valid any-hit output. The clean way to write the OR-tree in RTL is a loop that, for each set input position i, ORs i into the index — under the one-hot contract exactly one iteration contributes, so the accumulated OR is the index of the single hot line. The valid output is the OR-reduction of the whole input, which is one operator.

onehot_encoder.sv — parameterized one-hot → binary index, with valid
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module onehot_encoder #(
       parameter int WIDTH = 8,                       // number of one-hot input lines
       localparam int IDX_W = (WIDTH > 1) ? $clog2(WIDTH) : 1
   )(
       input  logic [WIDTH-1:0] one_hot_in,           // CONTRACT: exactly one bit high
       output logic [IDX_W-1:0] idx,                  // binary index of the hot line
       output logic             valid                 // any-hit: 1 if some line is high
   );
       // valid = OR-reduction of the whole input. It is what distinguishes
       // "nobody asked" (idx=0, valid=0) from "line 0 asked" (idx=0, valid=1).
       assign valid = |one_hot_in;                    // |  is the reduction-OR operator
 
       // OR-tree: for each set position i, OR its INDEX i into idx. Under the
       // one-hot contract exactly one i contributes, so the OR reads off the
       // index of the single hot line. Default 0 covers the zero-hot case.
       always_comb begin
           idx = '0;                                  // DEFAULT-ASSIGN FIRST → no latch
           for (int i = 0; i < WIDTH; i++)
               if (one_hot_in[i])
                   idx |= i[IDX_W-1:0];               // accumulate the position index
       end
   endmodule

The idx |= i line is the whole encoder: it is the OR-tree, expressed as an elaboration-unrolled loop. Under the one-hot contract exactly one one_hot_in[i] is true, so idx becomes that single i; the leading idx = '0 both keeps the block latch-free and defines the zero-hot output. The testbench drives every legal one-hot code (exhaustive: there are only WIDTH of them), checks idx against the position it set, and checks that valid tracks the input — including the zero-hot case where valid must deassert.

onehot_encoder_tb.sv — self-checking: each one-hot code, assert idx & valid
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module onehot_encoder_tb;
       localparam int WIDTH = 8;
       localparam int IDX_W = (WIDTH > 1) ? $clog2(WIDTH) : 1;
       logic [WIDTH-1:0] one_hot_in;
       logic [IDX_W-1:0] idx;
       logic             valid;
       int               errors = 0;
 
       onehot_encoder #(.WIDTH(WIDTH)) dut (.one_hot_in(one_hot_in), .idx(idx), .valid(valid));
 
       initial begin
           // EXHAUSTIVE over the legal one-hot inputs (only WIDTH of them).
           for (int i = 0; i < WIDTH; i++) begin
               one_hot_in = '0;
               one_hot_in[i] = 1'b1;                   // exactly-one-hot stimulus
               #1;
               assert ($onehot(one_hot_in));           // the input contract, checked
               assert (valid === 1'b1)
                   else begin $error("hot line %0d: valid=0", i); errors++; end
               assert (idx === i[IDX_W-1:0])           // index must name the hot line
                   else begin $error("hot line %0d: idx=%0d", i, idx); errors++; end
           end
           // Zero-hot corner: valid MUST deassert; idx is defined (0) but meaningless.
           one_hot_in = '0; #1;
           assert (valid === 1'b0)
               else begin $error("zero-hot: valid=%b (should be 0)", valid); errors++; end
           if (errors == 0) $display("PASS: encoder idx==position and valid==any-hit");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

The Verilog form is identical in intent; the differences are wire/reg typing, integer loop variables, and $display("FAIL") instead of assert. The valid is still a one-operator reduction-OR, and the OR-tree is the same idx = idx | i accumulation inside a combinational block.

onehot_encoder.v — one-hot → binary index in Verilog (reduction-OR valid + OR-tree)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module onehot_encoder #(
       parameter WIDTH = 8,
       parameter IDX_W = 3                             // = clog2(WIDTH); set by instantiator
   )(
       input  wire [WIDTH-1:0] one_hot_in,             // CONTRACT: exactly one bit high
       output wire             valid,                  // any-hit = reduction-OR of input
       output reg  [IDX_W-1:0] idx
   );
       integer i;
       // valid: 1 when any line is high — the disambiguator for the idx=0 case.
       assign valid = |one_hot_in;
 
       // OR-tree as a loop: OR each SET position's index into idx.
       always @(*) begin
           idx = {IDX_W{1'b0}};                        // DEFAULT FIRST → latch-free + zero-hot
           for (i = 0; i < WIDTH; i = i + 1)
               if (one_hot_in[i])
                   idx = idx | i[IDX_W-1:0];            // accumulate position index
       end
   endmodule
onehot_encoder_tb.v — self-checking Verilog testbench (compare idx & valid, print PASS/FAIL)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module onehot_encoder_tb;
       parameter WIDTH = 8;
       parameter IDX_W = 3;
       reg  [WIDTH-1:0] one_hot_in;
       wire [IDX_W-1:0] idx;
       wire             valid;
       integer          i, errors;
 
       onehot_encoder #(.WIDTH(WIDTH), .IDX_W(IDX_W)) dut
           (.one_hot_in(one_hot_in), .valid(valid), .idx(idx));
 
       initial begin
           errors = 0;
           for (i = 0; i < WIDTH; i = i + 1) begin      // EXHAUSTIVE over one-hot codes
               one_hot_in       = {WIDTH{1'b0}};
               one_hot_in[i]    = 1'b1;                  // exactly-one-hot stimulus
               #1;
               // Input-contract check: exactly one bit set.
               if (!((one_hot_in & (one_hot_in - 1)) == 0 && one_hot_in != 0))
                   $display("FAIL: stimulus not one-hot: %b", one_hot_in);
               if (valid !== 1'b1) begin
                   $display("FAIL: hot line %0d valid=0", i); errors = errors + 1;
               end
               if (idx !== i[IDX_W-1:0]) begin
                   $display("FAIL: hot line %0d idx=%0d", i, idx); errors = errors + 1;
               end
           end
           one_hot_in = {WIDTH{1'b0}}; #1;              // zero-hot: valid must deassert
           if (valid !== 1'b0) begin
               $display("FAIL: zero-hot valid=%b (should be 0)", valid); errors = errors + 1;
           end
           if (errors == 0) $display("PASS: onehot_encoder idx==position, valid==any-hit");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the valid is or_reduce(one_hot_in) from ieee.std_logic_misc (or a folded OR), and the OR-tree accumulates to_integer of each set position. The index is built in an integer/unsigned accumulator and assigned via to_unsigned, which is how VHDL turns a loop position into a binary code. The default (others => '0') covers zero-hot, exactly as the leading zero-assign does in SV/Verilog.

onehot_encoder.vhd — one-hot → binary index in VHDL (or_reduce valid + to_unsigned OR-tree)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   use ieee.std_logic_misc.all;                        -- for or_reduce
 
   entity onehot_encoder is
       generic ( WIDTH : positive := 8 );              -- generic == Verilog parameter
       port (
           one_hot_in : in  std_logic_vector(WIDTH-1 downto 0);  -- CONTRACT: exactly one bit high
           idx        : out std_logic_vector(natural(ceil(log2(real(WIDTH))))-1 downto 0);
           valid      : out std_logic                  -- any-hit: OR-reduce of the input
       );
   end entity;
 
   architecture rtl of onehot_encoder is
   begin
       -- valid is the OR-reduction of the whole input: 1 iff some line is high.
       valid <= or_reduce(one_hot_in);
 
       -- OR-tree: accumulate the INDEX of each set position. Under the one-hot
       -- contract exactly one iteration contributes, so acc becomes that index.
       process (all)
           variable acc : natural;
       begin
           acc := 0;                                    -- DEFAULT: zero-hot → 0
           for i in 0 to WIDTH-1 loop
               if one_hot_in(i) = '1' then
                   acc := acc or i;                     -- OR the position index in
               end if;
           end loop;
           idx <= std_logic_vector(to_unsigned(acc, idx'length));
       end process;
   end architecture;
onehot_encoder_tb.vhd — self-checking VHDL testbench (assert report severity on idx & valid)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity onehot_encoder_tb is
   end entity;
 
   architecture sim of onehot_encoder_tb is
       constant WIDTH : positive := 8;
       constant IDX_W : positive := 3;                  -- = clog2(WIDTH)
       signal one_hot_in : std_logic_vector(WIDTH-1 downto 0);
       signal idx        : std_logic_vector(IDX_W-1 downto 0);
       signal valid      : std_logic;
   begin
       dut : entity work.onehot_encoder
           generic map (WIDTH => WIDTH)
           port map (one_hot_in => one_hot_in, idx => idx, valid => valid);
 
       stim : process
       begin
           for i in 0 to WIDTH-1 loop                    -- EXHAUSTIVE over one-hot codes
               one_hot_in    <= (others => '0');
               one_hot_in(i) <= '1';                     -- exactly-one-hot stimulus
               wait for 1 ns;
               -- valid must be high, and idx must name the hot line.
               assert valid = '1'
                   report "encoder: valid=0 on a hot line" severity error;
               assert idx = std_logic_vector(to_unsigned(i, IDX_W))
                   report "encoder: idx does not match hot position" severity error;
           end loop;
           one_hot_in <= (others => '0');                -- zero-hot: valid must deassert
           wait for 1 ns;
           assert valid = '0'
               report "encoder: valid should be 0 when no line is hot" severity error;
           report "onehot_encoder self-check complete" severity note;
           wait;
       end process;
   end architecture;

4b. The multi-hot silent-smear bug — buggy vs fixed, in all three HDLs

Pattern (b) is the signature failure, shown here as buggy vs fixed RTL in each language, then dramatized narratively in §7. The bug is the same everywhere: the plain OR-tree encoder is deployed where the input is not guaranteed one-hot. On a one-hot input it is correct; the moment two lines assert, the OR-tree ORs their two position indices together and returns a third index that names neither line — silently, with no error and no flag. The fix is to make the one-hot contract real: either assert/assume exactly-one-hot (when the driver truly guarantees it) or, when it does not, upgrade to a priority encoder that is defined on multi-hot input by picking the highest-priority set line and computing its index — which is Chapter 1.4.

enc_bug.sv — BUGGY (plain OR-tree on non-one-hot input) vs FIXED (priority)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: plain OR-tree encoder used where TWO lines can be high at once.
   //        On req = 8'b0010_0100 (positions 2 and 5) it returns
   //        3'd2 | 3'd5 = 3'b111 = 3'd7 — an index that names line 7,
   //        which is NOT even asserted. Silent, confident, wrong.
   module req_encoder_bad #(
       parameter int WIDTH = 8,
       localparam int IDX_W = $clog2(WIDTH)
   )(
       input  logic [WIDTH-1:0] req,                   // NO one-hot guarantee here
       output logic [IDX_W-1:0] idx,
       output logic             valid
   );
       assign valid = |req;
       always_comb begin
           idx = '0;
           for (int i = 0; i < WIDTH; i++)
               if (req[i]) idx |= i[IDX_W-1:0];         // OR-of-positions => smear on multi-hot
       end
   endmodule
 
   // FIXED: a PRIORITY encoder — DEFINED on multi-hot input. It scans from the
   //        highest priority (here, lowest index wins) and returns the index of
   //        the FIRST set line, so two hot lines give a real, unambiguous index.
   module req_encoder_good #(
       parameter int WIDTH = 8,
       localparam int IDX_W = $clog2(WIDTH)
   )(
       input  logic [WIDTH-1:0] req,
       output logic [IDX_W-1:0] idx,
       output logic             valid
   );
       assign valid = |req;
       always_comb begin
           idx = '0;                                    // default (meaningful only if valid)
           for (int i = WIDTH-1; i >= 0; i--)           // scan high→low: lowest index wins
               if (req[i]) idx = i[IDX_W-1:0];          // LAST write wins => the lowest set i
       end
   endmodule
enc_bug_tb.sv — drive a MULTI-HOT input; the plain encoder returns a phantom index
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module enc_bug_tb;
       localparam int WIDTH = 8;
       localparam int IDX_W = $clog2(WIDTH);
       logic [WIDTH-1:0] req;
       logic [IDX_W-1:0] idx;
       logic             valid;
       int               errors = 0;
 
       // Bind to req_encoder_good to PASS; to _bad to observe the phantom index.
       req_encoder_good dut (.req(req), .idx(idx), .valid(valid));
 
       initial begin
           // Legal one-hot: both encoders agree (position 5).
           req = 8'b0010_0000; #1;
           assert (valid && idx === 3'd5)
               else begin $error("one-hot 5: idx=%0d valid=%b", idx, valid); errors++; end
 
           // MULTI-HOT: lines 2 AND 5 high. A PRIORITY encoder returns 3'd2 (lowest
           // set line). The plain OR-tree would return 3'd7 — line 7, not even set.
           req = 8'b0010_0100; #1;
           assert (valid && req[idx] === 1'b1)          // KEY: idx must name a line that IS set
               else begin $error("multi-hot: idx=%0d names an unset line", idx); errors++; end
           assert (idx === 3'd2)                        // priority: lowest index wins
               else begin $error("multi-hot: idx=%0d (expected 2)", idx); errors++; end
 
           if (errors == 0) $display("PASS: idx always names a genuinely-set line");
           else             $display("FAIL: %0d mismatches (phantom index on multi-hot)", errors);
           $finish;
       end
   endmodule

The Verilog buggy/fixed pair is the same story with reg outputs and always @(*). The decisive testbench check is req[idx] === 1'b1the returned index must name a line that is actually set. The plain OR-tree fails it on the very first multi-hot vector; the priority version passes.

enc_bug.v — BUGGY (OR-tree smear) vs FIXED (priority scan) in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // BUGGY: OR-tree on a non-one-hot input smears two positions together.
   module req_encoder_bad #(
       parameter WIDTH = 8, parameter IDX_W = 3
   )(
       input  wire [WIDTH-1:0] req,
       output wire             valid,
       output reg  [IDX_W-1:0] idx
   );
       integer i;
       assign valid = |req;
       always @(*) begin
           idx = {IDX_W{1'b0}};
           for (i = 0; i < WIDTH; i = i + 1)
               if (req[i]) idx = idx | i[IDX_W-1:0];    // OR-of-positions => multi-hot smear
       end
   endmodule
 
   // FIXED: priority scan (lowest index wins) — defined on multi-hot input.
   module req_encoder_good #(
       parameter WIDTH = 8, parameter IDX_W = 3
   )(
       input  wire [WIDTH-1:0] req,
       output wire             valid,
       output reg  [IDX_W-1:0] idx
   );
       integer i;
       assign valid = |req;
       always @(*) begin
           idx = {IDX_W{1'b0}};
           for (i = WIDTH-1; i >= 0; i = i - 1)         // high→low: last (lowest) set wins
               if (req[i]) idx = i[IDX_W-1:0];
       end
   endmodule
enc_bug_tb.v — self-checking; the KEY assertion is req[idx]==1 (idx names a set line)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   module enc_bug_tb;
       parameter WIDTH = 8, IDX_W = 3;
       reg  [WIDTH-1:0] req;
       wire [IDX_W-1:0] idx;
       wire             valid;
       integer          errors;
 
       // Bind to _good to PASS; to _bad to observe the phantom index on multi-hot.
       req_encoder_good dut (.req(req), .valid(valid), .idx(idx));
 
       initial begin
           errors = 0;
           req = 8'b0010_0000; #1;                      // one-hot line 5
           if (!(valid && idx === 3'd5)) begin
               $display("FAIL: one-hot 5 idx=%0d valid=%b", idx, valid); errors = errors + 1;
           end
           req = 8'b0010_0100; #1;                      // MULTI-HOT: lines 2 and 5
           if (req[idx] !== 1'b1) begin                 // idx must name a line that IS set
               $display("FAIL: multi-hot idx=%0d names an UNSET line", idx); errors = errors + 1;
           end
           if (idx !== 3'd2) begin                      // priority: lowest set index
               $display("FAIL: multi-hot idx=%0d (expected 2)", idx); errors = errors + 1;
           end
           if (errors == 0) $display("PASS: idx always names a genuinely-set line");
           else             $display("FAIL: %0d mismatches", errors);
           $finish;
       end
   endmodule

In VHDL the same story: the buggy architecture ORs to_integer-style position indices; the fixed one scans and keeps the lowest set index. The self-check reads req(to_integer(unsigned(idx))) and asserts it is '1' — the index must point at a line that is genuinely high.

enc_bug.vhd — BUGGY (OR-tree smear) vs FIXED (priority scan) in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
   use ieee.std_logic_misc.all;
 
   -- BUGGY: OR-tree on a non-one-hot input smears two positions.
   entity req_encoder_bad is
       generic ( WIDTH : positive := 8; IDX_W : positive := 3 );
       port (
           req   : in  std_logic_vector(WIDTH-1 downto 0);
           idx   : out std_logic_vector(IDX_W-1 downto 0);
           valid : out std_logic
       );
   end entity;
   architecture rtl of req_encoder_bad is
   begin
       valid <= or_reduce(req);
       process (all)
           variable acc : natural;
       begin
           acc := 0;
           for i in 0 to WIDTH-1 loop
               if req(i) = '1' then acc := acc or i; end if;  -- OR-of-positions => smear
           end loop;
           idx <= std_logic_vector(to_unsigned(acc, IDX_W));
       end process;
   end architecture;
 
   -- FIXED: priority scan — lowest set index wins; defined on multi-hot input.
   entity req_encoder_good is
       generic ( WIDTH : positive := 8; IDX_W : positive := 3 );
       port (
           req   : in  std_logic_vector(WIDTH-1 downto 0);
           idx   : out std_logic_vector(IDX_W-1 downto 0);
           valid : out std_logic
       );
   end entity;
   architecture rtl of req_encoder_good is
   begin
       valid <= or_reduce(req);
       process (all)
           variable sel : natural;
       begin
           sel := 0;                                          -- default (valid gates meaning)
           for i in WIDTH-1 downto 0 loop                     -- high→low: last (lowest) set wins
               if req(i) = '1' then sel := i; end if;
           end loop;
           idx <= std_logic_vector(to_unsigned(sel, IDX_W));
       end process;
   end architecture;
enc_bug_tb.vhd — self-checking; assert req(idx)='1' (index names a genuinely-set line)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;
 
   entity enc_bug_tb is
   end entity;
 
   architecture sim of enc_bug_tb is
       constant WIDTH : positive := 8;
       constant IDX_W : positive := 3;
       signal req   : std_logic_vector(WIDTH-1 downto 0);
       signal idx   : std_logic_vector(IDX_W-1 downto 0);
       signal valid : std_logic;
   begin
       -- Bind to req_encoder_good to PASS; to _bad to observe the phantom index.
       dut : entity work.req_encoder_good
           generic map (WIDTH => WIDTH, IDX_W => IDX_W)
           port map (req => req, idx => idx, valid => valid);
 
       stim : process
       begin
           req <= "00100000"; wait for 1 ns;                  -- one-hot line 5
           assert valid = '1' and idx = std_logic_vector(to_unsigned(5, IDX_W))
               report "one-hot 5: wrong idx/valid" severity error;
 
           req <= "00100100"; wait for 1 ns;                  -- MULTI-HOT: lines 2 and 5
           assert req(to_integer(unsigned(idx))) = '1'        -- idx must name a SET line
               report "multi-hot: idx names an unset line (phantom index)" severity error;
           assert idx = std_logic_vector(to_unsigned(2, IDX_W))
               report "multi-hot: expected lowest set index 2" severity error;
 
           report "enc_bug self-check complete" severity note;
           wait;
       end process;
   end architecture;

Across all three languages the lesson is one sentence: a plain OR-tree encoder is correct only under the one-hot contract; deploy it where two lines can assert and it silently returns an index that names neither — the fix is to assert one-hotness or, when you cannot, to use a priority encoder that is defined on multi-hot input.

5. Verification Strategy

An encoder is combinational, so its correctness is a truth table — but a conditional one: correctness is defined only over the one-hot inputs, so verification must both check the legal domain and pin down what happens outside it. The invariant that captures a correct encoder is:

For a one-hot input, idx is the position of the single set bit and valid is high; for the zero-hot input, valid is low; and idx never names a line that is not actually set.

Everything below is that invariant made checkable, mapped onto the testbenches in §4.

  • One-hot → right index (exhaustive, self-checking). Drive every legal one-hot code — there are only WIDTH of them, so this is complete, not sampled — and assert idx == position and valid == 1 each time. The three onehot_encoder testbenches in §4a do exactly this: SV assert (idx === i), Verilog if (idx !== i[...]) $display("FAIL…"), VHDL assert idx = to_unsigned(i,...) report … severity error. A for loop over the set position with expected = i proves the whole legal table.
  • valid deasserts on zero-hot. The one input state that carries no index must be observably distinct. Drive the all-zeros input and assert valid == 0 (the SV/Verilog/VHDL testbenches all add this corner). This is the check that would catch a valid accidentally tied to 1, which would make "nobody asked" masquerade as "line 0 asked."
  • Assume and assert the one-hot precondition. Because the OR-tree is correct only under one-hot input, verification must state that precondition on the stimulus (assert ($onehot(one_hot_in)) in SV; (v & (v-1))==0 && v!=0 in Verilog; a set-bit count in VHDL) — it documents the contract the driver owes and fails loudly if the testbench itself drives an illegal vector. The index-only-meaningful-when-valid output contract is the mirror: every consumer must gate on valid.
  • Multi-hot is undefined for a plain encoder — test the decision, don't ignore it. The plain OR-tree has no correct answer for two set lines; the §4b testbenches make the decision explicit by binding a priority encoder and asserting the one property that must survive multi-hot: req[idx] == 1 — the returned index always names a line that is genuinely set. The plain OR-tree fails that assertion (it returns a phantom index); the priority encoder passes it. This is exactly the motivation for Priority Encoders (1.4): they define the multi-hot case instead of smearing it.
  • Parameter-generic verification (WIDTH=4,8,16). A parameterized encoder must be verified generic, not assumed generic. Re-run the exhaustive one-hot sweep for WIDTH = 4, 8, 16 (and a non-power-of-two like WIDTH = 6, where IDX_W = 3 but two index codes are unreachable) and confirm idx and valid still hold. A width where WIDTH is not a power of two is where a sloppy IDX_W or an unguarded default first bites.
  • Expected waveform. Because an encoder has no clock, "correct" is immediate: as the hot line walks in[0] → in[1] → … → in[N-1], idx steps 0 → 1 → … → N-1 combinationally and valid stays high; drop the input to all-zeros and valid falls while idx returns to its default. The visual signature of the bug is a multi-hot input producing an idx that points at a line the waveform shows low — a phantom index.

6. Common Mistakes

Assuming one-hot input when it is not guaranteed. The signature encoder bug. The plain OR-tree encoder is a correct inverse of the decoder only on one-hot inputs; deploy it where two lines can assert at once and it ORs their two position indices into a third that names neither line — silently, with no error. It passes a one-hot-only testbench and corrupts in the field the first time two sources fire together. The fix is to make the contract real: either assert/assume exactly-one-hot (when the driver truly guarantees it) or upgrade to a priority encoder (§4b, 1.4) when it does not. (Full post-mortem in §7; buggy-vs-fixed RTL for all three HDLs in §4b.)

No valid output, so zero-hot ≡ "index 0". With a bare binary output, the all-zeros input produces idx = 0 — indistinguishable from "line 0 is hot." In an interrupt controller that means "no interrupt pending" and "interrupt from source 0" look identical, and the CPU services a phantom interrupt from source 0 whenever nothing is pending. Always emit valid = |in and make the index meaningful only when valid is high.

Using a plain encoder where a priority encoder is required. If your input can legitimately have multiple lines high — several interrupts pending, several requests to an arbiter — a plain encoder is the wrong structure, not a plain encoder with a bug. It has no defined answer for multi-hot input. The correct structure is a priority encoder: it is defined on multi-hot input because it picks the highest-priority set line and returns its index. Reaching for a plain encoder there is choosing a structure whose precondition your problem violates by design.

Masking the index with the raw position instead of ORing set positions. A subtle variant: writing idx = in[i] ? i : idx in the wrong direction, or ANDing where you meant to OR, produces an encoder that is correct for some inputs and silently wrong for others. The clean, contract-honest form is the OR-tree (idx |= i under one-hot) or an explicit priority scan; anything cleverer usually hides an unstated assumption.

Forgetting that a non-power-of-two width leaves unreachable index codes. For WIDTH = 6, IDX_W = 3 can express 0..7 but only 0..5 are reachable. That is fine, but if downstream logic assumes every 3-bit idx value is producible, or if the default path is sloppy, the two unreachable codes become a corner that a power-of-two test never exercises. Verify the odd widths (§5).

7. DebugLab

The interrupt that fired from a source that never asked — a plain encoder on multi-hot input

The engineering lesson: a plain encoder is a conditional inverse — correct only under a one-hot input contract its own logic cannot enforce — so deploying it where two lines can assert produces a confident index that names neither. The tell is a failure that appears only on simultaneous inputs and never in a one-hot-only sim: the untested domain is exactly the undefined one. Two habits make it impossible, identical across SystemVerilog, Verilog, and VHDL: verify the multi-hot input, not just the one-hot codes (assert the returned index names a genuinely-set line), and when the input can be multi-hot by design, reach for a priority encoder, not a plain one — which is the whole reason Chapter 1.4 exists.

8. Interview Q&A

9. Exercises

Design and reasoning exercises — no syntax drills. Each rehearses the "is the input really one-hot?" habit.

Exercise 1 — Predict the smear

An 8-to-3 plain OR-tree encoder receives in = 8'b1000_1000 (lines 3 and 7 both high). (a) Compute the idx it produces and show the OR arithmetic. (b) Does idx name a line that is actually high? (c) State the one property a testbench could assert that would flag this — and explain why a one-hot-only sweep never triggers it.

Exercise 2 — Zero-hot vs line-0

An engineer ships an 8-to-3 encoder with no valid output, arguing "the CPU can just check if idx == 0." Explain concretely why that is wrong for an interrupt controller: describe the two distinct system states that produce idx == 0, what goes wrong if the CPU cannot tell them apart, and the single output that fixes it. State the encoder's output contract in one sentence.

Exercise 3 — Plain or priority?

For each source of the input vector, decide whether a plain encoder (with an asserted one-hot contract) or a priority encoder is the correct structure, and justify in one line: (a) a one-hot-encoded FSM's state register, read for a debug ID; (b) eight peripheral interrupt lines that can fire independently; (c) a round-robin arbiter's one-hot grant vector; (d) a set of comparator flags where several can be true at once. (Hint: ask whether the driver guarantees mutual exclusion.)

Exercise 4 — Verify generic

You have a WIDTH-parameterized one-hot encoder that passes its exhaustive one-hot sweep at WIDTH = 8. Name two distinct things that could still be broken at WIDTH = 6 (a non-power-of-two) or WIDTH = 16, and for each state the specific check you would add. (Hint: think about IDX_W, unreachable index codes, and the default/zero-hot path.)

Continue in RTL Design Patterns (sibling and forward links unlock as they ship):

  • Priority Encoders — Chapter 1.4; the structure that removes this page's one-hot precondition — defined on multi-hot input by returning the highest-priority set line. The direct answer to the §7 bug.
  • Decoders — Chapter 1.2; the exact inverse of this page — index → one-hot, whose output is the one-hot vector the encoder consumes.
  • Multiplexers (2:1, N:1, one-hot) — Chapter 1.1; the one-hot-select mux consumes the same one-hot vectors an encoder compresses; selection and index-recovery are two sides of the one-hot world.
  • The Register Pattern (D-FF) — Chapter 2.1; where a registered idx/valid pair latches an encoder's result for a downstream consumer.

Backward / method:

  • The RTL Design Mindset — Chapter 0.2; the Interface → State → Datapath → Control → Verification lenses this page applied to the encoder (pure datapath, no state, with a load-bearing input contract).
  • What Is an RTL Design Pattern? — Chapter 0.1; why the encoder 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):

  • Reduction Operators — the |in reduction-OR that computes the valid/any-hit output in one operator.
  • Case Statements — the alternative case-table form of a small encoder, and where default and latch inference live.
  • Continuous Assignments — the assign / dataflow form used for the valid output and the small OR-tree.
  • Generate Blocks — the elaboration-time replication used to parameterize the OR-tree across WIDTH input lines.
  • Blocking and Non-Blocking Assignments — why the combinational encoder uses blocking (=) in its always @(*), and how that differs from the clocked patterns ahead.

11. Summary

  • An encoder is a decoder run backwards. It compresses a one-hot vector — exactly one of 2^n lines high — back into the n-bit binary index of the set line. It inverts precisely the map a decoder produces, using an OR-tree that reads the index off the single hot line.
  • It is a conditional inverse. Unlike the mux and decoder (total functions), the encoder is well-defined only on the one-hot domain. That one-hot precondition is a contract the encoder's own logic cannot enforce — whatever drives the input owes it, and verification must assume and assert it.
  • The two illegal domains bite differently. Zero-hot makes idx = 0 ambiguous with "line 0 is hot" — resolved by a valid = |in output that makes the index meaningful only when some line is high. Multi-hot ORs two position indices into a phantom third that names neither line — silently, with valid still high.
  • The signature bug is a plain encoder on non-one-hot input. It passes a one-hot-only testbench and smears in the field the first time two lines assert (§7). The verification that catches it drives a multi-hot input and asserts in[idx] == 1 — the returned index must name a genuinely-set line.
  • When the input can be multi-hot by design, the fix is a different structure, not a patched encoder — a priority encoder (Chapter 1.4), which is defined on multi-hot input because it returns the highest-priority set line. Built and self-check-verified here in SystemVerilog, Verilog, and VHDL, so the idea transfers across whatever HDL your team uses.