[RTL] Asynchronous FIFO 설계하기

지난 RTL CDC 글을 통해 우리는 단일 비트(1-bit) 신호를 동기화하는 2-FF Synchronizer와 Multi-bit 신호의 위험성에 대해 배웠습니다.

이제 CDC 이론의 ‘끝판왕’이자, SoC 및 FPGA 설계에서 가장 많이 사용되는 블록 중 하나인 Asynchronous FIFO를 직접 설계해 볼 차례입니다. 서로 다른 clock domain 사이에서 대량의 데이터를 안전하게 넘기기 위해 반드시 거쳐야 할 관문입니다.

단순히 코드를 복사해서 쓰는 것을 넘어, “도대체 왜 포인터를 그레이 코드(Gray Code)로 변환해야 하는지”, “Full/Empty 플래그는 어떤 기준으로 만들어지는지” 그 내부 원리를 파헤쳐 보겠습니다.

1. Asynchronous FIFO의 RTL 설계 난제

Async FIFO

FIFO(First-In-First-Out)의 원리는 간단합니다.

  • Write Pointer (wptr): 데이터를 쓸 주소. 데이터가 들어오면 1 증가합니다.
  • Read Pointer (rptr): 데이터를 읽을 주소. 데이터가 나가면 1 증가합니다.
  • Empty: wptr == rptr
  • Full: wptr가 한 바퀴를 돌아 rptr를 따라잡았을 때.

동기식(Synchronous) FIFO에서는 두 포인터가 같은 클럭을 사용하므로 비교가 쉽습니다. 하지만 비동기식(Asynchronous) FIFO에서는 Write Clock 도메인과 Read Clock 도메인이 서로 다릅니다.

상대방의 포인터를 내 도메인으로 가져와서 비교해야 하는데, 여기서 두 가지 치명적인 문제가 발생합니다.

  1. Metastability: 상대방 포인터를 샘플링하는 순간 값이 변할 수 있습니다.
  2. Data Incoherency (Multi-bit Skew): 포인터는 여러 비트(Multi-bit)로 구성됩니다. 예를 들어 이진수 0111에서 1000으로 변할 때, 4개의 비트가 동시에 변합니다. 배선 지연(Skew)으로 인해 수신 측에서는 찰나의 순간 1111이나 0000 같은 엉뚱한 값을 볼 수 있습니다.

이 문제를 해결하는 열쇠가 바로 그레이 코드(Gray Code)입니다.

2. 해결사: 그레이 코드 (Gray Code)

그레이 코드는 인접한 수로 변할 때 오직 하나의 비트만 변하는 특성을 가집니다.

  • Binary: 0111 (7) -> 1000 (8) (4비트 변화 -> 위험!)
  • Gray: 0100 (7) -> 1100 (8) (1비트 변화 -> 안전!)

만약 Gray code를 사용하면, Metastability이나 skew가 발생하더라도 수신 측에서는 “값이 변하기 전(7)” 혹은 “값이 변한 후(8)” 둘 중 하나로만 인식됩니다. 중간 단계의 엉뚱한 값이 생길 수 없으므로 포인터가 튀는 것을 막을 수 있습니다.

따라서 Asynchronous FIFO 설계의 대원칙은 다음과 같습니다.

“내 도메인의 포인터를 그레이 코드로 변환하여 상대방에게 넘겨준다.”

3. Full / Empty 생성의 비밀: 보수적 설계 (Pessimistic Design)

CDC 설계에서 가장 중요한 개념은 보수적(Pessimistic) 설계입니다.

상대방의 포인터가 2-FF Synchronizer를 거쳐 넘어오는 동안 최소 2~3 clock의 지연(Latency)이 발생합니다. 즉, 내가 지금 보고 있는 상대방의 포인터는 “현재 값이 아니라 아주 조금 전의 과거 값”입니다.

(1) Empty Generation (Read Domain)

  • Empty는 Read 도메인에서 만듭니다.
  • 조건: rptr == synchronized_wptr
  • Write Pointer는 계속 증가하고 있는데, 동기화 지연 때문에 Read 도메인에서는 아직 증가하지 않은 것처럼 보일 수 있습니다.
  • 결과: 실제로는 데이터가 1~2개 들어와서 Empty가 풀렸는데도, 여전히 Empty라고 판단할 수 있습니다.
  • 괜찮은가? 네! 데이터가 있는데 없다고 판단해서 읽기를 멈추는 것은 성능(Throughput) 저하일 뿐, 기능 고장(Underflow)은 아니기 때문에 안전합니다.

