uart_tx.sv / uart_rx.sv

Seungyun Lee·2026년 1월 27일

Comm_Accel(UART/SPI/I2C)

목록 보기
5/15

TX

TX FSM 동작 원리 (State Machine)

우리는 baud_gen에서 만든 tick 신호를 사용합니다. 데이터 한 비트(1 bit)를 전송하기 위해 tick을 16번 셉니다. (안정성을 위해 16x 오버샘플링 구조를 통일해서 사용합니다.)

  1. IDLE: 전송 요청(tx_start)이 올 때까지 대기 (TX Pin = 1 유지).
  2. START: Start Bit(0)를 내보내고 16 tick을 기다림.
  3. DATA: 8비트 데이터를 LSB(0번 비트)부터 하나씩 내보냄. (각 비트당 16 tick 유지).
  4. STOP: Stop Bit(1)를 내보내고 16 tick을 기다린 후 tx_done 신호 발생.

uart_tx.sv

`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

Testbench 소스 코드

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

시뮬레이션 실행 및 확인 방법

  1. Vivado Flow Navigator -> Simulation -> Run Simulation -> Run Behavioral Simulation 클릭.

  2. 시뮬레이션 창이 뜨면 상단 툴바의 Zoom Fit (돋보기 모양)을 누릅니다.

  3. 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이 떴다가 사라지는지 확인하세요.

  1. Start Bit 생성 확인:
    tx_start 신호(초록색 펄스)가 뜨자마자, 그 직후에 tx_pin이 1(High)에서 0(Low)으로 뚝 떨어졌죠?
    이게 바로 Start Bit입니다. UART 통신의 시작을 알리는 신호가 정확히 나갔습니다.

  2. LSB First (0x55) 확인:
    보내려는 데이터가 0x55 (이진수 01010101)입니다.
    UART는 뒤쪽 비트(LSB)부터 보냅니다. 즉, 맨 오른쪽 1부터 나가야 합니다.

노란색 커서 뒤를 보면 tx_pin이 다시 High(1)로 올라가죠? 첫 번째 데이터 비트인 1이 잘 나가고 있다는 증거입니다.

  1. 빨간색 XX (rx_xxx 신호들):
    rx_done, rx_data가 빨간색(XX)인 이유는 우리가 아직 RX 모듈을 연결하지 않았기 때문입니다. uart_core 코드에서 주석 처리해 놨으니 당연한 결과입니다. 걱정 마세요.

RX

[RX 설계 핵심 포인트 2가지]

  1. Metastability 방지 (싱크로나이저):
  • 외부 핀(rx_pin) 신호는 FPGA 내부 클럭과 타이밍이 맞지 않습니다.
  • 이 신호를 바로 쓰면 에러가 납니다. 플립플롭 2개를 거쳐서(Sync) 안전하게 만들어야 합니다.
  1. 중앙 샘플링 (Center Sampling):
  • 데이터가 흔들릴 수 있으니, 비트의 정중앙 (16 tick 중 7번째 tick)에서 값을 읽어야 가장 정확합니다.

uart_rx.sv

`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

TX, RX 전체 결과

  1. Loopback 성공 (0x55):
  • 첫 번째 패킷에서 tx_data가 55일 때, 잠시 후 rx_done 신호(초록색 펄스)가 뜨면서 rx_data가 00 → 55로 짠 하고 바뀌었죠?
  • "내가 보낸 55를 내가 정확히 받았다"는 뜻입니다.
  1. 연속 전송 성공 (0x33):
  • 두 번째 패킷 33도 마찬가지로 전송이 끝난 직후 rx_data가 33으로 업데이트되었습니다.
  1. 타이밍 일치:
  • tx_pin이 움직이는 동안에는 RX가 조용하다가, Stop Bit가 끝나는 시점에 정확히 rx_done이 떴습니다. 타이밍도 아주 좋습니다.
profile
Design Verification engineer

0개의 댓글