Gray Code Counter in Verilog: Code, Testbench, Explained

A Gray code counter steps through a sequence where exactly one bit changes between consecutive values. That single-bit property makes it indispensable in digital design: it eliminates the decoding glitches that a plain binary counter produces when several bits flip at once, and it is the backbone of safe clock-domain-crossing (CDC) in asynchronous FIFOs. If you have ever seen a FIFO pointer synchronized across two clocks, you have almost certainly seen a Gray counter underneath.

This tutorial gives you working, runnable gray code counter Verilog code. We build a binary counter, convert its output to Gray with a one-line XOR, parameterize the width, and finish with a self-checking testbench that decodes Gray back to binary to prove the conversion is correct. Every example below is complete Verilog-2005 and compiles with iverilog -g2005 -o sim file.v && vvp sim.

What is a Gray code counter?

Instead of counting 000, 001, 010, 011 like binary, a 3-bit Gray sequence runs 000, 001, 011, 010, 110, 111, 101, 100 and then wraps back to 000. Notice that every transition, including the wrap-around from the last value to the first, flips a single bit. The cleanest way to build one in hardware is not to generate the Gray pattern directly. Instead you keep an ordinary binary counter as the state register and derive the Gray output combinationally. This keeps the next-state logic trivial (a simple increment) while still giving you a glitch-free coded output.

Binary-to-Gray conversion

The conversion from binary to Gray is a famous identity: each Gray bit is the XOR of the binary bit with the binary bit immediately above it, and the most significant bit passes through unchanged. In Verilog this collapses into a single right-shift and XOR, with zero-extension handling the top bit automatically.

assign gray = bin ^ (bin >> 1);

Because the shift pads with a zero on the left, the top Gray bit equals the top binary bit, which is exactly the rule we want. This expression is width-agnostic, so the same line works whether bin is 4 bits or 32.

A 4-bit Gray code counter with testbench

Here is the first complete design. The register bin increments on every rising clock edge, an active-low synchronous reset clears it, and the Gray output is produced by the assign statement. The testbench drives a clock, releases reset mid-cycle to avoid a sampling-edge race, and uses $display to print the binary count beside its Gray code for one full 16-count period.

// gray_counter_4bit.v -- 4-bit binary counter with binary->gray conversion
module gray_counter_4bit (
    input  wire       clk,
    input  wire       rst_n,    // active-low synchronous reset
    output reg  [3:0] bin,      // underlying binary count
    output wire [3:0] gray      // Gray-coded output
);
    // Count up in binary on every rising clock edge
    always @(posedge clk) begin
        if (!rst_n)
            bin <= 4'b0000;
        else
            bin <= bin + 4'b0001;
    end

    // Binary-to-Gray: gray = bin XOR (bin >> 1)
    assign gray = bin ^ (bin >> 1);
endmodule

// --- Testbench (last module) ---
module tb_gray_counter_4bit;
    reg        clk;
    reg        rst_n;
    wire [3:0] bin;
    wire [3:0] gray;
    integer    i;

    gray_counter_4bit dut (
        .clk(clk), .rst_n(rst_n), .bin(bin), .gray(gray)
    );

    // 10-time-unit clock period: rising edges at 5, 15, 25, ...
    initial clk = 1'b0;
    always #5 clk = ~clk;

    initial begin
        rst_n = 1'b0;              // hold reset over the first edge
        #12 rst_n = 1'b1;          // release mid-cycle, away from any edge
        $display("time  bin   gray");
        for (i = 0; i < 16; i = i + 1) begin
            @(posedge clk);
            #1 $display("%4t  %b  %b", $time, bin, gray);
        end
        $finish;
    end
endmodule
time  bin   gray
  16  0001  0001
  26  0010  0011
  36  0011  0010
  46  0100  0110
  56  0101  0111
  66  0110  0101
  76  0111  0100
  86  1000  1100
  96  1001  1101
 106  1010  1111
 116  1011  1110
 126  1100  1010
 136  1101  1011
 146  1110  1001
 156  1111  1000
 166  0000  0000
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpkvjk_0qs/gray_counter_4bit.v:44: $finish called at 166 (1s)

Read the log column by column and you will see the Gray output change by exactly one bit on each line, right through the wrap from 1111/1000 back to 0000.