(2) Full Generation (Write Domain)

  • Full은 Write 도메인에서 만듭니다.
  • Gray Code에서의 Full 조건은 조금 복잡합니다.
    • MSB가 달라야 함 (한 바퀴 차이)
    • 2nd MSB도 달라야 함 (Gray Code의 대칭성 때문)
    • 나머지 비트는 같아야 함
  • Read Pointer가 계속 데이터를 읽어서 공간을 비우고 있는데, 동기화 지연 때문에 Write 도메인에서는 아직 꽉 찼다고 보일 수 있습니다.
  • 결과: 실제로는 공간이 생겼는데 Full이라고 판단해서 쓰기를 멈춥니다.
  • 괜찮은가? 네! 덮어쓰기(Overflow)를 방지하는 것이 목적이므로, 일찍 Full을 띄우는 것은 안전합니다.

4. Verilog 구현 (Full Source Code)

실전에서 바로 사용할 수 있는 5개의 전체 모듈 코드입니다.

(1) Asynchronous FIFO Top Module

module async_fifo #(
    parameter DSIZE = 8,  // Data Size
    parameter ASIZE = 4   // Address Size (Depth = 2^4 = 16)
)(
    input  wire             wclk, winc, wrst_n,
    input  wire             rclk, rinc, rrst_n,
    input  wire [DSIZE-1:0] wdata,
    output wire [DSIZE-1:0] rdata,
    output wire             wfull,
    output wire             rempty
);

    wire [ASIZE-1:0] waddr, raddr;
    wire [ASIZE:0]   wptr, rptr, wq2_rptr, rq2_wptr;

    // 1. Dual Port RAM
    fifomem #(DSIZE, ASIZE) u_mem (
        .wdata(wdata), .waddr(waddr), .wclk(wclk), .wclken(winc & ~wfull),
        .rdata(rdata), .raddr(raddr), .rclk(rclk), .rclken(rinc & ~rempty)
    );

    // 2. Synchronizers (2-FF)
    sync_r2w u_sync_r2w (.wclk(wclk), .wrst_n(wrst_n), .rptr(rptr), .wq2_rptr(wq2_rptr));
    sync_w2r u_sync_w2r (.rclk(rclk), .rrst_n(rrst_n), .wptr(wptr), .rq2_wptr(rq2_wptr));

    // 3. Write Control Logic (Binary -> Gray, Full Logic)
    wptr_full #(ASIZE) u_wptr_full (
        .wclk(wclk), .wrst_n(wrst_n), .winc(winc),
        .wq2_rptr(wq2_rptr),
        .wfull(wfull), .waddr(waddr), .wptr(wptr)
    );

    // 4. Read Control Logic (Binary -> Gray, Empty Logic)
    rptr_empty #(ASIZE) u_rptr_empty (
        .rclk(rclk), .rrst_n(rrst_n), .rinc(rinc),
        .rq2_wptr(rq2_wptr),
        .rempty(rempty), .raddr(raddr), .rptr(rptr)
    );

endmodule

(2) FIFO Memory (Dual Port RAM)

module fifomem #(
    parameter DSIZE = 8, // Data Width
    parameter ASIZE = 4  // Address Width
)(
    input  wire             wclk, wclken,
    input  wire [ASIZE-1:0] waddr,
    input  wire [DSIZE-1:0] wdata,
    input  wire             rclk, rclken,
    input  wire [ASIZE-1:0] raddr,
    output wire [DSIZE-1:0] rdata
);
    // 2^ASIZE depth memory array
    reg [DSIZE-1:0] mem [0:(1<<ASIZE)-1];

    // Write Logic
    always @(posedge wclk) begin
        if (wclken) mem[waddr] <= wdata;
    end

    // Read Logic (No Reset needed for memory cells usually)
    assign rdata = mem[raddr];

endmodule

(3) Synchronizers (2-FF)

안전한 포인터 전달을 위한 2-stage Flip-Flop입니다.

// Read Pointer to Write Domain Synchronizer
module sync_r2w #(
    parameter ASIZE = 4
)(
    input  wire             wclk, wrst_n,
    input  wire [ASIZE:0]   rptr,
    output reg  [ASIZE:0]   wq2_rptr
);
    reg [ASIZE:0] wq1_rptr;

    always @(posedge wclk or negedge wrst_n) begin
        if (!wrst_n) begin
            wq1_rptr <= 0;
            wq2_rptr <= 0;
        end else begin
            wq1_rptr <= rptr;
            wq2_rptr <= wq1_rptr;
        end
    end
