FPGA--SPI通信协议

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工作模式,主从设备必须工作在相同的模式下。

CPOLCPHA
模式0:CPOL=0,CPHA =0 

SCK空闲为高电平,数据在SCK的上升沿被采样(提取数据)

模式1:CPOL=0,CPHA =1
SCK空闲为低电平,数据在SCK的下降沿被采样(提取数据)
模式2:CPOL=1,CPHA =0SCK空闲为高电平,数据在SCK的下降沿被采样(提取数据)
模式3:CPOL=1,CPHA =1SCK空闲为高电平,数据在SCK的上升沿被采样(提取数据)

系统时钟是50MHzSPI时钟是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


原文链接:,转发请注明来源!