top of page

Gray-Code Counter (4-bit):

Concept → RTL → Testbench


What is a Gray-code counter?

 

Why not just count in Gray directly?

 

Because binary adders are cheap and fast. The usual trick is:

​

  1. Keep an internal binary counter.
     

  2. Convert the next binary value to Gray and register it as the output.
     

  3. This keeps the datapath simple and still guarantees one-bit transitions at the output.

Design Goals (Problem Statement)

 

  • 4-bit Gray up-counter (reflected Gray sequence, 16 states)
     

  • Enable input (counts only when en=1)
     

  • Synchronous reset (clear state on the clock edge)
     

  • Implementation hint: Use a conversion function (also re-used in the testbench)

​

​

Refined RTL (synchronous reset, one-bit transitions)

verilog

// 4-bit Gray-code up-counter with enable and synchronous reset

module gray_counter (

  input  wire       clk,

  input  wire       rst,   // synchronous reset (active-high)

  input  wire       en,

  output reg  [3:0] Q

);

 

  reg [3:0] bin_count;

 

  // Binary -> Gray: g = b ^ (b >> 1)

  function [3:0] bin2gray(input [3:0] b);

    begin

      bin2gray = b ^ (b >> 1);

    end

  endfunction

 

  always @(posedge clk) begin

    if (rst) begin

      bin_count <= 4'b0000;

      Q         <= 4'b0000;          // Gray(0) = 0000

    end else if (en) begin

      bin_count <= bin_count + 1'b1;  // increment binary

      // Use (bin_count + 1) so Q shows the *new* Gray value this cycle

      Q         <= bin2gray(bin_count + 1'b1);

    end

    // else: hold bin_count and Q

  end

 

endmodule

Why this structure works

 

  • Synchronous reset matches the spec and simplifies timing closure.
     

  • We register Q with the Gray of the next binary value (because non-blocking assignments sample the old bin_count on the clock edge). This keeps the DUT’s output and a software model perfectly aligned.
     

  • The conversion is just b ^ (b >> 1): tiny logic, no adder needed on the Gray side.
     

Note: Your original code used @(posedge clk or posedge arst) (asynchronous reset). The spec asked for a synchronous reset, so I adjusted to @(posedge clk) and testbench uses rst.

​

Refined Testbench

Requirements met: uses the same bin2gray function; prints a tabular sheet with DUT vs expected and Pass/Fail per cycle, plus a summary.

 Verilog

`timescale 1ns/1ps

 

module tb_gray_counter;

 

  reg        clk = 0;

  reg        rst = 0;   // synchronous reset

  reg        en  = 0;

  wire [3:0] Q;

 

  gray_counter dut (.clk(clk), .rst(rst), .en(en), .Q(Q));

 

  // Clock: 10 ns period

  always #5 clk = ~clk;

 

  // Same function for expected model

  function [3:0] bin2gray(input [3:0] b);

    begin

      bin2gray = b ^ (b >> 1);

    end

  endfunction

 

  // Reference model state

  reg [3:0] exp_bin = 4'd0;

  reg [3:0] exp_gray = 4'd0;

 

  integer i;

  integer pass_cnt = 0, fail_cnt = 0;

  string  verdict;

 

  initial begin

    $display(" time | rst en | exp_bin |   DUT_Q  | expected |  match ");

    $display("------+--------+---------+----------+----------+--------");

 

    // Apply sync reset for one cycle, then enable

    rst = 1; en = 0; @(posedge clk);

    // After this rising edge, DUT and model reset

    rst = 0; en = 1;

 

    // Run 20 cycles of checks

    for (i = 0; i < 20; i = i + 1) begin

      // Advance to next rising edge

      @(posedge clk);

 

      // Software model: on same edge, if enabled, increment binary first

      if (rst) begin

        exp_bin  <= 4'd0;

        exp_gray <= bin2gray(4'd0);

      end else if (en) begin

        exp_bin  <= exp_bin + 1'b1;

        exp_gray <= bin2gray(exp_bin + 1'b1);

      end

 

      // Small delay to let Q update after the clock edge (simulation order)

      #1;

 

      // Compare and print a row

      verdict = (Q === exp_gray) ? "PASS" : "FAIL";

      if (verdict == "PASS") pass_cnt = pass_cnt + 1; else fail_cnt = fail_cnt + 1;

 

      $display("%5t |  %0b   %0b |   %04b   |   %04b   |   %04b   |  %s",

               $time, rst, en, exp_bin, Q, exp_gray, verdict);

    end

 

    $display("------+--------+---------+----------+----------+--------");

    $display("Summary: PASS=%0d, FAIL=%0d", pass_cnt, fail_cnt);

    $display("Test completed.");

    $finish;

  end

 

endmodule

Why this TB is correct (and robust)

​

  • It uses the same conversion function as the DUT to avoid duplication bugs.
     

  • The reference model increments exp_bin on the same rising edge the DUT does, then computes exp_gray = bin2gray(exp_bin + 1).
     

  • A tiny #1 after the edge ensures the DUT’s Q has updated before printing, avoiding racey prints.
     

  • You get a clear table with time, inputs, expected binary, DUT Gray, expected Gray, and a PASS/FAIL verdict per cycle + a summary.

​

What you’ll see in simulation

 

  • After the reset cycle, the table will show Q traversing the reflected Gray sequence (e.g., 0000, 0001, 0011, 0010, 0110, 0111, 0101, 0100, ...)—only one bit flips on each enabled tick.
     

  • With en=1 the whole time, it will cycle through all 16 Gray states. If you drop en=0, both the DUT and the model hold their states (easy to add to the TB).

​

Common pitfalls (and how we dodged them)

 

  • Off-by-one expected values: If you compute expected Gray from the old binary, you’ll be one step behind. We compute from bin + 1 to align with the DUT’s registered output.
     

  • Asynchronous reset mismatch: The spec asked for synchronous reset. The RTL and TB both use rst synchronized with clk.
     

  • Glitchy comparisons: We used a post-edge #1 and compared registered values—no combinational probes.

​

Handy variations

 

  • Down-Gray counter: decrement bin_count and derive Gray from (bin_count - 1).
     

  • Parameterized width: replace [3:0] with [WIDTH-1:0] and write a generic bin2gray that uses b ^ (b >> 1) for any width.​

​

Gray→Binary (if you need it elsewhere): cumulative XOR:

 Verilog

function automatic [3:0] gray2bin(input [3:0] g);

  integer k;

  begin

    gray2bin[3] = g[3];

    for (k=2; k>=0; k=k-1) gray2bin[k] = gray2bin[k+1] ^ g[k];

  end

endfunction

TL;DR

  • Maintain a binary counter; output the Gray of the next binary value.
     

  • Use a synchronous reset and enable.
     

  • Test with a simple reference model + a tabular PASS/FAIL printout.

Interview Questions

Q1) Why use Gray code in counters?

​

Refined:
Gray code guarantees unit-distance transitions—only one bit changes between successive states. That:

  • Reduces decode glitches/hazards vs. binary (where many bits can flip at once).
     

  • Lowers sampling errors when another clock domain (or mechanical sensor) reads the count mid-transition.
     

  • Is ideal for CDC pointers, rotary encoders, and low-noise sequencing.​

​

​​

Q2) How do you convert Gray → Binary?

​

Refined:
MSB is identical. Each lower binary bit is the XOR of the previous binary bit and the current Gray bit.

  • Formula:
     

    • bin[N-1] = gray[N-1]
       

    • bin[i] = bin[i+1] ^ gray[i]
       

verilog

​

function automatic [3:0] gray2bin(input [3:0] g);

  integer i;

  begin

    gray2bin[3] = g[3];

    for (i=2; i>=0; i=i-1) gray2bin[i] = gray2bin[i+1] ^ g[i];

  end

endfunction

 

Hardware is just a short XOR chain.

​​​

​

Q3) Does Gray code always reduce switching power?

​

Refined:
It reduces output switching for sequential count: one toggling bit → lower activity factor α (P≈αCV²f). But:

  • You often keep a binary counter inside and convert to Gray → internal toggling is unchanged.
     

  • Added XOR logic has some cost.
    Net savings depends on where the capacitance is (e.g., wide off-chip bus? Gray helps a lot).
    ​

​

​

Q4) How to implement Gray counters for arbitrary widths?

​

Refined:
Keep an internal binary counter; output gray = bin ^ (bin >> 1). Scales trivially to any width, synthesizes cleanly, and keeps arithmetic in easy binary.

verilog

​

Q <= (bin + 1) ^ ((bin + 1) >> 1); // on increment

 

Direct Gray-state machines are possible but get complex fast for large N.

​​​

​

Q5) How to detect sequence errors in Gray counters?

​

Refined:
Check the Hamming distance between consecutive states:

  • Error if popcount(Q ^ Q_prev) != 1.
    SystemVerilog assertion:

     

systemverilog

​

assert property (@(posedge clk) disable iff (rst)

  $onehot(Q ^ $past(Q)));

 

In plain RTL you can approximate $onehot(x) with (x!=0) && ((x & (x-1))==0).

​​​​

​

Q6) How to synthesize Binary→Gray conversion?

​

Refined:
Use XORs: gray = bin ^ (bin >> 1). Tools map this to a small XOR network—tiny area & delay.

verilog

​

function automatic [W-1:0] bin2gray(input [W-1:0] b);

  bin2gray = b ^ (b >> 1);

endfunction

​​

​

Q7) Can Gray code be used for arithmetic?

​

Refined:
Not directly. Gray lacks positional weights. Convert to binary, do math, then convert back to Gray if needed.

​​​

​​

Q8) How to design Gray-like circular sequences shorter than 2^N?

​

Refined:
Construct a custom Hamiltonian cycle on the N-dimensional hypercube over just the states you need (one-bit transitions only). Practically: pick a subset of the reflected Gray sequence with care so first/last also differ by one bit (cyclic property), or use a small LUT-driven FSM for the exact pattern.

​

​​

Q9) Pitfalls when sampling Gray outputs asynchronously?

​

Refined:
Gray reduces multi-bit ambiguity, but metastability still exists if captured near an edge.

  • Use two-FF synchronizers per bit into the receiving clock domain.
     

  • In async FIFOs, synchronize Gray-coded pointers and compare with Gray-safe logic.
     

  • Constrain CDC paths; review with CDC tools

​

​​​​​​​​

Q10) How to test Gray-code counter correctness?

​

Refined checklist:

  • Verify all 2^N states appear exactly once before wrap (or desired truncated set).
     

  • Check unit-distance: $onehot(Q ^ $past(Q)).
     

  • Round-trip: gray2bin(Q) matches the expected binary count.
     

  • Randomize en/resets; ensure holds don’t violate one-bit change property (they shouldn’t—no change at all).​

​

​

Q11) How are Gray counters used in rotary encoders?

​

Refined:
Mechanical encoders output Gray so only one channel changes per detent. That tolerates switch bounce/misalignment and avoids ambiguous “halfway” reads—improving position accuracy with simple decoding.

​

​​

Q12) How to minimize logic for Gray generation?

​

Refined:
Use the XOR form—that’s already minimal. Don’t over-engineer; no adders needed on the Gray side.

​

​​

Q13) How to make a Gray up/down counter?

​

Refined:
Keep an internal binary up/down register (bin <= bin ± 1) and drive output as Gray:

verilog

​

bin <= dir ? bin + 1'b1 : bin - 1'b1;

Q   <= bin2gray(dir ? bin + 1'b1 : bin - 1'b1);

 

Direct Gray up/down logic exists but is trickier and not worth it for general RTL.

​

​​

Q14) Why use Gray counters in ADC interfaces?

​

Refined:
When digital logic samples counters across domains (e.g., conversion timing, FIFO pointers), unit-distance updates cut transient misreads and decoding hazards. Still synchronize the bus; Gray just reduces simultaneous bit changes.

​

​​​

Q15) Metastability in async sampling—what is it, and how to prevent it?

​

Refined:
A flip-flop sampling during a data transition can enter an undefined (meta) state briefly.

  • Gray helps (fewer changing bits) but doesn’t eliminate meta.
     

  • Mitigation: two-stage synchronizers, meet setup/hold timing, add CDC constraints, and confirm with STA/CDC analysis. For multi-bit buses (like pointers), synchronize each bit and compare in Gray.

Handy snippets you can quote in interviews

Binary→Gray (any width):

 Verilog

function automatic [W-1:0] bin2gray(input [W-1:0] b);

  return b ^ (b >> 1);

endfunction

Gray→Binary (any width):

 Verilog

function automatic [W-1:0] gray2bin(input [W-1:0] g);

  integer i;

  begin

    gray2bin[W-1] = g[W-1];

    for (i=W-2; i>=0; i=i-1)

      gray2bin[i] = gray2bin[i+1] ^ g[i];

  end

endfunction

Unit-distance assertion (SystemVerilog):

 Verilog

assert property (@(posedge clk) disable iff (rst)

  $onehot(Q ^ $past(Q)));

4-bit Gray counter (enable + sync reset):

 Verilog

always @(posedge clk) begin

  if (rst) begin

    bin <= '0;

    Q   <= '0;           // Gray(0) = 0

  end else if (en) begin

    bin <= bin + 1'b1;

    Q   <= bin2gray(bin + 1'b1);

  end

end

Verilog LAB - Johnson Counter 

Verilog LAB - Shift Registers

© Copyright 2025 VLSI Mentor. All Rights Reserved.©

Connect with us

  • Instagram
  • Facebook
  • Twitter
  • LinkedIn
  • YouTube
bottom of page