SPI 개념

Seungyun Lee·2026년 2월 1일

Comm_Accel(UART/SPI/I2C)

목록 보기
8/15

1. SPI 통신: 핵심 개념 (이것만 알면 끝)

SPI(Serial Peripheral Interface)는 "동기식(Synchronous)" 통신입니다. UART처럼 타이밍을 맞추느라 고생할 필요 없이, 마스터(FPGA)가 "자, 받어!" 하고 클럭을 쏴줄 때 데이터를 주고받습니다.

4개의 핵심 신호 (Wire)

  1. SCLK (Serial Clock): FPGA가 만들어내는 박자입니다. 이 박자에 맞춰 데이터가 이동합니다.
  2. MOSI (Master Out Slave In): FPGA가 슬레이브(센서 등)에게 보내는 데이터 선입니다.
  3. MISO (Master In Slave Out): 슬레이브가 FPGA에게 보내는 데이터 선입니다.
  4. CS_N (Chip Select, Active Low): "야, 너 나랑 얘기 좀 하자"라고 슬레이브를 깨우는 신호입니다. (평소 1, 통신할 때 0).

작동 원리: "원형 컨베이어 벨트"

SPI의 핵심은 교환(Exchange)입니다.

  • 마스터와 슬레이브가 각각 8비트 통을 가지고 있다고 상상해보세요.
  • 클럭이 한 번 뛸 때마다, 내 것 1비트를 주고 상대방 것 1비트를 가져옵니다.
  • 8번 뛰고 나면? 내 데이터는 저쪽에 가 있고, 저쪽 데이터는 나한테 와 있습니다. (이게 SPI의 전부입니다.)

2. Verilog 설계를 위한 전략 (Architecture)

우리는 SPI Master를 만들 겁니다. (FPGA가 주도권을 쥠) 가장 많이 쓰이는 SPI Mode 0 (CPOL=0, CPHA=0) 기준으로 설계 뼈대를 잡겠습니다.

  • Idle 상태: SCLK는 0 (Low)
  • 데이터 샘플링: 클럭의 상승 에지(Rising Edge)에서 데이터를 읽음.
  • 데이터 변경: 클럭의 하강 에지(Falling Edge)에서 데이터를 바꿈.

설계해야 할 모듈 내부 구조
1. Clock Divider: 100MHz 시스템 클럭을 우리가 원하는 SPI 속도(예: 1MHz)로 낮추는 카운터.
2. Bit Counter: 지금 몇 번째 비트(0~7)를 보내고 있는지 세는 녀석.
3. Shift Register: 데이터를 밀어내고(MOSI), 받아들이는(MISO) 8비트 레지스터.
4. State Machine (FSM): 전체 흐름 제어 (IDLE -> TRANSFER -> DONE).

3. SPI Master 코드 뼈대 (Skeleton Code)

[ Zynq CPU (Software) ]
       │
       │ (AXI 통신: "0x4000번지에 0xAA 써라!")
       ▼
+----------------------------------------+
|  spi_ip.sv (껍데기/포장지)                |
|                                        |
|  1. AXI 신호 해석 (awaddr, wdata...)     |
|  2. 레지스터 관리 (0x00, 0x04...)         |
|                                        |
|       +------------------------+       |
|       |  spi_master.sv (알맹이)  |      |
|       |                        |       |
|       |  - SCLK 만듦            |───────> 보드 밖 (Pmod)
|       |  - 비트 시프트 (Shift)   |       |
|       |  - MOSI/MISO 처리       |       |
|       +------------------------+       |
+----------------------------------------+

spi_master: SPI 파형을 만드는 진짜 기계. (핵심 기술)
spi_ip: 그 기계를 CPU가 쓰기 편하게 포장한 리모컨. (AXI 연결용)

spi_master.sv

