How to Design an Asynchronous FIFO for CDC Applications

Modern SoCs, ASICs and FPGA designs commonly contain multiple clock domains. A processor may run at one frequency, a peripheral at another, and a high-speed interface at yet another. When data needs to move between these unrelated clock domains, simply connecting a multi-bit bus from one domain to another can create serious clock domain crossing (CDC) problems.

An asynchronous FIFO is one of the most widely used solutions for transferring multi-bit data safely between independent clock domains.

Unlike a synchronous FIFO, an asynchronous FIFO has separate write and read clocks. The write side operates using one clock, while the read side operates using another clock with no fixed phase or frequency relationship.

The challenging part is not storing the data. The difficult part is safely communicating the read and write positions between the two clock domains.

This article explains how to design an asynchronous FIFO for CDC applications, including its architecture, binary and Gray-code pointers, two-flop synchronizers, full and empty detection, RTL implementation considerations, and verification strategy.

What Is an Asynchronous FIFO?

FIFO stands for First-In, First-Out.

Data written first into the FIFO is read first, preserving the order of transactions.

In an asynchronous FIFO, the write and read operations use independent clocks:

            WRITE CLOCK DOMAIN

                    |

              Write Controller

                    |

                    v

              +———–+

              |   Memory  |

              +———–+

                    |

                    ^

              Read Controller

                    |

             READ CLOCK DOMAIN

 

For example:

write_clk = 100 MHz

read_clk  = 75 MHz

 

The clocks may have completely different frequencies and phase relationships.

This makes the asynchronous FIFO useful when two subsystems cannot operate from the same clock.

Typical applications include:

  • SoC subsystem communication
  • Processor and peripheral interfaces
  • Network data paths
  • FPGA multi-clock designs
  • DMA data movement
  • Audio and video pipelines
  • High-speed interface buffering
  • Rate matching between independent blocks

Inskill’s CDC material also identifies asynchronous FIFOs as a common solution for multi-bit data transfer between clock domains.

Why Is an Asynchronous FIFO Needed for CDC?

Consider a simple 8-bit data bus crossing from clk_a to clk_b.

A beginner might try:

clk_a domain

     |

  8-bit data

     |

     v

clk_b domain

 

This is unsafe when the clocks are asynchronous.

Each bit can potentially be sampled at a different point in time. If the source data changes near the receiving clock edge, the destination may capture an inconsistent combination of bits.

A two-flop synchronizer is useful for a single-bit control signal, but it is not by itself a safe solution for transferring an arbitrary multi-bit data bus.

An asynchronous FIFO solves this problem by separating:

  • Data storage
  • Write control
  • Read control
  • Pointer synchronization

The data is stored in the FIFO memory, while the information about where the writer and reader are located is transferred safely between clock domains.

Basic Architecture of an Asynchronous FIFO

A typical asynchronous FIFO contains:

  1. Dual-clock memory
  2. Write binary pointer
  3. Write Gray-code pointer
  4. Read binary pointer
  5. Read Gray-code pointer
  6. Write-pointer synchronizer
  7. Read-pointer synchronizer
  8. Full detection logic
  9. Empty detection logic

The architecture can be represented as:

                WRITE CLOCK DOMAIN

                       |

                 Write Binary Ptr

                       |

                       v

                 Gray Conversion

                       |

                       v

                  Write Gray Ptr

                       |

                       | CDC

                       v

                +————–+

                | 2-FF Sync    |

                +————–+

                       |

                       v

                READ CLOCK DOMAIN



                 READ CLOCK DOMAIN

                       |

                  Read Binary Ptr

                       |

                       v

                 Gray Conversion

                       |

                       v

                  Read Gray Ptr

                       |

                       | CDC

                       v

                +————–+

                | 2-FF Sync    |

                +————–+

                       |

                       v

                WRITE CLOCK DOMAIN

 

The important design principle is:

Binary pointers are used locally for addressing and counting, while Gray-coded pointers are used for clock-domain crossing.

Why Are Gray-Code Pointers Used?

This is one of the most important concepts in asynchronous FIFO design.

A binary counter can change multiple bits during a single increment.

For example:

0111 → 1000

 

Four bits change at the same time.

If another asynchronous clock samples the counter during this transition, it could observe an unintended combination of bits.

That can produce an incorrect pointer value.

Gray code solves this problem because only one bit changes between adjacent Gray-code values.

For example:

Binary       Gray

0000         0000

0001         0001

0010         0011

0011         0010

0100         0110

 

Therefore, asynchronous FIFO designs normally convert the local binary pointer to Gray code before sending it to the other clock domain.

The standard approach is widely used in asynchronous FIFO implementations and CDC methodologies.

Binary-to-Gray Conversion

The standard conversion from binary to Gray code is:

gray = binary ^ (binary >> 1);

 

For example, in SystemVerilog:

