Binary to BCD in Verilog: Double-Dabble Shift-and-Add-3
Converting a binary number into packed binary to BCD verilog code is a classic exam and interview problem, and the cleanest hardware solution is the double-dabble (also called shift-and-add-3) algorithm. It needs no dividers or lookup tables — just shifts and a conditional +3 correction per decimal digit.
Below we build an 8-bit converter two ways: a purely combinational bin2bcd that produces 3 BCD digits (hundreds, tens, ones) for inputs 0–255, and a clocked FSM that performs one shift per clock with a start/done handshake. Both come with deterministic, self-checking testbenches.
Combinational double-dabble converter
The algorithm scans the binary input from MSB to LSB. Before each left shift, any BCD nibble already holding a value ≥5 gets +3 added, so that after the shift it stays a valid decimal digit (0–9). After W shifts the 12-bit register holds three packed BCD digits.
// Combinational binary-to-BCD using double-dabble (shift-and-add-3)
module bin2bcd #(parameter W=8)(input [W-1:0] bin, output reg [11:0] bcd);
integer i, j;
reg [11:0] d; // 3 BCD nibbles: hundreds, tens, ones
always @(*) begin
d = 12'd0;
for (i = W-1; i >= 0; i = i - 1) begin
// add 3 to any nibble >= 5 BEFORE the shift
for (j = 0; j < 12; j = j + 4)
if (d[j +: 4] >= 5)
d[j +: 4] = d[j +: 4] + 4'd3;
// shift left, bring in next binary bit
d = {d[10:0], bin[i]};
end
bcd = d;
end
endmodule
module tb;
reg [7:0] bin;
wire [11:0] bcd;
wire [3:0] h = bcd[11:8];
wire [3:0] t = bcd[7:4];
wire [3:0] o = bcd[3:0];
integer errors;
bin2bcd #(.W(8)) dut(.bin(bin), .bcd(bcd));
task check;
input [7:0] v;
input [3:0] eh, et, eo;
begin
bin = v; #1;
$display("bin=%3d -> %0d%0d%0d (h=%0d t=%0d o=%0d)", v, h, t, o, h, t, o);
if (h !== eh || t !== et || o !== eo) begin
errors = errors + 1;
$display(" MISMATCH expected %0d%0d%0d", eh, et, eo);
end
end
endtask
initial begin
errors = 0;
check(8'd0, 0,0,0);
check(8'd9, 0,0,9);
check(8'd10, 0,1,0);
check(8'd99, 0,9,9);
check(8'd123, 1,2,3);
check(8'd255, 2,5,5);
if (errors == 0) $display("ALL PASS");
else $display("FAIL: %0d errors", errors);
$finish;
end
endmodule
bin= 0 -> 000 (h=0 t=0 o=0) bin= 9 -> 009 (h=0 t=0 o=9) bin= 10 -> 010 (h=0 t=1 o=0) bin= 99 -> 099 (h=0 t=9 o=9) bin=123 -> 123 (h=1 t=2 o=3) bin=255 -> 255 (h=2 t=5 o=5) ALL PASSThe sweep covers the corner values 0, 9, 10, 99, 123 and 255; every digit is asserted against its expected value, and the run ends with
ALL PASS.Clocked (iterative) version with start/done
For area-critical designs you process one bit per clock instead of unrolling all W iterations into combinational logic. A small counter tracks progress;
startloads the input anddonepulses when the 12-bit result is ready.// Iterative (clocked) double-dabble: one shift per clock, start/done handshake module bin2bcd_seq( input clk, input rst, input start, input [7:0] bin, output reg [11:0] bcd, output reg done ); reg busy; reg [3:0] count; // bits processed (0..8) reg [7:0] sreg; // remaining binary bits reg [11:0] d; integer j; always @(posedge clk) begin if (rst) begin busy <= 1'b0; done <= 1'b0; count <= 4'd0; d <= 12'd0; sreg <= 8'd0; bcd <= 12'd0; end else if (start && !busy) begin busy <= 1'b1; done <= 1'b0; count <= 4'd0; d <= 12'd0; sreg <= bin; end else if (busy) begin if (count == 4'd8) begin bcd <= d; done <= 1'b1; busy <= 1'b0; end else begin // add-3 to nibbles >= 5, then shift in MSB of sreg for (j = 0; j < 12; j = j + 4) if (d[j +: 4] >= 5) d[j +: 4] = d[j +: 4] + 4'd3; d <= {d[10:0], sreg[7]}; sreg <= {sreg[6:0], 1'b0}; count <= count + 4'd1; end end end endmodule module tb; reg clk, rst, start; reg [7:0] bin; wire [11:0] bcd; wire done; integer errors; bin2bcd_seq dut(.clk(clk), .rst(rst), .start(start), .bin(bin), .bcd(bcd), .done(done)); initial clk = 0; always #5 clk = ~clk; initial begin errors = 0; rst = 1; start = 0; bin = 8'd0; @(posedge clk); @(posedge clk); rst = 0; @(posedge clk); bin = 8'd255; start = 1; @(posedge clk); start = 0; wait (done); @(posedge clk); $display("bin=255 -> h=%0d t=%0d o=%0d", bcd[11:8], bcd[7:4], bcd[3:0]); if (bcd[11:8] !== 4'd2 || bcd[7:4] !== 4'd5 || bcd[3:0] !== 4'd5) begin errors = errors + 1; $display(" MISMATCH expected 2,5,5"); end if (errors == 0) $display("ALL PASS"); else $display("FAIL: %0d errors", errors); $finish; end endmodulebin=255 -> h=2 t=5 o=5
ALL PASSThe testbench resets, pulses
startwith input 255, waits fordone, then checks the result decodes to 2, 5, 5.How double-dabble works
The trick exploits a property of binary-coded decimal: a left shift multiplies a value by two, but a decimal digit can only hold 0–9. If a nibble is 5 or more, doubling it would overflow past 9 and corrupt the digit. Adding 3 before the shift pre-biases the nibble so that after the shift (a ×2) it lands on the correct value with a proper carry into the next decimal column. The scan runs MSB-first for W iterations, so an 8-bit input needs eight shift-and-correct steps and yields three BCD digits (hundreds, tens, ones) in a 12-bit register.
Synthesis and timing notes
The combinational version synthesizes to a chain of eight correction-and-shift stages. Each stage is a few comparators (nibble ≥5) and adders, so the critical path grows roughly linearly with input width — fine for 8-bit, but for wide inputs the long combinational delay may limit
fmax. The clocked version trades latency (W+2 cycles here) for a tiny, pipeline-friendly datapath that reuses one correction stage per clock, which is the better choice when area or timing closure matters. Note the blocking=inside the synchronousforloop: it computes the corrected value within the same clock evaluation before the non-blocking<=registers the shifted result.Common pitfalls
The most frequent bug is applying the +3 correction after the shift instead of before — that produces wrong digits for any value above 4. Another is sizing the BCD register too small: an 8-bit input reaches 255, which needs three digits (12 bits), not two. Also watch the loop direction; scanning LSB-first instead of MSB-first breaks the algorithm entirely. Finally, keep this Verilog-2005 clean: use
reg/wireandintegerloop variables, not SystemVeriloglogicoralways_comb, if your toolchain is locked to the 2005 standard.Interview angle
Interviewers love this problem because it separates candidates who memorize code from those who understand why the +3 works. Be ready to explain the pre-bias intuition, to state that W bits take W shift iterations, and to size the output as
ceil(W / 3.32)decimal digits. A strong follow-up answer contrasts the combinational unrolled design (low latency, long critical path) against the iterative FSM (small area, multi-cycle), and mentions that the same shift-and-add-3 structure maps directly to an FPGA shift register.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