01signal.com

레지스터 추가로 FIFO 타이밍 개선하기

개요

이 페이지는 FIFO에 관한 시리즈의 마지막 글로, 기존 FIFO를 다른 FIFO로 변형하는 방법을 보여드립니다. 먼저 '표준 FIFO(standard FIFO)'를 FWFT FIFO로 바꾸는 법과 그 반대로 바꾸는 법을 다룬 뒤, FIFO의 타이밍(timing)을 개선하는, 즉 더 높은 주파수에서 동작하게 만드는 몇 가지 고급 기법을 소개합니다.

실용적인 관점에서 보면, 타이밍 제약 조건(timing constraints)을 달성하는 데 문제가 생겼고 그 문제가 FIFO와 관련된 상황이 아니라면 이 페이지를 읽는 것은 꽤 무의미합니다. 이 페이지의 내용은 어렵고, FIFO를 일상적으로 사용할 때 꼭 필요한 내용은 아닙니다. 그럼에도 데이터를 처리하는 로직, 특히 데이터 흐름을 다루는 로직을 설계할 때 쓸 감각을 단련하는 연습으로서 시간을 들일 만한 가치는 있습니다.

직접 관련된 내용은 아니지만, 외부 메모리(보통 DDR 메모리지만, AXI 인터페이스로 감싸여 있는 어떤 것이든 무방합니다)를 활용해 아주 깊은 FIFO를 만드는 방법을 보여주는 다른 페이지가 있습니다. 이 기법의 좋은 점은, 이 거대한 FIFO가 사용하는 외부 메모리만큼 깊어질 수 있음에도 불구하고 애플리케이션 로직에서는 그런 사실이 전혀 드러나지 않는다는 것입니다. 기준 FIFO(baseline FIFO)와 동일한 인터페이스를 갖기 때문입니다.

참고로 이 페이지의 Verilog 코드는 제가 여러 해 전에 작성한 것이라, 코딩 스타일이 지금과 조금 다릅니다.

표준 FIFO에서 FWFT FIFO로

앞선 페이지에서 살펴본 FWFT FIFO에 대해 간단히 다시 정리해 보겠습니다. '표준 FIFO'에서 @empty(비어 있음) 포트가 low라는 것은, 클록의 상승 에지에서 @rd_en이 high가 된 다음에야 FIFO 출력에 유효한 데이터가 나타난다는 뜻입니다. FWFT FIFO는 데이터가 준비되는 즉시 출력에 내보내므로, @empty(비어 있음) 신호가 low라는 것은 출력의 데이터가 유효하다는 뜻입니다.

@rd_en의 의미도 다릅니다. '표준 FIFO'에서는 '데이터를 내놓으라'는 뜻이지만, FWFT FIFO에서는 대략 '방금 데이터를 사용했으니, 있으면 다음 데이터를 내놓으라'는 뜻에 가깝습니다.

다음은 '표준 FIFO'를 FWFT FIFO로 바꿔 주는 모듈입니다. 놀랍지 않게도, 이 모듈은 @rd_en과 @empty(비어 있음)만 조작합니다. 나머지 신호는 그대로 통과시킵니다.

