01signal.com

FPGA FIFO 소개

이 페이지는 FIFO에 관한 다섯 편의 시리즈 중 첫 번째 글입니다.

개요

FPGA FIFO는 개념이 단순한 메모리 소자입니다. 애플리케이션 로직의 한 부분이 FIFO의 한쪽 끝에서 데이터 워드를 쓰고, 반대쪽 끝에서는 애플리케이션 로직의 또 다른 부분이 그 워드들을 같은 순서로 읽습니다(FIFO = First In First Out).

이 데이터는 FIFO 내부에 저장됩니다. FIFO의 depth(깊이)는 저장할 수 있는 워드 수를 뜻합니다. 폭(width, 즉 워드당 비트 수)과 깊이는 사용자가 FIFO마다 설정하는 파라미터입니다.

FIFO는 아마 FPGA 설계에서 가장 흔히 쓰이는 IP 코어(IP core)일 것입니다. 어떤 로직이 데이터를 만들어내고 또 다른 로직이 그 데이터를 소비해야 할 때, 가장 먼저 떠오르는 해결책은 두 로직 사이에 FIFO를 넣는 것입니다(물론 FIFO가 항상 정답은 아닙니다...).

명령줄 인터페이스(특히 UNIX/Linux)에 익숙한 분들에게 FIFO 사용은 명령어 사이의 파이프(pipe)에 비유될 수 있습니다. 한 프로그램의 출력이 다른 프로그램의 입력이 되고, 그 사이의 마법 같은 장치가 나머지를 처리합니다.

FIFO가 워낙 널리 쓰이는 탓에, FPGA FIFO가 어떻게 동작해야 하는지에 대한 사실상의 합의가 존재합니다. 모든 FPGA 개발 소프트웨어는 애플리케이션 설계에서 사용할 수 있는 FIFO IP 모듈을 생성하는 기능을 제공합니다. 뿐만 아니라, 이 FIFO 모듈은 다른 FPGA FIFO와 똑같이 동작하는 특정 포트들을 갖게 될 가능성이 매우 높습니다.

FPGA 벤더가 제공하는 소프트웨어로 자신의 필요에 맞는 FIFO를 만들 수 있습니다. 어떤 GUI 도구에서 속성(폭, 깊이, 그리고 앞으로 설명할 다른 속성)을 설정하기만 하면 됩니다. 나머지는 도구가 처리해 줍니다. 여러분이 할 일은 설계에 해당 모듈을 인스턴스화(instantiation)하는 것뿐입니다. FPGA 분야의 다른 많은 작업과 달리, 이 작업은 정말로 그렇게 간단합니다.

각 FPGA 벤더가 저마다의 FIFO IP를 제공하므로, 문서의 세부 조항까지 읽는 것이 물론 중요합니다. FPGA 벤더마다 FIFO를 설명하는 용어가 조금씩 다르고, 모듈의 포트 이름도 조금씩 다릅니다. 또한 각 벤더가 제공하는 추가 기능과 설정 옵션도 조금씩 다릅니다.

그렇기는 해도, 기본 설정은 대체로 이 글에서 '기준 FIFO(baseline FIFO)'라고 부르는 것에 해당합니다. 거기에 더해, 언제나 제공되는 추가 기능 세트도 분명히 있습니다.

다만 FIFO를 로직 패브릭(logic fabric)에 구현하는 방식은 벤더마다 다르므로, FIFO 속성의 의미를 이해해야 FPGA 자원을 효율적으로 사용할 수 있습니다.

결국 FPGA FIFO를 알고 이해하는 것은 한 번만 투자하면 됩니다. 하나의 FPGA에서 사용법을 익히고 나면, 다른 FPGA에서도 똑같이 사용하는 것은 아주 쉽습니다. 바로 그 점 때문에 FIFO가 그토록 보편화된 것이기도 합니다.

기준 FIFO

FPGA FIFO에 대한 문서화된 표준은 없지만, 동작 방식에 대한 폭넓은 합의는 있습니다.

모든 FIFO에는 워드를 쓰는 인터페이스와 읽는 인터페이스라는 두 인터페이스가 있습니다. 이제 제가 '기준 FIFO'라고 부를 모듈의 인스턴스화를 살펴보겠습니다. 여기에는 나중에 다룰 중요한 변형들이 있습니다.