assign wr_ptr_gray = wr_ptr_bin ^ (wr_ptr_bin >> 1);

assign rd_ptr_gray = rd_ptr_bin ^ (rd_ptr_bin >> 1);

 

The binary pointer remains local to its own clock domain.

The Gray-coded version is the one that crosses into the other domain.

Why Does the FIFO Pointer Need an Extra Bit?

An asynchronous FIFO generally uses a pointer that is one bit wider than the memory address.

For a FIFO with depth 8:

Address bits = 3

Pointer bits = 4

 

The lower bits identify the memory location.

The additional bit helps identify whether the write pointer has wrapped around and caught up with the read pointer.

This distinction is important because:

write pointer == read pointer

 

can mean that the FIFO is empty, but pointer equality can also occur after the writer has wrapped around and filled the FIFO.

The extra pointer bit helps distinguish these conditions.

Two-Flop Synchronization

After converting a pointer to Gray code, it still cannot be directly used in the opposite clock domain.

It must pass through a synchronizer.

For example:

Write Gray Pointer

       |

       v

   +——-+

   | Sync1 |

   +——-+

       |

       v

   +——-+

   | Sync2 |

   +——-+

       |

       v

Read Domain

 

A simplified SystemVerilog implementation is:

always_ff @(posedge rd_clk or negedge rd_rst_n) begin

    if (!rd_rst_n) begin

        wr_gray_sync1 <= ‘0;

        wr_gray_sync2 <= ‘0;

    end

    else begin

        wr_gray_sync1 <= wr_ptr_gray;

        wr_gray_sync2 <= wr_gray_sync1;

    end

end

 

The same approach is used to transfer the read pointer into the write domain.

The first synchronizer stage may encounter metastability. The second stage provides additional time for that metastability to resolve before the synchronized pointer is used by destination-domain logic.

Importantly, the synchronizer does not make metastability impossible. It reduces the probability that metastability propagates into functional logic.

Write Pointer Logic

The write pointer belongs entirely to the write clock domain.

A typical sequence is:

write request

     |

     v

FIFO full?

  /     \

YES      NO

 |        |

No write  Write data

          |

          v

      Increment pointer

          |

          v

      Binary → Gray

 

The pointer should advance only when a valid write is accepted.

Conceptually:

if (wr_en && !full)

    wr_ptr_bin <= wr_ptr_bin + 1’b1;

 

The corresponding Gray pointer is then generated from the next binary pointer.

The write address is derived from the local binary pointer.

Read Pointer Logic

The read pointer operates independently using the read clock.

The basic sequence is:

read request

     |

     v

FIFO empty?

  /      \

YES       NO

 |         |

No read    Read data

           |

           v

       Increment pointer

           |

           v

       Binary → Gray

 

Conceptually:

if (rd_en && !empty)

    rd_ptr_bin <= rd_ptr_bin + 1’b1;

 

The read address comes from the local read pointer.

How Is FIFO Empty Detected?

The empty condition is generated in the read clock domain.

Conceptually, the FIFO becomes empty when the next read pointer catches up with the synchronized write pointer.

A simplified expression is:

empty = next_read_gray == synchronized_write_gray

 

The important point is that the read side does not directly compare its pointer against an asynchronous write pointer.

It compares against the synchronized Gray-coded write pointer.

This keeps the comparison within the read clock domain.

How Is FIFO Full Detected?

Full detection is generated in the write clock domain.

The write side compares its next Gray-coded pointer against the synchronized read pointer.

For a conventional power-of-two asynchronous FIFO, the full comparison uses the wrap-around relationship represented by the additional pointer bits.

Conceptually:

next_write_pointer

        |

        v

Compare with

synchronized read pointer

        |

        v

       FULL

 

In a common Gray-pointer implementation, the required upper pointer bits are inverted for the full comparison while the remaining bits match. This distinguishes a full FIFO from an empty FIFO after pointer wrap-around.

Complete Asynchronous FIFO Data Flow

Putting the pieces together:

                WRITE DOMAIN

                     |

                  wr_en

                     |

                     v

                Full Check

                     |

                 +—+—+

                 |       |

               Full    Not Full

                 |       |

                Stop    Write

                         |

                         v

                   Memory Write

                         |

                   Binary Pointer

                         |

                    Gray Convert

                         |

                         v

                    2-FF Sync

                         |

                         v

                 READ DOMAIN



                 READ DOMAIN

                     |

                  rd_en

                     |

                     v

                Empty Check

                     |

                 +—+—+

                 |       |

               Empty   Not Empty

                 |       |

                Stop     Read

                         |

                         v

                   Memory Read

                         |

                   Binary Pointer

                         |

                    Gray Convert

                         |

                         v

                    2-FF Sync

                         |

                         v

                 WRITE DOMAIN

 

This architecture allows the two sides to operate independently.

Example RTL Structure

Instead of placing everything inside one large RTL module, a clean asynchronous FIFO can be divided into logical blocks:

