01signal.com

단일 클록 FIFO의 Verilog 구현

범위

이 페이지는 FIFO에 관한 다섯 편의 시리즈 중 세 번째 글입니다. 여기서는 기준 단일 클록 FIFO를 Verilog로 구현하는 방법을 보여드립니다. 이 구현은 이식성 있는 코드를 작성할 때 유용할 수 있지만, 이 페이지의 핵심 목적은 FIFO가 어떻게 동작하는지를 다시 한번 짚어 보는 것입니다. 그래서 '표준 FIFO(standard FIFO)'와 FWFT FIFO의 구현을 모두 소개하겠습니다. 그런데 먼저 두 FIFO가 모두 사용할 듀얼 포트 RAM(dual port RAM)이 필요합니다.

듀얼 포트 RAM

이 모듈은 추론(inference) 방식으로 RAM을 구현하는 Verilog 모듈입니다. 어떤 합성 도구(synthesizer)라도 이 코드를 제대로 처리할 가능성이 높지만, 원치 않는 종류의 RAM(블록 RAM(block RAM) 대 분산 RAM(distributed RAM))을 생성할 수도 있으므로 합성 도구 지시어를 추가해야 할 수 있습니다. 또는 FPGA 벤더가 공급하는 듀얼 포트 RAM용 IP를 사용하는 것이 더 잘 맞을 수도 있습니다.

그럼 모듈은 다음과 같습니다.

module dualport_ram #(parameter depth = 64,
                      log2_depth = 6,
                      width = 8
                     )
   (
    input                    clk,
    input [(log2_depth-1):0] wr_addr,
    input [(log2_depth-1):0] rd_addr,
    output reg [(width-1):0] rd_data,
    input [(width-1):0]      wr_data,
    input                    rd_en,
    input                    wr_en
    );

   reg [(width-1):0] inferred_ram[0:(depth-1)];

   always @(posedge clk)
     begin
        if (wr_en)
          inferred_ram[wr_addr] <= wr_data;

        if (rd_en)
          rd_data <= inferred_ram[rd_addr];
     end
endmodule

표준 FIFO

이제 '표준 FIFO'를 구현하는 모듈, 즉 FWFT FIFO가 아닌 모듈을 살펴볼 준비가 되었습니다.

module fifo
  #(parameter depth = 64, // Must equal 2^log2_depth exactly
    log2_depth = 6,
    width = 8
    )
   (
    input  clk,
    input  rst,

    input  wr_en,
    input [(width-1):0] din,

    input  rd_en,
    output [(width-1):0] dout,

    output reg full,
    output reg empty
    );

   reg [log2_depth:0]         next_words_in_ram; // Combinatorial
   reg [log2_depth:0]         words_in_ram;
   reg [(log2_depth-1):0]     rd_addr;
   reg [(log2_depth-1):0]     wr_addr;
   wire                       fetch_data, commit_data;

   assign fetch_data = rd_en && !empty;
   assign commit_data = wr_en && !full;

   always @(*)
     if (commit_data && !fetch_data)
       next_words_in_ram <= words_in_ram + 1;
     else if (!commit_data && fetch_data)
       next_words_in_ram <= words_in_ram - 1;
     else
       next_words_in_ram <= words_in_ram;

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        empty <= (next_words_in_ram == 0);

        if (fetch_data)
          rd_addr <= rd_addr + 1;

        if (commit_data)
          wr_addr <= wr_addr + 1;

        if (rst)
          begin
             empty <= 1;
             full <= 1;
             words_in_ram <= 0;
             rd_addr <= 0;
             wr_addr <= 0;
          end
     end

   dualport_ram
     #(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
       (.clk(clk), .wr_addr(wr_addr),
        .rd_addr(rd_addr),
        .wr_en(commit_data),
        .rd_en(fetch_data),
        .wr_data(din),
        .rd_data(dout)
        );
endmodule

여기서 유일하게 중요한 사용상의 주의점은 맨 위의 주석에 나와 있는 대로 depthlog2_depth 파라미터에 관한 것입니다. depth는 2log2_depth와 정확히 같아야 합니다.

이 모듈의 동작은 아주 단순합니다. @fetch_data는 @rd_en과 비슷하지만 @empty(비어 있음)를 고려합니다. 즉, @rd_en과 @empty(비어 있음)가 동시에 high가 되더라도(이것은 불법입니다) 그 경우 @rd_en이 무시되므로 아무 문제가 발생하지 않는다는 점에서, @fetch_data는 @rd_en의 안전한 버전입니다.

@commit_data는 마찬가지로 @full(가득 찬 상태)을 고려하는 @wr_en의 안전한 버전입니다.

@words_in_ram의 다음 값인 @next_words_in_ram은 @fetch_data와 @commit_data를 바탕으로 조합 논리 함수로 계산됩니다(always @(*) 문을 주목하세요). @next_words_in_ram은 아래 코드 조각에서 여러 레지스터의 값을 만드는 데 사용됩니다.

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        empty <= (next_words_in_ram == 0);
[ ... ]

여기서 @full(가득 찬 상태)과 @empty(비어 있음)가 정의된다는 점에 유의하세요.

이 FIFO가 듀얼 클록 FIFO(dual-clock FIFO)와 비교해 이렇게 구현하기 쉬운 이유는, @words_in_ram을 이처럼 정의하고 이 동일한 레지스터를 FIFO의 양쪽에서 사용할 수 있기 때문입니다.

