SPI (Serial Peripheral Interface)
概述 串行外设接口,由摩托罗拉提出,是一种同步、全双工、主从模式的串行通信协议。通常支持一个主设备(Master)和多个从设备(Slaves)。因其高速、简单的特性,广泛用于与闪存(Flash)、ADC/DAC、传感器等外设的通信。
物理层与信号线
4根线 (典型):
`SCLK` (Serial Clock): 时钟信号,由主设备产生,驱动整个通信过程。
`MOSI` (Master Out Slave In): 主设备输出、从设备输入的数据线。
`MISO` (Master In Slave Out): 主设备输入、从设备输出的数据线。
`CS` / `SS` (Chip Select / Slave Select): 片选信号,由主设备控制,用于选择与之通信的从设备。通常低电平有效。
工作原理
SPI是同步的,所有数据传输都由SCLK的边沿触发。主从设备内部都有一个移位寄存器。在SCLK的驱动下,主设备的数据通过MOSI线移入从设备的寄存器,同时从设备的数据通过MISO线移入主设备的寄存器,实现数据的同时交换。
通信协议/传输步骤
1. 片选: 主设备将目标从设备的CS线拉低,选中该从设备。
2. 时钟生成: 主设备开始在SCLK线上产生时钟脉冲。
3. 数据交换: 在SCLK的某个边沿(由CPOL/CPHA模式决定),主设备将要发送的数据位放在MOSI线上。在同一个时钟边沿,从设备读取MOSI线上的数据位,并将其锁存到移位寄存器的最低位。同时,从设备将自己移位寄存器最高位的数据放在MISO线上,供主设备读取。
4. 重复: 重复步骤3,直到一个字节(或一个字)的数据交换完成。
5. 结束: 主设备停止SCLK,并将CS线拉高,结束本次通信。
SPI模式 (CPOL/CPHA): 这是SPI的一个关键且易错点。
`CPOL` (Clock Polarity): 定义SCLK空闲时的电平状态 (0: 低电平, 1: 高电平)。
`CPHA` (Clock Phase): 定义数据采样的时钟边沿 (0: 第一个边沿采样, 1: 第二个边沿采样)。这两种组合构成了4种SPI工作模式,主从设备必须工作在相同的模式下。
CPOL | CPHA |
模式0:CPOL=0,CPHA =0 | SCK空闲为高电平,数据在SCK的上升沿被采样(提取数据) |
模式1:CPOL=0,CPHA =1 | SCK空闲为低电平,数据在SCK的下降沿被采样(提取数据) |
模式2:CPOL=1,CPHA =0 | SCK空闲为高电平,数据在SCK的下降沿被采样(提取数据) |
模式3:CPOL=1,CPHA =1 | SCK空闲为高电平,数据在SCK的上升沿被采样(提取数据) |
系统时钟是50MHz,SPI时钟是20MHz,模式1。
第一部分:verilog代码
1. `spi_master.v`
`timescale 1ns / 1ps // --- MODIFIED: spi_master.v --- module spi_master #( parameter SYS_CLK_FREQ = 50_000_000, // MODIFIED: Default SPI frequency updated parameter SPI_CLK_FREQ = 20_000_000 )( input wire i_clk, input wire i_rst_n, // ... (ports remain the same) ... input wire i_tx_dv, input wire [7:0] i_tx_byte, output wire o_tx_busy, output reg o_rx_dv, output reg [7:0] o_rx_byte, output reg o_cs_n, output wire o_sclk, output reg o_mosi, input wire i_miso ); localparam DIVIDER_HI = 2; // High for 2 sys_clk cycles localparam DIVIDER_LO = 3; // Low for 3 sys_clk cycles localparam DIVIDER_TOTAL = DIVIDER_HI + DIVIDER_LO; // FSM states localparam [1:0] S_IDLE = 2'b00; localparam [1:0] S_TRANSFER= 2'b01; localparam [1:0] S_DONE = 2'b10; reg [1:0] r_state = S_IDLE; // Counters reg [$clog2(DIVIDER_TOTAL)-1:0] r_clk_cnt = 0; reg [3:0] r_bit_cnt = 0; // Shift registers reg [7:0] r_tx_shift_reg; reg [7:0] r_rx_shift_reg; // Internal SCLK generation reg r_sclk = 0; assign o_sclk = r_sclk; assign o_tx_busy = (r_state != S_IDLE); // MODIFIED: SCLK generation block for 20MHz from 50MHz always @(posedge i_clk or negedge i_rst_n) begin if (!i_rst_n) begin r_clk_cnt <= 0; r_sclk <= 0; // CPOL=0, idle low end else begin if (r_state == S_TRANSFER) begin if (r_sclk == 0) begin // If SCLK is currently low if (r_clk_cnt == DIVIDER_LO - 1) begin r_clk_cnt <= 0; r_sclk <= 1'b1; // Go high end else begin r_clk_cnt <= r_clk_cnt + 1; end end else begin // If SCLK is currently high if (r_clk_cnt == DIVIDER_HI - 1) begin r_clk_cnt <= 0; r_sclk <= 1'b0; // Go low end else begin r_clk_cnt <= r_clk_cnt + 1; end end end else begin r_clk_cnt <= 0; r_sclk <= 0; // SCLK is low when idle (CPOL=0) end end end // MODIFIED: Main FSM logic for MODE 1 always @(posedge i_clk or negedge i_rst_n) begin if (!i_rst_n) begin r_state <= S_IDLE; o_cs_n <= 1'b1; o_mosi <= 1'b0; o_rx_byte <= 8'b0; o_rx_dv <= 1'b0; r_bit_cnt <= 0; r_tx_shift_reg <= 8'b0; r_rx_shift_reg <= 8'b0; end else begin o_rx_dv <= 1'b0; case (r_state) S_IDLE: begin o_cs_n <= 1'b1; if (i_tx_dv) begin r_tx_shift_reg <= i_tx_byte; r_bit_cnt <= 0; o_cs_n <= 1'b0; r_state <= S_TRANSFER; end end S_TRANSFER: begin // MODIFIED: For CPHA=1, data is changed on first edge (rising) // and sampled on second edge (falling). // This logic detects the moment just before the rising edge to change MOSI. if (r_sclk == 0 && r_clk_cnt == DIVIDER_LO - 1) begin o_mosi <= r_tx_shift_reg[7]; r_tx_shift_reg <= r_tx_shift_reg << 1; end // This logic detects the moment just before the falling edge to sample MISO. if (r_sclk == 1 && r_clk_cnt == DIVIDER_HI - 1) begin r_rx_shift_reg <= {r_rx_shift_reg[6:0], i_miso}; if (r_bit_cnt == 7) begin r_state <= S_DONE; end else begin r_bit_cnt <= r_bit_cnt + 1; end end end S_DONE: begin o_cs_n <= 1'b1; o_rx_byte <= r_rx_shift_reg; o_rx_dv <= 1'b1; r_state <= S_IDLE; end default: r_state <= S_IDLE; endcase end end endmodule
1. `spi_slave.v`
`timescale 1ns / 1ps // --- MODIFIED: spi_slave.v --- module spi_slave( input wire i_sclk, input wire i_cs_n, input wire i_mosi, output reg o_miso ); reg [7:0] r_tx_shift_reg; reg [7:0] r_rx_shift_reg; reg [7:0] r_internal_data; // MODIFIED: For MODE 1 (CPHA=1), data is sampled on the falling edge. always @(negedge i_sclk or negedge i_cs_n) begin if (!i_cs_n) begin // Chip is selected // Shift in data from master r_rx_shift_reg <= {r_rx_shift_reg[6:0], i_mosi}; // Shift out data to master o_miso <= r_tx_shift_reg[7]; r_tx_shift_reg <= r_tx_shift_reg << 1; end else begin // Chip is deselected o_miso <= 1'bz; // High impedance end end // Latch logic remains the same, triggered by CS always @(posedge i_cs_n) begin r_internal_data <= r_rx_shift_reg; r_tx_shift_reg <= r_rx_shift_reg + 1; end initial begin r_internal_data = 8'h00; r_tx_shift_reg = 8'h01; o_miso = 1'bz; end endmodule
3. `spi_top.v`
`timescale 1ns / 1ps module spi_top( input wire i_clk, input wire i_rst_n, input wire i_start_transfer, input wire [7:0] i_data_to_send, output wire o_transfer_busy, output wire o_data_received_valid, output wire [7:0] o_data_received ); wire w_cs_n; wire w_sclk; wire w_mosi; wire w_miso; // The master is instantiated with the default 20MHz parameter now spi_master u_spi_master ( .i_clk(i_clk), .i_rst_n(i_rst_n), .i_tx_dv(i_start_transfer), .i_tx_byte(i_data_to_send), .o_tx_busy(o_transfer_busy), .o_rx_dv(o_data_received_valid), .o_rx_byte(o_data_received), .o_cs_n(w_cs_n), .o_sclk(w_sclk), .o_mosi(w_mosi), .i_miso(w_miso) ); spi_slave u_spi_slave ( .i_sclk(w_sclk), .i_cs_n(w_cs_n), .i_mosi(w_mosi), .o_miso(w_miso) ); endmodule
4. `tb_spi_top.v`
`timescale 1ns / 1ps module tb_spi_top; localparam CLK_FREQ = 50_000_000; localparam CLK_PERIOD = 20; reg r_clk; reg r_rst_n; reg r_start_transfer; reg [7:0] r_data_to_send; wire w_transfer_busy; wire w_data_received_valid; wire [7:0] w_data_received; spi_top u_dut ( .i_clk(r_clk), .i_rst_n(r_rst_n), .i_start_transfer(r_start_transfer), .i_data_to_send(r_data_to_send), .o_transfer_busy(w_transfer_busy), .o_data_received_valid(w_data_received_valid), .o_data_received(w_data_received) ); initial begin r_clk = 0; forever #(CLK_PERIOD / 2) r_clk = ~r_clk; end initial begin r_rst_n = 0; r_start_transfer = 0; r_data_to_send = 8'h00; #200; r_rst_n = 1; end initial begin @(posedge r_rst_n); #1000; r_data_to_send <= 8'hA5; r_start_transfer <= 1; @(posedge r_clk); r_start_transfer <= 0; @(posedge w_transfer_busy); @(negedge w_transfer_busy); #1000; r_data_to_send <= 8'h00; r_start_transfer <= 1; @(posedge r_clk); r_start_transfer <= 0; @(posedge w_data_received_valid); if (w_data_received == 8'hA6) begin $display("TB PASS: Test Case 2 Passed. Received expected response: 0x%0h", w_data_received); end else begin $display("TB FAIL: Test Case 2 Failed. Expected 0xA6, but received 0x%0h", w_data_received); end $finish; end endmodule