이제 UART 기본 기능은 검증되었습니다. 하지만 지금 상태로는 '가속기'라고 부르기엔 부족합니다.
현재 문제점: CPU가 데이터 1바이트를 보내려면, 전송이 끝날 때까지 멍하니 기다려야 합니다. (Polling 방식)
해결책 (FIFO): CPU는 FIFO(창고)에 데이터를 후다닥 넣어두고 다른 일을 하러 갑니다. UART 하드웨어가 FIFO에서 데이터를 하나씩 꺼내서 알아서 보냅니다.
이것이 하드웨어 가속의 핵심입니다.
FIFO소스코드
fifo.sv
`timescale 1ns / 1ps
module fifo #(
parameter DATA_WIDTH = 8,
parameter ADDR_WIDTH = 4 // 2^4 = 16 depth
)(
input logic clk,
input logic reset_n,
// Write Interface (CPU -> FIFO)
input logic wr_en,
input logic [DATA_WIDTH-1:0] wr_data,
output logic full,
// Read Interface (FIFO -> UART Core)
input logic rd_en,
output logic [DATA_WIDTH-1:0] rd_data,
output logic empty
);
// 내부 메모리 배열
logic [DATA_WIDTH-1:0] mem [0:(2**ADDR_WIDTH)-1];
// [추가할 코드] 시뮬레이션을 위해 메모리를 0으로 초기화
initial begin
for (int i = 0; i < (2**ADDR_WIDTH); i++) begin
mem[i] = '0;
end
end
// 포인터들
logic [ADDR_WIDTH:0] wr_ptr, wr_ptr_next; // 1비트 더 크게 잡음 (Full/Empty 구분용)
logic [ADDR_WIDTH:0] rd_ptr, rd_ptr_next;
// --------------------------------------------------------
// Write & Read Logic
// --------------------------------------------------------
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
wr_ptr <= 0;
rd_ptr <= 0;
end else begin
wr_ptr <= wr_ptr_next;
rd_ptr <= rd_ptr_next;
end
end
// Memory Write (No Reset needed for memory)
always_ff @(posedge clk) begin
if (wr_en && !full) begin
mem[wr_ptr[ADDR_WIDTH-1:0]] <= wr_data;
end
end
// Next Pointer Logic
always_comb begin
wr_ptr_next = wr_ptr;
rd_ptr_next = rd_ptr;
if (wr_en && !full) begin
wr_ptr_next = wr_ptr + 1;
end
if (rd_en && !empty) begin
rd_ptr_next = rd_ptr + 1;
end
end
// --------------------------------------------------------
// Status Signals
// --------------------------------------------------------
// Read는 Combinational로 바로 출력 (First Word Fall Through 아님, 일반 모드)
assign rd_data = mem[rd_ptr[ADDR_WIDTH-1:0]];
// Empty: 포인터가 완전히 같을 때
assign empty = (wr_ptr == rd_ptr);
// Full: MSB(최상위 비트)만 다르고 나머지가 같을 때
// 예: wr_ptr=10000(16), rd_ptr=00000(0) -> 꽉 참
assign full = (wr_ptr[ADDR_WIDTH-1:0] == rd_ptr[ADDR_WIDTH-1:0]) &&
(wr_ptr[ADDR_WIDTH] != rd_ptr[ADDR_WIDTH]);
endmodule
이제 uart_core 안에 TX FIFO와 RX FIFO를 장착해야 합니다. uart_core.sv 전체를 아래 코드로 덮어쓰기 하세요.
[변경점]
1. 입출력 인터페이스 이름이 조금 바뀝니다 (tx_fifo_wr, rx_fifo_rd 등).
2. 내부에 fifo 인스턴스 2개가 추가됩니다.
3. Glue Logic: FIFO와 UART 사이를 이어주는 간단한 로직이 추가됩니다. (FIFO가 안 비었으면 UART에게 "보내!"라고 신호 줌)
uart_core.sv
`timescale 1ns / 1ps
module uart_core (
input logic clk,
input logic reset_n,
input logic [15:0] divisor,
// UART Physical Interface
input logic rx_pin,
output logic tx_pin,
// --- User Interface (FIFO 제어용) ---
// TX FIFO Write (CPU가 데이터를 넣음)
input logic tx_wr_en,
input logic [7:0] tx_wr_data,
output logic tx_full,
// RX FIFO Read (CPU가 데이터를 꺼냄)
input logic rx_rd_en,
output logic [7:0] rx_rd_data,
output logic rx_empty,
output logic rx_err // 에러 상태
);
// Internal Signals
logic tick;
// TX Channel Signals
logic tx_fifo_empty;
logic [7:0] tx_fifo_out;
logic tx_core_start;
logic tx_core_done;
logic tx_core_busy;
// RX Channel Signals
logic rx_core_done;
logic [7:0] rx_core_data;
logic rx_core_err;
// -------------------------------------------------------
// 1. Baud Rate Generator
// -------------------------------------------------------
baud_gen u_baud_gen (
.clk(clk), .reset_n(reset_n), .divisor(divisor), .tick(tick)
);
// -------------------------------------------------------
// 2. TX Channel (FIFO + UART TX)
// -------------------------------------------------------
// [TX FIFO]
fifo #(.DATA_WIDTH(8), .ADDR_WIDTH(4)) u_tx_fifo (
.clk(clk), .reset_n(reset_n),
.wr_en(tx_wr_en), .wr_data(tx_wr_data), .full(tx_full), // CPU Side
.rd_en(tx_core_done), .rd_data(tx_fifo_out), .empty(tx_fifo_empty) // UART Side
);
// 주의: 여기서는 간단하게 구현하기 위해 UART가 전송을 마치면(tx_core_done)
// 자동으로 FIFO의 다음 데이터를 꺼내도록(rd_en) 연결했습니다.
// 하지만, 첫 데이터를 시작시키기 위한 트리거 로직이 필요합니다.
// [TX Control Logic]
// FIFO에 데이터가 있고(not empty), UART가 바쁘지 않으면(not busy) 전송 시작!
assign tx_core_start = (!tx_fifo_empty) && (!tx_core_busy);
// [UART TX Core]
uart_tx u_uart_tx (
.clk(clk), .reset_n(reset_n), .tick(tick),
.tx_start(tx_core_start), .tx_data(tx_fifo_out),
.tx_pin(tx_pin), .tx_done(tx_core_done), .tx_busy(tx_core_busy)
);
// -------------------------------------------------------
// 3. RX Channel (UART RX + FIFO)
// -------------------------------------------------------
// [UART RX Core]
uart_rx u_uart_rx (
.clk(clk), .reset_n(reset_n), .tick(tick), .rx_pin(rx_pin),
.rx_done(rx_core_done), .rx_data(rx_core_data), .rx_err(rx_core_err)
);
// [RX FIFO]
// UART가 수신을 완료하면(rx_core_done) 자동으로 FIFO에 Write(wr_en) 합니다.
fifo #(.DATA_WIDTH(8), .ADDR_WIDTH(4)) u_rx_fifo (
.clk(clk), .reset_n(reset_n),
.wr_en(rx_core_done), .wr_data(rx_core_data), .full(), // UART Side (full은 무시)
.rd_en(rx_rd_en), .rd_data(rx_rd_data), .empty(rx_empty) // CPU Side
);
assign rx_err = rx_core_err;
endmodule

