Altifigence Academy

57 / 63 · 개념

단일 클록 FIFO의 포인터와 empty·full 처리

깊이 4인 FIFO의 포인터와 occupancy를 구현하고, empty·full 상태에서 push와 pop이 동시에 들어올 때의 수락 조건을 정합니다.

데이터를 옮기지 않고 순서를 지키기

FIFOFIFO 먼저 넣은 데이터를 먼저 꺼내는 큐입니다. 가득 차거나 비어 있을 때의 처리 방식과 읽기·쓰기를 동시에 허용할 조건도 정해야 합니다.는 먼저 넣은 데이터를 먼저 꺼냅니다. 단일 클록에서는 읽기와 쓰기가 같은 상승 에지에서 결정됩니다. 이 강의에서는 DEPTH=4, 데이터 폭 8비트로 고정합니다. rptr은 다음에 꺼낼 위치이고, wptr은 다음에 넣을 위치입니다. 각각 2비트이므로 3 다음에는 0으로 돌아옵니다. occupancy는 저장된 데이터의 개수인 0~4를 나타내야 하므로 3비트가 필요합니다.

rptr=wptr은 empty일 때도 full일 때도 가능합니다. 따라서 두 주소가 같다는 이유만으로 비어 있다고 판단하면 가득 찬 데이터를 잃게 됩니다. 여기서는 개수 CC를 따로 저장해 empty=(C=0)\mathrm{empty}=(C=0), full=(C=4)\mathrm{full}=(C=4)로 구분합니다. 깊이 D=4D=4일 때 주소 폭 WaddrW_{\mathrm{addr}}와 개수를 저장할 비트폭 WcountW_{\mathrm{count}}는 다음과 같습니다.

Waddr=⌈log⁡2D⌉=2Wcount=⌈log⁡2(D+1)⌉=3\begin{aligned} W_{\mathrm{addr}}&=\lceil\log_2 D\rceil=2\\ W_{\mathrm{count}}&=\lceil\log_2(D+1)\rceil=3 \end{aligned}

비어 있거나 가득 찼을 때 요청을 처리하는 규칙

push와 pop은 요청 신호입니다. p와 q가 각각 push와 pop을 실제로 수락했는지 나타낸다고 하면, reset이 아닌 에지 직전의 상태로 다음과 같이 계산합니다.

q=pop∧(C>0)p=push∧((C<4)∨q)C+=C+p−q\begin{aligned} q&=\mathrm{pop}\land(C>0)\\ p&=\mathrm{push}\land((C<4)\lor q)\\ C^{+}&=C+p-q \end{aligned}

empty에서 두 요청이 함께 와도 꺼낼 데이터가 없으므로 push만 수락합니다. 새 데이터가 그 에지에서 곧바로 출력되지는 않습니다. full에서 두 요청이 함께 오면 맨 앞의 데이터인 head를 꺼내 자리를 확보할 수 있으므로 둘 다 수락합니다. full은 “현재 4개가 저장되어 있음”을 나타냅니다. 이 규칙에서는 full이 1이어도 push를 허용할 수 있습니다.

엣지 직전 상태push만pop만push와 pop
C=01개 저장거절push만 수락, C=1
0<C<4C+1C−1둘 다 수락, C 유지
C=4거절C=3둘 다 수락, C=4

클록을 진행하며 경계 조건을 확인하기

처음에는 push와 pop을 함께 요청해 보세요. 비어 있으므로 push만 수락되어야 합니다. 다음에는 pop을 끄고 4칸을 채운 뒤, 새로운 입력 값과 함께 두 요청을 다시 켜 보세요. 점유량은 4를 유지하면서 출력에는 기존 head가, 저장 공간에는 새 tail이 남아야 합니다.

클록으로 진행하는 FIFO

4칸 FIFO. 초기 점유량 0, 읽기·쓰기 포인터 0. pop은 비어 있지 않을 때 수락합니다. push는 여유 공간이 있거나 같은 클록에서 pop을 수락할 때 수락합니다.

그림은 각 상승 에지상승 에지 클록이 0에서 1로 바뀌는 순간입니다. CLK=1인 구간 전체를 뜻하는 레벨과 구별합니다. 자세히 보기 직후의 상태를 보여 주는 교육용 모델입니다. pop이 수락되면 output valid가 1이 되고, 다음 에지에 pop이 수락되지 않으면 출력 레지스터레지스터 여러 비트의 상태를 저장하는 회로입니다. 이 과정의 동기식 레지스터는 클록 에지에서 정해진 입력을 저장합니다. 자세히 보기의 값은 유지되지만 valid는 0이 됩니다. 비어 있는 슬롯의 오래된 비트는 숨깁니다. ‘처음으로’는 실습 전체를 초기화하는 기능이며 RTL의 메모리 리셋리셋 상태를 미리 정한 초기값으로 되돌리는 제어입니다. 동기식인지 비동기식인지, 다른 제어보다 우선하는지를 함께 정해야 합니다. 자세히 보기 동작을 나타내지 않습니다.

출력 레지스터까지 포함한 RTL

