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 PASSEDThe 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 assertsbubble_errwhenever 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 endmodulePASS 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 PASSEDThe log shows the nine valid codes passing with
bubble=0, then four malformed inputs passing withbubble=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
Nhas 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 theforloop 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 andbubble_erris 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;$clog2is 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(andbubble_err) at the top of thealwaysblock, 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 LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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 Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres