지난 포스팅에서 UART Rx 모듈을 설계하면서, 외부에서 들어오는 비동기 신호(Rx)를 다룰 때 주의가 필요하다고 언급했었습니다.
RTL 설계를 하다 보면 CDC(Clock Domain Crossing)라는 말을 정말 많이 듣게 됩니다. 이 글에서는 서로 다른 clock domain을 사용할 때 발생하는 Metastability 문제와, 이를 완화하는 가장 기본적인 방법인 2-FF Synchronizer에 대해 정리해 보겠습니다.
CDC (Clock Domain Crossing)란?
CDC는 말 그대로 signal이 다른 clock domain을 건너간는 뜻입니다.
우리가 만드는 chip 내부의 모든 logic이 Single Clock으로만 동작하면 정말 좋겠지만, 실제 설계하는 chip은 그렇지 않습니다.
- 외부 입력: 버튼 입력, UART/SPI/I2C 같은 peripheral 통신 신호는 우리 칩의 system clock과 전혀 상관없는 타이밍에 들어옵니다. (Asynchronous)
- 다중 클럭: top module에 기본 clock은 100MHz로 돌고, 특정 sub module은 50MHz로 도는 등 서로 다른 속도의 clock이 사용되기도 합니다.
이렇게 Source(송신) 측의 clock과 Destination(수신) 측의 clock이 서로 다를 때 신호가 전달되는 것을 CDC라고 합니다.
왜 문제가 될까? : Metastability
“그냥 신호 연결하면 되는 거 아닌가?”라고 생각할 수 있지만, Flip-Flop의 Setup Time과 Hold Time 조건을 떠올려 봅시다.
- Setup Time: Rising edge 전, 데이터가 일정 시간 동안 안정적으로 유지되어야 함.
- Hold Time: Rising edge 후, 데이터가 일정 시간 동안 변하지 않아야 함.
만약 외부 신호가 우리 시스템 클럭이 딱! 뜨는 그 찰나의 순간(Setup/Hold window)에 0에서 1로 변해버리면 어떻게 될까요?
Flip-Flop은 0인지 1인지 판단하지 못하고, 중간 전압 레벨(Metastable state)에서 부들부들 떠는 상태가 됩니다. 이를 Metastability라고 합니다.
이 불안정한 값은 unknown 형태로 회로 내부로 전파되어 전체 시스템을 오작동(System Failure) 시킬 수 있습니다. 이것이 RTL 엔지니어가 Asynchronous signal을 다룰 때 조심해야 하는 이유입니다.
해결책: 2-FF Synchronizer (Double Flopping)
이런 상황을 최적화하는 가장 간단하고 강력한 방법은 Flip-Flop을 두 번 거치게 하는 것입니다. 이를 2-FF Synchronizer라고 부릅니다.
동작 원리
- 첫 번째 Flip-Flop(FF1)이 Metastability 상태에 빠졌다고 가정해 봅시다.
- 하지만 두 번째 Flip-Flop(FF2)은 다음 clock Rising edge까지 기다립니다.
- 그 한 clock 동안, FF1의 불안정한 전압이 서서히 0이나 1 중 하나의 안정된 상태로 자리를 잡게 됩니다. (이를 Settling Time이라고 합니다.)
- FF2는 안정된 값을 샘플링하여 내부 로직으로 전달합니다.
참고로, 여기서 말하는 해결책은 Metastability 문제를 완전히 해결하는 것이 아니라 발생 확률을 낮추는 것을 의미합니다.
Verilog code
module synchronizer (
input wire clk
,input wire resetn
,input wire async_in // 외부 비동기 입력
,output wire sync_out // 동기화된 안전한 출력
);
reg [1:0] shift_reg;
always @(posedge clk or negedge resetn) begin
if (!resetn) begin
shift_reg <= 2'b00;
end
else begin
// [0]번 비트로 받고, [1]번 비트로 넘김 (Shift)
shift_reg <= {shift_reg[0], async_in};
end
end
// 두 번째 플립플롭의 출력을 사용
assign sync_out = shift_reg[1];
endmodulePulse Synchronizer (Fast to Slow)
기본적인 2-FF Synchronizer에는 치명적인 약점이 하나 있습니다. 바로 빠른 clock에서 느린 clock으로 1주기 짜리 짧은 pulse를 보낼 때입니다.
- Source: 100MHz (10ns 주기)
- Destination: 10MHz (100ns 주기)
만약 Source에서 10ns짜리 pulse를 보냈는데, Destination clock이 그 10ns 사이를 비껴가서 샘플링한다면? 수신 측은 신호가 들어왔는지조차 모르게 됩니다.
해결책: Toggle 방식 사용
이때는 pulse를 토글(Toggle) 신호로 변환해서 보냅니다. 신호의 레벨(Level)을 바꿔주면 느린 클럭이 언젠가는 바뀐 레벨을 감지할 수 있기 때문입니다.
Verilog code
module pulse_synchronizer (
input wire clk_fast, // 송신 측 (빠른 클럭)
input wire resetn,
input wire pulse_in, // 송신할 1-cycle 펄스
input wire clk_slow, // 수신 측 (느린 클럭)
output wire pulse_out // 수신된 1-cycle 펄스
);
// 1. [Fast Domain] Pulse를 Toggle 신호로 변환
reg toggle_reg;
always @(posedge clk_fast or negedge resetn) begin
if (!resetn) toggle_reg <= 1'b0;
else if (pulse_in) toggle_reg <= ~toggle_reg; // 펄스가 올 때마다 뒤집기
end
// 2. [Slow Domain] Toggle 신호를 2-FF Synchronization
reg [2:0] sync_reg;
always @(posedge clk_slow or negedge resetn) begin
if (!resetn) begin
sync_reg <= 3'b000;
end
else begin
// sync_reg[2]는 Edge 감지용, [1]은 동기화 완료된 값
sync_reg <= {sync_reg[1:0], toggle_reg};
end
end
// 3. [Slow Domain] Edge Detection (Toggle이 변했음을 감지하여 다시 Pulse로 복원)
// 현재 값(sync_reg[1])과 과거 값(sync_reg[2])이 다르면 펄스 생성
assign pulse_out = sync_reg[1] ^ sync_reg[2];
endmodule주의사항
2-FF Synchronizer는 만능이 아닙니다. 이 방식은 Single-bit Control Signal (예: Enable, Start 신호 등)에만 사용해야 합니다.
Data Bus(여러 비트)에는 사용하면 안 됩니다. 예를 들어 1-byte 데이터를 2-FF로 넘기면, 각 비트마다 딜레이 차이(Skew)가 발생해서 엉뚱한 값(Glich)이 전달될 수 있습니다.
- Single Bit: 2-FF Synchronizer 사용
- Multi Bit (Data Bus): FIFO(Async FIFO)나 Handshaking 프로토콜 사용
심화: 단순한 2-FF만으로는 부족하다
앞서 살펴본 2-FF Synchronizer는 단일 비트(Single-bit) 신호를 처리하는 데에는 가장 효과적인 방법입니다. 하지만 실전 SoC 설계에서는 데이터 버스와 같은 멀티 비트(Multi-bit) 신호를 다뤄야 하고, 시뮬레이션만으로는 찾을 수 없는 코너 케이스들이 존재합니다.
Multi-bit 신호의 함정: Data Incoherency
“데이터 버스도 그냥 각 비트마다 2-FF를 붙이면 되지 않을까요?” 이것은 가장 위험한 생각입니다. 서로 다른 clock domain 간에는 신호의 도달 시간 차이(Skew)가 존재하기 때문입니다.
문제 상황: 비트 간 스큐(Skew)
예를 들어, Source 도메인에서 b00 → b11로 값을 변경했다고 가정해 봅시다. 배선 길이의 차이나 공정 미세 변동으로 인해, Destination 도메인 입장에서는 두 비트가 동시에 도착하지 않을 수 있습니다.
- Case 1: 하위 비트가 먼저 도착 →
b01로 오인 - Case 2: 상위 비트가 먼저 도착 →
b10으로 오인
결국 00 → 11로 보냈는데 중간에 01이나 10이라는 엉뚱한 값(Glitch)이 샘플링되어 시스템 오작동을 유발합니다. 이를 Data Incoherency(데이터 불일치)라고 합니다.
해결책: 상황별 동기화 기법
- Gray Code (그레이 코드):
- 용도: 카운터나 포인터(FIFO Pointer 등)를 넘길 때.
- 원리: 인접한 값끼리 오직 1개의 비트만 변하도록 인코딩합니다. (예: 00 -> 01 -> 11 -> 10)
- 장점: 1비트만 변하므로 skew로 인한 중간 값 오류가 발생하지 않습니다.
- Handshake Protocol (핸드쉐이크):
- 용도: 데이터가 자주 변하지 않을 때.
- 원리:
REQ(요청) 신호를 보내고, 수신 측에서 데이터를 잘 받았다는ACK(응답) 신호를 보낼 때까지 데이터를 유지합니다. - 단점: 속도가 느려집니다(Latency 증가).
- Async FIFO (비동기 FIFO):
- 용도: 대량의 데이터 스트림을 고속으로 넘길 때.
- 원리: 내부적으로 Gray Code 포인터와 Dual-port memory를 사용하여 가장 안전하게 데이터를 전달합니다. 검증된 IP를 사용하는 것이 좋습니다.
참고: Nandland