[RTL] RTL Valid-Ready Handshake와 Skid Buffer

RTL 설계를 하다 보면 필연적으로 ‘딜레마’에 빠지는 순간이 옵니다.

“타이밍(Timing)을 맞추기 위해 pipeline register를 넣었더니, 데이터 흐름 제어(Flow Control)가 꼬이고 성능(Throughput)이 떨어진다.”

단순히 데이터만 흘러가는 구조라면 레지스터만 추가하면 그만입니다. 하지만, 후단 모듈이 바쁠 때 전단 모듈에게 멈추라고(Back-pressure) 말해야 하는 상황이라면 이야기가 달라집니다.

이번 글에서는 AXI4-Stream 등 표준 인터페이스의 근간이 되는 Valid-Ready Handshake 프로토콜을 이해하고, 타이밍 위반을 해결하는 필살기인 Skid Buffer의 RTL 설계 원리를 알아보겠습니다.

1. 데이터 흐름 제어의 기본: Valid-Ready Handshake

가장 먼저 두 모듈 간에 데이터를 안전하게 주고받는 약속(Protocol)을 정의해야 합니다. 업계 표준인 AMBA AXI4-Stream 인터페이스가 바로 이 방식을 사용합니다.

  • Source (Sender): 데이터를 보내는 쪽. Valid 신호와 Data를 보냅니다.
  • Destination (Receiver): 데이터를 받는 쪽. 받을 준비가 되었다는 Ready 신호를 보냅니다.

핵심 규칙 (The Handshake)

데이터 전송(Transfer)은 오직 ValidReady가 동시에 ‘1’일 때(High)만 일어납니다.

  • Valid=1, Ready=0: Sender는 데이터를 보냈지만, Receiver가 바빠서 못 받습니다. Sender는 Ready가 1이 될 때까지 현재 데이터를 유지해야 합니다(Wait).
  • Valid=0, Ready=1: Receiver는 받을 준비가 됐지만, 보낼 데이터가 없습니다. 대기합니다.
  • Valid=1, Ready=1: Handshake, 데이터가 전송됩니다. 다음 clock에 새로운 데이터를 보낼 수 있습니다.

2. 문제 발생: Ready 신호의 타이밍 악몽

Ready signal
Ready signal

이 간단한 프로토콜을 모듈 A -> 모듈 B -> 모듈 C로 길게 연결한다고 상상해 봅시다.

DataValid는 순방향(Forward)으로 흐르지만, Ready 신호는 역방향(Backward)으로 흐른다는 것이 문제입니다.

  • 모듈 C가 바빠지면(Ready_C=0), 모듈 B도 멈춰야 하고(Ready_B=0), 그래야 모듈 A도 멈춥니다(Ready_A=0).
  • 즉, Ready 신호는 모든 모듈을 거쳐 맨 앞단까지 조합 회로(Combinational Path)로 직결됩니다.

이 경로가 길어지면 Setup Time Violation이 발생하여 동작 주파수를 올릴 수 없습니다. 그렇다고 Ready 신호에 단순히 레지스터(Flip-flop)를 넣으면, timing이 한 clock 밀려서 이미 멈춰야 할 때 데이터를 하나 더 보내버리는 Data Loss 사고가 발생합니다.

3. 해결책: Skid Buffer란 무엇인가?

Skid Buffer는 ‘pipeline register’ 역할을 하면서도 ‘Valid-Ready handshake’를 완벽하게 지원하는 작은 버퍼 회로입니다.

동작 원리: “일단 받고 생각하자”

Skid Buffer는 타이밍 문제를 끊기 위해 입력단과 출력단 사이에 레지스터를 둡니다. 그런데 후단(Destination)이 갑자기 Ready=0을 보내면 어떻게 할까요?

  1. Skid Buffer는 내부적으로 보조 저장소(Shadow Register)를 가지고 있습니다.
  2. 후단이 멈추라고 하면(Ready_out=0), 전단(Source)에게 바로 멈추라고 하는 대신, 일단 데이터 한 개를 내부 저장소에 저장(Skid)해 둡니다.
  3. 그리고 다음 클럭에 전단에게 Ready_in=0을 보내 멈추게 합니다.

