Binary to Gray Code Verilog Code
Gray code is an ordering of binary numbers where any two adjacent values differ in exactly one bit. That single-bit property is why it shows up in rotary encoders, Karnaugh maps, FSM state encoding, and clock-domain-crossing FIFOs. The good news for RTL designers is that converting plain binary to Gray code needs no adders and no state — a single XOR expression does it. This page gives you production-ready binary to gray code Verilog you can paste into a simulator today, a width-parameterized module, and a testbench that prints the full binary→gray mapping so you can eyeball the result.
📘 New to the concept? Read the theory first: Binary to Gray Code: the theory.
The one-line conversion rule
For an n-bit binary value bin, the Gray code is:
gray = bin ^ (bin >> 1);
The most-significant bit passes through unchanged (it is XORed with a shifted-in zero), and every other Gray bit is the XOR of two neighbouring binary bits: gray[i] = bin[i] ^ bin[i+1]. The right-shift lines bin[i+1] up under bin[i], so the whole vector converts in one combinational operation. No clock, no reset, purely wires.
Parameterized binary-to-gray converter + testbench
Here is the complete design. The module is parameterized on WIDTH so the same file works for a 4-bit encoder or a 32-bit FIFO pointer. The testbench sweeps every value and prints the mapping.
// bin2gray.v - parameterized binary-to-gray converter (combinational XOR)
module bin2gray #(
parameter WIDTH = 4
)(
input wire [WIDTH-1:0] bin,
output wire [WIDTH-1:0] gray
);
// Gray = Binary XOR (Binary >> 1)
assign gray = bin ^ (bin >> 1);
endmodule
// tb_bin2gray.v - sweep every 4-bit code and print the mapping
module tb_bin2gray;
localparam WIDTH = 4;
reg [WIDTH-1:0] bin;
wire [WIDTH-1:0] gray;
integer i;
bin2gray #(.WIDTH(WIDTH)) dut (.bin(bin), .gray(gray));
initial begin
$display("BIN -> GRAY (WIDTH=%0d)", WIDTH);
$display("---------------------------");
for (i = 0; i < (1 << WIDTH); i = i + 1) begin
bin = i[WIDTH-1:0];
#1; // let the combinational logic settle
$display("%b -> %b (%0d -> %0d)", bin, gray, bin, gray);
end
$finish;
end
endmoduleBIN -> GRAY (WIDTH=4) --------------------------- 0000 -> 0000 (0 -> 0) 0001 -> 0001 (1 -> 1) 0010 -> 0011 (2 -> 3) 0011 -> 0010 (3 -> 2) 0100 -> 0110 (4 -> 6) 0101 -> 0111 (5 -> 7) 0110 -> 0101 (6 -> 5) 0111 -> 0100 (7 -> 4) 1000 -> 1100 (8 -> 12) 1001 -> 1101 (9 -> 13) 1010 -> 1111 (10 -> 15) 1011 -> 1110 (11 -> 14) 1100 -> 1010 (12 -> 10) 1101 -> 1011 (13 -> 11) 1110 -> 1001 (14 -> 9) 1111 -> 1000 (15 -> 8) /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpou89u4_m/bin2gray.v:29: $finish called at 16 (1s)
Line-by-line walkthrough
module bin2gray #( parameter WIDTH = 4 )— the#(...)block declares a parameter with a default. A caller can override it per instance, so one source file scales to any bus width.input wire [WIDTH-1:0] bin/output wire [WIDTH-1:0] gray— both ports areWIDTHbits wide. The output is awirebecause it is driven by a continuous assignment, not a procedural block.assign gray = bin ^ (bin >> 1);— the entire converter.bin >> 1is a logical right shift that pads the vacated MSB with zero, and^is the bitwise XOR. This synthesizes to one XOR gate per bit.- In the testbench,
bin2gray #(.WIDTH(WIDTH)) dut (...)instantiates the design with named port connections — the clearest, least error-prone style. - The
forloop counts from 0 to2^WIDTH - 1.(1 << WIDTH)computes 16 for a 4-bit sweep;i[WIDTH-1:0]slices the integer down to the bus width before driving it. #1;advances simulation time by one unit so the combinational output settles before$displaysamples it. Without a delay the loop would race the assignment.$displaywith%bprints binary and%0dprints decimal with no padding.$finishends the simulation cleanly.
Build and run it with: iverilog -g2005 -o sim bin2gray.v && vvp sim. Notice in the log how codes 2 and 3 map to 3 and 2 — and that every row differs from its neighbour by a single bit.
Reusing the module at a different width
Because the width lives in a parameter, you reuse the exact same converter for an 8-bit value by overriding WIDTH at instantiation. This testbench checks a few landmark codes and confirms the single-bit-change property holds at the byte boundary.
// bin2gray.v - same converter, instantiated at WIDTH=8
module bin2gray #(
parameter WIDTH = 4
)(
input wire [WIDTH-1:0] bin,
output wire [WIDTH-1:0] gray
);
assign gray = bin ^ (bin >> 1);
endmodule
// tb_bin2gray_wide.v - override WIDTH to 8, check a few landmark values
module tb_bin2gray_wide;
localparam WIDTH = 8;
reg [WIDTH-1:0] bin;
wire [WIDTH-1:0] gray;
bin2gray #(.WIDTH(WIDTH)) dut (.bin(bin), .gray(gray));
task show;
input [WIDTH-1:0] value;
begin
bin = value;
#1;
$display("%3d : %b -> %b", bin, bin, gray);
end
endtask
initial begin
$display("8-bit binary -> gray (adjacent values differ by 1 bit)");
$display("-----------------------------------------------------");
show(8'd0);
show(8'd1);
show(8'd2);
show(8'd3);
show(8'd127);
show(8'd128);
show(8'd255);
$finish;
end
endmodule8-bit binary -> gray (adjacent values differ by 1 bit) ----------------------------------------------------- 0 : 00000000 -> 00000000 1 : 00000001 -> 00000001 2 : 00000010 -> 00000011 3 : 00000011 -> 00000010 127 : 01111111 -> 01000000 128 : 10000000 -> 11000000 255 : 11111111 -> 10000000 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpl6za7g0z/bin2gray_wide.v:38: $finish called at 7 (1s)
Look at the jump from 127 to 128 in the log: binary flips eight bits, but the Gray code changes only one. That is the whole reason Gray code is safe for asynchronous sampling.
Proving it with a round-trip check
A conversion is only trustworthy if it is reversible. Gray-to-binary is a running XOR: bin[i] = bin[i+1] ^ gray[i], starting from the MSB. The testbench below feeds binary through the encoder and back through the decoder, then flags any mismatch — a self-checking test you can drop into regression.
// bin2gray.v + gray2bin.v - converter and its inverse, used to self-check
module bin2gray #(
parameter WIDTH = 4
)(
input wire [WIDTH-1:0] bin,
output wire [WIDTH-1:0] gray
);
assign gray = bin ^ (bin >> 1);
endmodule
module gray2bin #(
parameter WIDTH = 4
)(
input wire [WIDTH-1:0] gray,
output reg [WIDTH-1:0] bin
);
integer k;
always @(*) begin
bin[WIDTH-1] = gray[WIDTH-1];
for (k = WIDTH-2; k >= 0; k = k - 1)
bin[k] = bin[k+1] ^ gray[k];
end
endmodule
// tb_roundtrip.v - bin -> gray -> bin must return the original value
module tb_roundtrip;
localparam WIDTH = 4;
reg [WIDTH-1:0] bin;
wire [WIDTH-1:0] gray;
wire [WIDTH-1:0] back;
integer i, errors;
bin2gray #(.WIDTH(WIDTH)) enc (.bin(bin), .gray(gray));
gray2bin #(.WIDTH(WIDTH)) dec (.gray(gray), .bin(back));
initial begin
errors = 0;
$display("BIN -> GRAY -> BIN status");
$display("----------------------------");
for (i = 0; i < (1 << WIDTH); i = i + 1) begin
bin = i[WIDTH-1:0];
#1;
if (back === bin)
$display("%b -> %b -> %b OK", bin, gray, back);
else begin
$display("%b -> %b -> %b MISMATCH", bin, gray, back);
errors = errors + 1;
end
end
$display("----------------------------");
$display("Round-trip errors: %0d", errors);
$finish;
end
endmoduleBIN -> GRAY -> BIN status ---------------------------- 0000 -> 0000 -> 0000 OK 0001 -> 0001 -> 0001 OK 0010 -> 0011 -> 0010 OK 0011 -> 0010 -> 0011 OK 0100 -> 0110 -> 0100 OK 0101 -> 0111 -> 0101 OK 0110 -> 0101 -> 0110 OK 0111 -> 0100 -> 0111 OK 1000 -> 1100 -> 1000 OK 1001 -> 1101 -> 1001 OK 1010 -> 1111 -> 1010 OK 1011 -> 1110 -> 1011 OK 1100 -> 1010 -> 1100 OK 1101 -> 1011 -> 1101 OK 1110 -> 1001 -> 1110 OK 1111 -> 1000 -> 1111 OK ---------------------------- Round-trip errors: 0 /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpdpuowzr5/roundtrip.v:52: $finish called at 16 (1s)
Common mistakes
- Using an arithmetic or signed shift. With unsigned
wire/regvectors,>>is a logical shift and pads with zero — exactly what you want. If you make the operandsigned,>>>would replicate the sign bit and corrupt the MSB. Keep the ports unsigned. - Driving a
wireoutput from analwaysblock (or anassignoutput declaredreg). The combinational version usesassign+wire; thegray2binloop usesalways @(*)+reg. Mixing them is a compile error. - Forgetting a settle delay in the testbench. Sampling the output in the same time step you drive the input can read the stale value. A
#1(or a clocked sample) fixes it. - Hard-coding the width. Writing
bin[3:0] ^ {1'b0, bin[3:1]}works but locks you to 4 bits. The parameterizedbin ^ (bin >> 1)is width-agnostic and far easier to maintain. - Confusing the two directions. Binary→Gray is a single XOR; Gray→Binary is a cumulative XOR chain. They are not the same expression.
Quick check
What single Verilog expression converts binary to Gray code?
gray = bin ^ (bin >> 1); — each Gray bit is the XOR of a binary bit and its left neighbour, with the MSB passing through unchanged.
Why does the MSB of the Gray code equal the MSB of the binary input?
The right shift pads the top position with 0, so gray[MSB] = bin[MSB] ^ 0 = bin[MSB]. The most-significant bit is never altered.
For 4-bit binary 0100 (decimal 4), what is the Gray code?
0110. Compute 0100 ^ 0010 = 0110 (decimal 6), which matches the simulation log.
Is the converter combinational or sequential, and what does that imply for synthesis?
Combinational — it is pure XOR logic with no clock or reset, so it synthesizes to one XOR gate per bit and introduces only gate-delay latency, no register stages.
Share your question in comments or talk to our mentor team for batch guidance.
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