myfifo myfifo_ins
  (
   .rst(rst),       // Asynchronous reset input

   // Write interface ports
   .wr_clk(wr_clk), // Write clock input
   .wr_en(wr_en),   // Write Enable input
   .din(din),       // Write word input 
   .full(full),     // Full output

   // Read interface ports
   .rd_clk(rd_clk), // Read Clock input
   .rd_en(rd_en),   // Read Enable input
   .dout(dout),     // Read word output
   .empty(empty)    // Empty output
);

포트 이름은 Xilinx 도구에서 쓰는 이름이지만, 다른 FPGA 벤더도 비슷한 이름을 사용합니다.

FIFO 모듈의 포트는 세 그룹으로 나뉩니다. 리셋 신호(@rst)가 있고, 이 신호에 대해서는 나중에 다시 설명하겠습니다. 그리고 예상할 수 있듯이 네 개의 포트로 이루어진 쓰기 인터페이스와 읽기 인터페이스가 있습니다.

@din과 @dout는 FIFO로 들어가고 나오는 데이터 워드를 옮기는 두 개의 다중 비트(벡터) 포트입니다. 워드의 폭을 얼마로 할지는 FIFO를 설정할 때 관련 소프트웨어 도구를 통해 정합니다. FIFO의 depth(깊이), 즉 담을 수 있는 워드 수도 그때 설정합니다. 이 두 파라미터는 FIFO가 사용하는 FPGA 메모리 자원의 양에 영향을 줍니다.

클록

두 인터페이스는 각자 고유한 클록(@wr_clk, @rd_clk)을 가진다는 점에 주목할 만합니다. 각 인터페이스의 나머지 포트들은 각각의 이 두 클록에 동기화되어 있습니다.

FIFO는 두 개의 클록을 갖기 때문에, 데이터를 한 클록 도메인(clock domain)에서 다른 클록 도메인으로 옮기는 데 자주 쓰입니다. 설계의 어떤 로직이 clk_A와 동기화되어 있고 다른 로직이 clk_B와 동기화되어 있다면, 이 둘을 어떻게 함께 동작시킬까요? FPGA 엔지니어라면 가장 먼저 두 로직 사이에 FIFO를 넣는 방법을 떠올립니다. 특히 클록 도메인 간 이동(clock domain crossing)이 골칫거리일 때, FIFO를 쓰면 문제를 쉽고 안전하게 해결할 수 있기 때문입니다.

쓰기 인터페이스

쓰기 인터페이스는 단순합니다. @wr_clk, @wr_en, @din은 FIFO 입력이고, @full은 출력입니다.

@wr_clk의 상승 에지에서 @wr_en이 high이면 @din의 데이터가 FIFO에 쓰입니다. @full 포트는 FIFO가 full(가득 찬 상태)일 때 high가 됩니다.

예를 들어, 다음은 FIFO에 다섯 개의 워드를 쓰는 파형입니다.

Example waveform for writing words to a FIFO

이 파형에서 애플리케이션 로직은 먼저 D0과 D1 워드를 씁니다. FIFO는 D1을 성공적으로 쓴 후 자신이 full(가득 찬 상태)이 되었음을 알리기 위해 @full 출력을 high로 올립니다. 애플리케이션 로직은 이에 응답하여 같은 클록 사이클 동안 @wr_en을 low로 내립니다. 몇 클록 사이클이 지난 뒤, FIFO는 반대쪽에서 데이터가 읽히는 등의 활동이 있었기 때문에 @full을 low로 바꾸어 다시 써도 된다는 것을 알립니다.

애플리케이션 로직은 @full이 low로 바뀐 클록 사이클에 바로 쓰기를 시작할 수도 있었지만(이 예시에서는) 조금 늦게 시작했습니다. 파형에서 보듯, 그 후 세 개의 워드가 추가로 쓰입니다.

파형에서 @din이 'Dx'로 표시된 구간은 해당 값이 무시된다는 뜻이므로, 거기에 어떤 값이 있든 결과는 같습니다. 예를 들어 D1과 D2 사이의 'Dx' 구간에서 @din은 D1을 유지해도 되고, 표시된 것보다 더 일찍 D2로 바뀌어도 되며, 전혀 다른 값으로 바뀌어도 무방합니다. 결과는 동일합니다.

