Clock Domain Crossing - First In First Out Memory (cdc_fifo)
Multi-bit clock domain crossing (CDC) synchroniser using a First In First Out (FIFO) memory.
Design
Performs synchronisation of a multi-bit bus to the destination clock domain using a FIFO with empty and full flags. The flags are pessimistic, meaning that they assert immediately but are removed late, ensuring the user is always informed immediately to either stop en-queueing to (writing), or de-queuing from (reading), the FIFO to prevent overflow or underflow conditions from occurring. The late removal of the flags occur because of the time taken for pointers to be synchronised across clock domains. The FIFO may fill to a slightly higher level than would otherwise be the case, as the empty flag will not be removed to allow data to be dequeued until the enqueue pointer is synchronised. However, this is essential for correct clock domain crossing to occur.
Implementation
An active high level on the enqueue input (src_enqueue) causes the input data (src_in) to be written to the FIFO memory on the source clock domain (src_clk). Providing the FIFO is not empty then data will automatically be read out of the FIFO and presented at the output (dst_out), with the dequeue input (dst_dequeue) acting as an acknowledgement that the data has been taken and the FIFO can update it with the next stored value. The reading of the data out of the FIFO is performed on the destination clock domain (dst_clk) but does not require any extra synchronisation since the memory location containing the data is guaranteed to be stable, i.e. was written some time ago and will not be overwritten during the read, as explained below. The FIFO full flag (src_full_n) is produced on the source clock domain and should be used to prevent more data being enqueued into the FIFO when it is full. The FIFO flag empty (dst_empty_n) is produced on the destination clock domain and should be used to prevent more data being dequeued from the FIFO when it is empty.
The flags, and the pointers used to control them, also play an important part in the safe clock domain crossing of the data. As data is enqueued a pointer (src_lucal_code_next) is incremented, and as data is dequeued another pointer (dst_lucal_code_next) is also incremented. These pointers are used to both address the FIFO memory for the writes and reads, and compared to inform the other clock domain of their relative levels, via 2-stage synchronisation, to control the full/empty flags. The delay for the pointers to be synchronised ensures that the FIFO memory location has been written long before the pointer indicates that the location may be read. Halting en-queueing when the full flag is active, ensures that the FIFO memory location should not be overwritten until it’s current contents have been read.
An updated value on the pointers could reach the synchroniser as it also receives a new clock edge which, due to different track lengths, could result in some of the bits being synchronised and some not. If the pointers used a traditional binary code where a single increment often requires multiple bits changing at the same time, then this could result in wildly inaccurate values being synchronised. Whereas using Gray coding, where only a single bit changes at a time, could still be sampled mid-word but the worst that can happen would be the pointer does not increment immediately, and the value would catch up on a later synchroniser clock edge. Rather than performing a binary count and then converting to Gray code which would increase the combinatorial delay, a Lucal code has been used for the pointers. The Lucal code is essentially a Gray code with an extra LSB providing even parity and enable computations, such as incrementing, to be performed directly on the Gray code.
The enqueue pointer increments when data is added to the FIFO and the dequeue pointer increments when data is removed. If the pointers were set to the same width as that required to write to the FIFO memory, they would equal each other when the FIFO was empty. However, when the FIFO was full the pointers would also equal each other. Providing an extra most significant bit for the pointers allows the implementation to differentiate between full and empty. A problem would occur if the Gray code most significant bit was used to only differentiate between full and empty conditions, and the lower bits used to directly address the FIFO memory. As the whole Gray coded pointer overflows into the most significant bit, and since only a single bit changes at a time in a Gray code, then the values of the lower Gray coded bits would equal each other, i.e. not change. This would result in the FIFO memory location being overwritten. The problem is solved by using the lower bits of the Lucal code rather than using the lower bits of the Gray code, providing a FIFO address with the correct number of unique addresses which repeat even when the pointers overflow into the upper bits. An example of a 4 bit Lucal code, consisting of a 3 bit Gray code followed by an even parity bit, can be used to generate a Gray code pointer for comparison and flag generation by taking the 3 most significant bits, and a FIFO memory address which overflows correctly by taking the 2 least significant bits, as shown in Table 1:
Lucal Code |
Comparison Pointer (Gray code) |
FIFO Memory Address |
|---|---|---|
0000 0011 0110 0101 1100 1111 1010 1001 |
000 001 011 010 110 111 101 100 |
00 11 10 01 00 11 10 01 |
Using this coding scheme:
The FIFO is empty when the dequeue and synchronised enqueue pointer Gray codes are equal.
The FIFO is full when the enqueue and synchronised dequeue pointer Gray codes two most significant bits are not equal and the remaining bits are equal.
Where the Gray code is all bits of the Lucal code apart from the least significant bit.
A block diagram representation of the implementation is given in Figure 217:.
Figure 217: Block diagram of a multi-bit clock domain crossing (CDC) synchroniser using a First In First Out (FIFO) memory implementation.
Further information on metastability and synchronisation can be found at:
The primitive passes signals between different clock domains and has been designed for that purpose. The primitive may contain attributes and should be paired with constraints to control their placement, optimising the MTBF through the synchronisers, and preventing the timing analyser from needlessly attempting to time the crossing signals. The Xilinx Vivado tool has the capability to report on the quality of the clock domain crossings in the design and can accept waivers for warnings generated for CDC architectures that it can not automatically verify. Platform/application developers should therefore consider adding the constraints, or similar if not using Vivado, to designs that instantiate the primitive. Refer to constraints listed below together with the documentation for all clock domain clocking primitives identified in the dependencies section, and all of their dependencies.
### CDC constraints for cdc_fifo.vhd instantiated throughout the design ###
# Applies Max delay (controlling skew) to D input of the CDC registers used to
# transfer the Gray code pointers and FIFO data across the clock domains. This
# is valid because the Gray code ensures that only a single bit of the pointers
# changes at a time and the FIFO implementation is not corrupted if an updated
# pointer is missed, it just catches up later. Also, the FIFO data is held
# steady whilst transferring from the RAM to the receiving registers. The RAM
# location will have been written long before a read is attempted (i.e. long
# setup time) due to the delay for the enqueue pointer to be synchronised
# across the clock domains. The RAM location will be prevented from being
# overwritten until the location has been read and the dequeue pointer has
# been synchronised across the clock domain (i.e. long hold time). The
# constraint applies:
# from the source of signals feeding the D input of the *cdc_data* registers
# to the destination D input of the *cdc_data* registers
# to a single period of the clock which drives the *cdc_data* registers
# Does not apply to similarly named signals, only to those in the cdc_fifo primitive
set_max_delay -datapath_only \
-from [all_fanin -startpoints_only -flat -to [get_pins -filter {REF_PIN_NAME == D} -of [get_cells -hier -filter {NAME =~ *src*cdc_data* && FILE_NAME =~ */cdc_fifo.vhd}]]] \
-to [get_pins -filter {REF_PIN_NAME == D} -of [get_cells -hier -filter {NAME =~ *src*cdc_data* && FILE_NAME =~ */cdc_fifo.vhd}]] \
[get_property PERIOD [get_clocks -of_objects [get_cells -hier -filter {NAME =~ *src*cdc_data* && FILE_NAME =~ */cdc_fifo.vhd}]]]
set_max_delay -datapath_only \
-from [all_fanin -startpoints_only -flat -to [get_pins -filter {REF_PIN_NAME == D} -of [get_cells -hier -filter {NAME =~ *dst*cdc_data* && FILE_NAME =~ */cdc_fifo.vhd}]]] \
-to [get_pins -filter {REF_PIN_NAME == D} -of [get_cells -hier -filter {NAME =~ *dst*cdc_data* && FILE_NAME =~ */cdc_fifo.vhd}]] \
[get_property PERIOD [get_clocks -of_objects [get_cells -hier -filter {NAME =~ *dst*cdc_data* && FILE_NAME =~ */cdc_fifo.vhd}]]]
# Waiver warning when running a CDC report
create_waiver -type CDC -id CDC-15 \
-to [get_pins -filter {REF_PIN_NAME == D} -of [get_cells -hier -filter {NAME =~ *src*cdc_data* && FILE_NAME =~ */cdc_fifo.vhd}]] \
-user "username" -description "The implementation ensures that the source FIFO data is held steady whilst the parallel bus crosses the clock domains"
create_waiver -type CDC -id CDC-15 \
-to [get_pins -filter {REF_PIN_NAME == D} -of [get_cells -hier -filter {NAME =~ *dst*cdc_data* && FILE_NAME =~ */cdc_fifo.vhd}]] \
-user "username" -description "The implementation ensures that the source FIFO data is held steady whilst the parallel bus crosses the clock domains"
Interface
Generics
width_g(natural): Width of the data busessrc_inanddst_out.
depth_g(natural): Sets the depth of the internal FIFO.
Ports
src_clk(std_logic), in: Source clock. Input and FIFO write control registered on rising edge.
src_reset(std_logic), in: Source reset. Active high, synchronous with rising edge of source clock.
src_enqueue(std_logic), in: Source enqueue. Active high, write to FIFO. Should not be asserted whensrc_full_nindicates the FIFO is full.
src_in(std_logic_vector,width_gbits), in: Input bus to be resynchronised.
src_full_n(std_logic), out: Active low FIFO full flag.
dst_clk(std_logic), in: Destination clock. Output and FIFO read control registered on rising edge.
dst_reset(std_logic), in: Destination reset. Active high, synchronous with rising edge of clock.
dst_dequeue(std_logic), in: Destination dequeue. Active high, read acknowledge to FIFO. Should not be asserted whendst_empty_nindicates the FIFO is empty.
dst_out(std_logic_vector,width_gbits), out: Output bus synchronised to the destination clock.
dst_empty_n(std_logic), out: Active low FIFO empty flag.
Dependencies
The dependencies to other elements in OpenCPI are:
None.
There is also a dependency on:
ieee.std_logic_1164
ieee.numeric_std
ieee.math_real
ieee.std_logic_misc
Limitations
Limitations of cdc_fifo are:
The
depth_gshould be a power of 2 to ensure the Gray code pointers maintain the single bit change characteristic during roll over.The
depth_gshould be greater or equal to 16 to ensure the latency of clearing the full/empty flags does not adversely affect performance.