Asynchronous FIFO를 제대로 이해하기 위해서는, 먼저 Synchronous FIFO 구조를 머릿속에 명확히 고정할 필요가 있다. 이를 위해 가장 기초적인 FIFO(First-In First-Out) 구조부터 정리한다.
FIFO (First-In First-Out)란?

먼저 들어온 데이터가 먼저 나가는 메커니즘이다. FIFO 메커니즘을 사용하는 대표적인 방식으로 큐(Queue) 자료 구조가 있다. 데이터를 순서대로 안전하게 저장하고 전달하기 위한 가장 기본적이면서도 중요한 하드웨어 구조이다.
FIFO는 주로 다음과 같은 상황에서 사용된다.
- Producer / Consumer 구조
- Burst 입력 → 일정 속도 출력
- 파이프라인 stage 사이 버퍼
즉, 데이터 생성 속도와 소비 속도가 일치하지 않을 때 데이터 손실 없이 이를 완충(buffering)하기 위해 사용된다. 이는 고속 시스템에서 특히 중요하다.


위 이미지의 상황처럼 송신쪽과 수신쪽의 속도가 다르거나, 송신쪽에서 데이터를 전송하고 있을 때 수신쪽에서 다른 일을 해야할 때 FIFO가 사용된다. 이러한 상황은 동기 시스템에서도 발생할 수 있으며, 송신과 수신 클럭이 다를 경우 Asynchronous FIFO가 사용된다.
FIFO 주요 구성요소
FIFO 주요 인터페이스
| 신호 | 방향 | 설명 |
| wr_en | Input | FIFO에 데이터를 쓰기 위한 Write Enable 신호 |
| din | Input | FIFO로 입력되는 데이터 (Data Input) |
| rd_en | Input | FIFO에서 데이터를 읽기 위한 Read Enable 신호 |
| dout | Output | FIFO에서 출력되는 데이터 (Data Output) |
| full | Output | FIFO가 가득 찼음을 나타냄 |
| empty | Output | FIFO가 비어 있음을 나타냄 |
FIFO 주요 내부 신호
| 신호 | 설명 |
| wr_ptr | Write Pointer |
| rd_ptr | Read Pointer |
Write Pointer와 Read Pointer 두 개가 있으며, Write Pointer는 새 입력 데이터가 기록될 위치를 추적하고 Read Pointer 는 데이터를 읽을 위치를 추적한다. 두 포인터는 모두 클록에 동일 클럭 도메인이다.
MSB wrap bit
FIFO에서 empty와 full을 구분하려면 포인터에 주소 비트 + 1비트(MSB toggle) 가 반드시 필요하다. 이 비트는 같은 위치지만 다른 시간을 구분하기 위한 것이다.
- 주소 비트 + wrap bit
- empty = 완전 동일
- full = 주소 동일 + MSB 다름
// empty
wptr == rptr
→ empty = 1
// full
wptr = 1_000
rptr = 0_000
addr 동일 + MSB 다름 → full = 1
FIFO 동작 방식
FIFO는 클락에 동기되어 쓰기와 읽기 동작을 수행한다.
Write 동작
write enable(wr_en) 신호가 활성화되어 있고, FIFO가 full 상태가 아닐 때, 매 클락 사이클마다 데이터가 FIFO에 기록된다.
- 입력 데이터(din)는 현재 write pointer(wr_ptr)가 가리키는 위치에 저장된다.
- 데이터가 정상적으로 기록되면, 다음 클락에서 wr_ptr는 1 증가한다.
- FIFO가 full 상태일 경우, wr_en이 활성화되어 있어도 쓰기 동작은 수행되지 않는다.
Read 동작
read enable(rd_en) 신호가 활성화되어 있고, FIFO가 empty 상태가 아닐 때, 매 클락 사이클마다 데이터가 FIFO에서 읽힌다.
- 출력 데이터(dout)는 현재 read pointer(rd_ptr)가 가리키는 위치의 값이다.
- 데이터가 정상적으로 읽히면, 다음 클락에서 rd_ptr는 1 증가한다.
- FIFO가 empty 상태일 경우, rd_en이 활성화되어 있어도 읽기 동작은 수행되지 않는다.
Code
module sync_fifo #(
parameter int DEPTH = 8,
parameter int DWIDTH = 16
)(
input logic clk,
input logic rst_n,
input logic wr_en,
input logic rd_en,
input logic [DWIDTH-1:0] din,
output logic [DWIDTH-1:0] dout,
output logic full,
output logic empty
);
// --------------------------------------
// Local parameters
// --------------------------------------
localparam int ADDR_W = $clog2(DEPTH);
// --------------------------------------
// Memory
// --------------------------------------
logic [DWIDTH-1:0] mem [0:DEPTH-1];
// --------------------------------------
// Pointers (Binary + wrap bit)
// --------------------------------------
logic [ADDR_W:0] wptr; // MSB = wrap bit
logic [ADDR_W:0] rptr;
// --------------------------------------
// Write logic
// --------------------------------------
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wptr <= '0;
end
else if (wr_en && !full) begin
mem[wptr[ADDR_W-1:0]] <= din;
wptr <= wptr + 1'b1;
end
end
// --------------------------------------
// Read logic
// --------------------------------------
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rptr <= '0;
dout <= '0;
end
else if (rd_en && !empty) begin
dout <= mem[rptr[ADDR_W-1:0]];
rptr <= rptr + 1'b1;
end
end
// --------------------------------------
// Empty condition
// --------------------------------------
assign empty = (wptr == rptr);
// --------------------------------------
// Full condition
// --------------------------------------
assign full =
(wptr[ADDR_W] != rptr[ADDR_W]) && // wrap bit 다름
(wptr[ADDR_W-1:0] == rptr[ADDR_W-1:0]); // 주소 같음
endmodule
module tb_sync_fifo;
// --------------------------------------
// Parameters
// --------------------------------------
localparam int DEPTH = 8;
localparam int DWIDTH = 16;
// --------------------------------------
// DUT signals
// --------------------------------------
logic clk;
logic rst_n;
logic wr_en;
logic rd_en;
logic [DWIDTH-1:0] din;
logic [DWIDTH-1:0] dout;
logic full;
logic empty;
// --------------------------------------
// Clock generation
// --------------------------------------
initial clk = 0;
always #5 clk = ~clk; // 100MHz
// --------------------------------------
// DUT instantiation
// --------------------------------------
sync_fifo #(
.DEPTH (DEPTH),
.DWIDTH(DWIDTH)
) dut (
.clk (clk),
.rst_n (rst_n),
.wr_en (wr_en),
.rd_en (rd_en),
.din (din),
.dout (dout),
.full (full),
.empty (empty)
);
// --------------------------------------
// Test sequence
// --------------------------------------
initial begin
// Init
rst_n = 0;
wr_en = 0;
rd_en = 0;
din = '0;
#20;
rst_n = 1;
// ----------------------------------
// 1. Write until FULL
// ----------------------------------
$display("\n--- WRITE PHASE ---");
repeat (DEPTH) begin
@(posedge clk);
wr_en = 1;
din = din + 16'h1;
end
@(posedge clk);
wr_en = 0;
// Attempt write when FULL
@(posedge clk);
wr_en = 1;
din = 16'hFFFF;
@(posedge clk);
wr_en = 0;
// ----------------------------------
// 2. Read until EMPTY
// ----------------------------------
$display("\n--- READ PHASE ---");
repeat (DEPTH) begin
@(posedge clk);
rd_en = 1;
end
@(posedge clk);
rd_en = 0;
// Attempt read when EMPTY
@(posedge clk);
rd_en = 1;
@(posedge clk);
rd_en = 0;
// ----------------------------------
// 3. Simultaneous Read & Write
// ----------------------------------
$display("\n--- SIMULTANEOUS R/W PHASE ---");
repeat (5) begin
@(posedge clk);
wr_en = 1;
rd_en = 1;
din = din + 16'h10;
end
@(posedge clk);
wr_en = 0;
rd_en = 0;
// ----------------------------------
// Finish
// ----------------------------------
#50;
$finish;
end
// --------------------------------------
// Monitor
// --------------------------------------
initial begin
$display("time | wr rd | din dout | full empty");
$monitor("%4t | %0b %0b | 0x%04h 0x%04h | %0b %0b",
$time, wr_en, rd_en, din, dout, full, empty);
end
// --------------------------------------
// Dump waveform
// --------------------------------------
initial begin
$dumpfile("sync_fifo.vcd");
$dumpvars(0, tb_sync_fifo);
end
endmodule
Simulation
time | wr rd | din dout | full empty
0 | 0 0 | 0x0000 0x0000 | 0 1
--- WRITE PHASE ---
25 | 1 0 | 0x0001 0x0000 | 0 0
35 | 1 0 | 0x0002 0x0000 | 0 0
45 | 1 0 | 0x0003 0x0000 | 0 0
55 | 1 0 | 0x0004 0x0000 | 0 0
65 | 1 0 | 0x0005 0x0000 | 0 0
75 | 1 0 | 0x0006 0x0000 | 0 0
85 | 1 0 | 0x0007 0x0000 | 0 0
95 | 1 0 | 0x0008 0x0000 | 1 0
105 | 0 0 | 0x0008 0x0000 | 1 0
115 | 1 0 | 0xffff 0x0000 | 1 0
--- READ PHASE ---
125 | 0 0 | 0xffff 0x0000 | 1 0
135 | 0 1 | 0xffff 0x0001 | 0 0
145 | 0 1 | 0xffff 0x0002 | 0 0
155 | 0 1 | 0xffff 0x0003 | 0 0
165 | 0 1 | 0xffff 0x0004 | 0 0
175 | 0 1 | 0xffff 0x0005 | 0 0
185 | 0 1 | 0xffff 0x0006 | 0 0
195 | 0 1 | 0xffff 0x0007 | 0 0
205 | 0 1 | 0xffff 0x0008 | 0 1
215 | 0 0 | 0xffff 0x0008 | 0 1
225 | 0 1 | 0xffff 0x0008 | 0 1
--- SIMULTANEOUS R/W PHASE ---
235 | 0 0 | 0xffff 0x0008 | 0 1
245 | 1 1 | 0x000f 0x0008 | 0 0
255 | 1 1 | 0x001f 0x000f | 0 0
265 | 1 1 | 0x002f 0x001f | 0 0
275 | 1 1 | 0x003f 0x002f | 0 0
285 | 1 1 | 0x004f 0x003f | 0 0
295 | 0 0 | 0x004f 0x003f | 0 0

여기까지는 Write Pointer와 Read Pointer가 같은 클럭 도메인에 있기 때문에 포인터 비교가 즉각적이고 안전하다. 하지만 두 포인터가 서로 다른 클럭 도메인에 존재하게 되면, 이 단순한 비교는 더 이상 안전하지 않으며, 이를 해결하기 위해 Gray Code + Synchronizer가 필요해진다.
Rerference
https://www2.advantech.com/ia/newsletter/ADAMLINK/Aug2005/IO.htm
'VLSI > Design' 카테고리의 다른 글
| [CDC] 9_Multi Bit CDC: Asynchronous FIFO (Full 판단, Pointer Jump) (0) | 2025.12.21 |
|---|---|
| [CDC] 8_Multi Bit CDC: Asynchronous FIFO (Full/Empty Pessimistic Design) (0) | 2025.12.21 |
| [CDC] 6_Multi Bit CDC: Asynchronous FIFO (Introduction) (0) | 2025.12.19 |
| [CDC] 5_Multi Bit CDC: Gray Code (0) | 2025.12.15 |
| [CDC] 4_Multi Bit CDC: Handshake (1) | 2025.12.15 |