간단한 코딩 예로, 가능할 때마다 FIFO를 1씩 증가하는 워드로 채우고 싶다고 가정해 보겠습니다.

assign wr_en = !full;

always @(posedge wr_clk)
  if (wr_en)
    din <= din + 1;

이 예는 @full과 @wr_en 사이의 올바른 관계를 보여줍니다. @full이 high이면 같은 클록 사이클에서 @wr_en은 low여야 합니다. 만약 그렇지 않으면 어떻게 될까요? @full 신호를 무시하면 어떻게 될까요? 아마 FIFO는 이 경우 @wr_en을 무시할 것입니다. 즉, 마치 wr_en 포트가 다음과 같이 정의된 @the_real_wr_en에 연결된 것처럼 동작할 것입니다.

assign the_real_wr_en = wr_en && !full;

하지만 일부 FPGA 도구는 이 안전장치 없이 FIFO를 설정할 수 있게 해줍니다. 그런 경우 FIFO가 full(가득 찬 상태)일 때 데이터를 쓰려 하면 사실상 어떤 일이라도 일어날 수 있습니다.

어느 쪽이든, @full 신호를 존중해야 합니다. 그렇지 않으면 데이터가 새어 나간 것처럼 보일 것입니다. 위 예를 다시 보면, @wr_en이 항상 high였다면 @din은 데이터가 실제로 FIFO에 쓰였는지와 상관없이 계속 1씩 증가했을 것입니다. 그래서 반대쪽에서 데이터를 읽으면 증가값이 중간에 끊긴 것처럼 보일 것입니다.

@full은 쓰기 사이클의 결과로만 low에서 high로 바뀔 수 있습니다. 즉 @wr_en이 high인 상승 클록 에지 직후에만 그렇게 바뀝니다. 아래에서 설명할 FIFO 리셋 때는 예외입니다.

읽기 인터페이스

읽기 인터페이스도 쓰기 인터페이스와 상당히 비슷하지만 완전히 같지는 않습니다. @rd_clk와 @rd_en은 FIFO 입력이고, @dout와 @empty는 출력입니다.

@rd_clk의 상승 에지에서 @rd_en이 high이면 FIFO 메모리에서 새 워드가 읽힙니다. 그러면 @dout는 그 상승 에지 이후에, 즉 다음 클록 사이클에서 그 값으로 갱신됩니다. @empty 포트는 FIFO가 empty(비어 있음) 상태일 때 high입니다.

이 예시 파형에서는 FIFO에서 다섯 워드를 읽습니다.

Example waveform for reading words from a FIFO

이 파형에서 애플리케이션 로직은 먼저 세 개의 워드를 읽습니다. @empty가 high로 바뀌면(그와 함께 @dout에 D2가 나타납니다) 애플리케이션 로직은 같은 클록 사이클에서 @rd_en을 low로 바꿉니다. 앞에서와 마찬가지로, @empty가 low로 바뀐 클록 사이클(반대쪽에서 FIFO에 데이터가 쓰였기 때문입니다)에 @rd_en을 다시 high로 바꾸어도 괜찮았을 것입니다. 하지만 이 예시에서는 몇 클록 사이클을 기다린 뒤 두 개의 워드를 추가로 읽었습니다.

이제 사소한 참고 사항 하나: 이 파형을 바로 위 파형과 비교하면 다섯 워드가 쓰였고 다섯 워드가 읽힌 것을 알 수 있습니다. 그런데 D4가 나타날 때 @empty는 왜 high가 되지 않았을까요? 그것은 FIFO가 empty(비어 있음) 상태가 아니어도 읽기를 멈추어도 된다는 점을 보여주고 싶었기 때문입니다. 즉, 이 가상의 예에서는 FIFO에 워드가 더 쓰였다고 가정하므로 D4를 읽은 뒤에도 FIFO는 empty(비어 있음) 상태가 아니었던 것입니다.

@rd_en이 low일 때 @dout는 이전 값을 유지합니다. 애플리케이션 로직은 이 성질을 활용할 수 있습니다. @dout는 리셋 이후를 제외하면 FIFO에서 마지막으로 읽은 워드의 값을 항상 담고 있습니다.