endmodule

// Write Pointer to Read Domain Synchronizer
module sync_w2r #(
    parameter ASIZE = 4
)(
    input  wire             rclk, rrst_n,
    input  wire [ASIZE:0]   wptr,
    output reg  [ASIZE:0]   rq2_wptr
);
    reg [ASIZE:0] rq1_wptr;

    always @(posedge rclk or negedge rrst_n) begin
        if (!rrst_n) begin
            rq1_wptr <= 0;
            rq2_wptr <= 0;
        end else begin
            rq1_wptr <= wptr;
            rq2_wptr <= rq1_wptr;
        end
    end
endmodule

(4) Read Control Logic (Empty Generation)

Binary to Gray 변환과 Empty 플래그 생성 로직입니다.

module rptr_empty #(
    parameter ASIZE = 4
)(
    input  wire             rclk, rrst_n, rinc,
    input  wire [ASIZE:0]   rq2_wptr, // Synced Write Pointer (Gray)
    output reg              rempty,
    output wire [ASIZE-1:0] raddr,
    output reg  [ASIZE:0]   rptr      // Read Pointer (Gray)
);
    reg  [ASIZE:0] rbin;
    wire [ASIZE:0] rbin_next, rgray_next;

    // 1. Binary Counter Update
    // Empty가 아닐 때만 rinc를 받아들임
    assign rbin_next = rbin + (rinc & ~rempty);
    assign raddr     = rbin[ASIZE-1:0];

    // 2. Binary to Gray Conversion
    assign rgray_next = (rbin_next >> 1) ^ rbin_next;

    // 3. Empty Flag Generation
    // 조건: Read Pointer(Gray) == Synced Write Pointer(Gray)
    wire rempty_val = (rgray_next == rq2_wptr);

    always @(posedge rclk or negedge rrst_n) begin
        if (!rrst_n) begin
            rbin   <= 0;
            rptr   <= 0;
            rempty <= 1; // Reset 상태에서는 Empty여야 함
        end else begin
            rbin   <= rbin_next;
            rptr   <= rgray_next;
            rempty <= rempty_val;
        end
    end
endmodule

(5) Write Control Logic (Full Generation)

Binary to Gray 변환과 Full 플래그 생성 로직입니다.

module wptr_full #(
    parameter ASIZE = 4
)(
    input  wire             wclk, wrst_n, winc,
    input  wire [ASIZE:0]   wq2_rptr, // Synced Read Pointer (Gray)
    output reg              wfull,
    output wire [ASIZE-1:0] waddr,
    output reg  [ASIZE:0]   wptr      // Write Pointer (Gray)
);

    reg  [ASIZE:0] wbin;
    wire [ASIZE:0] wbin_next, wgray_next;

    // 1. Binary Counter Update
    // Full이 아닐 때만 winc를 받아들임
    assign wbin_next = wbin + (winc & ~wfull);
    assign waddr     = wbin[ASIZE-1:0];

    // 2. Binary to Gray Conversion
    assign wgray_next = (wbin_next >> 1) ^ wbin_next;

    // 3. Full Flag Generation
    // 조건 (Gray Code): MSB와 2nd MSB는 다르고, 나머지는 같아야 함
    wire wfull_val = (wgray_next == {~wq2_rptr[ASIZE:ASIZE-1], wq2_rptr[ASIZE-2:0]});

    always @(posedge wclk or negedge wrst_n) begin
        if (!wrst_n) begin
            wbin  <= 0;
            wptr  <= 0;
            wfull <= 0;
        end else begin
            wbin  <= wbin_next;
            wptr  <= wgray_next;
            wfull <= wfull_val;
        end
    end
endmodule

5. 결론 및 요약

Asynchronous FIFO는 겉보기엔 데이터를 저장하는 통처럼 보이지만, 그 내부에는 CDC 문제를 해결하기 위한 치밀한 전략이 숨어 있습니다.

  1. Gray Code 사용: Multi-bit 포인터가 도메인을 넘어갈 때 값이 깨지는 것(Glitch)을 방지합니다.
  2. 보수적 설계: 동기화 지연으로 인해 Full/Empty가 조금 일찍 뜨는 것은 허용하지만, 늦게 뜨는 것은 절대 허용하지 않습니다. (Safety First)
  3. 2n Depth: Gray Code의 대칭성을 활용하여 Full/Empty 로직을 간단히 하기 위해 FIFO의 깊이는 보통 2의 승수(16, 32, 1024 등)로 설정합니다.

참고: VLSI verify

Similar Posts