즉, ‘자동차가 급정거할 때 미끄러지는(Skid) 거리’만큼의 여유 공간을 둬서 충돌을 방지하는 원리입니다.

4. Skid Buffer의 종류와 Trade-off

① Half-Bandwidth Skid Buffer (Simple Register)

가장 단순한 형태입니다. 데이터가 오면 레지스터에 저장하고 보냅니다.

  • 특징: 데이터를 내보낸 후, 레지스터가 비워져야만 다음 데이터를 받습니다.
  • 단점: ValidReady가 핑퐁을 치면서 최대 처리량(Throughput)이 50%로 반토막 납니다. 매 클럭 데이터를 처리해야 하는 고속 설계에는 부적합합니다.

② Full-Bandwidth Skid Buffer (True Skid Buffer)

우리가 실무에서 써야 할 진짜 Skid Buffer입니다.

  • 구조: 메인 레지스터 + 백업 레지스터(Skid Register) + MUX
  • 동작:
    • 평소에는 메인 레지스터를 통해 데이터를 매 클럭(100% Throughput) 전달합니다.
    • 후단이 멈추면(Ready=0), 들어오는 데이터를 백업 레지스터에 잠시 피신시킵니다.
    • 후단이 다시 열리면, 백업해둔 데이터를 먼저 보내고 정상 모드로 돌아갑니다.
  • 장점: Ready 신호의 타이밍 경로를 끊으면서도(Pipeline), 성능 저하가 전혀 없습니다.

5. Verilog 구현의 핵심 (Full-Bandwidth)

직접 구현하려면 상태 머신(FSM)이 복잡해집니다. 보통은 아래와 같은 논리 구조를 가집니다.

// Skid Buffer 개념적 의사 코드 (Conceptual Code)
module skid_buffer (
    input clk, rst_n,
    // Upstream (Input)
    input  i_valid,
    output o_ready,
    input  [31:0] i_data,
    // Downstream (Output)
    output o_valid,
    input  i_ready,
    output [31:0] o_data
);
    // 내부 상태: 0=Empty, 1=Busy(Normal), 2=Full(Skid Active)
    reg [1:0] state; 
    reg [31:0] main_reg, skid_reg;

    // Output Logic
    assign o_valid = (state != 0); // 데이터가 있으면 Valid
    assign o_ready = (state != 2); // 꽉 차지 않았으면 받을 수 있음 (핵심!)

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= 0;
        end else begin
            case (state)
                0: if (i_valid) begin // Empty -> Load
                       main_reg <= i_data;
                       state <= 1; 
                   end
                1: begin // Normal Mode
                       if (i_ready && i_valid) main_reg <= i_data; // Pass-through
                       else if (!i_ready && i_valid) begin // Back-pressure! -> Skid
                           skid_reg <= i_data; // Save to Skid
                           state <= 2;
                       end
                       else if (i_ready && !i_valid) state <= 0; // Drain
                   end
                2: if (i_ready) begin // Full -> Output Skid
                       main_reg <= skid_reg; // Move Skid to Main
                       state <= 1;
                   end
            endcase
        end
    end
    assign o_data = main_reg;
endmodule

6. 결론: 언제 써야 하는가?

Skid Buffer는 만병통치약이 아닙니다. MUX와 추가 레지스터로 인해 면적(Area)을 더 차지합니다. 다음과 같은 상황에 전략적으로 사용해야합니다.

  1. Long Path Breaking: 모듈 간 거리가 멀어 배선 지연(Routing Delay)이 큰 경우.
  2. Timing Closure: Ready 신호 경로에서 Setup Violation이 발생할 때.
  3. Modular Design: IP를 설계할 때 외부와의 타이밍 의존성을 끊고 싶을 때 (Output 단에 Skid Buffer 배치).

참고: ZipCPU

Similar Posts