Thermometer-to-Binary Encoder in Verilog with Code

A thermometer-to-binary encoder converts a thermometer code (a contiguous run of 1s starting from the LSB, like 00001111) into an ordinary unsigned binary number that equals the number of 1s. This is exactly the digital back-end of a flash ADC, where a bank of comparators produces one thermometer word per sample and an encoder turns it into the binary output code.

This page gives clean, synthesizable thermometer to binary verilog code: a parameterized population-count encoder that is self-checked against every valid thermometer code, and a second variant that also raises a bubble_err flag when the input is an illegal (non-monotonic) code. Both examples are complete, runnable Verilog-2005 files with self-checking testbends.

The clearest implementation simply counts the 1s on the bus. Because a valid thermometer code is a run of 1s from the LSB, the population count equals the position of the 0/1 boundary, which is the binary value. The width is parameterized by N, and the output is sized with $clog2(N+1) so it can represent 0..N.

module therm2bin #(parameter N = 8) (
  input  [N-1:0] therm,
  output reg [$clog2(N+1)-1:0] bin
);
  integer i;
  always @(*) begin
    bin = 0;
    for (i = 0; i < N; i = i + 1)
      bin = bin + therm[i];
  end
endmodule

module tb;
  localparam N = 8;
  reg  [N-1:0] therm;
  wire [$clog2(N+1)-1:0] bin;
  integer k;
  reg [31:0] expected;
  integer errors;

  therm2bin #(.N(N)) dut (.therm(therm), .bin(bin));

  initial begin
    errors = 0;
    // sweep every valid thermometer code: k ones from LSB, k=0..N
    for (k = 0; k <= N; k = k + 1) begin
      therm = (k == N) ? {N{1'b1}} : ((1 << k) - 1);
      expected = k;
      #1;
      if (bin !== expected[$clog2(N+1)-1:0]) begin
        $display("FAIL therm=%b expected=%0d got=%0d", therm, expected, bin);
        errors = errors + 1;
      end else begin
        $display("PASS therm=%b -> bin=%0d", therm, bin);
      end
    end
    if (errors == 0) $display("ALL TESTS PASSED");
    else $display("TOTAL FAILURES=%0d", errors);
    $finish;
  end
endmodule
PASS therm=00000000 -> bin=0
PASS therm=00000001 -> bin=1
PASS therm=00000011 -> bin=2
PASS therm=00000111 -> bin=3
PASS therm=00001111 -> bin=4
PASS therm=00011111 -> bin=5
PASS therm=00111111 -> bin=6
PASS therm=01111111 -> bin=7
PASS therm=11111111 -> bin=8
ALL TESTS PASSED

The testbench sweeps all nine valid 8-bit thermometer codes (0 through 8 ones) and compares the encoder output against the loop index, which is the independently known expected count. Every line reads PASS and the run ends with ALL TESTS PASSED, so the popcount encoder maps each valid code to the correct binary value.

Real flash-ADC comparators produce bubble errors: a 1 sitting above a 0 (for example 10001111), caused by comparator offset, noise, or metastability near the decision point. This variant still outputs the ones-count, but it also asserts bubble_err whenever the input is not a clean monotonic thermometer code. The testbench checks valid codes and four deliberately illegal codes against an independent golden model.

module therm2bin_bubble #(parameter N = 8) (
  input      [N-1:0] therm,
  output reg [$clog2(N+1)-1:0] bin,
  output reg         bubble_err
);
  integer i;
  always @(*) begin
    bin = 0;
    bubble_err = 1'b0;
    for (i = 0; i < N; i = i + 1)
      bin = bin + therm[i];
    // bubble: a 1 sitting above a 0 (therm[i]=0 but therm[i+1]=1)
    for (i = 0; i < N-1; i = i + 1)
      if (~therm[i] & therm[i+1])
        bubble_err = 1'b1;
  end
endmodule

module tb;
  localparam N = 8;
  reg  [N-1:0] therm;
  wire [$clog2(N+1)-1:0] bin;
  wire bubble;
  integer k, j;
  reg [31:0] exp_cnt;
  reg exp_err;
  integer errors;

  therm2bin_bubble #(.N(N)) dut (.therm(therm), .bin(bin), .bubble_err(bubble));

  // golden reference computed independently in the TB
  task ref_model;
    input [N-1:0] v;
    output [31:0] cnt;
    output err;
    integer m;
    begin
      cnt = 0; err = 0;
      for (m = 0; m < N; m = m + 1) cnt = cnt + v[m];
      for (m = 0; m < N-1; m = m + 1) if (~v[m] & v[m+1]) err = 1;
    end
  endtask

  initial begin
    errors = 0;
    // valid thermometer codes: expect no bubble error
    for (k = 0; k <= N; k = k + 1) begin
      therm = (k == N) ? {N{1'b1}} : ((1 << k) - 1);
      ref_model(therm, exp_cnt, exp_err);
      #1;
      if (bin !== exp_cnt[$clog2(N+1)-1:0] || bubble !== exp_err) begin
        $display("FAIL valid therm=%b exp(cnt=%0d,err=%0d) got(cnt=%0d,err=%0d)",
                 therm, exp_cnt, exp_err, bin, bubble);
        errors = errors + 1;
      end else begin
        $display("PASS therm=%b -> bin=%0d bubble=%0d", therm, bin, bubble);
      end
    end

    // deliberately illegal codes (bubbles) checked against golden model
    for (j = 0; j < 4; j = j + 1) begin
      case (j)
        0: therm = 8'b00000010; // 1 above a 0
        1: therm = 8'b10001111; // bubble in the middle
        2: therm = 8'b01010101; // alternating
        3: therm = 8'b11110001; // broken run
      endcase
      ref_model(therm, exp_cnt, exp_err);
      #1;
      if (bin !== exp_cnt[$clog2(N+1)-1:0] || bubble !== exp_err) begin
        $display("FAIL illegal therm=%b exp(cnt=%0d,err=%0d) got(cnt=%0d,err=%0d)",
                 therm, exp_cnt, exp_err, bin, bubble);
        errors = errors + 1;
      end else begin
        $display("PASS illegal therm=%b -> ones=%0d bubble=%0d (flagged)", therm, bin, bubble);
      end
    end

    if (errors == 0) $display("ALL TESTS PASSED");
    else $display("TOTAL FAILURES=%0d", errors);
    $finish;
  end
endmodule
PASS therm=00000000 -> bin=0 bubble=0
PASS therm=00000001 -> bin=1 bubble=0
PASS therm=00000011 -> bin=2 bubble=0
PASS therm=00000111 -> bin=3 bubble=0
PASS therm=00001111 -> bin=4 bubble=0
PASS therm=00011111 -> bin=5 bubble=0
PASS therm=00111111 -> bin=6 bubble=0
PASS therm=01111111 -> bin=7 bubble=0
PASS therm=11111111 -> bin=8 bubble=0
PASS illegal therm=00000010 -> ones=1 bubble=1 (flagged)
PASS illegal therm=10001111 -> ones=5 bubble=1 (flagged)
PASS illegal therm=01010101 -> ones=4 bubble=1 (flagged)
PASS illegal therm=11110001 -> ones=5 bubble=1 (flagged)
ALL TESTS PASSED

The log shows the nine valid codes passing with bubble=0, then four malformed inputs passing with bubble=1. Both the ones-count and the error flag are compared against an independent golden model in the testbench, so if the detector missed a bubble or miscounted, a FAIL would print. ALL TESTS PASSED confirms the encoder counts correctly and flags every illegal code.

How the encoder works

A thermometer code of width N has a simple invariant: the lowest bits are 1 and the highest bits are 0, with a single transition in between. The binary value you want is the index of that transition, which is identical to the number of 1s in the word. That is why a population count (popcount) over the bus is a correct and complete encoder for valid inputs.

The combinational loop bin = bin + therm[i] adds each bit once. For an 8-bit input the count ranges from 0 to 8, which needs 4 bits, so the output is declared [$clog2(N+1)-1:0]. Using $clog2(N+1) rather than $clog2(N) matters: with N=8 the maximum legal value is 8, and $clog2(8) is 3 bits (0..7), which would truncate the all-ones case.

Timing

Both modules are purely combinational (always @(*) with no clock). In a real flash ADC the thermometer word is first captured in comparator latches on the sampling clock; the encoder then settles within one clock period so the binary code is ready before the next sample. The critical path is the adder/priority chain, not the loop itself, since the for loop is unrolled at elaboration into parallel logic.

Flash-ADC context

A flash (or "direct-conversion") ADC is the fastest ADC architecture. An N-bit flash converter uses a resistor ladder to generate 2^N-1 reference voltages and one comparator per reference. For an input voltage, every comparator below the input reads 1 and every comparator above reads 0, producing a thermometer code of width 2^N-1. The thermometer-to-binary encoder on this page is the block that turns that comparator bus into the N-bit output word.

Flash ADCs trade area and power for speed: the comparator count grows exponentially with resolution, so flash is common at low resolution (4-8 bits) and very high sample rates, and as the sub-ADC inside pipelined and folding converters.

Thermometer vs one-hot vs binary

  • Thermometer code: a run of 1s from the LSB, e.g. 00001111. The value is the number of 1s. Used at flash-ADC comparator outputs because it changes only one bit per LSB step, which bounds glitch energy.
  • One-hot code: exactly one bit set, e.g. 00001000. The value is the position of the single 1. You can get a one-hot "edge" code by XORing adjacent thermometer bits (therm[i] ^ therm[i+1]), which is how some encoders locate the transition before a one-hot-to-binary step.
  • Binary code: the compact weighted representation this encoder produces.

A thermometer code is deliberately redundant: N+1 valid patterns out of 2^N, which is what lets you detect illegal codes in the first place.

Bubble errors and why they happen

A bubble is a 1 that appears above a 0 in the thermometer word, breaking the monotonic run (for example 10001111, where the MSB should be 0). Bubbles arise from comparator offset voltage, thermal and reference noise near the decision level, clock skew between comparators, and metastability when the input sits almost exactly at a reference. Near a code transition two adjacent comparators are both close to their trip point and can resolve in the wrong order.

A naive popcount encoder will still output a value for a bubbled code, but it may be wrong by one or more LSBs. Production flash ADCs therefore use bubble correction (for example majority-of-three voting on each comparator or Gray-code intermediate encoding) and often expose an error flag. The second example detects a bubble with the condition ~therm[i] & therm[i+1]: a 0 immediately below a 1. If that is true anywhere, the code is not a clean thermometer code and bubble_err is asserted.

Popcount implementation and synthesis notes

The accumulate-in-a-loop style shown here is the most readable, but it is worth knowing what a synthesizer does with it. The unrolled loop becomes a tree of adders; good tools balance it into a carry-save or compressor tree, so delay grows roughly logarithmically with N rather than linearly. For wide inputs you can also build the popcount explicitly as a 3:2 or 6:3 compressor tree, or defer to a vendor popcount primitive.

A few portability and synthesis points for this thermometer to binary verilog code:

  • Keep the output width at $clog2(N+1) so the maximum count N is representable; $clog2 is legal Verilog-2005.
  • always @(*) with a variable assigned on every path (both loops write before reads) infers pure combinational logic with no latch.
  • The bubble detector is cheap: N-1 two-input AND-OR terms feeding one OR reduction.

Common mistakes

  • Sizing the output as $clog2(N), which silently drops the all-ones (full-scale) code.
  • Assuming the encoder is correct for any input: a plain popcount does not equal the transition position once a bubble is present, which is why the detector or explicit bubble correction is needed.
  • Forgetting to initialize bin (and bubble_err) at the top of the always block, which creates an unintended latch and a non-combinational result.

FAQ

Is counting ones really the same as finding the 0/1 boundary? Yes, but only for valid thermometer codes, where all 1s are contiguous from the LSB. For arbitrary inputs the two differ, which is the whole point of bubble detection.

Does this scale to a 6-bit flash ADC? A 6-bit flash produces a 63-bit thermometer word (N=63); set the parameter accordingly. The output becomes $clog2(64) = 6 bits.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD