The 2-FF synchronizer solves metastability for single bits. But what about crossing a multi-bit value, like a counter or FIFO pointer? Gray code is the answer: a binary code where only 1 bit changes between consecutive values. Even if that single bit is delayed by metastability, the result is always a valid code, never an invalid state.
Consider crossing a 4-bit counter from clock domain A to domain B. In regular binary, consecutive numbers differ in multiple bits:
If you cross these asynchronously, metastability can delay some bits but not others. You might read an intermediate invalid value:
Never cross a multi-bit binary value asynchronously without Gray code conversion. The synchronizer can't protect you — multiple bits can be in inconsistent states during the transition.
Gray code (also called reflected binary code) is a binary code where consecutive values differ by exactly 1 bit:
| Decimal | Binary | Gray | Bits changed |
|---|---|---|---|
| 0 | 0000 | 0000 | — |
| 1 | 0001 | 0001 | 1 bit |
| 2 | 0010 | 0011 | 1 bit |
| 3 | 0011 | 0010 | 1 bit |
| 4 | 0100 | 0110 | 1 bit |
| 5 | 0101 | 0111 | 1 bit |
| 6 | 0110 | 0101 | 1 bit |
| 7 | 0111 | 0100 | 1 bit |
| 8 | 1000 | 1100 | 1 bit |
| 9 | 1001 | 1101 | 1 bit |
| 10 | 1010 | 1111 | 1 bit |
| 11 | 1011 | 1110 | 1 bit |
| 12 | 1100 | 1010 | 1 bit |
| 13 | 1101 | 1011 | 1 bit |
| 14 | 1110 | 1001 | 1 bit |
| 15 | 1111 | 1000 | 1 bit |
Notice: every transition changes only 1 bit. This is the magic of Gray code.
If only 1 bit changes between consecutive Gray values, then even if metastability delays that bit:
Convert binary to Gray, sync the Gray value across clock domains with a 2-FF synchronizer per bit, then convert back to binary. The receiver always reads a valid Gray code, even if metastability delays individual bits. This is the standard pattern for synchronizing FIFO pointers, event counters, and state information.
Binary to Gray:
Gray = Binary XOR (Binary >> 1)
Example: Binary 0101 (5)
Gray to Binary: Reverse the process by XOR-ing all more-significant bits:
// Binary to Gray and Gray to Binary converters
module gray_converter #(parameter WIDTH = 8) (
input [WIDTH-1:0] binary_in,
output [WIDTH-1:0] gray_out
);
assign gray_out = binary_in ^ (binary_in >> 1);
endmodule
module gray_to_binary #(parameter WIDTH = 8) (
input [WIDTH-1:0] gray_in,
output [WIDTH-1:0] binary_out
);
genvar i;
generate
for (i = 0; i < WIDTH; i = i + 1) begin : g2b
if (i == WIDTH - 1)
assign binary_out[i] = gray_in[i];
else
assign binary_out[i] = gray_in[i] ^ binary_out[i + 1];
end
endgenerate
endmodule
// Example: 4-bit Gray counter
module gray_counter #(parameter WIDTH = 4) (
input clk, rst_n,
output [WIDTH-1:0] gray_out,
output [WIDTH-1:0] binary_out
);
reg [WIDTH-1:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 0;
else
counter <= counter + 1;
end
gray_converter #(.WIDTH(WIDTH)) gc (.binary_in(counter), .gray_out(gray_out));
assign binary_out = counter;
endmodule
6. Using Gray code in FIFO pointer synchronization
A dual-clock FIFO (covered in depth on Day 5) uses Gray code like this:
This pattern ensures pointers never produce invalid intermediate states during crossing.
// Simplified: FIFO pointer synchronization using Gray code
module fifo_wr_ptr_gray #(parameter AWIDTH = 4) (
input clk_w, rst_n,
input wr_en,
output [AWIDTH-1:0] wr_ptr_gray,
output [AWIDTH-1:0] wr_ptr_bin
);
reg [AWIDTH-1:0] wr_ptr;
always @(posedge clk_w or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;
else if (wr_en)
wr_ptr <= wr_ptr + 1;
end
assign wr_ptr_bin = wr_ptr;
assign wr_ptr_gray = wr_ptr ^ (wr_ptr >> 1);
endmodule
// In read domain, synchronize wr_ptr_gray
wire [AWIDTH-1:0] wr_ptr_gray_sync;
sync_2ff #(.WIDTH(AWIDTH)) wr_sync (
.clk(clk_r),
.rst_n(rst_n),
.async_in(wr_ptr_gray),
.sync_out(wr_ptr_gray_sync)
);
// Convert back to binary for comparison
wire [AWIDTH-1:0] wr_ptr_sync;
gray_to_binary #(.WIDTH(AWIDTH)) g2b (
.gray_in(wr_ptr_gray_sync),
.binary_out(wr_ptr_sync)
);
// Now wr_ptr_sync is safe to use in read_ptr comparisons
7. Best practices and limitations
| Use Gray code for: | Don't use Gray code for: |
|---|---|
| FIFO read/write pointers | Random multi-bit data values |
| Event counters crossing domains | Commands or control words (use handshake) |
| Address buses | Data that doesn't increment sequentially |
| State machine pointers | Situations where you need exact binary value (convert back from Gray) |
Gray code adds a small logic delay for binary↔gray conversion. For very high-frequency designs, pre-compute Gray conversion stages or pipeline them. Also, Gray code only helps with sequential values; for random data, you need a handshake (Day 4).
Gray = Binary XOR (Binary >> 1)Only 1 bit changes between consecutive Gray values. If that bit is delayed by metastability, you always read a valid code (or adjacent code), never an invalid intermediate state. Binary does not have this property.
Binary to Gray: G = B XOR (B >> 1). Gray to Binary: iteratively XOR all more-significant bits. Most HDL libraries have parameterized Gray converters available.
Gray code is best for counters and pointers (strictly incrementing values). For random multi-bit data, Gray code doesn't help — you need a handshake protocol with valid/ready signaling (Day 4).
Use a 2-FF synchronizer for each Gray code bit, just like single-bit signals. The Gray code property (1-bit change) prevents invalid states, but metastability resolution still requires 2 FFs.
No. For random data, use a valid/ready handshake (Day 4). For commands, use encoded handshakes or CDC-safe protocols. Gray code is specific to strictly-incrementing sequences like FIFO pointers.