async_fifo

│

├── FIFO memory

│

├── write pointer logic

│

├── read pointer logic

│

├── write-to-read synchronizer

│

├── read-to-write synchronizer

│

├── full generation

│

└── empty generation

 

This modular structure makes the design easier to review, debug and verify.

It also makes CDC analysis more straightforward.

Why Can’t We Synchronize the Binary Pointer Directly?

This is a common interview question.

Suppose a binary pointer changes:

0111 → 1000

 

Several bits change simultaneously.

Because the receiving clock is asynchronous, it may sample the transition at an arbitrary point.

The resulting value can be invalid.

With Gray code:

Gray pointer

changes by one bit

        ↓

Only one CDC bit changes

        ↓

Synchronize Gray pointer

        ↓

Use synchronized value

 

That is why asynchronous FIFO designs normally transfer Gray-coded pointers rather than raw binary pointers.

Reset Considerations

Reset handling is another important part of asynchronous FIFO design.

At reset, both pointers are normally initialized to zero:

write pointer = 0

read pointer  = 0

 

This results in the FIFO initially being empty.

However, reset signals themselves cross clock-domain boundaries in many real designs, so reset assertion and especially reset de-assertion must be designed carefully.

For independently reset clock domains, engineers need to consider:

  • Reset synchronization
  • Reset release ordering
  • Pointer initialization
  • Synchronizer initialization
  • Empty/full flag behavior
  • Recovery after reset

A FIFO that works correctly during normal operation can still have a CDC problem if reset behavior is not considered.

How to Verify an Asynchronous FIFO

Designing the FIFO is only half of the task.

A proper verification environment should test the FIFO with independent clocks.

For example:

write_clk = 10 ns

read_clk  = 14 ns

 

The relative phase continuously changes, giving the testbench different sampling relationships.

Useful verification scenarios include:

Basic write and read

Write several values and confirm that they are read in exactly the same order.

FIFO empty condition

Read until the FIFO becomes empty and verify that no additional read is accepted.

FIFO full condition

Write until the FIFO becomes full and verify that additional writes are blocked.

Simultaneous read and write

Perform reads and writes at the same time using independent clocks.

Different clock frequencies

Try several combinations:

Fast write / slow read

Slow write / fast read

Equal frequency / unrelated phase

 

Reset during operation

Test reset at different points in the FIFO transaction sequence.

Randomized traffic

Generate random write/read operations and compare the FIFO output against a reference queue.

A self-checking reference model is especially useful because it can verify both data ordering and data integrity. Current asynchronous-FIFO verification examples commonly combine reference models, assertions and randomized traffic.

Important Assertions for an Async FIFO

SystemVerilog Assertions can help verify important FIFO properties.

For example, the write pointer should not advance when the FIFO is full:

full && wr_en

        |

        v

write pointer must remain unchanged

 

Similarly:

empty && rd_en

        |

        v

read pointer must remain unchanged

 

Another useful property is checking the Gray-code transition.

Only one Gray-code bit should change between consecutive pointer values.

These checks help detect CDC implementation errors early.

CDC Tools and Asynchronous FIFO

Simulation alone cannot prove that a CDC architecture is safe.

Static CDC analysis tools can inspect clock crossings and identify issues such as:

  • Missing synchronizers
  • Unsafe crossings
  • Incorrect clock relationships
  • Reconvergence
  • Synchronizer structures
  • Multi-bit CDC problems

Inskill’s Lint and CDC training specifically includes asynchronous FIFO analysis, binary-to-Gray synchronization, multi-bit crossings and CDC labs.

For production RTL, the asynchronous FIFO should therefore be checked using both functional verification and dedicated CDC analysis.

Where Are Asynchronous FIFOs Used?

Asynchronous FIFOs are useful whenever data needs to cross between unrelated clock domains while maintaining ordering and buffering.

Typical applications include:

  • SoC interconnects
  • Processor-to-peripheral communication
  • Network interfaces
  • DMA engines
  • Video processing
  • Audio interfaces
  • FPGA designs
  • Multi-clock data pipelines
  • High-speed communication systems

They are especially useful when the producer and consumer operate at different rates.

Conclusion

Designing an asynchronous FIFO for CDC applications requires more than implementing a memory with read and write pointers.

The key is safely transferring pointer information between independent clock domains.

The standard architecture combines:

Binary pointers → Gray-code conversion → clock-domain synchronization → full/empty detection

The write and read sides maintain their own local state, while synchronized Gray-coded pointers provide the information required to determine whether the FIFO is full or empty.

For beginners, the most important concepts to master are CDC, metastability, Gray code, pointer synchronization, full/empty detection and reset handling.

Once these concepts are clear, asynchronous FIFO design becomes much easier to understand and is an excellent practical RTL project for learning real-world CDC design.

Leave a Reply

Your email address will not be published. Required fields are marked *