Asynchronous FIFO의 SystemVerilog 코드와 시뮬레이션을 확인해보자.
Code
module async_fifo #(
parameter int DEPTH = 8,
parameter int DWIDTH = 16
)(
input logic wclk,
input logic rclk,
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];
// --------------------------------------
// Binary pointers (with wrap bit)
// --------------------------------------
logic [ADDR_W:0] wptr_bin, rptr_bin;
// --------------------------------------
// Gray pointers
// --------------------------------------
logic [ADDR_W:0] wptr_gray, rptr_gray;
logic [ADDR_W:0] wptr_gray_next, rptr_gray_next;
// --------------------------------------
// Synchronized Gray pointers
// --------------------------------------
logic [ADDR_W:0] rptr_gray_wclk_ff1, rptr_gray_wclk_ff2;
logic [ADDR_W:0] wptr_gray_rclk_ff1, wptr_gray_rclk_ff2;
// --------------------------------------
// Binary → Gray
// --------------------------------------
function automatic [ADDR_W:0] bin2gray(input [ADDR_W:0] bin);
return (bin >> 1) ^ bin;
endfunction
// ================================================================
// WRITE DOMAIN
// ================================================================
assign wptr_gray = bin2gray(wptr_bin);
assign wptr_gray_next = bin2gray(wptr_bin + 1'b1);
// Write pointer
always_ff @(posedge wclk or negedge rst_n) begin
if (!rst_n)
wptr_bin <= '0;
else if (wr_en && !full) begin
mem[wptr_bin[ADDR_W-1:0]] <= din;
wptr_bin <= wptr_bin + 1'b1;
end
end
// Read pointer Gray sync into write clock
always_ff @(posedge wclk or negedge rst_n) begin
if (!rst_n) begin
rptr_gray_wclk_ff1 <= '0;
rptr_gray_wclk_ff2 <= '0;
end else begin
rptr_gray_wclk_ff1 <= rptr_gray;
rptr_gray_wclk_ff2 <= rptr_gray_wclk_ff1;
end
end
// Full condition (Gray style)
assign full =
(wptr_gray_next[ADDR_W:ADDR_W-1] ==
~rptr_gray_wclk_ff2[ADDR_W:ADDR_W-1]) &&
(wptr_gray_next[ADDR_W-2:0] ==
rptr_gray_wclk_ff2[ADDR_W-2:0]);
// ================================================================
// READ DOMAIN
// ================================================================
assign rptr_gray = bin2gray(rptr_bin);
assign rptr_gray_next = bin2gray(rptr_bin + 1'b1);
// Read pointer
always_ff @(posedge rclk or negedge rst_n) begin
if (!rst_n) begin
rptr_bin <= '0;
dout <= '0;
end else if (rd_en && !empty) begin
dout <= mem[rptr_bin[ADDR_W-1:0]];
rptr_bin <= rptr_bin + 1'b1;
end
end
// Write pointer Gray sync into read clock
always_ff @(posedge rclk or negedge rst_n) begin
if (!rst_n) begin
wptr_gray_rclk_ff1 <= '0;
wptr_gray_rclk_ff2 <= '0;
end else begin
wptr_gray_rclk_ff1 <= wptr_gray;
wptr_gray_rclk_ff2 <= wptr_gray_rclk_ff1;
end
end
// Empty condition
assign empty = (rptr_gray == wptr_gray_rclk_ff2);
endmodule
module tb_async_fifo;
// --------------------------------------------------
// Parameters
// --------------------------------------------------
localparam int DEPTH = 8;
localparam int DWIDTH = 16;
// --------------------------------------------------
// DUT signals
// --------------------------------------------------
logic wclk;
logic rclk;
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 wclk = 0;
always #5 wclk = ~wclk; // 100 MHz
initial rclk = 0;
always #7 rclk = ~rclk; // ~71 MHz (async)
// --------------------------------------------------
// DUT
// --------------------------------------------------
async_fifo #(
.DEPTH (DEPTH),
.DWIDTH(DWIDTH)
) dut (
.wclk (wclk),
.rclk (rclk),
.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;
#50;
rst_n = 1;
// ----------------------------------------------
// 1. WRITE until FULL
// ----------------------------------------------
$display("\n=== WRITE UNTIL FULL ===");
repeat (DEPTH) begin
@(posedge wclk);
if (!full) begin
wr_en = 1;
din = din + 1;
end
end
@(posedge wclk);
wr_en = 0;
// Attempt overflow
@(posedge wclk);
wr_en = 1;
din = 16'hDEAD;
@(posedge wclk);
wr_en = 0;
// ----------------------------------------------
// 2. READ until EMPTY
// ----------------------------------------------
$display("\n=== READ UNTIL EMPTY ===");
repeat (DEPTH) begin
@(posedge rclk);
if (!empty)
rd_en = 1;
end
@(posedge rclk);
rd_en = 0;
// Attempt underflow
@(posedge rclk);
rd_en = 1;
@(posedge rclk);
rd_en = 0;
// ----------------------------------------------
// Finish
// ----------------------------------------------
#100;
$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
// --------------------------------------------------
// Waveform
// --------------------------------------------------
initial begin
$dumpfile("async_fifo.vcd");
$dumpvars(0, tb_async_fifo);
end
endmodule
wptr_gray_next는 “이번 사이클에 write를 하면 다음 사이클에 포인터가 가리킬 값”을 미리 계산해서, overflow를 사전에 막기 위해 필요하다.
조금만 풀면, Async FIFO에서 full은 “이미 이동한 포인터”가 아니라 “이동하려는 다음 포인터” 기준으로 판단해야 한다.
예를 들어 write 도메인에서 full을 현재 wptr_gray로 비교하면, 이미 한 번 더 써버린 뒤에야 full이 검출되어 메모리를 한 칸 침범할 수 있다. 그래서 wptr_gray_next = bin2gray(wptr_bin + 1) 를 미리 만들어서 “지금 write를 하면 다음 포인터가 read 포인터를 넘어서지 않는가?” 를 사전에 체크하는 것이다.
rptr_gray_next도 선언은 했지만 굳이 사용하지 않아도 문제가 없다. 보수적 설계에서 언급했듯이, read 쪽은 조금 늦게 막아도 메모리의 데이터를 망가뜨리지 않는다. 즉 지연되도 문제가 없다는 의미이다.
Simulation
time | wr rd | din dout | full empty
0 | 0 0 | 0x0000 0x0000 | 0 1
=== WRITE UNTIL FULL ===
55 | 1 0 | 0x0001 0x0000 | 0 1
65 | 1 0 | 0x0002 0x0000 | 0 1
75 | 1 0 | 0x0003 0x0000 | 0 1
77 | 1 0 | 0x0003 0x0000 | 0 0
85 | 1 0 | 0x0004 0x0000 | 0 0
95 | 1 0 | 0x0005 0x0000 | 0 0
105 | 1 0 | 0x0006 0x0000 | 0 0
115 | 1 0 | 0x0007 0x0000 | 1 0
135 | 0 0 | 0x0007 0x0000 | 1 0
145 | 1 0 | 0xdead 0x0000 | 1 0
=== READ UNTIL EMPTY ===
155 | 0 0 | 0xdead 0x0000 | 1 0
161 | 0 1 | 0xdead 0x0001 | 1 0
175 | 0 1 | 0xdead 0x0002 | 0 0
189 | 0 1 | 0xdead 0x0003 | 0 0
203 | 0 1 | 0xdead 0x0004 | 0 0
217 | 0 1 | 0xdead 0x0005 | 0 0
231 | 0 1 | 0xdead 0x0006 | 0 0
245 | 0 1 | 0xdead 0x0007 | 0 1
273 | 0 0 | 0xdead 0x0007 | 0 1
287 | 0 1 | 0xdead 0x0007 | 0 1
301 | 0 0 | 0xdead 0x0007 | 0 1

WRITE 구간에서 wr_en이 wclk 기준으로 올라가면서 wptr_bin이 증가하고 데이터가 메모리에 정상적으로 저장된다. 이때 empty 신호는 즉시 0으로 내려가지 않는데, 이는 write pointer(wptr_gray)가 read 클럭 도메인으로 Gray + 2FF synchronizer를 거쳐 전달되기 때문이다. 따라서 몇 번의 write 이후에 empty가 1 → 0으로 전이되는 지연이 보이며, 이는 CDC 구조상 정상적인 동작이다. full은 wptr_gray_next와 동기화된 rptr_gray를 비교해 판단되며, DEPTH(8)만큼 정확히 write가 누적된 시점에 1로 올라가고 이후 write는 차단된다.
READ 구간에서는 rd_en이 rclk 기준으로 올라가며 rptr_bin이 증가하고, write 시 저장된 데이터가 순서대로 dout에 출력된다. full 신호가 read 중간에 1 → 0으로 내려가는 것도 read pointer(rptr_gray)가 write 클럭 도메인으로 전달되는 데 필요한 동기화 지연 때문이다. 모든 데이터가 읽힌 뒤 rptr_gray와 동기화된 wptr_gray가 같아지는 시점에 empty가 1로 올라가며, 이후 read 요청은 무시되어 underflow가 발생하지 않는다.
결론적으로, full과 empty가 즉각 반응하지 않고 몇 클럭 늦게 변하는 현상은 Async FIFO의 필수적인 CDC 안정화 특성이며, 포인터 비교·데이터 순서·overflow/underflow 차단 모두가 정상적으로 동작하고 있음을 이 결과가 보여준다.
'VLSI > Design' 카테고리의 다른 글
| [Architecture] GALS: Globally Asynchronous, Locally Synchronous (0) | 2025.12.31 |
|---|---|
| [CDC] 11_Shadow Register, Active Register (0) | 2025.12.21 |
| [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] 7_Multi Bit CDC: Asynchronous FIFO (First-In First Out) (0) | 2025.12.20 |