
SPI(Serial Peripheral Interface)는 "동기식(Synchronous)" 통신입니다. UART처럼 타이밍을 맞추느라 고생할 필요 없이, 마스터(FPGA)가 "자, 받어!" 하고 클럭을 쏴줄 때 데이터를 주고받습니다.
SPI의 핵심은 교환(Exchange)입니다.
우리는 SPI Master를 만들 겁니다. (FPGA가 주도권을 쥠) 가장 많이 쓰이는 SPI Mode 0 (CPOL=0, CPHA=0) 기준으로 설계 뼈대를 잡겠습니다.
설계해야 할 모듈 내부 구조
1. Clock Divider: 100MHz 시스템 클럭을 우리가 원하는 SPI 속도(예: 1MHz)로 낮추는 카운터.
2. Bit Counter: 지금 몇 번째 비트(0~7)를 보내고 있는지 세는 녀석.
3. Shift Register: 데이터를 밀어내고(MOSI), 받아들이는(MISO) 8비트 레지스터.
4. State Machine (FSM): 전체 흐름 제어 (IDLE -> TRANSFER -> DONE).
[ 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 연결용)
`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
`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
`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
