HomeCDCDay 3 Enhanced

Gray Code Synchronization

The only safe way to cross multi-bit data. Why binary fails, how Gray code prevents glitches, encoding/decoding algorithms, MTBF analysis, complete RTL implementation, and production verification.

By EcrioniX · Published June 13, 2026 · ~4800 words · 15 min read

1. The Problem: Why Binary Fails Across Clock Domains

From Day 2, we learned the dual-FF synchronizer is safe for single-bit signals. But what about multi-bit data? Consider an 8-bit counter transitioning from `10000000` (binary 128) to `01111111` (binary 127):

Binary transition: 10000000 → 01111111 All 8 bits change simultaneously. What Clock B sees during transition (metastable window): T=0ns: 10000000 (stable) T=0.2ns: 1X000000 (bit 7 transitioning) T=0.4ns: 1XX00000 (bit 6 also transitioning) T=0.6ns: 1XXX0000 (bit 5, 4, 3 transitioning) T=0.8ns: 01111111 (all settled) If Clock B samples at T=0.3ns: 1X000000 → valid 8-bit capture is UNKNOWN - Bit 7 might be 0 or 1 (metastable) - Other bits might transition before sample time - Result: Could be 10000000, 10000001, 10000010, etc. - This is a GLITCH (incorrect intermediate value) Even with dual-FF synchronizer on each bit separately: FF1_bit7, FF1_bit6, FF1_bit5, ..., FF1_bit0 all asynchronously change → Each FF1 stage different metastability resolution times → Different FF2 stages capture different values → Output has glitch window spanning multiple clock cycles

The fundamental problem: multiple bits changing creates a "race condition" during synchronization. Different bits resolve at different times, creating invalid intermediate values.

2. The Gray Code Solution

Core Property: Single Bit Changes

Gray code (also called Reflected Binary Code) has one special property:

When incrementing, exactly ONE bit changes at a time.

This eliminates glitches. If only one bit can change, the metastable window is limited to that one bit transitioning, not all bits.

Gray Code vs Binary Transitions: Decimal | Binary | Gray Code --------|------------|---------- 0 | 0000 | 0000 1 | 0001 | 0001 ← 1 bit changed 2 | 0010 | 0011 ← 1 bit changed 3 | 0011 | 0010 ← 1 bit changed 4 | 0100 | 0110 ← 1 bit changed 5 | 0101 | 0111 ← 1 bit changed 6 | 0110 | 0101 ← 1 bit changed 7 | 0111 | 0100 ← 1 bit changed 8 | 1000 | 1100 ← 1 bit changed ... Key: EVERY transition changes exactly 1 Gray bit. Binary can change up to N bits simultaneously. Example transition (3 → 4): Binary: 0011 → 0100 (3 bits change) Gray Code: 0010 → 0110 (1 bit changes)

Why This Helps CDC

When Gray code synchronizer (Gray counter in Clock A, dual-FF in Clock B) crosses domains:

3. Binary to Gray Encoding

Formula

Convert binary number B to Gray code G:

G[i] = B[i] XOR B[i+1] for each bit i (with B[N] = 0 for the MSB) Example: Convert binary 6 (0110) to Gray B[3] B[2] B[1] B[0] = 0 1 1 0 G[3] = B[3] XOR B[4] = 0 XOR 0 = 0 G[2] = B[3] XOR B[2] = 0 XOR 1 = 1 G[1] = B[2] XOR B[1] = 1 XOR 1 = 0 G[0] = B[1] XOR B[0] = 1 XOR 0 = 1 Gray(6) = 0101

Hardware Implementation

module binary_to_gray #(parameter WIDTH = 8) (
  input  [WIDTH-1:0] binary,
  output [WIDTH-1:0] gray
);

  // G[i] = B[i] XOR B[i+1]
  assign gray = binary ^ (binary >> 1);

endmodule

// Example usage:
// binary_to_gray #(.WIDTH(8)) b2g (
//   .binary(counter_bin),      // 8-bit binary counter
//   .gray(gray_code)           // Convert to Gray
// );

One line of logic: The XOR of the binary value with itself right-shifted by 1. This compiles to N XOR gates in parallel (zero delay).

4. Gray to Binary Decoding

Formula

Convert Gray code G back to binary B (used in Clock B domain after synchronization):