module basic_fwft_fifo(rst,
                       rd_clk, rd_en, dout, empty,
                       wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg                   dout_valid;
   wire                  fifo_rd_en, fifo_empty;

   // orig_fifo is just a normal (non-FWFT) synchronous or asynchronous FIFO
   fifo orig_fifo
      (
       .rst(rst),
       .rd_clk(rd_clk),
       .rd_en(fifo_rd_en),
       .dout(dout),
       .empty(fifo_empty),
       .wr_clk(wr_clk),
       .wr_en(wr_en),
       .din(din),
       .full(full)
       );

   assign fifo_rd_en = !fifo_empty && (!dout_valid || rd_en);
   assign empty = !dout_valid;

   always @(posedge rd_clk or posedge rst)
      if (rst)
         dout_valid <= 0;
      else
         begin
            if (fifo_rd_en)
               dout_valid <= 1;
            else if (rd_en)
               dout_valid <= 0;
         end
endmodule

평소라면 여기서 코드를 설명하겠지만, 그렇게 하면 이전 페이지에서 FWFT FIFO에 대해 했던 설명이 반복될 뿐입니다.

FWFT FIFO에서 표준 FIFO로

이 변환은 정말 간단합니다. FWFT FIFO에서 @empty(비어 있음)가 low라는 것은 출력 포트에 데이터가 있다는 뜻이므로, @rd_en이 high일 때 이 데이터를 샘플링(sampling)하는 레지스터를 만들면 됩니다.

즉, 아래와 같습니다.

module standard_fifo(rst,
                     rd_clk, rd_en, dout, empty,
                     wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg [(width-1):0]     dout;
   wire [(width-1):0]    dout_w;

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

   fwft_fifo wrapper
     (
      .wr_clk(wr_clk),
      .rd_clk(rd_clk),
      .rst(rst),
      .din(din),
      .wr_en(wr_en),
      .rd_en(rd_en && !empty),
      .dout(dout_w),
      .full(full),
      .empty(empty)
      );
endmodule

여기서는 @dout만 손댄다는 점에 유의하세요. @empty(비어 있음)는 그대로 통과됩니다. @empty(비어 있음)가 high이면 @dout_w가 유효하지 않으므로 @dout이 그 값을 샘플링(sampling)할 수 없습니다.

타이밍 개선을 위한 요령

이제 FIFO에 관한 이 시리즈의 대미를 장식할 시간입니다. 읽기에 가장 어려운 부분이기도 합니다.

그러다 보면 FPGA 설계가 타이밍 제약 조건(timing constraints), 즉 목표 클록 주파수에 도달하지 못하는 이유를 분석할 때마다, 임계 경로(critical path)가 FIFO에서 시작하거나 끝나는 경우가 있다는 것을 발견하게 됩니다. 먼저 해결하기 쉬운 사례들부터 살펴보고, 마지막에 가장 까다로운 문제를 다루겠습니다.

@empty(비어 있음) 또는 @full(가득 찬 상태)이 임계 경로에 있을 때

@empty(비어 있음) 신호와 @full(가득 찬 상태) 신호는 임계 경로에 나타날 수 있습니다. 특히 @wr_en과 @rd_en이 이들 신호의 조합 함수(combinatorial function)로 만들어질 때 그렇습니다. 주된 이유는 이러한 신호들이 단순히 FIFO에 쓰기 동작이나 읽기 동작을 요청하는 용도로만 쓰이지 않고, 데이터를 소비하거나 생산하는 애플리케이션 로직의 인에이블(enable) 신호 역할도 하기 때문입니다. 데이터 흐름이 멈추면 로직도 함께 멈추어야 하니까요.

그래서 @wr_en과 @rd_en에 의존하는 논리식이 많아지는 경우가 흔하고, 논리 함수도 꽤 복잡해지는 경우가 많습니다. 그 결과 팬아웃(fan-out)이 커집니다. 이런 모든 것이 결국 문제가 되는 전파 지연(propagation delay)으로 이어집니다.

제대로 작성된 FIFO에서 @empty(비어 있음)와 @full(가득 찬 상태)은 모두 플립플롭(flip-flop)의 출력이므로, 그 자체로 개선할 여지가 많지 않습니다. 하지만 FPGA 소프트웨어가 FIFO를 합성된 넷리스트(netlist) 형태로 제공하는 경우가 많기 때문에, 팬아웃을 줄이기 위해 이 레지스터들을 복제하는 것은 불가능하거나 적어도 어렵습니다. 게다가 FPGA의 로직 패브릭(logic fabric) 위에서 이 레지스터들과 그 출력값을 사용하는 애플리케이션 로직 사이의 물리적 거리가 멀어질 수 있습니다. 큰 FPGA에서는 이것이 경로 지연에 결정적인 영향을 주기도 합니다.

이 문제에 대한 해결책은 이 페이지에서 @almost_empty와 @almost_full을 논의할 때 이미 제시했습니다. 이 포트들을 사용하면 @wr_en과 @rd_en이 레지스터 출력이 될 수 있습니다. 이렇게 하면 조합 함수 문제가 해결되고, 이들 신호의 팬아웃을 제어할 수도 있습니다. 게다가 이들 레지스터를 그 값을 소비하는 로직에 더 가깝게 배치하도록 도구에 도움을 주므로, 전파 지연을 줄이는 데도 도움이 됩니다.

@wr_en 또는 @din이 임계 경로에 있을 때

이 상황은 분명히 가장 풀기 쉬운 경우입니다. 레지스터 한 겹만 추가하면 됩니다. 예를 들어 다음과 같이 말이죠.

always @(posedge wr_clk)
  begin
    wr_en_reg <= wr_en;
    din_reg <= din;
  end

그런 다음 @wr_en_reg와 @din_reg를 FIFO에 연결하면 됩니다. FIFO의 오버플로(overflow)를 막으려면 @full(가득 찬 상태) 대신 @almost_full을 사용해야 합니다. 더 일반적으로 말하면, FIFO를 채우는 기준값을 하나 줄여야 합니다.

@rd_en 또는 @dout이 임계 경로에 있을 때

이제 진짜 어려운 문제가 나옵니다. 이 문제는 풀기가 상대적으로 어려울 뿐만 아니라, 실제로 가장 자주 발생하는 문제이기도 합니다. 여기에는 몇 가지 이유가 있습니다.

@dout에 관해서는:

따라서 목표는 @rd_en과 FIFO 내부 로직 사이의 조합 경로(combinatorial path)를 없애고, @dout에 대해서도 마찬가지로 조합 경로를 없애는 것입니다.

@dout의 조합 경로만 끊기

이것을 실제 해결책으로 제시하려는 것은 아닙니다. 다만 이 논의는 다음 단계를 이해하기 위한 준비 과정으로 도움이 될 수 있습니다. 혹시 헷갈리기만 한다면 이 절은 건너뛰어도 됩니다.

그럼 @dout의 조합 경로만 끊고 싶다고 가정해 보겠습니다. 위에서 보여준 FWFT FIFO를 '표준 FIFO'로 변환하는 래퍼(wrapper) 모듈이 정확히 그 일을 한다는 점에 유의하세요. 레지스터를 하나 추가함으로써 @dout의 조합 경로를 끊습니다. 다만 시작점이 FWFT FIFO여야 한다는 제약이 있습니다.

하지만 '표준 FIFO'를 FWFT FIFO로 변환하는 래퍼 모듈도 있었습니다. 그러니 FIFO를 앞뒤로 변환하면 되지 않을까요? 아니면 동일한 역할을 하는 단일 모듈을 작성하면 될까요? 어느 쪽이든, 이런 형태의 해결책은 @rd_en의 상황을 악화시킵니다.

하지만 이 해결책을 더 자세히 볼 가치는 있습니다. FWFT FIFO로의 변환은 내부 FIFO의 @dout이 언제 유효한지 추적하고, @dout이 유효하지 않을 때(그리고/또는 외부 @rd_en이 high일 때) @fifo_rd_en을 high로 유지하는 것에 불과했습니다.

다시 '표준 FIFO'로 되돌리는 변환은 @rd_en이 high일 때 내부 FIFO의 @dout 값을 레지스터로 복사하는 방식으로 이루어졌습니다.

요컨대 첫 번째 메커니즘은 가능할 때 내부 FIFO의 @dout을 유효하게 유지했고, 두 번째 메커니즘은 외부 @rd_en이 요청할 때 @dout을 또 다른 레지스터로 복사했습니다.

하지만 이것으로 @rd_en의 조합 경로 문제는 해결되지 않습니다. 연속 읽기를 허용하려면, 외부 @rd_en이 high인 모든 클록에서 원래 FIFO로부터 워드 하나가 읽혀야 합니다. 그렇지 않으면 FWFT의 @dout은 소비되었는데 갱신되지 않았기 때문에 무효 상태가 됩니다. 따라서 내부 FIFO의 @rd_en은 외부 @rd_en의 조합 함수일 수밖에 없습니다. 이 문제를 바꾸려면 다음 절에서 보여주듯 @dout 경로에 레지스터를 하나 더 추가해야 합니다.

reg_fifo로 두 조합 경로 모두 끊기

더 이상의 설명 없이, @rd_en과 @dout의 조합 경로를 끊어 주는 reg_fifo 모듈은 다음과 같습니다.

module reg_fifo(rst,
                rd_clk, rd_en, dout, empty,
                wr_clk, wr_en, din, full);

   parameter width = 8;

   input                 rst;
   input                 rd_clk;
   input                 rd_en;
   input                 wr_clk;
   input                 wr_en;
   input [(width-1):0]   din;
   output                empty;
   output                full;
   output [(width-1):0]  dout;

   reg                   fifo_valid, middle_valid;
   reg [(width-1):0]     dout, middle_dout;

   wire [(width-1):0]    fifo_dout;
   wire                  fifo_empty, fifo_rd_en;
   wire                  will_update_middle, will_update_dout;

   // orig_fifo is "standard" (non-FWFT) FIFO
   fifo orig_fifo
      (
       .rst(rst),
       .rd_clk(rd_clk),
       .rd_en(fifo_rd_en),
       .dout(fifo_dout),
       .empty(fifo_empty),
       .wr_clk(wr_clk),
       .wr_en(wr_en),
       .din(din),
       .full(full)
       );

   assign will_update_middle = fifo_valid && (middle_valid == will_update_dout);
   assign will_update_dout = rd_en && !empty;
   assign fifo_rd_en = !fifo_empty && !(middle_valid && fifo_valid);
   assign empty = !(fifo_valid || middle_valid);

   always @(posedge rd_clk)
      if (rst)
         begin
            fifo_valid <= 0;
            middle_valid <= 0;
            dout <= 0;
            middle_dout <= 0;
         end
      else
         begin
            if (will_update_middle)
               middle_dout <= fifo_dout;

            if (will_update_dout)
               dout <= middle_valid ? middle_dout : fifo_dout;

            if (fifo_rd_en)
               fifo_valid <= 1;
            else if (will_update_middle || will_update_dout)
               fifo_valid <= 0;

            if (will_update_middle)
               middle_valid <= 1;
            else if (will_update_dout)
               middle_valid <= 0;
         end
endmodule

가장 먼저 주목할 점은 @dout이 이 모듈에서 정의된 레지스터이고, @rd_en이 이 레지스터를 갱신하게 만든다는 것입니다. 그리고 이 두 신호를 내부 FIFO에 연결된 비슷한 신호들, 즉 @fifo_dout 및 @fifo_rd_en과 혼동하지 않는 것이 중요합니다.

이제 이 모듈이 어떻게 동작하는지 살펴보겠습니다.

파이프라인 이해하기

이 구조는 본질적으로 스키드 버퍼(skid buffer)입니다. 유효 플래그(valid flag)를 갖춘 2단계 파이프라인(pipeline)으로, 연속적인 데이터 흐름을 허용하면서도 외부 @rd_en과 내부 FIFO의 읽기 인에이블을 분리합니다.

FWFT FIFO로 변환하는 모듈에서와 마찬가지로, 여기서도 일반 FIFO인 orig_fifo의 인스턴스화(instantiation)가 있습니다. reg_fifo 모듈의 로직은 @fifo_dout에 유효한 값이 들어 있지 않을 때 orig_fifo에서 워드를 읽어 @fifo_dout의 값을 유효하게 유지하려고 시도합니다. 여기에 더해 @middle_dout이라는 두 번째 레지스터도 있습니다. 이 레지스터도 가능하면 @fifo_dout의 값을 가져와 유효하게 유지하려고 합니다.

따라서 @fifo_dout, @middle_dout, @dout을 orig_fifo 안의 데이터를 앞으로 이동시키는 파이프라인(pipeline)으로 볼 수 있습니다.

이 파이프라인 단계들이 언제 유효한지를 추적하는 레지스터가 두 개 있습니다. @fifo_valid는 @fifo_dout이 유효할 때 high이고, @middle_valid는 @middle_dout이 유효할 때 high입니다.

이 파이프라인의 목적은 중간 단계를 우회(bypass)할 수 있는 능력입니다. @rd_en이 high이고 @empty(비어 있음)가 low일 때 @dout은 @middle_dout 또는 @fifo_dout 중 하나에서 새 값을 가져오는데, 항상 @middle_dout을 우선합니다. 즉, @middle_dout이 유효하면 @dout은 @middle_dout을 사용하고, 그렇지 않으면 @fifo_dout을 대신 사용합니다. 이것이 어떻게 @rd_en의 조합 경로를 끊는 핵심이 되는지는 뒤에서 설명합니다.

그럼 먼저 구현의 세부 사항을 살펴보겠습니다. FIFO의 데이터 출력에서 reg_fifo의 출력 레지스터까지는 두 개의 분리된 경로가 있습니다. 이 경로들은 그림에서 좌우로 각각 나타나 있습니다.

Data flow with extra registers

두 파이프라인 단계(@fifo_dout 및 @middle_dout) 중 어느 것도 유효하지 않으면, @empty(비어 있음)가 high가 되어 가져올 데이터가 없음을 알립니다.

assign empty = !(fifo_valid || middle_valid);

파이프라인 단계들을 유효하게 유지하려는 시도는 다음 코드에 반영되어 있습니다.

assign fifo_rd_en = !fifo_empty && !(middle_valid && fifo_valid);

이 코드는 두 파이프라인 단계 중 어느 하나라도 유효하지 않으면, 가능할 때 orig_fifo에서 읽으라는 뜻입니다. 만약 @fifo_dout이 이미 유효하다면, @fifo_dout이 갱신되는 동시에 그 값이 @middle_dout으로 복사됩니다. (자세한 내용은 아래에서 다룹니다.)

이제 @will_update_* 쌍의 정의를 살펴보겠습니다.

assign will_update_middle = fifo_valid && (middle_valid == will_update_dout);
assign will_update_dout = rd_en && !empty;

먼저 @will_update_dout이 @rd_en에 @empty(비어 있음)가 high일 때 reg_fifo에서 읽지 못하도록 하는 안전장치를 더한 것과 같다는 점에 유의하세요.

다음으로 @middle_dout의 갱신을 제어하는 @will_update_middle이 있습니다. 코드는 다음과 같습니다.

always @(posedge rd_clk)
  if (will_update_middle)
     middle_dout <= fifo_dout;

위의 @will_update_middle 정의를 보면, @middle_dout을 갱신하기 위한 조건이 두 가지입니다. 하나는 @fifo_dout 값이 유효해야 한다는 것으로, 이는 아주 당연합니다. 또 하나는 (middle_valid == will_update_dout)이라는 식입니다. 이 식이 전체 메커니즘을 설명해 주므로 네 가지 가능한 경우로 나누어 보겠습니다. 그리고 이 모든 것은 @fifo_dout이 유효할 때만 의미가 있다는 점을 기억하세요.

@middle_valid와 @fifo_valid가 동시에 high이면 @fifo_rd_en이 low가 된다는 점에 유의하세요. 그 결과, 마지막 두 경우의 시나리오가 발생할 때는 orig_fifo에서 데이터를 가져오지 않습니다.

특히 두 파이프라인 단계가 모두 유효하고 @rd_en이 high이면 @fifo_dout의 값이 @middle_dout으로 복사됩니다. 그리고 @fifo_rd_en이 low이므로 @fifo_valid는 다음 클록 사이클에 low로 바뀝니다. 이것은 문제없습니다. @middle_valid가 high로 유지되므로, 필요하다면 그다음 클록 사이클에 데이터를 공급할 수 있기 때문입니다. 그다음 클록 사이클에서는 @fifo_valid가 다시 high가 됩니다. (orig_fifo에 데이터가 있다면 말이죠.)

그렇다면 이런 특정 상황에서 @fifo_dout을 유효하게 유지하도록 @fifo_rd_en을 정의하지 않는 이유는 무엇일까요? 그렇게 하면 @fifo_rd_en이 @rd_en의 조합 함수여야 하기 때문입니다. 그런 의존성을 없애기 위해 이 2단계 파이프라인을 설계한 것입니다.

이 정도 이해했으면, 이제 @dout이 어떻게 정의되는지 살펴볼 차례입니다. 리셋을 제외한 정의는 다음과 같습니다.

always @(posedge rd_clk)
  if (will_update_dout)
    dout <= middle_valid ? middle_dout : fifo_dout;

@will_update_dout을 그 정의로 치환하면 다음과 같이 됩니다.

always @(posedge rd_clk)
  if (rd_en && !empty)
    dout <= middle_valid ? middle_dout : fifo_dout;

이 방식은 FWFT FIFO를 '표준 FIFO'로 변환하는 것과 비슷하지만, 선택할 수 있는 소스가 두 개라는 점이 다릅니다. @middle_dout에 유효한 값이 있으면 그 값을 가져옵니다. 그렇지 않으면 @fifo_dout을 사용합니다. 둘 다 유효하지 않으면 @empty(비어 있음)가 high이므로 어차피 아무 일도 일어나지 않습니다.

이것이 왜 도움이 될까요? 출력 타이밍 측면에서 보면 @dout은 분명히 레지스터입니다. @rd_en에 관해서는, @fifo_rd_en이 레지스터인 @middle_valid와 @fifo_valid에만 의존한다는 점에 유의하세요. 여기에 더해 orig_fifo 자체의 출력인 @fifo_empty에도 의존합니다. 이 조합 경로는 피할 수 없습니다. 따라서 @fifo_rd_en은 외부 @rd_en에 의존하지 않으므로, @rd_en에서 orig_fifo로 가는 조합 경로가 없습니다.

파이프라인 단계 레지스터들의 유효성 추적

전체 그림을 완성하기 위해 설명하면, 두 *_valid 플래그는 해당 레지스터에 유효한 데이터가 들어 있는지를 알려줍니다. @fifo_valid에 관한 코드는 다음과 같습니다.

 if (fifo_rd_en)
   fifo_valid <= 1;
 else if (will_update_middle || will_update_dout)
   fifo_valid <= 0;

이것은 위에서 '표준 FIFO'를 FWFT FIFO로 변환할 때 본 @dout_valid의 정의와 비슷합니다. 상승 에지에서 @fifo_rd_en이 high이면 그 결과 @fifo_valid도 high가 됩니다. 이는 타당합니다. orig_fifo에서 데이터를 읽었다면 그 FIFO의 출력은 결과적으로 유효한 것으로 간주되니까요. 하지만 @fifo_rd_en이 low였는데도 데이터가 @middle_dout이나 @dout 중 하나로 복사되었다면, @fifo_dout은 더 이상 유효하다고 보지 않습니다. FIFO의 출력이 방금 사용되었는데 FIFO가 새 데이터로 교체해 주지 않았기 때문입니다.

@middle_valid도 같은 논리를 따릅니다.

 if (will_update_middle)
   middle_valid <= 1;
 else if (will_update_dout)
   middle_valid <= 0;

@will_update_middle이 high이면 @middle_dout으로 데이터가 복사되므로 @middle_valid도 high가 됩니다. 그렇지 않고 @middle_dout의 데이터가 @dout으로 복사되면 @middle_valid는 low로 바뀝니다. 이때 @will_update_dout이면 충분합니다. 위에서 보았듯이 @dout은 가능하면 @middle_dout에서 복사하는 것을 선호하기 때문입니다.

정말 동작하기는 할까?

이 모듈은 너무 복잡해서, 동작을 거의 형식적으로 증명해야 할 정도입니다. 이 질문에 답하는 한 가지 방법은 두 파이프라인 단계인 @fifo_dout과 @middle_dout 중 몇 개가 유효한지를 세어 보는 것입니다. 이 값은 reg_fifo 모듈에 정의되어 있지 않지만, 다음과 같이 정의할 수 있었을 것입니다.

wire [1:0] valid_count;
assign valid_count = fifo_valid + middle_valid;

이 가상의 @valid_count는 당연히 0, 1, 2 중 하나의 값을 가질 수 있습니다. 이 값은 다음과 같이 증가하거나 감소합니다.

논리식을 직접 살펴보고 이 세 가지 설명이 맞다는 것을 확인해 보세요.

그럼 orig_fifo에 데이터가 있고 애플리케이션 로직이 연속으로 읽기를 원할 때 어떤 일이 벌어지는지 살펴보겠습니다.

reg_fifo의 로직은 orig_fifo에서 읽어 @valid_count를 2까지 높이려고 합니다. 한편 @valid_count가 0이 아니면 @empty(비어 있음)가 low이므로, @valid_count가 1이 되는 즉시 @rd_en이 high로 올라갈 수 있습니다. 따라서 @valid_count가 1일 때 @fifo_rd_en은 high가 됩니다. @valid_count가 2가 아니기 때문입니다.

하지만 @valid_count는 2에 도달하지 못합니다. @rd_en이 high를 유지하며 그것을 막기 때문입니다. 그래서 데이터는 @fifo_rd_en과 @rd_en이 모두 high로 유지되고 @valid_count가 1에 머무른 채 흐릅니다. 처음 시작할 때를 제외하면 데이터는 @fifo_dout에서 @dout으로 복사됩니다.

이 균형은 orig_fifo가 empty(비어 있음) 상태가 되면 깨집니다. 이 경우 @fifo_rd_en이 더 이상 high가 될 수 없으므로 @valid_count는 0이 됩니다. 또 다른 균형 깨짐은 FIFO가 empty(비어 있음) 상태가 아닌데 애플리케이션 로직이 더 읽기를 원하지 않아 @rd_en이 low가 될 때입니다. 이 경우 @valid_count는 2까지 올라가 그 상태를 유지합니다.

하지만 나중에 @rd_en이 다시 high가 되면 @valid_count는 1로 내려가고, 그 시점에야 @fifo_rd_en이 high로 바뀝니다. 물론 orig_fifo가 empty(비어 있음) 상태가 아닐 때만 그렇습니다.

다시 강조하지만, @valid_count는 모듈에 실제로 구현되지 않은 이론적인 신호일 뿐입니다. 이 설명이 추가된 두 파이프라인 단계가 왜 연속적인 데이터 흐름을 보장하는지 이해하는 데 도움이 되었기를 바랍니다.

사용상의 주의사항

이 모듈은 내부에 감싸고 있는 '표준 FIFO'를 대체하는 용도로 그대로 사용할 수 있습니다. 기능적인 관점에서는 아무것도 변하지 않습니다. 다만 orig_fifo는 @rd_en, @dout 및 @empty(비어 있음)의 동작이 약간 달라진 것처럼 보일 수 있지만, orig_fifo가 FIFO로서 올바르게 동작하기만 하면(이는 당연히 성립하는 가정입니다) 문제되지 않습니다. 데이터 쓰기와 관련된 포트들은 아무런 변경 없이 그대로 통과하므로, 쓰기 쪽에서는 어떤 차이도 없습니다.

이 모듈은 파이프라인 단계를 몇 개 추가하므로, orig_fifo의 채움 카운터(fill counter)가 reg_fifo에 실제로 저장된 총 워드 수(즉 orig_fifo와 파이프라인 단계에 저장된 워드를 모두 합친 수)보다 적은 값을 보여줄 수 있습니다. 따라서 orig_fifo에서 @almost_empty 또는 유사한 포트를 활성화했다면, 이들이 실제보다 비관적인 상태를 보여줄 수 있습니다.

reg_fifo의 약간의 단점은 @empty(비어 있음) 출력이 레지스터가 아니라 두 레지스터의 조합 함수라는 점입니다. 타이밍 측면에서 최적은 아니지만, 대부분의 사용 사례에서 영향은 미미합니다. 이 문제는 이 페이지에서 보여준 @next_words_in_ram과 같은 방식으로 @next_fifo_valid, @next_middle_valid 같은 조합 레지스터를 정의하면 해결할 수 있습니다. 여기서는 구현하지 않았습니다. 주된 이유는 reg_fifo가 그 자체로 이미 충분히 복잡하기 때문입니다.

개선된 타이밍을 갖춘 FWFT FIFO

개선된 타이밍을 갖춘 FWFT FIFO가 필요하다면 간단합니다. 위에서 보여준 basic_fwft_fifo 예제를 사용하되, 일반 FIFO 대신 reg_fifo를 인스턴스화(instantiation)하면 됩니다. 이 해법은 레지스터를 하나 더 요구하지만, 지불하기에는 작은 대가입니다.

이상으로 FIFO에 관한 이 시리즈를 마칩니다.

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