Skip to content

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 → 1000four 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.

the-need.v — a binary count sampled by another clock can be caught mid-flight
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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 flow
A gray counter — binary state, registered gray view, one crossingbinary counterbin_cnt <= bin_cnt + 1 (3.1) — the real statebin → grayg = b ^ (b>>1) (1.7) — flat XOR rowregister graygray_cnt is a flop output, glitch-freecross the domainONLY gray crosses — one bit changes per stepkeep binary localfull/empty, address, occupancy use binarypower-of-2 wrap2^n length => the rollover is single-bit too
The binary counter is the authoritative state; the gray output is a registered combinational view of it (g = b ^ b>>1, from 1.7). Local arithmetic — full/empty, addressing, occupancy — stays on binary. Only the registered gray copy crosses the clock boundary, where its single-bit-per-step property means any mid-transition sample is the old or the new count. The single-bit property extends across the wrap only for a power-of-2 modulus. This structure is identical in SystemVerilog, Verilog, and VHDL.

The 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.

gray_counter.sv — binary counter + registered bin2gray (both outputs, WIDTH-generic)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

Two 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.

gray_counter_tb.sv — clocked: assert exactly ONE bit changes per step over a full cycle
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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").

gray_counter.v — binary counter + registered bin2gray in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
gray_counter_tb.v — clocked self-check; one-bit-change via $countones, PASS/FAIL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

gray_counter.vhd — binary counter + registered bin2gray in VHDL (rising_edge, generic)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
gray_counter_tb.vhd — clocked self-check; count changed bits per step, assert = 1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

gray_monitor.sv — reusable clocked assertion: popcount(gray ^ gray_prev) == 1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
gray_monitor.v — same reusable one-bit-change monitor in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
gray_monitor.vhd — clocked one-bit-change assertion in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

ptr_cross.sv — BUGGY (binary pointer) vs FIXED (gray pointer) across a clock domain
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
   endmodule

The 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.

ptr_cross_tb.sv — clocked; received pointer must always be a REAL count (gray fix passes)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

The 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).

ptr_cross.v — BUGGY (binary) vs FIXED (gray) pointer crossing in Verilog
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // 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
ptr_cross_tb.v — self-checking; received pointer must decode to a real count
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
   endmodule

In 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.

ptr_cross.vhd — BUGGY (binary) vs FIXED (gray) pointer crossing in VHDL
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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;
ptr_cross_tb.vhd — self-checking; received gray must decode to a real count
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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^WIDTH values and, on every step, assert popcount(gray[t] ^ gray[t-1]) == 1. Because a counter of width WIDTH has exactly 2^WIDTH distinct states, sweeping one full cycle is complete — it proves the property everywhere, not on a sampled corner. The three gray_counter testbenches in §4a do exactly this: SV assert ($countones(gray_cnt ^ gray_prev) == 1), Verilog if ($countones(gray ^ gray_prev) !== 1) $display("FAIL"), VHDL a bit-count loop with assert cnt = 1 severity error. The reusable gray_monitor of §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 decoding gray_cnt with 1.7's prefix-XOR scan returns bin_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) + 2 steps 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_cnt stepping 0,1,2,3,… while gray_cnt steps 0,1,3,2,6,7,5,4,… — and the visual signature of correctness is that from one gray_cnt value 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.

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/gray2bin this 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_cnt and the registered gray_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 >> 1 that supplies "the bit above" in the bin2gray XOR.
  • Blocking and Non-Blocking Assignments — why the counter's clocked block uses non-blocking (<=) for bin_cnt and gray_cnt, and the combinational bin_next uses 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 +1 binary counter and register gray_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^WIDTH cycle and assert popcount(gray[t] ^ gray[t-1]) == 1 on 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 returns bin_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.