이전 챕터에서는 2-FF Synchronizer, Pulse Stretching, Handshake, Toggle Synchronizer를 직접 설계하고 시뮬레이션을 통해 동작을 확인했다. 이번 챕터에서는 Multi-Bit CDC 방법 중 실무에서 가장 대표적으로 사용되는 Handshake 방식을 중심으로 살펴본다.
Multi-Bit CDC에서 2-FF를 쓰면 안 되는 이유
CDC의 핵심 목표를 다시 정리하면 다음 두 가지다.
- Metastability의 격리
- 신호/데이터 전달의 무결성 보장
2-FF Synchronizer는 이 중 메타안정성 완화만을 목적으로 한다. 즉, 메타안정 상태가 뒤쪽 로직으로 전파될 확률을 낮춰줄 뿐, 멀티비트 데이터의 무결성(coherence)이나 정합성(atomicity)은 전혀 보장하지 못한다.
따라서 멀티비트 데이터에 대해 각 비트마다 2-FF Synchronizer를 적용하는 방식은 구조적으로 잘못된 설계이며, 반드시 피해야 한다.
2-FF Synchronizer의 정확한 역할
2-FF Synchronizer의 목적은 단 하나다.
Metastability가 뒤 로직으로 퍼지는 것을 확률적으로 줄이는 것
이 때문에 2-FF는 다음과 같은 싱글 비트 제어 신호에만 사용된다.
- enable
- valid / ready
- reset deassertion
- 기타 단일 비트 control signal
Multi-Bit CDC의 진짜 문제: Metastability가 아니라 Bit 간의 Skew
멀티비트 CDC에서 가장 치명적인 문제는 Metastability 자체가 아니라 bit 간의 skew이다.
멀티비트 데이터의 각 비트는 서로 독립적으로 샘플링되며, 클럭 도메인이 다를 경우 다음과 같은 문제가 발생할 수 있다.
- 어떤 비트는 이전 값으로 샘플링되고
- 어떤 비트는 새로운 값으로 샘플링되며
- 어떤 비트는 메타안정 상태에서 resolve됨
그 결과, 송신 도메인에서는 존재한 적 없는 데이터 조합이 수신 도메인에서 만들어질 수 있다.
이 문제는 메타안정성이 발생하지 않더라도 언제든지 발생 가능한 구조적인 문제이며, 단순한 2-FF Synchronizer로는 절대 해결할 수 없다.
멀티비트 CDC에서는 다음이 반드시 보장되어야 한다.
수신 클럭 도메인에서 멀티비트 데이터가 하나의 일관된 값으로 샘플링될 것
이를 위해서는 bit 간의 skew를 원천적으로 차단할 수 있는 CDC 구조가 필요하며, 그 대표적인 해법이 바로 Handshake 기반 Multi-Bit CDC이다. Toggle Synchronizer는 구조 상 멀티비트 CDC 처리가 불가능하며, Streching 방식은 유지보수의 이유로 사용하지 않는다.
Handshake CDC
Handshake CDC는 멀티비트 데이터 자체를 동기화하지 않고, 데이터가 안정적으로 유지되는 구간(stable window)을 명확히 만들어 수신 도메인이 그 구간에서 데이터를 샘플링하도록 하는 구조이다. 단순 Stretching과 유사해 보일 수 있지만, Handshake CDC는 두 클럭 도메인의 속도 비율이 어떠하든지 간에 동작이 보장되는 구조라는 점에서 본질적인 차이가 있다.
Handshake CDC의 기본 구조: Request / Acknowledge