더 중요한 것은, @dout의 새 값이 @rd_en이 high인 상승 에지 이후에 나타난다는 점입니다. 따라서 FIFO는 다음 Verilog 코드처럼 동작합니다.

always @(posedge rd_clk)
  if (rd_en && !empty)
    dout <= next_word_to_show;

이 가짜 Verilog 코드는 대부분의 FIFO가 같은 클록 사이클에서 @empty가 high이면 @rd_en을 무시한다는 사실도 보여줍니다. 쓰기 인터페이스와 마찬가지로, @empty가 high인 클록 사이클에서 @rd_en도 high가 되어서는 안 됩니다. 다시 말하지만, 일부 FIFO는 이 보호 기능이 없도록 설정할 수 있으므로 이 규칙을 어기지 마세요.

@empty는 읽기 사이클 후에만 low에서 high로 바뀔 수 있습니다. 정확히는 클록의 상승 에지에서 @rd_en이 high였던 후에만 바뀝니다. 유일한 예외는 FIFO가 리셋될 때입니다.

실제 예로, 다음은 (리셋 없이) FIFO에서 워드를 읽어 누적 합계를 계산하는 간단한 Verilog 코드 조각입니다.

assign rd_en = want_to_read_now && !empty;

always @(posedge rd_clk)
  begin
    rd_en_d <= rd_en;

    if (rd_en_d)
      sum <= sum + dout; // Don't try this at home: @sum is never reset.
  end

설명을 위해 @want_to_read_now 신호를 추가했습니다. 이 신호는 로직이 읽기를 원하는지를 나타냅니다. 그런데도 @rd_en은 FIFO가 empty(비어 있음) 상태가 아닐 때만 high가 됩니다.

@rd_en_d에 주목하세요. 이 신호는 @rd_en의 값을 한 클록 사이클 지연시킨 것입니다. 따라서 @rd_en_d는 @dout에 새롭고 유효한 값이 나타나는 시점에 high가 됩니다. @dout의 값을 소비하는 조건으로 @rd_en_d를 사용하는 이유가 바로 이것입니다. 이 예에서 보듯, @rd_en과 @dout 사이의 지연 때문에 상황이 다소 까다로워질 수 있습니다.

동기화와 지연

앞에서 쓰기와 읽기 파형을 따로 그렸기 때문에 중요한 한 가지가 빠져 있었습니다. empty(빈) FIFO에 첫 워드를 쓴 뒤 @empty가 low로 바뀌기까지 몇 클록 사이클이 걸립니다. 마찬가지로 full(가득 찬) FIFO에서 첫 워드를 읽은 뒤 @full이 low로 바뀌기까지도 몇 클록 사이클이 걸립니다.

이런 지연이 생기는 이유는 FIFO에 쓰기가 일어났다는 정보가 반대편에 도달하기 전에 두 클록 도메인을 가로질러 전파되어야 하기 때문입니다. 클록 도메인을 건너는 데 필요한 로직 때문에 몇 클록 사이클의 지연이 발생하고, 그 결과 @empty 포트가 약간 늦게 응답합니다. @full 포트도 마찬가지입니다.

그렇다면 이 지연은 몇 클록 사이클일까요? 그 순간의 두 클록 에지 사이의 시간 관계를 비롯한 많은 요소에 따라 달라집니다. 요컨대 정확히 말하기 어렵습니다.

지연에 영향을 주는 요소 중에는 동기화 스테이지(synchronization stage) 수가 있습니다. 이는 FIFO에서 설정할 수 있는 파라미터인 경우가 많습니다. 보통 2단계를 선택하지만, 더 큰 수도 선택할 수 있습니다. 동기화 스테이지 수를 늘리면 FIFO의 신뢰성을 높이는 데 도움이 되는 대신 로직 자원을 조금 더 사용하고, 방금 말한 대로 @empty 포트와 @full 포트의 응답 지연도 커집니다.

그러니 정말 FIFO를 아낌없이 대접하고 싶다면 동기화 스테이지를 3단계로 늘려 아주아주 안전하다고 느껴도 됩니다.

리셋 입력

