This lab focuses on clock domain crossing (CDC) design. The system receives input data in clk1, transfers it to clk2 through a handshake synchronizer for convolution, and sends the output data back to clk1 through an asynchronous FIFO.
The PATTERN, CONV_TOP, DESIGN_module, and generated test data are AI-assisted generated. My main implementation and study focus is the synchronizer part:
Handshake_syn.vFIFO_syn.v
The handshake synchronizer transfers one stable multi-bit data word from the source clock domain to the destination clock domain. It is a toggle-based request / acknowledge protocol: sreq toggles when the source sends a new data word, and dack is updated to match the received request after the destination accepts the data. Instead of synchronizing every data bit directly, the source holds the data stable in dhold, and only the request / acknowledge control signals cross clock domains through 2FF synchronizers.
| Port | Direction | Clock Domain | Width | Description |
|---|---|---|---|---|
sclk |
input | source | 1 | Source clock. |
dclk |
input | destination | 1 | Destination clock. |
rst_n |
input | both | 1 | Active-low asynchronous reset. |
svalid |
input | source | 1 | Source asserts this when din is valid and should be transferred. |
din |
input | source | WIDTH |
Multi-bit data from the source domain. The data is latched into dhold and kept stable during transfer. |
sready |
output | source | 1 | Indicates that the handshake synchronizer can accept a new source data word. |
dvalid |
output | destination | 1 | Indicates that dout is valid in the destination domain. |
dout |
output | destination | WIDTH |
Data delivered to the destination domain. |
dready |
input | destination | 1 | Destination asserts this when it can accept dout. |
The block diagram includes two important control conditions. These conditions work because the handshake uses toggle signals instead of level pulses:
sready = (sack == sreq);
new_req = (dreq != dack);sready is generated in the source clock domain by comparing the request toggle sreq with the synchronized acknowledge toggle sack. When sack == sreq, the previous request toggle has already been acknowledged, so the source side is idle and can accept the next din.
new_req is generated in the destination clock domain by comparing the synchronized request toggle dreq with the local acknowledge toggle dack. When dreq != dack, the destination side has observed a new request toggle that has not been acknowledged yet, so it latches dhold into dout and asserts dvalid.
- The source domain asserts
svalidwhendinis ready. - If
sready = (sack == sreq)is true, the synchronizer is idle. - When
svalid && sreadyis true, the source latchesdinintodholdand togglessreq. sreqcrosses to the destination domain through a 2FF synchronizer and becomesdreq.- The destination checks
new_req = (dreq != dack). - If
new_reqis true anddvalidis low, the destination latches the stabledholdintodoutand assertsdvalid. - When
dvalid && dreadyis true, the destination accepts the data and updatesdackto the currentdreqtoggle value. dackcrosses back to the source domain through a 2FF synchronizer and becomessack.- When the source sees
sack == sreq, the transfer is complete and the next data can be sent.
This protocol avoids directly synchronizing an arbitrary multi-bit data bus. The data bus is safe because it is held stable while the request and acknowledge signals complete the CDC handshake.
FIFO_syn.v transfers a stream of 8-bit output data from clk2 back to clk1.
| Port | Direction | Clock Domain | Width | Description |
|---|---|---|---|---|
wclk |
input | write | 1 | Write-side clock. |
rclk |
input | read | 1 | Read-side clock. |
rst_n |
input | both | 1 | Active-low asynchronous reset. |
winc |
input | write | 1 | Write request. A write occurs when winc && !wfull. |
wdata |
input | write | WIDTH |
Data written into FIFO memory. |
wfull |
output | write | 1 | FIFO full flag in the write clock domain. |
rinc |
input | read | 1 | Read request. A read occurs when rinc && !rempty. |
rdata |
output | read | WIDTH |
Data read from FIFO memory. |
rempty |
output | read | 1 | FIFO empty flag in the read clock domain. |
The FIFO uses binary pointers internally for memory addressing and Gray-coded pointers for cross-clock-domain synchronization:
wptr_bin/rptr_bin: binary pointers for local address increment.wptr_gray/rptr_gray: Gray-coded pointers sent across clock domains.wptr_gray_sync/rptr_gray_sync: synchronized current Gray-coded pointers from the opposite clock domain.wptr_gray_next/rptr_gray_next: locally calculated next Gray-coded pointers used for full / empty comparison.
Gray code is used because only one bit changes per pointer increment. Therefore, synchronizing the Gray pointer bus with per-bit 2FF synchronizers is acceptable for asynchronous FIFO pointer transfer. This should not be used for arbitrary multi-bit data buses.
The conversion rule is:
gray = (binary >> 1) ^ binary;The FIFO compares the next pointer value instead of only the current pointer value.
Read-side empty:
rempty_next = (rptr_gray_next == wptr_gray_sync);If the next read pointer equals the synchronized write pointer, the FIFO will be empty after the current read operation.
Write-side full:
wfull_next = (wptr_gray_next == {
~rptr_gray_sync[ADDR_WIDTH:ADDR_WIDTH-1],
rptr_gray_sync[ADDR_WIDTH-2:0]
});For a power-of-two asynchronous FIFO, full is detected when the next write pointer equals the synchronized read pointer with the upper two Gray-code bits inverted. This distinguishes full from empty even when the lower address bits are the same.
Using next-pointer comparison lets wfull and rempty update in the same cycle as the accepted write/read operation, which prevents one-cycle-late status flags.
The generated test data contains 10 patterns by default.
CDC verification can be run from the JasperGold directory:
cd Lab7/Exercise/05_JG
make jgThe CDC report checks the handshake and asynchronous FIFO crossing schemes.