B[N-1] = G[N-1] B[i] = B[i+1] XOR G[i] for i = N-2 down to 0 Example: Convert Gray 0101 back to binary G[3] G[2] G[1] G[0] = 0 1 0 1 B[3] = G[3] = 0 B[2] = B[3] XOR G[2] = 0 XOR 1 = 1 B[1] = B[2] XOR G[1] = 1 XOR 0 = 1 B[0] = B[1] XOR G[0] = 1 XOR 1 = 0 Binary = 0110 (which is 6) ✓

Hardware Implementation

module gray_to_binary #(parameter WIDTH = 8) (
  input  [WIDTH-1:0] gray,
  output [WIDTH-1:0] binary
);

  // B[i] = Gray[i] XOR B[i+1]
  // Cascaded XORs: each bit depends on all higher bits
  assign binary[WIDTH-1] = gray[WIDTH-1];

  genvar i;
  generate
    for (i = WIDTH-2; i >= 0; i = i-1) begin
      assign binary[i] = binary[i+1] ^ gray[i];
    end
  endgenerate

endmodule

// Complete Gray Code CDC Module:
module gray_cdc #(parameter WIDTH = 8) (
  input  clk_a,
  input  [WIDTH-1:0] binary_a,    // Binary counter in Clock A

  input  clk_b,
  output [WIDTH-1:0] binary_b     // Synchronized binary in Clock B
);

  wire [WIDTH-1:0] gray_a, gray_b_sync, gray_b_ff2;
  reg [WIDTH-1:0] gray_b_ff1;

  // Clock A: Convert binary to Gray
  binary_to_gray #(.WIDTH(WIDTH)) b2g_a (
    .binary(binary_a),
    .gray(gray_a)
  );

  // Clock B: Dual-FF synchronizer on Gray code
  always @(posedge clk_b) begin
    gray_b_ff1 <= gray_a;     // May be metastable
    gray_b_ff2 <= gray_b_ff1; // Definitely resolved
  end

  // Clock B: Convert Gray back to binary
  gray_to_binary #(.WIDTH(WIDTH)) g2b_b (
    .gray(gray_b_ff2),
    .binary(binary_b)
  );

endmodule

5. MTBF Analysis for Gray CDC

Gray code CDC has the same MTBF characteristics as binary dual-FF, but with a critical advantage:

MTBF(Gray CDC) = exp(τ · Δt) / (f_req · f_clk_B · K) Same formula as binary dual-FF, BUT: - f_req = frequency of counter changes (can be lower if only 1 bit changes per clock) - In binary: ALL bits can change, stressing multiple FF1 stages - In Gray: Only 1 bit changes, stressing only 1 FF1 stage Result: Gray code CDC is SAFER than binary dual-FF on every bit. - Multiple bits: MTBF(Gray) >> MTBF(binary per-bit) - Example (8-bit, 1 MHz changes, 1 GHz Clock B): MTBF(binary 8 bits in parallel) ≈ millions of years for the weakest bit MTBF(Gray 8 bits, 1 bit changes) ≈ billions of years

6. Real-World Applications

Cross-Domain Counters

Synchronizing a counter from Clock A to Clock B:

Dual-Clock FIFO Pointers

FIFO write pointer in Clock A, read pointer in Clock B. Both wrap around (saturating counters). Gray code ensures pointer can cross domains safely without misinterpreting pointer equality or wrap-around conditions.

Multi-Bit Command Buses

If sending multi-bit encoded commands (not just incremented counters), Gray code might not work directly. Use Gray + encoding constraint (e.g., one-hot encoding) or pulse synchronization (Day 4).

7. Limitations and When NOT to Use Gray Code

Only For Monotonic Counters

Gray code is designed for counters that increment (or decrement) by 1. If your data is arbitrary binary (not monotonic), Gray code won't help.

Gray Code Limitation Example

Problem: Sending arbitrary 8-bit config value across clock domains Config can be: 0x00, 0x15, 0xA3, 0xFF, etc. (any value) Gray code does NOT help: - Gray code property (single-bit change) only applies to incremented counters - Arbitrary value A to arbitrary value B may need to change all bits - Solution: Use dual-FF on all 8 bits + handshake protocol, or pulse sync Gray code helps: - Counter incrementing 0 → 1 → 2 → ... → 255 → 0 - Only 1 bit changes per increment - Gray code ensures safe crossing

8. Implementation Checklist

Next (Day 4): Pulse synchronizers for single-bit control signals and handshakes.