`timescale 1ns / 1ps

module spi_master (
    input  logic        clk,        // 시스템 클럭 (100MHz)
    input  logic        rst_n,      // 리셋 (Active Low)
    
    // [User Interface]
    input  logic        start,      // 전송 시작 신호 (Pulse)
    input  logic [7:0]  tx_data,    // 보낼 데이터
    output logic [7:0]  rx_data,    // 받은 데이터
    output logic        busy,       // 동작 중 상태
    output logic        done,       // 완료 신호 (Pulse)

    // [Physical Interface]
    output logic        sclk,       // SPI Serial Clock
    output logic        mosi,       // Master Out Slave In
    input  logic        miso,       // Master In Slave Out
    output logic        cs_n        // Chip Select (Active Low)
);

    // ====================================================
    // 1. 파라미터 및 타입 정의 (SystemVerilog의 장점!)
    // ====================================================
    localparam int CLK_DIV = 100;       // 100MHz / 100 = 1MHz
    localparam int CNT_MID = CLK_DIV/2; // 50 (Rising Edge 지점)

    // [Enum] 상태를 명시적인 이름으로 정의 (디버깅할 때 숫자가 아닌 이름으로 보임)
    typedef enum logic [1:0] {
        IDLE,
        START,
        TRANS,
        DONE_STATE
    } state_t;

    state_t state;  // 현재 상태 변수

    // 내부 신호 정의 (reg/wire 대신 logic 하나로 통일)
    logic [6:0] clk_cnt;    // 분주 카운터 (0~99)
    logic [2:0] bit_cnt;    // 비트 카운터 (7~0)
    logic [7:0] shift_reg;  // 시프트 레지스터

    // ====================================================
    // 2. 메인 시퀀셜 로직 (always_ff 사용)
    // ====================================================
    // always_ff: 플립플롭 합성을 명시하여 실수를 방지함
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state     <= IDLE;
            cs_n      <= 1'b1;
            sclk      <= 1'b0;  // Mode 0
            mosi      <= 1'b0;
            busy      <= 1'b0;
            done      <= 1'b0;
            rx_data   <= 8'b0;
            clk_cnt   <= '0;    // '0은 모든 비트를 0으로 채움
            bit_cnt   <= '0;
            shift_reg <= '0;
        end else begin
            case (state)
                // ------------------------------------------------
                // [IDLE] 대기 상태
                // ------------------------------------------------
                IDLE: begin
                    cs_n <= 1'b1;
                    sclk <= 1'b0;
                    done <= 1'b0;
                    
                    if (start) begin
                        shift_reg <= tx_data; // 데이터 로드
                        busy      <= 1'b1;
                        state     <= START;
                    end else begin
                        busy      <= 1'b0;
                    end
                end

                // ------------------------------------------------
                // [START] Setup 단계
                // ------------------------------------------------
                START: begin
                    clk_cnt <= '0;
                    bit_cnt <= 3'd7; 
                    
                    cs_n    <= 1'b0;      // Slave 선택
                    
                    // Mode 0: 첫 클럭 Rising 전에 데이터가 나가있어야 함
                    mosi    <= shift_reg[7]; 
                    
                    state   <= TRANS;
                end

                // ------------------------------------------------
                // [TRANS] 데이터 전송 (Mode 0: Rising Sample, Falling Shift)
                // ------------------------------------------------
                TRANS: begin
                    // 클럭 분주 카운터
                    if (clk_cnt < CLK_DIV - 1) 
                        clk_cnt <= clk_cnt + 1;
                    else 
                        clk_cnt <= '0;

                    // --- Timing 1: Rising Edge (Sample MISO) ---
                    if (clk_cnt == 0) begin
                        sclk <= 1'b0; 
                    end
                    else if (clk_cnt == CNT_MID) begin
                        sclk <= 1'b1;         // SCLK Rising
                        shift_reg[0] <= miso; // LSB에 수신 데이터 저장
                    end

                    // --- Timing 2: Falling Edge (Shift & MOSI Update) ---
                    else if (clk_cnt == CLK_DIV - 1) begin
                        sclk <= 1'b0;         // SCLK Falling
                        
                        if (bit_cnt > 0) begin
                            bit_cnt <= bit_cnt - 1;
                            // Shift Left: {기존[6:0], 0}
                            shift_reg <= {shift_reg[6:0], 1'b0}; 
                            mosi      <= shift_reg[6]; // 다음 비트 출력
                        end else begin
                            state <= DONE_STATE; // 8비트 완료
                        end
                    end
                end

                // ------------------------------------------------
                // [DONE] 종료 처리
                // ------------------------------------------------
                DONE_STATE: begin
                    cs_n    <= 1'b1;
                    busy    <= 1'b0;
                    done    <= 1'b1;      // 완료 펄스 발생
                    rx_data <= shift_reg; // 수신 데이터 업데이트
                    state   <= IDLE;
                end
                
                default: state <= IDLE;
            endcase
        end
    end

endmodule

spi_ip

`timescale 1ns / 1ps

