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:
​
-
Keep an internal binary counter.
-
Convert the next binary value to Gray and register it as the output.
-
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