왜 RX 데이터가 XX (Red)로 나올까요?
파형을 보면 rx_rd_data가 XX였다가 맨 마지막에 cc로 바뀌죠? 이것은 TX(송신부)가 "빈 FIFO를 읽어서 생긴 쓰레기 값(X)"을 보냈기 때문입니다.
[범인은 바로 타이밍!]
정상 동작: UART가 AA, BB, CC를 잘 보냅니다.
문제 발생: CC를 다 보내고 나면 FIFO가 "비었음(Empty)" 상태가 됩니다.
버그: 하지만 아주 미세한 타이밍 차이로, UART 모듈이 "아직 데이터가 있네?" 라고 착각하고 한 번 더 전송(Start)을 시도합니다.
결과: FIFO는 이미 비었기 때문에 XX (알 수 없는 값)이 튀어나오고, UART는 이 빨간색 XX 데이터를 전송해버립니다. 그래서 RX도 XX를 받게 된 것입니다.
해결 솔루션 (코드 수정)
이 "유령 전송"을 막기 위해 FIFO 메모리를 깨끗하게 초기화해주고, Start 조건을 조금 더 엄격하게 잡아주면 됩니다.
FIFO 메모리의 초기값이 XX라서 문제가 더 커 보입니다. 00으로 초기화해줍시다. fifo.sv 파일을 열고 mem 선언 바로 아래에 초기화 코드를 추가하세요.
// 내부 메모리 배열
logic [DATA_WIDTH-1:0] mem [0:(2**ADDR_WIDTH)-1];
// [추가할 코드] 시뮬레이션을 위해 메모리를 0으로 초기화
initial begin
for (int i = 0; i < (2**ADDR_WIDTH); i++) begin
mem[i] = '0;
end
end
UART가 너무 급하게 출발하지 않도록 조건을 수정합니다. uart_core.sv에서 tx_core_start 부분을 찾아 아래처럼 수정하세요.
// [TX Control Logic] 수정
// FIFO가 비어있지 않고 + UART가 바쁘지 않고 + "방금 전송이 끝나지 않았을 때(!tx_core_done)"
// tx_core_done을 추가해서 연속 전송 시 한 템포 쉬어가게 만듭니다.
assign tx_core_start = (!tx_fifo_empty) && (!tx_core_busy) && (!tx_core_done);

원인 분석: "성격 급한 rx_done"
지금 코드는 데이터를 저장하기도 전에 "다 했어!(rx_done)"라고 소리치는 상황입니다.
그래서 AA, BB가 들어갈 자리에 업데이트 전 값인 00이 들어간 것이고, 마지막 CC는 운 좋게 타이밍이 맞았거나 이전 값이 밀려서 나온 것입니다.
rx_done 신호를 데이터(rx_data)와 똑같이 한 박자 늦게(Registered) 나오도록 만들어주면 됩니다. 그러면 FIFO가 완벽하게 업데이트된 데이터를 가져갑니다.
uart_rx.sv 전체를 아래 코드로 교체하세요. (핵심 변경: rx_done을 always_comb에서 빼고, always_ff 안으로 넣어서 클럭에 맞췄습니다.)
tick 신호도 잘 뛰고 있고, 보내주신 파형(image_42e5e2.png)을 분석해보니 버그가 아닙니다. 단지 "시뮬레이션 시간이 너무 짧아서" 고장난 것처럼 보이는 착시 현상입니다.
"TX가 멈춘 게 아니다" (과학적 증거)
1. 왜 계속 0(Low)인가?

핵심은 데이터가 밀리지 않고 들어오며, 마지막 값을 정확히 잡았다는 점입니다.