module spi_ip #
(
    // AXI4-Lite 파라미터
    parameter integer C_S_AXI_DATA_WIDTH = 32,
    parameter integer C_S_AXI_ADDR_WIDTH = 4
)
(
    // [1] 시스템 신호
    input  logic                                s_axi_aclk,
    input  logic                                s_axi_aresetn,

    // [2] AXI4-Lite Slave 인터페이스
    // Write Address Channel
    input  logic [C_S_AXI_ADDR_WIDTH-1 : 0]     s_axi_awaddr,
    input  logic [2 : 0]                        s_axi_awprot,
    input  logic                                s_axi_awvalid,
    output logic                                s_axi_awready,
    // Write Data Channel
    input  logic [C_S_AXI_DATA_WIDTH-1 : 0]     s_axi_wdata,
    input  logic [(C_S_AXI_DATA_WIDTH/8)-1 : 0] s_axi_wstrb,
    input  logic                                s_axi_wvalid,
    output logic                                s_axi_wready,
    // Write Response Channel
    output logic [1 : 0]                        s_axi_bresp,
    output logic                                s_axi_bvalid,
    input  logic                                s_axi_bready,
    // Read Address Channel
    input  logic [C_S_AXI_ADDR_WIDTH-1 : 0]     s_axi_araddr,
    input  logic [2 : 0]                        s_axi_arprot,
    input  logic                                s_axi_arvalid,
    output logic                                s_axi_arready,
    // Read Data Channel
    output logic [C_S_AXI_DATA_WIDTH-1 : 0]     s_axi_rdata,
    output logic [1 : 0]                        s_axi_rresp,
    output logic                                s_axi_rvalid,
    input  logic                                s_axi_rready,

    // [3] 사용자 포트 (보드 밖으로 나갈 SPI 핀들)
    output logic                                spi_sclk,
    output logic                                spi_mosi,
    input  logic                                spi_miso,
    output logic                                spi_cs_n
);

    // ====================================================
    // 레지스터 정의 (Address Map)
    // ====================================================
    // 0x00: Control Reg (Write Only) - bit 0: Start
    // 0x04: Status Reg  (Read Only)  - bit 0: Busy, bit 1: Done
    // 0x08: TX Data Reg (Write Only) - lower 8 bits
    // 0x0C: RX Data Reg (Read Only)  - lower 8 bits
    
    logic [31:0] reg0_ctrl;
    logic [31:0] reg1_status; 
    logic [31:0] reg2_tx;
    logic [31:0] reg3_rx;     

    // AXI 핸드쉐이크 내부 신호
    logic axi_awready, axi_wready, axi_arready, axi_rvalid, axi_bvalid;
    logic [C_S_AXI_ADDR_WIDTH-1 : 0] axi_awaddr;
    logic [C_S_AXI_ADDR_WIDTH-1 : 0] axi_araddr;
    logic [C_S_AXI_DATA_WIDTH-1 : 0] axi_rdata;

    // SPI 모듈 연결용 신호
    logic        spi_start;
    logic        spi_busy;
    logic        spi_done;
    logic [7:0]  spi_tx_data;
    logic [7:0]  spi_rx_data;

    // 출력 할당
    assign s_axi_awready = axi_awready;
    assign s_axi_wready  = axi_wready;
    assign s_axi_bvalid  = axi_bvalid;
    assign s_axi_bresp   = 2'b00; // OKAY
    assign s_axi_arready = axi_arready;
    assign s_axi_rvalid  = axi_rvalid;
    assign s_axi_rdata   = axi_rdata;
    assign s_axi_rresp   = 2'b00; // OKAY

    // ====================================================
    // [1] SPI Master 모듈 인스턴스화 (하위 모듈 연결)
    // ====================================================
    // 주의: 하위 모듈 파일 이름은 여전히 spi_master.sv 여야 합니다.
    spi_master u_spi_master (
        .clk     (s_axi_aclk),
        .rst_n   (s_axi_aresetn), 
        
        // User Interface -> Register와 연결
        .start   (spi_start),
        .tx_data (spi_tx_data),
        .rx_data (spi_rx_data),
        .busy    (spi_busy),
        .done    (spi_done),

        // Physical Interface -> 외부 핀과 연결
        .sclk    (spi_sclk),
        .mosi    (spi_mosi),
        .miso    (spi_miso),
        .cs_n    (spi_cs_n)
    );

    // 레지스터와 신호 연결
    assign spi_start   = reg0_ctrl[0]; 
    assign spi_tx_data = reg2_tx[7:0];

    // ====================================================
    // [2] AXI Write Logic (PS -> FPGA)
    // ====================================================
    always_ff @(posedge s_axi_aclk) begin
        if (!s_axi_aresetn) begin
            axi_awready <= 1'b0;
            axi_wready  <= 1'b0;
            axi_bvalid  <= 1'b0;
            reg0_ctrl   <= 32'b0;
            reg2_tx     <= 32'b0;
        end else begin
            // 1. Write Address Handshake
            if (~axi_awready && s_axi_awvalid && s_axi_wvalid) begin
                axi_awready <= 1'b1;
                axi_wready  <= 1'b1;
                axi_awaddr  <= s_axi_awaddr; 
            end else begin
                axi_awready <= 1'b0;
                axi_wready  <= 1'b0;
            end

            // 2. Write Data Latching
            if (axi_awready && axi_wready) begin
                case (axi_awaddr[3:2]) 
                    2'b00: reg0_ctrl <= s_axi_wdata; // 0x00: Control
                    2'b10: reg2_tx   <= s_axi_wdata; // 0x08: TX Data
                    default: ;
                endcase
            end

            // 3. Write Response
            if (axi_awready && axi_wready) begin
                axi_bvalid <= 1'b1;
            end else if (s_axi_bready && axi_bvalid) begin
                axi_bvalid <= 1'b0;
            end
        end
    end

    // ====================================================
    // [3] AXI Read Logic (PS <- FPGA)
    // ====================================================
    always_ff @(posedge s_axi_aclk) begin
        if (!s_axi_aresetn) begin
            axi_arready <= 1'b0;
            axi_rvalid  <= 1'b0;
            axi_rdata   <= 32'b0;
        end else begin
            // 1. Read Address Handshake
            if (~axi_arready && s_axi_arvalid) begin
                axi_arready <= 1'b1;
                axi_araddr  <= s_axi_araddr;
            end else begin
                axi_arready <= 1'b0;
            end

            // 2. Read Data Output
            if (axi_arready && s_axi_arvalid && ~axi_rvalid) begin
                axi_rvalid <= 1'b1;
                
                case (axi_araddr[3:2])
                    2'b00: axi_rdata <= reg0_ctrl; 
                    2'b01: axi_rdata <= {30'b0, spi_done, spi_busy}; // 0x04: Status
                    2'b10: axi_rdata <= reg2_tx;
                    2'b11: axi_rdata <= {24'b0, spi_rx_data};        // 0x0C: RX Data
                    default: axi_rdata <= 32'b0;
                endcase
            end else if (axi_rvalid && s_axi_rready) begin
                axi_rvalid <= 1'b0;
            end
        end
    end