코드의 다음 부분에서는 @rd_addr와 @wr_addr를 갱신하고, 그다음 @rst에 대한 조건문이 나옵니다. 리셋 결과로 @empty(비어 있음)와 @full(가득 찬 상태)이 모두 high로 바뀌지만, 리셋이 해제되면 @full(가득 찬 상태)은 low로 돌아간다는 점에 유의하세요.

코딩 스타일에 관한 참고 사항입니다. @rst가 high이면 if 문의 begin-end 블록 안의 대입들이 그 이전에 이루어졌을 수도 있는 대입들을 덮어쓰므로, @rst는 실제로 모든 레지스터를 초기값으로 리셋합니다. 이것은 가장 흔한 코딩 스타일은 아니지만, 모든 레지스터를 리셋하지 않을 때 분명한 장점이 있습니다. 이 특정 예에서는 그 장점이 드러나지 않지만, 이 페이지를 참고하세요.

마지막으로 듀얼 포트 RAM의 인스턴스화(instantiation)가 있습니다.

FWFT FIFO

이 페이지에서 보여드렸듯이 '표준 FIFO'를 FWFT FIFO로 변환하는 것은 매우 쉽습니다. 하지만 FWFT FIFO를 직접 구현하면 몇 가지 흥미로운 점을 논의할 수 있으므로, 그 구현은 다음과 같습니다.

module fwft_fifo
  #(parameter depth = 64, // Must equal 2^log2_depth exactly
    log2_depth = 6,
    width = 8
    )
   (
    input  clk,
    input  rst,

    input  wr_en,
    input [(width-1):0] din,

    input  rd_en,
    output [(width-1):0] dout,

    output reg full,
    output reg empty
    );

   reg [log2_depth:0]         next_words_in_ram; // Combinatorial
   reg [log2_depth:0]         words_in_ram;
   reg [(log2_depth-1):0]     rd_addr;
   reg [(log2_depth-1):0]     wr_addr;
   reg                        has_more_words;
   wire                       fetch_data, commit_data;

   assign fetch_data = (rd_en || empty) && has_more_words;
   assign commit_data = wr_en && !full;

   always @(*)
     if (commit_data && !fetch_data)
       next_words_in_ram <= words_in_ram + 1;
     else if (!commit_data && fetch_data)
       next_words_in_ram <= words_in_ram - 1;
     else
       next_words_in_ram <= words_in_ram;

   always @(posedge clk)
     begin
        words_in_ram <= next_words_in_ram;
        full <= (next_words_in_ram == depth);
        has_more_words <= (next_words_in_ram != 0);

        if (fetch_data)
          rd_addr <= rd_addr + 1;

        if (commit_data)
          wr_addr <= wr_addr + 1;

        if (fetch_data)
          empty <= 0;
        else if (rd_en)
          empty <= 1;

        if (rst)
          begin
             empty <= 1;
             full <= 1;
             words_in_ram <= 0;
             has_more_words <= 0;
             rd_addr <= 0;
             wr_addr <= 0;
          end
     end

   dualport_ram
     #(.depth(depth), .log2_depth(log2_depth), .width(width)) dp_ins
       (.clk(clk), .wr_addr(wr_addr),
        .rd_addr(rd_addr),
        .wr_en(commit_data),
        .rd_en(fetch_data),
        .wr_data(din),
        .rd_data(dout)
        );
endmodule

먼저 @has_more_words라는 새 레지스터가 추가되었음을 주목하세요. 그 정의를 위의 '표준 FIFO'와 비교해 보면, @has_more_words가 @empty(비어 있음)의 논리적 NOT이라는 것을 확인할 수 있을 것입니다.

다음으로 @fetch_data의 정의가 바뀌었다는 점에 유의하세요. 이제 (놀랍지 않게) @has_more_words가 그 역할을 대신 보장하며, 정의는 다음과 같습니다.

assign fetch_data = (rd_en || empty) && has_more_words;

앞에서 FWFT FIFO에서 @empty는 '@dout이 유효하지 않음'을 뜻한다고 했던 것을 기억하세요. 따라서 이 대입은 @rd_en이 high일 때뿐 아니라, 출력이 유효하지 않고 메모리 배열에서 읽을 데이터가 있을 때에도 읽기를 수행한다는 뜻입니다. 이것이 바로 첫 번째 워드가 자동으로 출력에 나타나는(fall-through) 동작입니다.

마지막으로 @empty를 대입하는 로직이 다음과 같이 바뀌었습니다.

   always @(posedge clk)
     if (fetch_data)
       empty <= 0;
     else if (rd_en)
       empty <= 1;

이 코드가 말하는 바는 단순합니다. 메모리 배열에서 워드 하나가 읽히면, 분명히 새롭고 유효한 데이터가 있게 되므로 @empty(비어 있음)는 다음 클록 사이클에 low로 바뀝니다. 하지만 그런 일이 일어나지 않았는데도 @rd_en이 여전히 high라면, 애플리케이션 로직이 방금 사용 가능한 마지막 워드를 가져간 것이므로 @empty(비어 있음)를 high로 바꿉니다. @rd_en이 high인데 @fetch_data가 low라면 @has_more_words는 확실히 low라는 점에 유의하세요(위의 @fetch_data 대입문 참조). 그래서 이 조건은 FIFO에서 마지막 워드를 읽는 것과 동등합니다.

이상으로 FIFO에 관한 이 시리즈의 세 번째 페이지를 마칩니다. 다음 페이지에서는 데이터 수집(data acquisition) 응용 프로그램에서 사용하도록 '표준 FIFO'를 변형하는 방법을 보여드립니다.

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