SystemVerilog
module fifo4 (
  input  logic       clk, rst, push, pop,
  input  logic [7:0] din,
  output logic [7:0] dout,
  output logic       dout_valid, full, empty,
  output logic [2:0] occupancy
);
  timeunit 1ns;
  timeprecision 1ps;
  logic [7:0] mem [0:3];
  logic [1:0] rptr, wptr;
  logic take_push, take_pop;

  assign empty = (occupancy == 3'd0);
  assign full  = (occupancy == 3'd4);
  assign take_pop  = !rst && pop && !empty;
  assign take_push = !rst && push && (!full || take_pop);

  always_ff @(posedge clk) begin
    if (rst) begin
      rptr <= 2'd0;
      wptr <= 2'd0;
      occupancy <= 3'd0;
      dout_valid <= 1'b0;
    end else begin
      dout_valid <= take_pop;
      if (take_push) begin
        mem[wptr] <= din;
        wptr <= wptr + 2'd1;
      end
      if (take_pop) begin
        dout <= mem[rptr];
        rptr <= rptr + 2'd1;
      end
      case ({take_push, take_pop})
        2'b10: occupancy <= occupancy + 3'd1;
        2'b01: occupancy <= occupancy - 3'd1;
        default: occupancy <= occupancy;
      endcase
    end
  end
endmodule

occupancy의 증가와 감소를 두 개의 독립된 if문으로 대입하지 않았습니다. push와 pop을 동시에 수락할 때 같은 레지스터에 nonblockingnonblocking 순차 RTL에서 <=로 표현하는 대입입니다. 우변을 평가한 뒤 갱신을 예약하므로 같은 에지의 레지스터들이 이전 상태를 기준으로 계산할 수 있습니다. 자세히 보기 대입을 두 번 하면 마지막 대입만 남기 때문입니다. 두 포인터는 각 요청을 수락할 때 이동하지만, 개수는 네 경우를 함께 고려해 결정합니다. 수락 조건을 통해 0에서 감소하거나 4에서 증가하는 경우도 막습니다.

full에서 같은 주소를 읽고 쓸 때

A, B, C, D를 차례로 넣으면 rptr=wptr=0, occupancy=4가 됩니다. 여기서 pop=push=1, din=E이면 에지 뒤에 dout=A, dout_valid=1이 되고, FIFO에 남은 데이터는 순서대로 [B,C,D,E]입니다. 두 포인터는 1이 되고 개수는 4를 유지합니다. mem[0]은 E로 바뀌지만 dout은 nonblocking 대입의 오른쪽에서 이전 값 A를 읽습니다. full에서 기존 데이터를 꺼내면서 새 데이터로 교체하려면 이 read-first 동작이 필요합니다.

그다음 push=0, pop=1이면 B가 나오고 개수는 3이 됩니다. 반대로 empty에서 din=Z와 두 요청을 함께 주면 개수만 1이 되고 dout_valid=0입니다. 다음 에지에서 pop을 요청해야 Z가 출력됩니다. empty에서 데이터를 즉시 전달할지와 full에서 데이터를 교체할지는 따로 정해야 합니다.

rst는 active-high 동기 reset이며 포인터, 개수, dout_valid를 초기화합니다. 메모리와 dout 값은 지우지 않습니다. reset 뒤에 이전 비트가 남아 있어도 empty가 pop을 막고, 이후에는 새 push로 기록한 위치만 읽습니다. 따라서 정상 동작을 위해 배열 전체를 reset할 필요는 없습니다. dout_valid=0인 값은 사용하지 않습니다. valid가 1인 출력은 수락한 pop에 대한 응답입니다. 출력 쪽에는 별도 ready 신호가 없으므로 다음 회로는 그 응답을 바로 받아야 합니다.

DEPTH가 2의 거듭제곱이 아니면 2비트 overflow로 주소를 되돌리는 이 방식을 그대로 쓸 수 없습니다. 예를 들어 깊이 3에서는 주소 2 다음에 0으로 돌아가도록 명시해야 합니다. 이번 예제를 합성한 회로가 RAM을 사용한다면, 같은 주소를 읽고 쓸 때 이전 값을 읽는 old-data 동작을 지원하는지도 확인해야 합니다. 이 코드는 단일 클록용이며, 읽기와 쓰기의 클록이 다른 FIFO의 포인터 동기화에는 그대로 적용할 수 없습니다.

직접 생각해 보기

reset 직후 다음 요청을 한 에지씩 적용합니다: (1) push A와 pop 동시, (2) push B만, (3) push C만, (4) push D만, (5) push E와 pop 동시, (6) pop만. 각 단계 뒤의 큐 내용, occupancy, dout_valid와 유효한 출력값을 적으세요. 다섯 번째 단계에서 read-first가 필요한 이유도 설명하세요.

해설 보기

(1) [A],1,valid=0. (2) [A,B],2,valid=0. (3) [A,B,C],3,valid=0. (4) [A,B,C,D],4,valid=0. (5) [B,C,D,E],4,valid=1,dout=A. (6) [C,D,E],3,valid=1,dout=B. 1단계에서는 꺼낼 항목이 없어 pop을 거절합니다. 5단계에서는 rptr=wptr=0이고 같은 슬롯에 읽기와 쓰기가 겹치므로, 기존 head인 A를 읽어야 합니다. write-first로 E를 반환하면 FIFO의 데이터 순서와 내용이 잘못됩니다. valid=0인 출력값은 채점 대상이 아닙니다.

선택은 이 브라우저에만 적용됩니다. 언제든 푸터에서 변경할 수 있습니다.