endmodule

Testbench

`timescale 1ns / 1ps

module tb_spi_ip();

    // 1. 신호 선언
    logic clk;
    logic rst_n;

    // AXI4-Lite Signals
    logic [3:0]  s_axi_awaddr;
    logic [2:0]  s_axi_awprot;
    logic        s_axi_awvalid;
    logic        s_axi_awready;
    logic [31:0] s_axi_wdata;
    logic [3:0]  s_axi_wstrb;
    logic        s_axi_wvalid;
    logic        s_axi_wready;
    logic [1:0]  s_axi_bresp;
    logic        s_axi_bvalid;
    logic        s_axi_bready;
    logic [3:0]  s_axi_araddr;
    logic [2:0]  s_axi_arprot;
    logic        s_axi_arvalid;
    logic        s_axi_arready;
    logic [31:0] s_axi_rdata;
    logic [1:0]  s_axi_rresp;
    logic        s_axi_rvalid;
    logic        s_axi_rready;

    // SPI Signals
    logic spi_sclk;
    logic spi_mosi;
    logic spi_miso;
    logic spi_cs_n;

    // 2. DUT (Device Under Test) 연결
    spi_ip u_dut (
        .s_axi_aclk    (clk),
        .s_axi_aresetn (rst_n),

        // Write Channel
        .s_axi_awaddr  (s_axi_awaddr),
        .s_axi_awprot  (3'b000),
        .s_axi_awvalid (s_axi_awvalid),
        .s_axi_awready (s_axi_awready),
        .s_axi_wdata   (s_axi_wdata),
        .s_axi_wstrb   (4'b1111),
        .s_axi_wvalid  (s_axi_wvalid),
        .s_axi_wready  (s_axi_wready),
        .s_axi_bresp   (s_axi_bresp),
        .s_axi_bvalid  (s_axi_bvalid),
        .s_axi_bready  (s_axi_bready),

        // Read Channel
        .s_axi_araddr  (s_axi_araddr),
        .s_axi_arprot  (3'b000),
        .s_axi_arvalid (s_axi_arvalid),
        .s_axi_arready (s_axi_arready),
        .s_axi_rdata   (s_axi_rdata),
        .s_axi_rresp   (s_axi_rresp),
        .s_axi_rvalid  (s_axi_rvalid),
        .s_axi_rready  (s_axi_rready),

        // SPI Ports
        .spi_sclk      (spi_sclk),
        .spi_mosi      (spi_mosi),
        .spi_miso      (spi_miso),
        .spi_cs_n      (spi_cs_n)
    );

    // [Loopback] 보낸 걸 그대로 받도록 연결 (MISO = MOSI)
    assign spi_miso = spi_mosi;

    // 3. 클럭 생성 (100MHz)
    always #5 clk = ~clk;

    // 4. AXI Write Task (CPU가 쓰는 척하는 함수)
    task axi_write(input [3:0] addr, input [31:0] data);
        begin
            @(posedge clk);
            s_axi_awaddr  <= addr;
            s_axi_awvalid <= 1'b1;
            s_axi_wdata   <= data;
            s_axi_wvalid  <= 1'b1;
            s_axi_bready  <= 1'b0;

            // Ready 신호 기다리기 (Handshake)
            wait(s_axi_awready && s_axi_wready);
            
            @(posedge clk);
            s_axi_awvalid <= 1'b0;
            s_axi_wvalid  <= 1'b0;

            // 응답 대기
            s_axi_bready <= 1'b1;
            wait(s_axi_bvalid);
            @(posedge clk);
            s_axi_bready <= 1'b0;
        end
    endtask

    // 5. 시나리오 실행
    initial begin
        // 초기화
        clk = 0;
        rst_n = 0;
        s_axi_awvalid = 0;
        s_axi_wvalid  = 0;
        s_axi_arvalid = 0;
        s_axi_bready  = 0;
        s_axi_rready  = 0;

        // 리셋 해제
        #100;
        rst_n = 1;
        #100;

        $display("=== Simulation Start ===");

        // [Step 1] TX 데이터 레지스터(0x08)에 0x55 쓰기 (01010101)
        $display("[CPU] Writing Data 0x55 to TX Register...");
        axi_write(4'h8, 32'h0000_0055);

        // [Step 2] Control 레지스터(0x00)에 1을 써서 START 신호 주기
        $display("[CPU] Writing Start Command...");
        axi_write(4'h0, 32'h0000_0001);

        // [Step 3] SPI 전송이 끝날 때까지 대기
        // 1MHz SPI는 1비트에 1us, 8비트면 최소 8us 이상 걸림
        // 넉넉하게 20us 대기
        #20000; 
        
        // [Step 4] Control 레지스터(0x00) 끄기 (Optional)
        axi_write(4'h0, 32'h0000_0000);

        $display("=== Simulation Done ===");
        $finish;
    end

endmodule

  1. 데이터 패턴 (spi_mosi):
  • 우리가 보낸 값은 0x55 즉, 01010101입니다.
  • 파형을 자세히 보세요. Low(0) -> High(1) -> Low(0) -> High(1)... 순서로 정확하게 8번 바뀝니다. 비트가 깨지지 않고 정확히 나갔다는 뜻입니다.
  1. 칩 선택 신호 (spi_cs_n):
  • 통신하는 동안 바닥(0, Low)으로 뚝 떨어져서 슬레이브를 꽉 잡고 있다가, 다 끝나니까 다시 위(1, High)로 올라갑니다. 아주 정확합니다.
  1. 클럭 (spi_sclk):
  • cs_n이 내려가 있는 동안 정확하게 8개의 클럭 펄스가 나옵니다. 개수도 딱 맞습니다.
  1. 완료 신호 (spi_done):
  • 맨 오른쪽 끝에 cs_n이 올라가자마자 spi_done이 1로 톡 튀어오르는 게 보이죠? Zynq에게 "다 보냈어요!"라고 보고하는 기능도 잘 작동합니다.
profile
Design Verification engineer

0개의 댓글