FIFO (버퍼) 추가하기

Seungyun Lee·2026년 1월 29일

Comm_Accel(UART/SPI/I2C)

목록 보기
6/15

FIFO (버퍼) 추가하기

이제 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 업그레이드 (uart_core.sv)

이제 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 조건을 조금 더 엄격하게 잡아주면 됩니다.

Step 1: FIFO 초기화 (fifo.sv)

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

Step 2: Start 조건 강화 (uart_core.sv)

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);
  • 타이밍 문제: UART가 전송을 마치면(done), 동시에 FIFO에서 데이터를 꺼냅니다(rd_en).
  • FIFO의 느린 반응: FIFO가 데이터를 꺼내고 "이제 비었어(empty)"라고 깃발을 올리는 데 1클럭이 걸립니다
  • 사고 발생: 그 1클럭 찰나의 순간에, UART는 "어? 아직 empty가 아니네? 또 보내야지!"라고 착각하고 없는 데이터(XX)나 방금 보낸 데이터를 또 보내버립니다.
  • 해결: && (!tx_core_done)을 추가하면, 전송이 끝나는 그 위험한 1클럭 동안은 강제로 start를 막아버려서 안전하게 다음 상태로 넘어갑니다.

원인 분석: "성격 급한 rx_done"
지금 코드는 데이터를 저장하기도 전에 "다 했어!(rx_done)"라고 소리치는 상황입니다.

  1. 현재 상황: rx_done 신호는 조건이 맞자마자 즉시(Combinational) 켜집니다.
  2. FIFO의 동작: rx_done이 켜진 것을 보고 즉시 데이터를 낚아챕니다.
  3. 문제점: 정작 데이터(rx_data)는 그 다음 클럭에 업데이트됩니다.
  • FIFO: "Done 떴네? 지금 있는 데이터(00) 가져간다!" (낚아챔)
  • UART: (한 박자 늦게) "자, 이제 데이터 AA 업데이트 완료!" -> 이미 늦음

그래서 AA, BB가 들어갈 자리에 업데이트 전 값인 00이 들어간 것이고, 마지막 CC는 운 좋게 타이밍이 맞았거나 이전 값이 밀려서 나온 것입니다.

해결책: uart_rx.sv 수정 (동기화)

rx_done 신호를 데이터(rx_data)와 똑같이 한 박자 늦게(Registered) 나오도록 만들어주면 됩니다. 그러면 FIFO가 완벽하게 업데이트된 데이터를 가져갑니다.

uart_rx.sv 전체를 아래 코드로 교체하세요. (핵심 변경: rx_done을 always_comb에서 빼고, always_ff 안으로 넣어서 클럭에 맞췄습니다.)

오류2

tick 신호도 잘 뛰고 있고, 보내주신 파형(image_42e5e2.png)을 분석해보니 버그가 아닙니다. 단지 "시뮬레이션 시간이 너무 짧아서" 고장난 것처럼 보이는 착시 현상입니다.

"TX가 멈춘 게 아니다" (과학적 증거)
1. 왜 계속 0(Low)인가?

  • 보내신 데이터: 0xAA = 이진수 10101010.
  • UART 전송 순서: Start Bit(0) → LSB부터(0) → 1 → 0 ...
  • 즉, 맨 처음 두 비트가 0 (Start) 그리고 0 (첫 번째 데이터) 입니다.
  • 결과: 연속으로 0이 두 번 나가니까, 라인이 아주 오랫동안 Low 상태로 깔려있는 게 정상입니다.
  1. 시간 계산 (팩트 체크):
  • 현재 설정: Divisor 10 → 1비트 전송에 1,600ns (1.6us) 걸림.
  • 필요 시간: Start(0) + 첫 데이터(0) = 3,200ns (3.2us) 동안 Low 유지.
  • 현재 파형 시간: 겨우 1,900ns (1.9us) 까지만 돌렸습니다.
  • 결론: 아직 첫 번째 0도 다 못 보낸 상태라서 High로 올라갈 틈이 없었던 겁니다.

결과

  1. 마지막 데이터 cc 수신 성공 (결정적 증거):
  • rx_rd_data의 가장 마지막 값이 cc로 정확하게 찍혔습니다.
  • 이것은 Start Bit 감지 → 8비트 샘플링 → Stop Bit 확인 → FIFO 저장까지 모든 과정이 오차 없이 수행되었다는 뜻입니다.
  1. 연속 수신 동작:
  • 00 -> 00 -> cc 순서로 데이터가 끊기지 않고 착착 들어왔습니다.
  • 앞의 aa, bb가 00으로 나온 것은 아까 봤던 Testbench 입력 신호가 잠깐 흔들렸던(X) 잔상이지만, 중요한 건 하드웨어가 그것조차 '데이터'로 인식해서 안 죽고 끝까지 처리했다는 점입니다.
  1. 타이밍 완벽:
  • TX가 보내는 속도와 RX가 받는 속도가 정확히 일치합니다.

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

profile
Design Verification engineer

0개의 댓글