우리는 baud_gen에서 만든 tick 신호를 사용합니다. 데이터 한 비트(1 bit)를 전송하기 위해 tick을 16번 셉니다. (안정성을 위해 16x 오버샘플링 구조를 통일해서 사용합니다.)
`timescale 1ns / 1ps
module uart_tx (
input logic clk,
input logic reset_n,
input logic tick, // baud_gen에서 오는 16x tick
input logic tx_start, // 전송 시작 신호
input logic [7:0] tx_data, // 전송할 8비트 데이터
output logic tx_pin, // 외부로 나가는 시리얼 핀
output logic tx_done, // 전송 완료 신호 (1클럭 펄스)
output logic tx_busy // 전송 중 상태 표시
);
// FSM 상태 정의
typedef enum logic [1:0] {
IDLE,
START,
DATA,
STOP
} state_t;
state_t state, state_next;
// 내부 레지스터
logic [3:0] tick_cnt, tick_cnt_next; // 0~15 세는 카운터 (한 비트 길이)
logic [2:0] bit_cnt, bit_cnt_next; // 0~7 세는 카운터 (데이터 비트 수)
logic [7:0] shift_reg, shift_reg_next; // 데이터 저장 및 시프트용
logic tx_reg, tx_reg_next; // TX 핀 출력용 레지스터
// -----------------------------------------------------------
// Sequential Logic: 클럭에 맞춰 상태 및 레지스터 업데이트
// -----------------------------------------------------------
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
state <= IDLE;
tick_cnt <= 0;
bit_cnt <= 0;
shift_reg <= 0;
tx_reg <= 1'b1; // Idle 상태에서는 High 유지
end else begin
state <= state_next;
tick_cnt <= tick_cnt_next;
bit_cnt <= bit_cnt_next;
shift_reg <= shift_reg_next;
tx_reg <= tx_reg_next;
end
end
// -----------------------------------------------------------
// Combinational Logic: Next State Logic
// -----------------------------------------------------------
always_comb begin
// Default 값 유지 (Latch 방지)
state_next = state;
tick_cnt_next = tick_cnt;
bit_cnt_next = bit_cnt;
shift_reg_next = shift_reg;
tx_reg_next = tx_reg;
tx_done = 1'b0;
tx_busy = 1'b1; // 기본적으로 바쁨 상태, IDLE에서만 0
case (state)
IDLE: begin
tx_busy = 1'b0;
tx_reg_next = 1'b1; // Idle Line is High
if (tx_start) begin
state_next = START;
tick_cnt_next = 0;
shift_reg_next = tx_data; // 데이터 캡쳐
end
end
START: begin
tx_reg_next = 1'b0; // Start Bit = Low
if (tick) begin // baud_gen에서 펄스가 튈 때만 카운트 증가
if (tick_cnt == 15) begin
state_next = DATA;
tick_cnt_next = 0;
bit_cnt_next = 0;
end else begin
tick_cnt_next = tick_cnt + 1;
end
end
end
DATA: begin
tx_reg_next = shift_reg[0]; // LSB부터 전송
if (tick) begin
if (tick_cnt == 15) begin
tick_cnt_next = 0;
shift_reg_next = shift_reg >> 1; // 오른쪽으로 시프트
if (bit_cnt == 7) begin // 8비트 전송 완료
state_next = STOP;
end else begin
bit_cnt_next = bit_cnt + 1;
end
end else begin
tick_cnt_next = tick_cnt + 1;
end
end
end
STOP: begin
tx_reg_next = 1'b1; // Stop Bit = High
if (tick) begin
if (tick_cnt == 15) begin // Stop bit 시간 충족
state_next = IDLE;
tx_done = 1'b1; // 완료 펄스 발생
end else begin
tick_cnt_next = tick_cnt + 1;
end
end
end
endcase
end
// 출력 연결
assign tx_pin = tx_reg;
endmodule
tb_uart_core.sv
`timescale 1ns / 1ps
module tb_uart_core();
// 1. 신호 선언 (FIFO 인터페이스로 변경됨)
logic clk;
logic reset_n;
logic [15:0] divisor;
logic tx_pin; // Loopback으로 연결될 핀
// TX FIFO Interface
logic tx_wr_en;
logic [7:0] tx_wr_data;
logic tx_full;
// RX FIFO Interface
logic rx_rd_en;
logic [7:0] rx_rd_data;
logic rx_empty;
logic rx_err;
// 2. DUT 연결 (Loopback 설정: rx_pin <- tx_pin)
uart_core u_dut (
.clk (clk),
.reset_n (reset_n),
.divisor (divisor),
.rx_pin (tx_pin),
.tx_pin (tx_pin),
.tx_wr_en (tx_wr_en),
.tx_wr_data (tx_wr_data),
.tx_full (tx_full),
.rx_rd_en (rx_rd_en),
.rx_rd_data (rx_rd_data),
.rx_empty (rx_empty),
.rx_err (rx_err)
);
// 3. 클럭 생성
always #5 clk = ~clk;
// 4. 테스트 시나리오
initial begin
// 초기화
clk = 0;
reset_n = 0;
divisor = 16'd10; // 시뮬레이션용 고속 설정
tx_wr_en = 0;
tx_wr_data = 0;
rx_rd_en = 0;
// 리셋 해제
#100;
reset_n = 1;
#20;
$display("---------------------------------------------------");
$display(" Test Start: FIFO Burst Write -> UART Loopback");
$display("---------------------------------------------------");
// --------------------------------------------------------
// Step 1: Foolproof Write (확실하게 넣기)
// --------------------------------------------------------
// 1. 첫 번째 데이터 0xAA
@(negedge clk); // 클럭이 내려갈 때 준비 (타이밍 확보)
tx_wr_en = 1;
tx_wr_data = 8'hAA;
@(negedge clk); // <--- [핵심] 다음 클럭 내려갈 때까지 기다림 (1주기 유지)
tx_wr_en = 0; // 이제 끔
// --------------------------------------------------------
// 2. 두 번째 데이터 0xBB
@(negedge clk); // 1클럭 쉬고
tx_wr_en = 1; // 다시 켬
tx_wr_data = 8'hBB;
@(negedge clk); // <--- [핵심] 또 1주기 유지
tx_wr_en = 0;
// 3. 세 번째 데이터 0xCC
@(negedge clk);
tx_wr_en = 1;
tx_wr_data = 8'hCC;
@(negedge clk); // <--- [핵심] 1주기 유지
tx_wr_en = 0;
// --------------------------------------------------------
// Step 2: 수신 대기 및 읽기 (Polling)
// --------------------------------------------------------
// UART가 느리게 돌면서 하나씩 전송하고, RX FIFO에 쌓일 때까지 기다림
// 첫 번째 데이터(0xAA) 수신 대기
wait(rx_empty == 1'b0);
@(posedge clk);
rx_rd_en = 1; // FIFO Pop
@(posedge clk);
rx_rd_en = 0;
$display("[CPU] Read from RX FIFO: 0x%h (Expected 0xAA)", rx_rd_data);
// 두 번째 데이터(0xBB) 수신 대기
wait(rx_empty == 1'b0);
@(posedge clk);
rx_rd_en = 1;
@(posedge clk);
rx_rd_en = 0;
$display("[CPU] Read from RX FIFO: 0x%h (Expected 0xBB)", rx_rd_data);
// 세 번째 데이터(0xCC) 수신 대기
wait(rx_empty == 1'b0);
@(posedge clk);
rx_rd_en = 1;
@(posedge clk);
rx_rd_en = 0;
$display("[CPU] Read from RX FIFO: 0x%h (Expected 0xCC)", rx_rd_data);
#100;
$display("---------------------------------------------------");
$display(" Test Complete: FIFO Logic Verified!");
$display("---------------------------------------------------");
$finish;
end
endmodule
Vivado Flow Navigator -> Simulation -> Run Simulation -> Run Behavioral Simulation 클릭.
시뮬레이션 창이 뜨면 상단 툴바의 Zoom Fit (돋보기 모양)을 누릅니다.
Waveform 확인 포인트 (중요!):
tx_start가 1이 되는 순간을 찾으세요.
그 직후 tx_pin이 High(1)에서 Low(0)로 떨어지는지 확인하세요. (Start Bit)
그 뒤로 데이터 비트가 나가는지 확인하세요.
0x55 (01010101)를 보냈지만, UART는 LSB First이므로 파형 순서는 1 -> 0 -> 1 -> 0 -> 1 -> 0 -> 1 -> 0 순서로 나갑니다.
마지막에 tx_pin이 다시 High(1)로 올라가는지 확인하세요. (Stop Bit)
Stop bit가 끝나는 시점에 tx_done 신호가 1이 떴다가 사라지는지 확인하세요.

Start Bit 생성 확인:
tx_start 신호(초록색 펄스)가 뜨자마자, 그 직후에 tx_pin이 1(High)에서 0(Low)으로 뚝 떨어졌죠?
이게 바로 Start Bit입니다. UART 통신의 시작을 알리는 신호가 정확히 나갔습니다.
LSB First (0x55) 확인:
보내려는 데이터가 0x55 (이진수 01010101)입니다.
UART는 뒤쪽 비트(LSB)부터 보냅니다. 즉, 맨 오른쪽 1부터 나가야 합니다.

노란색 커서 뒤를 보면 tx_pin이 다시 High(1)로 올라가죠? 첫 번째 데이터 비트인 1이 잘 나가고 있다는 증거입니다.
[RX 설계 핵심 포인트 2가지]
`timescale 1ns / 1ps
module uart_rx (
input logic clk,
input logic reset_n,
input logic tick,
input logic rx_pin,
output logic rx_done, // 이제 레지스터(Flip-flop) 출력이 됩니다.
output logic [7:0] rx_data,
output logic rx_err
);
typedef enum logic [1:0] {IDLE, START, DATA, STOP} state_t;
state_t state, state_next;
logic [3:0] tick_cnt, tick_cnt_next;
logic [2:0] bit_cnt, bit_cnt_next;
logic [7:0] shift_reg, shift_reg_next;
// Sync Logic
logic rx_sync_0, rx_sync_1;
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
rx_sync_0 <= 1'b1;
rx_sync_1 <= 1'b1;
end else begin
rx_sync_0 <= rx_pin;
rx_sync_1 <= rx_sync_0;
end
end
// ----------------------------------------------------------------
// [핵심 변경] State Machine과 Output Logic을 분리하지 않고
// rx_done과 rx_data를 '동시에' 업데이트하도록 수정 (Glitch 방지)
// ----------------------------------------------------------------
always_ff @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
state <= IDLE;
tick_cnt <= 0;
bit_cnt <= 0;
shift_reg <= 0;
rx_data <= 0;
rx_done <= 0; // Reset
rx_err <= 0;
end else begin
// Default
rx_done <= 0; // 기본적으로 0 (1클럭 펄스 만들기 위함)
// Next State Logic을 여기에 통합하여 타이밍을 맞춤
case (state)
IDLE: begin
if (rx_sync_1 == 1'b0) begin
state <= START;
tick_cnt <= 0;
end
end
START: begin
if (tick) begin
if (tick_cnt == 7) begin
if (rx_sync_1 == 1'b0) begin
tick_cnt <= 0;
bit_cnt <= 0;
state <= DATA;
end else begin
state <= IDLE;
end
end else begin
tick_cnt <= tick_cnt + 1;
end
end
end
DATA: begin
if (tick) begin
if (tick_cnt == 15) begin
tick_cnt <= 0;
shift_reg <= {rx_sync_1, shift_reg[7:1]};
if (bit_cnt == 7) begin
state <= STOP;
end else begin
bit_cnt <= bit_cnt + 1;
end
end else begin
tick_cnt <= tick_cnt + 1;
end
end
end
STOP: begin
if (tick) begin
if (tick_cnt == 15) begin
state <= IDLE;
rx_done <= 1'b1; // Done 신호 발생!
rx_data <= shift_reg; // 데이터 업데이트! (동시에 발생)
if (rx_sync_1 == 1'b0) rx_err <= 1'b1;
else rx_err <= 1'b0;
end else begin
tick_cnt <= tick_cnt + 1;
end
end
end
endcase
end
end
endmodule