Handshake CDC는 기본적으로 Request(req) / Acknowledge(ack) 두 개의 싱글 비트 제어 신호를 이용해 동작한다.
- req (request)
송신 도메인(Source Clock Domain)에서 "멀티비트 데이터가 준비되었고 현재 안정 상태임"을 알리는 신호 - ack (acknowledge)
수신 도메인(Destination Clock Domain)에서 "데이터를 정상적으로 샘플링했음"을 알리는 신호
이 두 신호만이 2-FF Synchronizer를 통해 서로의 클럭 도메인으로 전달되며, 멀티비트 데이터 버스 자체는 CDC 대상이 아니다.
Handshake CDC 동작 시퀀스
Handshake CDC의 동작은 다음과 같은 순서로 진행된다.
1. Source Domain: Request 발생
송신 도메인은 전송할 멀티비트 데이터를 내부 레지스터에 저장한 뒤, req 신호를 1로 설정한다.
- req = 1 인 동안 데이터는 절대 변경되지 않는다
- 이 구간이 바로 수신 도메인이 데이터를 안전하게 샘플링할 수 있는 stable window가 된다
2. Request 신호의 CDC
req 신호는 싱글 비트 제어 신호이므로 2-FF Synchronizer를 통해 수신 도메인으로 전달된다.
- req의 도착 시점은 클럭 비율에 따라 달라질 수 있음
- 하지만 level 신호이므로 도착 지연 자체는 기능적 문제가 되지 않음
3. Destination Domain: 데이터 샘플링 및 Acknowledge 발생
수신 도메인은 동기화된 req 신호를 감지하면, 해당 클럭 엣지에서 멀티비트 데이터 버스를 샘플링한다.
- 이 시점에서 데이터는 Source Domain에서 고정된 상태
- bit 간 skew 없이 하나의 일관된 값으로 샘플링됨
샘플링이 완료되면 ack 신호를 1로 설정하여 데이터 수신 완료를 알린다.
4. Acknowledge 신호의 역방향 CDC
ack 신호 역시 2-FF Synchronizer를 통해 송신 도메인으로 전달된다.
송신 도메인은 ack를 감지한 후 req를 0으로 내리고, 다음 데이터 전송을 준비한다.
Handshake CDC와 Stretching 방식의 차이
Stretching 방식은 송신 도메인에서 pulse를 충분히 늘려 수신 도메인에서 감지하도록 하는 방식이다. 그러나 이 방식은 다음과 같은 한계를 가진다.
- 두 클럭의 주파수 비율을 정확히 가정해야 함
- 클럭 비율이 변경되면 재설계 필요
- worst-case를 가정한 과도한 stretch 필요
반면 Handshake CDC는 ack를 기반으로 동작 완료를 명확히 확인하기 때문에 클럭 속도 비율과 무관하게 항상 안전한 동작을 보장한다.
Code
module handshake_cdc #(
parameter DATA_W = 8
)(
// Source domain
input logic clk_src,
input logic rst_src_n,
input logic valid_src, // <-- VALID
input logic [DATA_W-1:0] data_in,
// Destination domain
input logic clk_dst,
input logic rst_dst_n,
output logic [DATA_W-1:0] data_out,
output logic valid_dst // one transfer indication
);
// =================================
// Source domain
// =================================
logic [DATA_W-1:0] data_bus;
logic req;
// =================================
// Destination domain
// =================================
logic ack;
// =================================
// CDC synchronizers
// =================================
logic req_ff1, req_sync;
logic ack_ff1, ack_sync;
// -------------------------------------------------
// Source domain logic
// -------------------------------------------------
always_ff @(posedge clk_src or negedge rst_src_n) begin
if (!rst_src_n) begin
req <= 1'b0;
data_bus <= '0;
end else begin
if (!req && valid_src) begin
data_bus <= data_in; // 데이터 고정
req <= 1'b1; // request 발생
end
else if (req && ack_sync) begin
req <= 1'b0; // handshake 완료
end
end
end
// -------------------------------------------------
// REQ synchronizer (SRC -> DST)
// -------------------------------------------------
always_ff @(posedge clk_dst or negedge rst_dst_n) begin
if (!rst_dst_n) begin
req_ff1 <= 1'b0;
req_sync <= 1'b0;
end else begin
req_ff1 <= req;
req_sync <= req_ff1;
end
end
// -------------------------------------------------
// Destination domain logic
// -------------------------------------------------
always_ff @(posedge clk_dst or negedge rst_dst_n) begin
if (!rst_dst_n) begin
ack <= 1'b0;
data_out <= '0;
valid_dst <= 1'b0;
end else begin
if (req_sync && !ack) begin
data_out <= data_bus; // stable window sampling
valid_dst <= 1'b1;
ack <= 1'b1;
end
else if (!req_sync) begin
ack <= 1'b0;
valid_dst <= 1'b0;
end
end
end
// -------------------------------------------------
// ACK synchronizer (DST -> SRC)
// -------------------------------------------------
always_ff @(posedge clk_src or negedge rst_src_n) begin
if (!rst_src_n) begin
ack_ff1 <= 1'b0;
ack_sync <= 1'b0;
end else begin
ack_ff1 <= ack;
ack_sync <= ack_ff1;
end
end
endmodule
module tb_handshake_cdc;
localparam DATA_W = 8;
logic clk_src = 0;
logic clk_dst = 0;
logic rst_n = 0;
always #5 clk_src = ~clk_src; // 100 MHz
always #11 clk_dst = ~clk_dst; // ~45 MHz (비동기)
logic valid_src;
logic [DATA_W-1:0] data_in;
logic [DATA_W-1:0] data_out;
logic valid_dst;
handshake_cdc #(.DATA_W(DATA_W)) dut (
.clk_src (clk_src),
.rst_src_n (rst_n),
.valid_src (valid_src),
.data_in (data_in),
.clk_dst (clk_dst),
.rst_dst_n (rst_n),
.data_out (data_out),
.valid_dst (valid_dst)
);
initial begin
valid_src = 0;
data_in = 0;
rst_n = 0;
#40;
rst_n = 1;
// Transfer #1
#30;
data_in = 8'hA5;
valid_src = 1;
#10;
valid_src = 0;
// Transfer #2
#200;
data_in = 8'h3C;
valid_src = 1;
#10;
valid_src = 0;
// Transfer #3
#200;
data_in = 8'hF0;
valid_src = 1;
#10;
valid_src = 0;
#400;
$finish;
end
// Waveform dump
initial begin
$dumpfile("handshake_cdc.vcd");
$dumpvars(0, tb_handshake_cdc);
end
endmodule
Simulation