모든 FPGA FIFO에는 리셋 신호가 있습니다. FIFO는 두 개의 클록을 사용하므로, 이 리셋 신호는 어느 한 클록에도 동기화되어 있지 않을 것으로 예상됩니다. 그래서 비동기(asynchronous)입니다. FIFO 내부 로직이 두 클록 도메인 각각에 맞춰 리셋을 내부적으로 동기화합니다.

그럼 리셋은 어떤 일을 할까요? 우선 FIFO를 empty(비어 있음) 상태로 만들고 @empty를 high로 설정합니다. FIFO에 데이터가 있었다면 그 데이터는 잃게 됩니다.

@full 출력의 경우, 리셋 후 FIFO가 데이터를 받을 준비가 될 때까지(즉 쓰기 동작이 이루어질 수 있게 될 때까지) 이 출력을 high로 유지하는 것이 일반적이며 권장되기도 합니다. 하지만 이 동작은 선택 사항일 수 있으므로 FIFO 문서에서 확인하는 것이 좋습니다. 어차피 리셋 직후의 FIFO는 실제로 full(가득 찬) 상태가 아닙니다. 또 리셋 때문에 @full이 high로 바뀌는 것은 앞서 언급한 규칙, 즉 @full은 오직 데이터 쓰기의 결과로만 high로 바뀌어야 한다는 규칙을 어기는 것이기도 합니다.

리셋 신호가 활성화된 순간부터 @empty 포트와 @full 포트가 high로 바뀌기까지 몇 클록 사이클이 걸린다는 점을 알아야 합니다. FIFO 내부의 동기화 로직 때문입니다. 따라서 리셋이 활성화되는 전후 몇 클록 사이클 동안 상태가 다소 불명확합니다. 리셋 전후 몇 클록 사이클 동안 애플리케이션 로직이 FIFO에 쓰기나 읽기를 시도하지 않도록 하세요.

리셋 신호는 비동기일지라도 FPGA의 레지스터(플립플롭) 출력이어야 합니다. 조합 논리(combinatorial logic) 출력을 리셋으로 쓰면 안 됩니다. 글리치 때문에 FIFO가 의도하지 않은 리셋을 받을 수 있기 때문입니다.

사실 많은 FPGA 엔지니어가 리셋 포트에 거의 무엇이든 연결하면 동작할 것이라고 잘못 생각합니다. 하지만 FPGA 벤더는 리셋 신호에 대해 예상 밖의 요구 사항을 제시하기도 합니다. 예를 들어, 다음은 Xilinx의 FIFO 제품 가이드(PG057)에서 인용한 내용입니다.

비동기 리셋(asynchronous reset)의 폭이 가장 느린 클록 한 사이클이고, 그 활성화가 가장 느린 클록의 상승 에지에 아주 가깝게 발생하면 리셋 검출이 제대로 이루어지지 않아 예기치 않은 동작이 나타날 수 있습니다. 이런 상황을 피하려면 비동기 리셋을 최소한 3 [ ... ] 클록 사이클(가장 느린 클록 기준) 동안 활성화하는 것이 항상 권장됩니다.

(제3장, "Resets")

그러니까 Xilinx는 리셋을 최소한 세 클록 사이클 동안 활성화할 것을 권장합니다. 이 권장 사항을 아는 사람이 얼마나 될지는 모르겠습니다. 어쨌든 여러분이 사용하는 FPGA 벤더의 FIFO 사용자 가이드에서 이 리셋 신호를 올바르게 생성하는 방법을 꼭 읽어 보시길 바랍니다.

FIFO의 구현 방식

벤더의 소프트웨어 도구가 FIFO가 올바르게 동작하도록 모든 것을 처리해 주지만, FPGA의 어떤 자원이 사용되는지 알아두는 것이 좋습니다. 특히 특정 종류의 자원이 부족해지는 것을 피하는 데 유용합니다.

FPGA마다 지원하는 방식이 다르지만, 여기서는 몇 가지 일반적인 방식을 간단히 언급하겠습니다.

이것으로 FIFO에 관한 이 시리즈의 첫 페이지를 마칩니다. 다음 페이지에서는 FIFO의 흔한 변형들과 추가 기능에 대해 다룹니다.

이 페이지는 영어 원문을 기계 번역한 것입니다. 의문이 드는 부분이 있으면 원문을 참조하시기 바랍니다.
Copyright © 2021-2026. All rights reserved. (dcc38493)