Parameterized-width Gray code counter

Hardcoding a width is fine for a demo, but reusable IP should be parameterized. This version adds a WIDTH parameter and a count-enable input en. The reset and increment constants are built with Verilog replication operators so they scale to any width. The testbench instantiates a 3-bit counter and runs two full cycles to show the wrap-around.

// gray_counter.v -- parameterized-width binary counter + Gray conversion
module gray_counter #(
    parameter WIDTH = 4
) (
    input  wire              clk,
    input  wire              rst_n,
    input  wire              en,     // count enable
    output reg  [WIDTH-1:0]  bin,
    output wire [WIDTH-1:0]  gray
);
    always @(posedge clk) begin
        if (!rst_n)
            bin <= {WIDTH{1'b0}};
        else if (en)
            bin <= bin + {{(WIDTH-1){1'b0}}, 1'b1};
    end

    // Reflected binary Gray code
    assign gray = bin ^ (bin >> 1);
endmodule

// --- Testbench (last module) ---
module tb_gray_counter;
    localparam W = 3;              // instantiate a 3-bit counter
    reg          clk;
    reg          rst_n;
    reg          en;
    wire [W-1:0] bin;
    wire [W-1:0] gray;
    integer      i;

    gray_counter #(.WIDTH(W)) dut (
        .clk(clk), .rst_n(rst_n), .en(en), .bin(bin), .gray(gray)
    );

    initial clk = 1'b0;
    always #5 clk = ~clk;

    initial begin
        rst_n = 1'b0;
        en    = 1'b1;
        #12 rst_n = 1'b1;
        $display("WIDTH=%0d", W);
        $display("step  bin  gray");
        for (i = 0; i < 2*(1<<W); i = i + 1) begin
            @(posedge clk);
            #1 $display("%4d  %b  %b", i, bin, gray);
        end
        $finish;
    end
endmodule
WIDTH=3
step  bin  gray
   0  001  001
   1  010  011
   2  011  010
   3  100  110
   4  101  111
   5  110  101
   6  111  100
   7  000  000
   8  001  001
   9  010  011
  10  011  010
  11  100  110
  12  101  111
  13  110  101
  14  111  100
  15  000  000
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpuzqplaxy/gray_counter.v:49: $finish called at 166 (1s)

To build an 8-bit counter you change nothing but the override: gray_counter #(.WIDTH(8)) u0 (...). The single-bit increment constant {{(WIDTH-1){1'b0}}, 1'b1} is the idiomatic way to write a width-correct “1” and avoids the width-mismatch warnings you get from a bare literal.

Decoding Gray back to binary

A Gray counter is only half the story; a consumer usually needs the binary value back. Decoding reverses the rule: the MSB passes through, and each lower bit is the XOR of its Gray bit with the already-decoded bit above it. The module below instantiates the counter, decodes its Gray output, and the testbench is self-checking: it compares the decoded value against the reference binary count and prints OK or MISMATCH, then reports a total error count.

// gray_roundtrip.v -- Gray counter + Gray->binary decoder, self-checking
module gray_roundtrip #(
    parameter WIDTH = 4
) (
    input  wire              clk,
    input  wire              rst_n,
    output reg  [WIDTH-1:0]  bin,     // reference binary count
    output wire [WIDTH-1:0]  gray,    // Gray output
    output reg  [WIDTH-1:0]  decoded  // Gray decoded back to binary
);
    integer k;

    always @(posedge clk) begin
        if (!rst_n)
            bin <= {WIDTH{1'b0}};
        else
            bin <= bin + {{(WIDTH-1){1'b0}}, 1'b1};
    end

    // Encode: binary -> Gray
    assign gray = bin ^ (bin >> 1);

    // Decode: Gray -> binary. MSB passes through; each lower bit
    // is the XOR of the Gray bit with the decoded bit above it.
    always @(*) begin
        decoded[WIDTH-1] = gray[WIDTH-1];
        for (k = WIDTH-2; k >= 0; k = k - 1)
            decoded[k] = gray[k] ^ decoded[k+1];
    end
endmodule

// --- Testbench (last module) ---
module tb_gray_roundtrip;
    localparam W = 4;
    reg          clk;
    reg          rst_n;
    wire [W-1:0] bin;
    wire [W-1:0] gray;
    wire [W-1:0] decoded;
    integer      i;
    integer      errors;

    gray_roundtrip #(.WIDTH(W)) dut (
        .clk(clk), .rst_n(rst_n), .bin(bin), .gray(gray), .decoded(decoded)
    );

    initial clk = 1'b0;
    always #5 clk = ~clk;

    initial begin
        errors = 0;
        rst_n  = 1'b0;
        #12 rst_n = 1'b1;
        $display("step  bin   gray  decoded  check");
        for (i = 0; i < 16; i = i + 1) begin
            @(posedge clk);
            #1;
            if (decoded !== bin) errors = errors + 1;
            $display("%4d  %b  %b   %b     %s",
                     i, bin, gray, decoded,
                     (decoded === bin) ? "OK" : "MISMATCH");
        end
        $display("decode errors = %0d", errors);
        $finish;
    end
endmodule
step  bin   gray  decoded  check
   0  0001  0001   0001           OK
   1  0010  0011   0010           OK
   2  0011  0010   0011           OK
   3  0100  0110   0100           OK
   4  0101  0111   0101           OK
   5  0110  0101   0110           OK
   6  0111  0100   0111           OK
   7  1000  1100   1000           OK
   8  1001  1101   1001           OK
   9  1010  1111   1010           OK
  10  1011  1110   1011           OK
  11  1100  1010   1100           OK
  12  1101  1011   1101           OK
  13  1110  1001   1110           OK
  14  1111  1000   1111           OK
  15  0000  0000   0000           OK
decode errors = 0
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpdin_cmta/gray_roundtrip.v:64: $finish called at 166 (1s)

A zero error count is your proof that encode and decode are exact inverses across the whole range.

How the code works

Three ideas carry all three designs. First, the state is always plain binary; Gray is a view of that state, never the state itself. Second, the encoder is combinational (assign), so the Gray output tracks the register with no extra latency. Third, the decoder ripples from MSB down because each bit depends on the one above it. Keeping the register binary is what makes the counter easy to reason about and easy to extend, for example to an up/down counter or a FIFO pointer.

Common mistakes

  • Trying to increment the Gray value directly. There is no simple “+1” in Gray space. Always increment binary and re-encode. Attempting Gray-domain arithmetic leads to tangled, buggy logic.
  • Width mismatches on the increment. Writing bin + 1 against a sized vector can trigger lint warnings and surprising sign extension. Use a width-correct constant such as {{(WIDTH-1){1'b0}}, 1'b1}.
  • Releasing reset exactly on the clock edge. In simulation, changing rst_n at the same timestep as the sampling edge creates a race and can leave the register at X. Deassert reset mid-cycle, as the testbenches above do with #12.
  • Reading the counter across clock domains without synchronizing. The single-bit-change property makes Gray safe to synchronize, but you still need a multi-flop synchronizer in the receiving domain; the Gray code alone does not remove the need for it.
  • Using a blocking assignment for the register. The sequential bin must use non-blocking (<=) to model a flip-flop correctly; the combinational decode correctly uses blocking (=).

Quick check

Why does a Gray code counter change only one bit per step, and why does that matter?

Gray code is defined so consecutive values differ in a single bit position. This removes the multi-bit transition hazards of binary, which is critical when the value is sampled by another clock domain or decoded asynchronously, because only one bit can be mid-transition at any sampling instant.

What does the expression bin ^ (bin >> 1) compute?

It converts an unsigned binary value to its reflected Gray code. Each Gray bit becomes the XOR of the binary bit with the next higher binary bit; the right shift pads a zero on the left, so the MSB passes through unchanged.

In the parameterized design, why write the increment as {{(WIDTH-1){1'b0}}, 1'b1} instead of just 1?

It builds a constant of exactly WIDTH bits representing the value one, which keeps the addition width-correct for any WIDTH and avoids width-mismatch or sign-extension warnings from a bare literal.

How do you decode Gray back to binary, and why does the loop run from MSB to LSB?

The MSB of the binary equals the MSB of the Gray value; every lower binary bit is the XOR of its Gray bit with the binary bit immediately above it. Because each bit depends on the already-computed bit above it, the loop must proceed from the most significant bit downward.

Have questions about this topic?
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
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