Source Domain (송신 도메인)
- valid_src가 assert되면 입력 데이터를 내부 data_bus에 래치
- req를 assert하여 전송 요청 발생
- ack가 동기화되어 돌아오면 (ack_sync)
- handshake 완료로 판단
- req deassert
- req가 deassert될 때까지 데이터는 고정됨
Destination Domain (수신 도메인)
- 동기화된 req_sync를 감지
- req_sync가 assert되면
- 데이터 버스를 샘플링
- valid_dst를 통해 데이터 수신을 알림
- ack assert
- req_sync가 deassert되면
- ack 및 valid_dst deassert
본 설계는 단일 outstanding transaction만 허용한다.
- req == 1인 동안은 새로운 데이터 전송이 불가능
- valid_src는 반드시 req == 0 상태에서만 assert되어야 함
- handshake 완료 이전에 들어온 데이터는 무시됨
이 구조가 의도하지 않은 것
- 연속 데이터 스트리밍
- multiple outstanding transaction
이러한 기능은 FIFO 결합 또는 VALID/READY 구조를 통해 확장되어야 한다.
'VLSI > Design' 카테고리의 다른 글
| [CDC] 6_Multi Bit CDC: Asynchronous FIFO (Introduction) (0) | 2025.12.19 |
|---|---|
| [CDC] 5_Multi Bit CDC: Gray Code (0) | 2025.12.15 |
| [CDC] 3_Single Bit CDC: Simulation Result (0) | 2025.12.07 |
| [CDC] 2_Single Bit CDC: Toggle Synchronizer (0) | 2025.12.07 |
| [CDC] 1_Single Bit CDC: 2-FF Synchronizer (0) | 2025.12.07 |