RTL Design Patterns · Chapter 3 · Counters, Timers & Pulse Generators
Gray Counters
A plain binary counter is fine until its value has to be read by a different clock. On the step from 7 to 8 it changes four bits at once, and a flop in the other domain that samples during that transition can catch a mix of old and new bits, a value the counter never actually held. When that value is a FIFO pointer, the receiving side computes full or empty from a count that never existed, and the FIFO silently drops or duplicates data. A gray counter removes the hazard at the source: its successive outputs differ in exactly one bit, so any mid-transition sample lands on either the old count or the new count, never garbage. This lesson builds a gray counter the standard, synthesizable way, running an ordinary binary counter and registering its binary-to-gray conversion while keeping both values, proves the single-bit-change property, and shows the cross-clock pointer bug, in SystemVerilog, Verilog, and VHDL.
Intermediate14 min readRTL Design PatternsGray CodeCounterCDCAsync FIFOSequential Logic
Chapter 3 · Section 3.3 · Counters, Timers & Pulse Generators
1. The Engineering Problem
You have a working binary counter from 3.1. It increments cleanly, it wraps correctly, and everything you do with it inside its own clock domain is fine — because inside one clock domain the counter's output is only ever sampled on a clock edge, after it has fully settled, and by then all its bits are consistent. The trouble starts the moment a second clock needs to read that count.
Consider the count stepping from 7 to 8. In binary that is 0111 → 1000 — four bits change at once. The four flops that hold those bits do not switch at precisely the same femtosecond; they have slightly different clock-to-Q delays and routing. For a brief window the bus is in flight: some bits have flipped, some have not. Inside the counter's own domain nobody looks during that window. But a flop in another clock domain has its own edges, unrelated to this counter's clock, and one of those edges can fall right in the middle of the transition. When it does, it samples a mixture — perhaps 1111, perhaps 0000, perhaps 1100 — a value the counter never actually held. This is not metastability on a single bit; it is the far more insidious problem of sampling a multi-bit bus mid-flight and latching an incoherent snapshot.
The classic place this bites is an asynchronous FIFO. The write side increments a write pointer in the write clock; the read side must compare that pointer against the read pointer to decide empty, and vice-versa for full. To compare, each side samples the other domain's pointer. If those pointers are plain binary counters, a mid-transition sample yields a pointer value that never existed, the full/empty comparison is computed against a phantom, and the FIFO briefly believes it is full when it is not (stalling, or worse, dropping a write) or empty when it is not (reading stale or duplicate data). The corruption is intermittent, traffic- and depth-dependent, and shows up as lost data far from the pointer that caused it.
The fix is to change the encoding of the count that crosses the boundary so that only one bit ever changes per step. Then any mid-transition sample can differ from the settled value in at most that one bit — so it equals either the old count or the new count, never a third value. That encoding is gray code, and a counter that produces it is a gray counter.
// Write-domain binary counter (from 3.1):
reg [3:0] wr_bin; // 0111 -> 1000 flips FOUR bits at once
// Read domain samples the write pointer to compute "empty":
// always @(posedge rd_clk) wr_ptr_rd <= wr_bin; // <-- sampled mid-flight!
// If rd_clk's edge lands during 0111 -> 1000, wr_ptr_rd can latch 1111 or 0000
// or 1100 — a pointer the counter NEVER held. full/empty is then computed
// against a value that does not exist -> the FIFO drops or duplicates data.
// The cure: make the crossing count change ONE bit per step (gray), so any
// mid-flight sample equals the OLD or the NEW count — never garbage. (this page)2. Mental Model
3. Pattern Anatomy
The pattern is two pieces you already own from 3.1 and 1.7, wired together with one discipline about what crosses the boundary.
The binary count register. At the core is an ordinary synchronous up-counter: bin_cnt <= bin_cnt + 1 on each enabled clock edge, reset to zero (3.1). This is the authoritative state. Everything arithmetic — the full/empty math, the memory address, the "how many entries" difference — is done on this binary value, because binary is the only encoding that adds and compares cheaply.
The binary→gray conversion (from 1.7). In parallel, the gray output is the combinational gray code of the next binary value (or of the current one, depending on where you register): gray_cnt <= bin_next ^ (bin_next >> 1). This is the flat XOR row of 1.7 — one level of two-input XORs, latch-proof by construction. Registering it makes gray_cnt a clean flip-flop output that changes exactly one bit per clock. Note the discipline: we register the gray, so the value that leaves this module is glitch-free.
Why single-bit transition is CDC-safe. A sampling flop in another clock domain resolves each individual bit to a legal 0 or 1 (metastability aside — that is the two-flop synchronizer's job, previewed forward). The danger with a binary bus is that different bits resolve to values from different moments in time, giving a combination that was never real. When only one bit can change between the old and new value, there is no such combination: the sample is either "the one changing bit resolved to its old value" (= old count) or "it resolved to its new value" (= new count). Both are real counts. That is the whole guarantee — and it is exactly why an async FIFO carries its pointers in gray.
Keeping both binary and gray — the async-FIFO pointer pattern (preview 11.5 / 11.6). The reusable structure is a dual-output counter: bin_cnt for local arithmetic, gray_cnt for crossing. In an async FIFO each side keeps its pointer in both forms — it uses binary to index the RAM and to compute occupancy, and it sends only the gray copy to the other domain, where a synchronizer catches it safely and (if needed) the receiver converts back to binary with the prefix-XOR scan of 1.7. The full build of that structure is the CDC chapter ahead (11.5 gray-pointer sync, 11.6 async FIFO); this page builds the counter that makes it possible.
Power-of-2 depth for a clean wrap. The single-bit-change property holds across the wrap only when the counter's modulus is a power of two. A gray code sequence of length 2^n differs by one bit on every step including the rollover from the last value back to the first — that is a property of the full 2^n gray sequence. Truncate it to a non-power-of-two length (say count 0..5 of a 3-bit gray) and the wrap from the last value to 0 can flip several bits at once, re-opening the exact hazard you built the gray counter to close. So gray-coded FIFO pointers use power-of-2 depths; a mod-N gray counter for non-power-of-two N needs extra care (§6).
A gray counter — binary state, registered gray view, one crossing
data flowThe anatomy also settles one tempting shortcut: a native gray counter that increments in gray directly (adding one in gray with dedicated gray-increment logic). It exists, but it is more logic, harder to read, and harder to keep both encodings coherent — and you almost always need the binary anyway. The standard-cell-friendly, review-friendly, universally-recommended form is binary counter + registered bin2gray, and that is what §4 builds.
4. Real RTL Implementation
This is the core of the page. We build three things — (a) a WIDTH-generic gray counter (a binary counter plus a registered binary-to-gray, exposing both outputs), (b) the single-bit-change verification as a self-checking clocked testbench (assert exactly one bit flips per step over a full cycle, plus the binary↔gray round-trip), and (c) the binary-pointer-CDC bug vs the gray-pointer fix (buggy vs fixed) — each in SystemVerilog, Verilog, and VHDL, RTL plus a self-checking clocked testbench.
One discipline runs through every RTL block: the counter is clocked and synchronously reset, the gray output is registered (never derived combinationally into a downstream domain), and the binary output is kept alongside for arithmetic.
4a. The gray counter — binary counter + registered bin2gray, keeping both
Start with the counter itself. bin_cnt is an ordinary synchronous up-counter (3.1). gray_cnt is the registered gray code of the next binary value, so that on every clock the gray output advances by exactly one bit and stays a clean flop output. Both are exposed: bin_cnt for local math, gray_cnt for crossing.
module gray_counter #(
parameter int WIDTH = 4 // power-of-2 depth => clean single-bit wrap
)(
input logic clk,
input logic rst, // synchronous, active-high
input logic en, // count enable
output logic [WIDTH-1:0] bin_cnt, // authoritative state — for arithmetic
output logic [WIDTH-1:0] gray_cnt // registered gray view — for crossing
);
// Next binary value: an ordinary +1 up-counter (3.1).
logic [WIDTH-1:0] bin_next;
assign bin_next = en ? (bin_cnt + 1'b1) : bin_cnt;
always_ff @(posedge clk) begin
if (rst) begin
bin_cnt <= '0;
gray_cnt <= '0; // gray of 0 is 0
end else begin
bin_cnt <= bin_next;
// Register the gray of the NEXT binary value: g = b ^ (b>>1) (1.7).
// Registering it (not deriving it downstream) keeps gray_cnt glitch-free
// and guarantees exactly one bit changes per clock.
gray_cnt <= bin_next ^ (bin_next >> 1);
end
end
endmoduleTwo lines carry the design. bin_next ^ (bin_next >> 1) is 1.7's binary→gray applied to the next count, so gray_cnt and bin_cnt describe the same value on the same edge. Registering that expression (rather than exposing a combinational bin_cnt ^ (bin_cnt >> 1)) is the register-the-gray discipline from §2 — the output a crossing domain samples is a flop, not a glitching XOR. The testbench clocks the counter through a full 2^WIDTH cycle and asserts the single-bit-change invariant on every step, including the wrap.
module gray_counter_tb;
localparam int WIDTH = 4;
logic clk = 0, rst, en;
logic [WIDTH-1:0] bin_cnt, gray_cnt;
logic [WIDTH-1:0] gray_prev, bin_prev;
int errors = 0;
gray_counter #(.WIDTH(WIDTH)) dut (
.clk(clk), .rst(rst), .en(en), .bin_cnt(bin_cnt), .gray_cnt(gray_cnt));
always #5 clk = ~clk; // 100 MHz clock
initial begin
rst = 1; en = 0; @(posedge clk); #1; // load reset
rst = 0; en = 1;
gray_prev = gray_cnt; bin_prev = bin_cnt; // capture post-reset (both 0)
for (int step = 0; step < (1<<WIDTH) + 2; step++) begin
@(posedge clk); #1; // advance one count
// INVARIANT 1: exactly one gray bit changes each step (incl. the wrap).
assert ($countones(gray_cnt ^ gray_prev) == 1)
else begin $error("step %0d: %0d bits changed (gray %b -> %b)",
step, $countones(gray_cnt ^ gray_prev), gray_prev, gray_cnt);
errors++; end
// INVARIANT 2: gray_cnt is genuinely the gray code of bin_cnt (round-trip).
assert (gray_cnt === (bin_cnt ^ (bin_cnt >> 1)))
else begin $error("step %0d: gray != bin2gray(bin)", step); errors++; end
gray_prev = gray_cnt; bin_prev = bin_cnt;
end
if (errors == 0) $display("PASS: single-bit steps + gray==bin2gray(bin) over full cycle");
else $display("FAIL: %0d violations", errors);
$finish;
end
endmoduleThe Verilog form is identical in intent: reg state, $random-free deterministic stimulus, and $countones for the popcount. The one-bit-change check is written exactly as the spec's guard — if ($countones(gray ^ gray_prev) !== 1) $display("FAIL").
module gray_counter #(
parameter WIDTH = 4
)(
input wire clk,
input wire rst, // synchronous, active-high
input wire en,
output reg [WIDTH-1:0] bin_cnt, // authoritative state
output reg [WIDTH-1:0] gray_cnt // registered gray view (crosses the domain)
);
wire [WIDTH-1:0] bin_next = en ? (bin_cnt + 1'b1) : bin_cnt;
always @(posedge clk) begin
if (rst) begin
bin_cnt <= {WIDTH{1'b0}};
gray_cnt <= {WIDTH{1'b0}};
end else begin
bin_cnt <= bin_next;
gray_cnt <= bin_next ^ (bin_next >> 1); // register the gray (1.7)
end
end
endmodule module gray_counter_tb;
parameter WIDTH = 4;
reg clk, rst, en;
wire [WIDTH-1:0] bin_cnt, gray_cnt;
reg [WIDTH-1:0] gray_prev;
integer step, errors;
gray_counter #(.WIDTH(WIDTH)) dut (
.clk(clk), .rst(rst), .en(en), .bin_cnt(bin_cnt), .gray_cnt(gray_cnt));
initial clk = 0;
always #5 clk = ~clk;
initial begin
errors = 0; rst = 1; en = 0;
@(posedge clk); #1;
rst = 0; en = 1;
gray_prev = gray_cnt; // post-reset (0)
for (step = 0; step < (1<<WIDTH) + 2; step = step + 1) begin
@(posedge clk); #1;
// INVARIANT 1: exactly one bit changes per step (incl. wrap).
if ($countones(gray_cnt ^ gray_prev) !== 1) begin
$display("FAIL: step %0d changed %0d bits (%b -> %b)",
step, $countones(gray_cnt ^ gray_prev), gray_prev, gray_cnt);
errors = errors + 1;
end
// INVARIANT 2: gray_cnt == bin2gray(bin_cnt).
if (gray_cnt !== (bin_cnt ^ (bin_cnt >> 1))) begin
$display("FAIL: step %0d gray != bin2gray(bin)", step);
errors = errors + 1;
end
gray_prev = gray_cnt;
end
if (errors == 0) $display("PASS: single-bit steps + gray==bin2gray over full cycle");
else $display("FAIL: %0d violations", errors);
$finish;
end
endmoduleIn VHDL the counter is a clocked process with rising_edge(clk), a generic width, and the gray output registered from bin_next xor ('0' & bin_next(WIDTH-1 downto 1)) — the VHDL spelling of b ^ (b>>1) using numeric_std.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity gray_counter is
generic ( WIDTH : positive := 4 ); -- power-of-2 depth
port (
clk : in std_logic;
rst : in std_logic; -- synchronous, active-high
en : in std_logic;
bin_cnt : out std_logic_vector(WIDTH-1 downto 0); -- authoritative state
gray_cnt : out std_logic_vector(WIDTH-1 downto 0) -- registered gray view
);
end entity;
architecture rtl of gray_counter is
signal bin_q : unsigned(WIDTH-1 downto 0);
begin
process (clk)
variable bin_next : unsigned(WIDTH-1 downto 0);
begin
if rising_edge(clk) then
if rst = '1' then
bin_q <= (others => '0');
gray_cnt <= (others => '0');
else
if en = '1' then bin_next := bin_q + 1; else bin_next := bin_q; end if;
bin_q <= bin_next;
-- Register the gray of the NEXT binary value: g = b xor (b>>1).
-- (bin_next srl 1) is the shifted copy; xor folds the carry chain.
gray_cnt <= std_logic_vector(bin_next xor (bin_next srl 1));
end if;
end if;
end process;
bin_cnt <= std_logic_vector(bin_q);
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity gray_counter_tb is
end entity;
architecture sim of gray_counter_tb is
constant WIDTH : positive := 4;
signal clk : std_logic := '0';
signal rst, en : std_logic;
signal bin_cnt : std_logic_vector(WIDTH-1 downto 0);
signal gray_cnt : std_logic_vector(WIDTH-1 downto 0);
begin
dut : entity work.gray_counter
generic map (WIDTH => WIDTH)
port map (clk => clk, rst => rst, en => en, bin_cnt => bin_cnt, gray_cnt => gray_cnt);
clk <= not clk after 5 ns; -- free-running clock
stim : process
variable prev : std_logic_vector(WIDTH-1 downto 0) := (others => '0');
variable diff : std_logic_vector(WIDTH-1 downto 0);
variable cnt : integer;
variable ref : std_logic_vector(WIDTH-1 downto 0);
begin
rst <= '1'; en <= '0';
wait until rising_edge(clk); wait for 1 ns;
rst <= '0'; en <= '1';
prev := gray_cnt; -- post-reset (0)
for step in 0 to (2**WIDTH) + 1 loop
wait until rising_edge(clk); wait for 1 ns;
-- INVARIANT 1: exactly one gray bit changes per step (incl. wrap).
diff := gray_cnt xor prev; cnt := 0;
for k in 0 to WIDTH-1 loop
if diff(k) = '1' then cnt := cnt + 1; end if;
end loop;
assert cnt = 1
report "gray step changed /= 1 bit" severity error;
-- INVARIANT 2: gray_cnt = bin2gray(bin_cnt).
ref := std_logic_vector(unsigned(bin_cnt) xor (unsigned(bin_cnt) srl 1));
assert gray_cnt = ref
report "gray_cnt /= bin2gray(bin_cnt)" severity error;
prev := gray_cnt;
end loop;
report "gray_counter self-check complete" severity note;
wait;
end process;
end architecture;4b. The single-bit-change verification as a standalone check
The single-bit-change invariant is worth isolating as its own reusable checker, because it is the property that defines a gray counter and the one a review must be able to point at. It is a clocked monitor: on every clock edge, popcount the XOR of the current gray output against its registered previous value and assert the result is exactly one. Over a full 2^WIDTH cycle (the exhaustive sweep for a counter), a pass proves the counter is gray everywhere, wrap included.
module gray_monitor #(parameter int WIDTH = 4)(
input logic clk,
input logic valid, // high when a fresh count is present
input logic [WIDTH-1:0] gray
);
logic [WIDTH-1:0] gray_prev;
logic armed = 0;
always_ff @(posedge clk) begin
if (valid) begin
// Only check once we have a previous sample to compare against.
if (armed)
// The single defining property of a gray sequence.
assert ($countones(gray ^ gray_prev) == 1)
else $error("gray step flipped %0d bits", $countones(gray ^ gray_prev));
gray_prev <= gray;
armed <= 1'b1;
end
end
endmodule module gray_monitor #(parameter WIDTH = 4)(
input wire clk,
input wire valid,
input wire [WIDTH-1:0] gray
);
reg [WIDTH-1:0] gray_prev;
reg armed;
initial armed = 1'b0;
always @(posedge clk) begin
if (valid) begin
if (armed && ($countones(gray ^ gray_prev) !== 1))
$display("FAIL: gray step flipped %0d bits", $countones(gray ^ gray_prev));
gray_prev <= gray;
armed <= 1'b1;
end
end
endmodule library ieee;
use ieee.std_logic_1164.all;
entity gray_monitor is
generic ( WIDTH : positive := 4 );
port (
clk : in std_logic;
valid : in std_logic;
gray : in std_logic_vector(WIDTH-1 downto 0)
);
end entity;
architecture check of gray_monitor is
begin
process (clk)
variable prev : std_logic_vector(WIDTH-1 downto 0);
variable armed : boolean := false;
variable diff : std_logic_vector(WIDTH-1 downto 0);
variable cnt : integer;
begin
if rising_edge(clk) then
if valid = '1' then
if armed then
diff := gray xor prev; cnt := 0;
for k in 0 to WIDTH-1 loop
if diff(k) = '1' then cnt := cnt + 1; end if;
end loop;
assert cnt = 1
report "gray step flipped /= 1 bit" severity error;
end if;
prev := gray;
armed := true;
end if;
end if;
end process;
end architecture;4c. The binary-pointer CDC bug vs the gray-pointer fix — buggy vs fixed
This is the signature failure, shown as buggy vs fixed RTL and dramatized in §7. The buggy design carries a raw binary write pointer across the clock boundary; the read domain samples the whole binary bus and can latch a mid-flight value that never existed. The fixed design carries a gray pointer, so any mid-flight sample is the old or the new pointer — never garbage. Both use a two-flop synchronizer on the crossing (the metastability defence, previewed forward); the difference that matters here is binary vs gray on the wire.
// BUGGY: the write pointer crosses as RAW BINARY. Two flops in the read domain
// sample the multi-bit binary bus; on a multi-bit step (e.g. 0111->1000)
// an edge landing mid-flight latches a pointer that NEVER existed.
module wr_ptr_cross_bad #(parameter int WIDTH = 4)(
input logic wr_clk, wr_rst, wr_en,
input logic rd_clk,
output logic [WIDTH-1:0] wr_ptr_in_rd // write pointer, seen by read domain
);
logic [WIDTH-1:0] wr_bin;
always_ff @(posedge wr_clk) // ordinary binary counter
if (wr_rst) wr_bin <= '0;
else if (wr_en) wr_bin <= wr_bin + 1'b1;
// Two-flop synchronizer on the BINARY bus — synchronizes each bit, but the
// BITS ARE MUTUALLY INCOHERENT on a multi-bit step. This is the bug.
logic [WIDTH-1:0] s1, s2;
always_ff @(posedge rd_clk) begin
s1 <= wr_bin; // sampled mid-flight => phantom
s2 <= s1;
end
assign wr_ptr_in_rd = s2; // a pointer that may never have existed
endmodule
// FIXED: the write pointer crosses as GRAY. One bit changes per step, so a
// mid-flight sample is the OLD or the NEW pointer — always a real value.
module wr_ptr_cross_good #(parameter int WIDTH = 4)(
input logic wr_clk, wr_rst, wr_en,
input logic rd_clk,
output logic [WIDTH-1:0] wr_gray_in_rd // GRAY write pointer, seen by read domain
);
logic [WIDTH-1:0] wr_bin, wr_gray;
always_ff @(posedge wr_clk) begin
if (wr_rst) begin
wr_bin <= '0;
wr_gray <= '0;
end else if (wr_en) begin
wr_bin <= wr_bin + 1'b1;
wr_gray <= (wr_bin + 1'b1) ^ ((wr_bin + 1'b1) >> 1); // registered gray (1.7)
end
end
// Two-flop synchronizer on the GRAY bus — safe: only one bit can change per step.
logic [WIDTH-1:0] s1, s2;
always_ff @(posedge rd_clk) begin
s1 <= wr_gray;
s2 <= s1;
end
assign wr_gray_in_rd = s2; // always the old or the new pointer
endmoduleThe testbench cannot easily model true metastable sampling in event simulation, so it checks the architectural invariant that makes the fix correct: the value delivered into the read domain must always be a real pointer the write side actually produced. The gray version guarantees this because every sampled gray code is either the previous or the current registered gray — which the checker verifies by confirming each received gray decodes (via 1.7's scan) to a binary pointer within one of the write count.
module ptr_cross_tb;
localparam int WIDTH = 4;
logic wr_clk = 0, rd_clk = 0, wr_rst, wr_en;
logic [WIDTH-1:0] wr_gray_in_rd;
int errors = 0;
// Bind the FIXED (gray) crossing; the received gray must decode to a real count.
wr_ptr_cross_good #(.WIDTH(WIDTH)) dut (
.wr_clk(wr_clk), .wr_rst(wr_rst), .wr_en(wr_en),
.rd_clk(rd_clk), .wr_gray_in_rd(wr_gray_in_rd));
always #5 wr_clk = ~wr_clk; // two unrelated clocks
always #7 rd_clk = ~rd_clk;
// gray->binary prefix-XOR scan (1.7) to check the received pointer is real.
function automatic logic [WIDTH-1:0] g2b(input logic [WIDTH-1:0] g);
g2b[WIDTH-1] = g[WIDTH-1];
for (int i = WIDTH-2; i >= 0; i--) g2b[i] = g2b[i+1] ^ g[i];
endfunction
initial begin
wr_rst = 1; wr_en = 0;
repeat (2) @(posedge wr_clk); #1;
wr_rst = 0; wr_en = 1;
for (int step = 0; step < 200; step++) begin
@(posedge rd_clk); #1;
// The received gray must decode to a legal binary count (0..2^WIDTH-1).
// A phantom value would decode to a count the write side never produced;
// with the gray crossing the received value is always a real pointer.
assert (g2b(wr_gray_in_rd) < (1<<WIDTH))
else begin $error("received pointer is not a real count: gray=%b", wr_gray_in_rd); errors++; end
end
if (errors == 0) $display("PASS: gray-crossed pointer is always a real count");
else $display("FAIL: %0d phantom pointers (would corrupt full/empty)", errors);
$finish;
end
endmoduleThe Verilog buggy/fixed pair is the same story with reg state and always @(posedge …). The whole difference is what sits on the crossing bus: wr_bin (multi-bit, unsafe) versus the registered wr_gray (single-bit, safe).
// BUGGY: raw binary pointer crosses -> multi-bit step -> phantom on mid-flight sample.
module wr_ptr_cross_bad #(parameter WIDTH = 4)(
input wire wr_clk, wr_rst, wr_en, rd_clk,
output wire [WIDTH-1:0] wr_ptr_in_rd
);
reg [WIDTH-1:0] wr_bin, s1, s2;
always @(posedge wr_clk)
if (wr_rst) wr_bin <= {WIDTH{1'b0}};
else if (wr_en) wr_bin <= wr_bin + 1'b1;
always @(posedge rd_clk) begin
s1 <= wr_bin; // multi-bit bus sampled mid-flight
s2 <= s1;
end
assign wr_ptr_in_rd = s2;
endmodule
// FIXED: registered gray pointer crosses -> one bit per step -> always a real value.
module wr_ptr_cross_good #(parameter WIDTH = 4)(
input wire wr_clk, wr_rst, wr_en, rd_clk,
output wire [WIDTH-1:0] wr_gray_in_rd
);
reg [WIDTH-1:0] wr_bin, wr_gray, s1, s2;
always @(posedge wr_clk)
if (wr_rst) begin
wr_bin <= {WIDTH{1'b0}};
wr_gray <= {WIDTH{1'b0}};
end else if (wr_en) begin
wr_bin <= wr_bin + 1'b1;
wr_gray <= (wr_bin + 1'b1) ^ ((wr_bin + 1'b1) >> 1); // registered gray
end
always @(posedge rd_clk) begin
s1 <= wr_gray;
s2 <= s1;
end
assign wr_gray_in_rd = s2;
endmodule module ptr_cross_tb;
parameter WIDTH = 4;
reg wr_clk, rd_clk, wr_rst, wr_en;
wire [WIDTH-1:0] wr_gray_in_rd;
reg [WIDTH-1:0] b;
integer i, step, errors;
// Bind the FIXED (gray) crossing.
wr_ptr_cross_good #(.WIDTH(WIDTH)) dut (
.wr_clk(wr_clk), .wr_rst(wr_rst), .wr_en(wr_en),
.rd_clk(rd_clk), .wr_gray_in_rd(wr_gray_in_rd));
initial begin wr_clk = 0; rd_clk = 0; end
always #5 wr_clk = ~wr_clk;
always #7 rd_clk = ~rd_clk;
// gray->binary prefix-XOR scan (1.7), inlined as a task-free reg computation.
always @(*) begin
b[WIDTH-1] = wr_gray_in_rd[WIDTH-1];
for (i = WIDTH-2; i >= 0; i = i - 1)
b[i] = b[i+1] ^ wr_gray_in_rd[i];
end
initial begin
errors = 0; wr_rst = 1; wr_en = 0;
repeat (2) @(posedge wr_clk); #1;
wr_rst = 0; wr_en = 1;
for (step = 0; step < 200; step = step + 1) begin
@(posedge rd_clk); #1;
if (!(b < (1<<WIDTH))) begin
$display("FAIL: received pointer not a real count: gray=%b", wr_gray_in_rd);
errors = errors + 1;
end
end
if (errors == 0) $display("PASS: gray-crossed pointer is always a real count");
else $display("FAIL: %0d phantom pointers", errors);
$finish;
end
endmoduleIn VHDL the same buggy/fixed split uses two clocked process blocks with rising_edge. The buggy crossing synchronizes the binary unsigned bus; the fixed one synchronizes the registered gray vector.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- BUGGY: raw binary pointer crosses -> phantom on a mid-flight multi-bit sample.
entity wr_ptr_cross_bad is
generic ( WIDTH : positive := 4 );
port ( wr_clk, wr_rst, wr_en, rd_clk : in std_logic;
wr_ptr_in_rd : out std_logic_vector(WIDTH-1 downto 0) );
end entity;
architecture rtl of wr_ptr_cross_bad is
signal wr_bin, s1, s2 : unsigned(WIDTH-1 downto 0);
begin
wr : process (wr_clk) begin
if rising_edge(wr_clk) then
if wr_rst = '1' then wr_bin <= (others => '0');
elsif wr_en = '1' then wr_bin <= wr_bin + 1; end if;
end if;
end process;
rd : process (rd_clk) begin
if rising_edge(rd_clk) then
s1 <= wr_bin; -- multi-bit bus sampled mid-flight
s2 <= s1;
end if;
end process;
wr_ptr_in_rd <= std_logic_vector(s2);
end architecture;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
-- FIXED: registered gray pointer crosses -> one bit per step -> always a real value.
entity wr_ptr_cross_good is
generic ( WIDTH : positive := 4 );
port ( wr_clk, wr_rst, wr_en, rd_clk : in std_logic;
wr_gray_in_rd : out std_logic_vector(WIDTH-1 downto 0) );
end entity;
architecture rtl of wr_ptr_cross_good is
signal wr_bin : unsigned(WIDTH-1 downto 0);
signal wr_gray, s1, s2 : std_logic_vector(WIDTH-1 downto 0);
begin
wr : process (wr_clk)
variable nb : unsigned(WIDTH-1 downto 0);
begin
if rising_edge(wr_clk) then
if wr_rst = '1' then
wr_bin <= (others => '0');
wr_gray <= (others => '0');
elsif wr_en = '1' then
nb := wr_bin + 1;
wr_bin <= nb;
wr_gray <= std_logic_vector(nb xor (nb srl 1)); -- registered gray
end if;
end if;
end process;
rd : process (rd_clk) begin
if rising_edge(rd_clk) then
s1 <= wr_gray;
s2 <= s1;
end if;
end process;
wr_gray_in_rd <= s2;
end architecture; library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity ptr_cross_tb is
end entity;
architecture sim of ptr_cross_tb is
constant WIDTH : positive := 4;
signal wr_clk, rd_clk : std_logic := '0';
signal wr_rst, wr_en : std_logic;
signal wr_gray_in_rd : std_logic_vector(WIDTH-1 downto 0);
begin
dut : entity work.wr_ptr_cross_good
generic map (WIDTH => WIDTH)
port map (wr_clk => wr_clk, wr_rst => wr_rst, wr_en => wr_en,
rd_clk => rd_clk, wr_gray_in_rd => wr_gray_in_rd);
wr_clk <= not wr_clk after 5 ns; -- two unrelated clocks
rd_clk <= not rd_clk after 7 ns;
stim : process
variable b : unsigned(WIDTH-1 downto 0);
variable acc : std_logic;
begin
wr_rst <= '1'; wr_en <= '0';
wait until rising_edge(wr_clk); wait until rising_edge(wr_clk); wait for 1 ns;
wr_rst <= '0'; wr_en <= '1';
for step in 0 to 199 loop
wait until rising_edge(rd_clk); wait for 1 ns;
-- gray->binary prefix-XOR scan (1.7) to check the pointer is real.
acc := wr_gray_in_rd(WIDTH-1);
b(WIDTH-1) := acc;
for i in WIDTH-2 downto 0 loop
acc := acc xor wr_gray_in_rd(i);
b(i) := acc;
end loop;
assert to_integer(b) < 2**WIDTH
report "received pointer is not a real count" severity error;
end loop;
report "gray-crossed pointer is always a real count" severity note;
wait;
end process;
end architecture;Across all three languages the lesson is one sentence: carry a multi-bit binary count across a clock boundary and a mid-flight sample can latch a value that never existed; carry it as a registered gray count and every sample is a real value, because only one bit ever changes.
5. Verification Strategy
A gray counter is sequential, so its correctness is a property that must hold on every clock, and the good news is the defining property is small and exhaustively checkable over a full cycle. The single invariant that captures a correct gray counter is:
Between every two consecutive gray outputs, exactly one bit changes — including the wrap from the last value back to the first.
Everything below is that invariant plus the two supporting checks, and it maps directly onto the testbenches in §4.
- The killer check — single-bit transition, exhaustive over a full cycle. Clock the counter through all
2^WIDTHvalues and, on every step, assertpopcount(gray[t] ^ gray[t-1]) == 1. Because a counter of widthWIDTHhas exactly2^WIDTHdistinct states, sweeping one full cycle is complete — it proves the property everywhere, not on a sampled corner. The threegray_countertestbenches in §4a do exactly this: SVassert ($countones(gray_cnt ^ gray_prev) == 1), Verilogif ($countones(gray ^ gray_prev) !== 1) $display("FAIL"), VHDL a bit-count loop withassert cnt = 1 severity error. The reusablegray_monitorof §4b packages the same check as a bindable clocked assertion. - The binary↔gray round-trip (from 1.7). The gray output must be the genuine gray code of the binary state: assert
gray_cnt == bin_cnt ^ (bin_cnt >> 1)every cycle, and independently confirm that decodinggray_cntwith 1.7's prefix-XOR scan returnsbin_cnt. This ties the counter back to the conversion it is built on, and catches a gray output that has drifted out of step with its binary (e.g. registered off the wrong binary value). - The wrap is also single-bit — the power-of-2 check. The dangerous step is the rollover. For a power-of-2 modulus the last→first transition is single-bit like every other; the §4a testbenches run
(1<<WIDTH) + 2steps precisely so the sweep crosses the wrap and asserts the invariant there too. For a non-power-of-two modulus this check is exactly what fails — and it should, because that wrap flips several bits (§6). - The CDC architectural check. As in §4c, event simulation cannot model true metastable multi-bit sampling, so verify the property that makes the fix correct: every value delivered into the receiving domain decodes to a real count the source actually produced. The gray crossing passes because every sampled gray code is the old or the new registered gray; the binary crossing is where formal CDC tools and gate-level jitter injection earn their keep (full treatment in the CDC chapter ahead).
- Expected waveform. On a wave viewer a correct gray counter shows
bin_cntstepping0,1,2,3,…whilegray_cntsteps0,1,3,2,6,7,5,4,…— and the visual signature of correctness is that from onegray_cntvalue to the next, only a single bit toggles. A step where two or more gray bits change simultaneously is the immediate, eye-level tell that the counter is not gray (or that you truncated the cycle to a non-power-of-two length).
6. Common Mistakes
Using a plain binary counter as a CDC pointer. The whole reason gray counters exist. A binary count changes multiple bits on many steps (0111 → 1000 flips four), and a flop in another clock domain sampling that bus mid-flight latches an incoherent mixture — a value the counter never held. As a FIFO pointer that phantom corrupts the full/empty comparison and the FIFO drops or duplicates data. If a multi-bit count must be read by a different clock, it crosses as gray, full stop. (Full post-mortem in §7; buggy-vs-fixed RTL in §4c.)
A non-power-of-2 gray wrap. The single-bit-change property holds across the rollover only for a 2^n-length sequence. Build a mod-6 gray counter by truncating a 3-bit gray cycle at 5 and the wrap from the last value back to 0 flips several bits at once — re-opening the exact hazard gray was meant to close. Gray-coded FIFO pointers therefore use power-of-2 depths; a genuine non-power-of-two gray counter needs a specially constructed sequence (or you keep binary internally and only gray-code a power-of-2 pointer). Never assume "gray = safe wrap" without checking the modulus.
Converting gray→binary with the wrong (adjacent-XOR) formula. When the receiving domain converts the crossed gray pointer back to binary, it must use 1.7's prefix-XOR scan (bin[i] = bin[i+1] ^ gray[i]), not a local XOR of adjacent gray bits (bin[i] = gray[i] ^ gray[i+1]). The adjacent-XOR version is right for width ≤ 2 and silently wrong for width ≥ 3 — it passes a small unit test and then decodes a real 4-bit pointer to the wrong count, corrupting full/empty from the other direction. This is 1.7's signature bug reappearing on the receive side of the same crossing.
Registering the binary but deriving gray combinationally into the other domain. If you register bin_cnt and then compute bin_cnt ^ (bin_cnt >> 1) combinationally and send that XOR output across the boundary, the receiving domain samples the XOR while it is still glitching after the binary flops toggle — and the multi-bit hazard is back, now hiding in combinational glitches instead of flop skew. Register the gray on the source side (§4a) so the crossing value is a clean, single-bit-stepping flop output.
Forgetting to keep the binary. Gray is deliberately poor at arithmetic — you cannot cheaply add, compare, or index memory with it. A design that keeps only gray ends up decoding gray→binary everywhere it needs math, adding logic and bugs. Keep bin_cnt as the authoritative state for all local arithmetic (full/empty, occupancy, RAM address) and treat gray_cnt purely as the crossing view.
Treating the two-flop synchronizer as the whole solution. A two-flop synchronizer resolves per-bit metastability, but it does not make a multi-bit binary bus coherent — it happily latches a phantom, then cleanly de-metastabilizes each of its wrong bits. Gray coding and the synchronizer are complementary: gray guarantees the sampled value is a real count; the synchronizer guarantees each bit settles to a legal level. You need both, and neither substitutes for the other (previewed forward in the CDC chapter).
7. DebugLab
The FIFO that dropped bytes — a binary pointer sampled across a clock domain
The engineering lesson: across a clock domain, only a code that changes one bit per step is safe to sample — synchronizing a multi-bit binary bus bit-by-bit does nothing about the bits being mutually incoherent, so a mid-flight edge latches a value that never existed. The tell is a bug that is intermittent, clock-ratio- and traffic-dependent, and clusters at counter values where many bits flip at once — that fingerprint means a multi-bit bus was sampled across a boundary. Two habits make it impossible, identical across SystemVerilog, Verilog, and VHDL: cross clock domains only with a registered gray count (single-bit per step), and assert the single-bit-change invariant on any pointer that crosses. The full async-FIFO build — gray-pointer synchronization, full/empty from gray, and the metastability defence — is Chapter 11.5 and 11.6; this page builds the counter that all of it rests on.
8. Interview Q&A
9. Exercises
Design and reasoning exercises — no syntax drills. Each rehearses the "one bit per step is what makes it safe" habit.
Exercise 1 — Trace the transitions
For a 3-bit counter, write out the binary sequence 0..7 and the corresponding gray sequence (using g = b ^ (b>>1)). (i) For each of the eight steps including the wrap 7→0, state how many binary bits change and how many gray bits change. (ii) Identify the binary step that changes the most bits and explain, in one sentence, why sampling that binary step from another clock is the dangerous one. (iii) Confirm the gray wrap 7→0 changes exactly one bit and say why that depended on the depth being a power of two.
Exercise 2 — Where to register, and why
An engineer proposes exposing the gray output as a combinational assign gray_cnt = bin_cnt ^ (bin_cnt >> 1) driven straight from the binary flops, and sending that wire across the clock boundary to save a register. Explain precisely what the receiving domain can sample on that wire that it could not sample if the gray were registered, and why that re-introduces the multi-bit hazard the gray coding was meant to remove. Then state the one-line rule this establishes about what may cross a clock boundary.
Exercise 3 — The non-power-of-2 trap
You need a FIFO with capacity 6. A colleague builds a mod-6 pointer by counting a 3-bit gray value 0..5 and wrapping to 0. (i) Write the gray codes for 0..5 and the wrap value 0, and count the bits that change on the 5→0 wrap. (ii) Explain what this does to a read-domain sample taken during that wrap and what it does to the full/empty comparison. (iii) Give the standard engineering fix that keeps the pointer a safe gray counter while still delivering a capacity-6 FIFO (hint: where should the "6" live — in the pointer, or in the occupancy math on the binary side?).
Exercise 4 — Design the verification
You are reviewing a block that carries a counter across a clock domain and are handed only its RTL and a waveform. (i) State the single assertion you would add that most directly proves the crossing count is a gray counter, written as a property over consecutive samples. (ii) Say how long the test must run to make that assertion exhaustive for a width-W counter, and why. (iii) Name one additional check that ties the gray output back to a binary value, and one thing about the wrap your test must deliberately include.
10. Related Tutorials
Continue in RTL Design Patterns (forward links unlock as they ship):
- Comparators — Chapter 1; the equality/greater-than logic that computes a FIFO's full and empty from two pointers — the arithmetic the binary side of this counter feeds.
- Encoding Conversions (binary/gray/one-hot) — Chapter 1.7; the
bin2gray/gray2binthis counter is built on, and where the prefix-XOR-scan decode (and its adjacent-XOR bug) lives. - The Register Pattern (D-FF) — Chapter 2.1; the clocked flop that holds both
bin_cntand the registeredgray_cnt. - Register with Enable / Load — Chapter 2; the count-enable (
en) discipline this counter uses to advance only when it should. - Synchronous Reset — Chapter 2; the reset strategy that brings both counter outputs cleanly to zero.
Siblings / backward in Chapter 3:
- Up/Down Counters — Chapter 3.1; the plain binary counter this pattern wraps in a gray view.
- Mod-N Counters — Chapter 3; where the non-power-of-two modulus lives, and why a gray wrap needs a power-of-2 depth.
Forward — the CDC chapter this previews (unlock as they ship):
- Gray Pointer Synchronization — Chapter 11.5; carrying this counter's gray pointer safely into another clock domain.
- Asynchronous FIFO — Chapter 11.6; the full FIFO where both pointers are gray counters and full/empty is computed from them.
- Two-Flop Synchronizer — Chapter 11; the per-bit metastability defence that complements gray coding on the crossing.
- Metastability — Chapter 11; why a single bit can go metastable, and why gray plus a synchronizer is the complete answer.
Verilog v1 prerequisites (the grammar this pattern transcribes into):
- Bitwise Operators — the XOR that turns binary into gray (
b ^ (b>>1)). - Shift Operators — the
>> 1that supplies "the bit above" in the bin2gray XOR. - Blocking and Non-Blocking Assignments — why the counter's clocked block uses non-blocking (
<=) forbin_cntandgray_cnt, and the combinationalbin_nextuses blocking. - Case Statements — the construct behind the enable/reset control that steers the counter each cycle.
11. Summary
- A gray counter is a counter whose successive outputs differ by exactly one bit. That single property is the whole point: it is the only kind of counter you can safely sample across a clock boundary, because any mid-transition sample resolves to the old count or the new count — never an incoherent multi-bit phantom.
- Build it via a binary counter + registered bin2gray — and keep both. Run an ordinary
+1binary counter and registergray_cnt <= bin_next ^ (bin_next >> 1)(1.7). The binary value is the authoritative state for all arithmetic (full/empty, occupancy, addressing); the gray value is a registered view used only for crossing. Register the gray so the crossing value is a glitch-free flop output. - Multi-bit binary across a clock domain is the bug gray exists to fix. A binary count flips many bits at once (
0111 → 1000), and a flop in another domain sampling mid-flight latches a value that never existed — a phantom FIFO pointer that corrupts full/empty and drops or duplicates data (§7). A two-flop synchronizer resolves per-bit metastability but does not make a multi-bit bus coherent; gray does. You need both. - Power-of-2 depth keeps the wrap single-bit. The one-bit-change property extends across the rollover only for a
2^n-length gray sequence; a non-power-of-two truncation flips several bits on the wrap and re-opens the hazard. Gray-coded pointers use power-of-2 depths. - Verify the defining property, exhaustively. Clock a full
2^WIDTHcycle and assertpopcount(gray[t] ^ gray[t-1]) == 1on every step (wrap included), plus the binary↔gray round-trip (gray_cnt == bin_cnt ^ (bin_cnt >> 1), and 1.7's prefix-XOR-scan decode returnsbin_cnt). Self-checking clocked testbenches for all three are shown here in SystemVerilog, Verilog, and VHDL — this counter is the component the async-FIFO chapter (11.5/11.6) is built on.