Buses and Gray Code
Why you cannot just synchronize a bus bit by bit
The two-flop synchronizer is perfect for one bit. The most common CDC mistake is assuming you can use one on every bit of a bus. You cannot, and here is exactly why.
The bus skew problem
Put a separate two-flop synchronizer on each bit of a multi-bit value and the bits resolve independently. When several bits change at once, some may settle on the new value and some on the old value in the same cycle. The destination then reads a combination that never actually existed.
For a binary counter going from 0111 to 1000, all four bits change at once. If three resolve to the new value and one lags, the destination could read 1111 or 0000 for a cycle. That garbage value can be catastrophic.
Gray code - one bit at a time
Gray code is a counting sequence where only one bit changes between consecutive values. If only one bit ever changes, then even if that bit is caught mid-transition, the synchronized result is either the old value or the new value, never a corrupt mix.
// Binary to gray: one bit changes per increment
assign gray = bin ^ (bin >> 1);
// Gray back to binary (when you need to use the value)
integer i;
always @(*) begin
bin_back[WIDTH-1] = gray[WIDTH-1];
for (i = WIDTH-2; i >= 0; i = i - 1)
bin_back[i] = bin_back[i+1] ^ gray[i];
end| Decimal | Binary | Gray |
|---|---|---|
| 0 | 000 | 000 |
| 1 | 001 | 001 |
| 2 | 010 | 011 |
| 3 | 011 | 010 |
| 4 | 100 | 110 |
Notice each gray value differs from the one above it in exactly one bit. That single-bit-change property is what makes it safe to synchronize.
Gray code only helps for values that increment or decrement by one, like FIFO pointers. It does not make an arbitrary data bus safe to cross. For general data you hold the bus steady and synchronize a control signal instead, which is the handshake approach.
This is exactly why asynchronous FIFOs use gray-coded read and write pointers. The next lesson builds one.