FPGA常见接口及逻辑实现(三)—— SPI

本文围绕SPI协议展开,介绍其与UART和I2C的不同,阐述了Verilog实现思路,包括产生SPI时钟、处理四种模式等。给出了SPI主机和从机的Verilog代码实现,进行了主从仿真,最后用SPI主机读nor flash的ID完成板级验证。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、SPI协议简介

        SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种同步串行接口,相对于之前介绍过的UART和I2C,SPI的速率就高出很多,最高能到100M左右,SPI协议比较简单,就不多做介绍,主要介绍下与UART和I2C的不同,SPI是一主多从协议,每个从机通过一根片选信号线和主机连接,所以从接口线数量上没有I2C的优势,但是SPI是全双工通信,两根数据线,同时读写,而且还有带宽更大的DUAL SPI和QUAD SPI,分别是两根数据线同时写和四根数据线同时写,不过DSPI和QSPI一般就是半双工的了,读写不能同时进行。相较于UART,最大的不同就是SPI是同步协议,数据随总线时钟一同发送,所以接收方不需要进行波特率匹配,使用更灵活。

二、SPI协议的verilog实现思路

        SPI的时序简单,开始发送后,拉低对应从机的片选信号,然后随着总线时钟一位一位地发送数据到总线上,不同于UART的是SPI一般先发送最高位,发送完一个字节就接着发送下一个字节,没有停止位或者起始位,知道主机重新拉高片选信号,一次操作结束。

        要实现SPI,首先要产生SPI时钟,时钟一般通过系统时钟分频产生,将其参数化,以便灵活设置SPI速率。

        其次就是SPI的四种模式,由时钟极性和时钟相位两个参数来控制,其中模式0和3是上升沿采样,模式1和2是下降沿采样,模式0和1空闲时为低电平,模式2和3空闲时为高电平,记住这两个最重要的区别即可。

        在上一篇I2C的内容中,最后实现了用寄存器去控制I2C接口,这次SPI也用同样的方法来实现,所以提前预留出各种寄存器的接口。

        对参数和接口进行规划之后,就是具体时序的实现了,首先时钟分频自然是通过计数器来实现,计数的过程中还能顺便在时钟的跳变沿产生脉冲信号方便使用,同时每个时钟周期对应一比特操作,对操作的比特也进行计数,每八个比特就是一个字节;至于数据线的操作就和串口一模一样,根据比特计数器的值发送或接收对应位的比特即可。

        根据上述的思路画出时序图:

        可以看到图中片选信号提前一个时钟周期就拉低了,但是实际操作并不需要这样,只要在读写的过程中片选信号保持为低即可。

        以上都是SPI主机的实现思路,对于SPI的从机,一般想到的首先就是SPI flash,或者SPI屏幕等等,可以看出SPI的从机不像I2C的从机那样有比较通用的实现方法,很难写出一个很通用的从机模块,不过万变不离其宗,从机无论如何都是要接收主机发来的数据的,所以对于从机,我就只实现一个兼容性较好的可以完成接收数据操作和完成写回数据操作的模块,具体要实现的功能就基于此模块的基础上去修改,应该也会减少很多工作量。

        对于从机的参数,主要需要兼容主机的四种模式,一般读写flash的时候,SPI flash都同时支持模式0和模式3,或是模式1和模式2,而前文我提到模式0和模式3的共同点是上升沿采样,所以我们就用采样沿参数化,当采样沿为上升沿时,兼容模式0和3,反之兼容模式1和2。但是对于不同的采样沿,显然需要两套不同的代码,这种情况下就需要generate关键字在不同的情况下生成不同的逻辑。

三、SPI主机的Verilog实现

        主机实现的难点主要在于兼容四种模式,一般对于四种模式的描述都是第一个变化沿怎么样,第二个变化沿怎么样,我只能说这种描述太抽象了,又难记又不方便转化为逻辑语言,对于模式0和3,无论是第几个变化沿,都是在上升沿采样,下降沿发送,这些是确定的,时钟空闲状态也很好确定,是协议规定的时钟极性,至于第一个变化沿的问题直接简化为发送时的时钟初始状态,无论时钟空闲状态是高还是低,都从一个低电平开始,假如空闲状态为高,自然会产生一个下降沿,假如空闲状态为低,则继续保持低,这样真正的第一个变化沿自然是上升沿了。

        综上所述,编写的SPI主机代码如下:

`timescale 1ns / 1ps

module spi_master#(
    parameter CLK_PHA = 0,                  // SPI时钟相位
    parameter CLK_POL = 0,                  // SPI时钟极性
    parameter SCK_DIV_CNT = 4               // SPI时钟分频系数
)(
    input                       clk,        // 输入时钟
    input                       rst_n,      // 同步复位

    input                       op_start,   // 操作开始信号
    output                      op_busy,    // 操作忙碌信号
    input   [7:0]               op_len,     // 操作长度
    input   [7:0]               cs_ctrl,    // 片选信号

    output                      txc,        // 数据请求
    input   [7:0]               txd,        // 数据输入
    output                      rxv,        // 数据有效
    output  [7:0]               rxd,        // 数据输出

    output                      sck,        // SPI时钟
    output                      mosi,       // SPI主机输出从机输入
    input                       miso,       // SPI主机输入从机输出
    output  [7:0]               cs_n        // SPI片选
    );

// 参数变量声明
    // SPI时钟空闲状态
    localparam [0:0] SCK_IDLE = CLK_POL;
    // SPI时钟初始状态
    localparam [0:0] SCK_INIT = CLK_PHA ? ~CLK_POL : CLK_POL;

    // 寄存器
    reg                         spi_clk;
    reg                         master_out;

    reg     [3:0]               clk_cnt;
    reg     [3:0]               bit_cnt;
    reg     [7:0]               byte_cnt;

    reg                         spi_busy;

    reg                         data_req;
    reg                         data_valid;
    reg     [7:0]               data_out;

    reg                         start_ff1;
    reg                         start_ff2;
    reg                         start_flag;

    reg                         sck_r;
    reg                         mosi_r;
    reg     [7:0]               cs_n_r;

// 组合逻辑
    wire    half_bit = clk_cnt == SCK_DIV_CNT/2 - 1;
    wire    one_bit = clk_cnt == SCK_DIV_CNT - 1;
    wire    one_byte = bit_cnt == 7;
    wire    one_op = byte_cnt == (op_len - 1) & one_byte & one_bit;

// 模块输出连线
    assign op_busy = spi_busy;
    assign txc = data_req;
    assign rxv = data_valid;
    assign rxd = data_out;
    assign sck = sck_r;
    assign mosi = mosi_r;
    assign cs_n = cs_n_r;

// 时序逻辑
    // SPI主机接口输出
    always @(posedge clk) begin
        if(spi_busy) begin
            sck_r <= spi_clk;
            mosi_r <= master_out;
            cs_n_r <= cs_ctrl;
        end else begin
            sck_r <= SCK_IDLE;
            mosi_r <= 1'b0;
            cs_n_r <= 8'hff;
        end
    end

    // 启动信号二级同步
    always @(posedge clk) begin
        start_ff1 <= op_start;
        start_ff2 <= start_ff1;
    end

    always @(posedge clk) begin
        if(start_ff1 & ~start_ff2)
            start_flag <= 1'b1;
        else if(spi_busy)
            start_flag <= 1'b0;
    end

    // 产生SPI时钟,忙碌状态下,每半个比特周期翻转时钟信号
    always @(posedge clk) begin
        if(!rst_n)
            spi_clk <= SCK_INIT;
        else if(spi_busy & (half_bit | one_bit))
            spi_clk <= ~spi_clk;
    end

    // 忙碌标志信号,接收到启动信号拉高,发送完操作长度个字节后拉低
    always @(posedge clk) begin
        if(!rst_n)
            spi_busy <= 0;
        else if(start_flag)
            spi_busy <= 1;
        else if(one_op)
            spi_busy <= 0;
    end

    // SPI时钟周期计数器,忙碌状态下计数,计满一比特清零
    always @(posedge clk) begin
        if(!rst_n)
            clk_cnt <= 0;
        else if(spi_busy) begin
            if(one_bit)
                clk_cnt <= 0;
            else
                clk_cnt <= clk_cnt + 1;
        end
    end

    // 发送比特计数,发送完一字节计数器清零
    always @(posedge clk) begin
        if(!rst_n)
            bit_cnt <= 0;
        else if(spi_busy & one_bit) begin
            if(one_byte)
                bit_cnt <= 0;
            else
                bit_cnt <= bit_cnt + 1;
        end
    end

    // 在发送每比特的中间时刻对输入线进行采样
    always @(posedge clk) begin
        if(!rst_n)
            data_out <= 0;
        else if(spi_busy & half_bit) begin
            case (bit_cnt)
                0:data_out[7] <= miso;
                1:data_out[6] <= miso;
                2:data_out[5] <= miso;
                3:data_out[4] <= miso;
                4:data_out[3] <= miso;
                5:data_out[2] <= miso;
                6:data_out[1] <= miso;
                7:data_out[0] <= miso;
                default: data_out <= data_out;
            endcase
        end
    end

    // 依次发送每个比特到输出线
    always @(posedge clk) begin
        if(!rst_n)
            master_out <= 0;
        else if(start_flag & !spi_busy)
            master_out <= txd[7];
        else if(spi_busy & one_bit) begin
            case (bit_cnt)
                0:master_out <= txd[6];
                1:master_out <= txd[5];
                2:master_out <= txd[4];
                3:master_out <= txd[3];
                4:master_out <= txd[2];
                5:master_out <= txd[1];
                6:master_out <= txd[0];
                7:master_out <= txd[7];
                default: master_out <= master_out;
            endcase
        end
    end

    // 每字节发送结束前一个比特拉高数据请求信号
    always @(posedge clk) begin
        if(!rst_n)
            data_req <= 0;
        else if((bit_cnt == 6) & one_bit)
            data_req <= 1;
        else 
            data_req <= 0;
    end

    // 每字节发送结束的比特拉高数据有效信号
    always @(posedge clk) begin
        if(!rst_n)
            data_valid <= 0;
        else if(one_byte & one_bit)
            data_valid <= 1;
        else 
            data_valid <= 0;
    end

    // 每字节操作完成后字节计数加一,计数达到操作长度后清零
    always @(posedge clk) begin
        if(!rst_n)
            byte_cnt <= 0;
        else if(one_byte & one_bit) begin
            if(one_op)
                byte_cnt <= 0;
            else
                byte_cnt <= byte_cnt + 1;
        end
    end
    
endmodule

        本次的代码和串口很像,只不过加入了部分寄存器配置端口。以下是spi寄存器代码:

`timescale 1ns / 1ps

module spi_reg(
    input               clk,
    input               en,
    input               we,
    input   [7:0]       din,
    output  [7:0]       dout,
    input   [7:0]       addr,

    output              op_start,
    input               op_busy,
    output  [7:0]       op_len,
    output  [7:0]       cs_ctrl,
    input               txc,
    output  [7:0]       txd,
    input               rxv,
    input   [7:0]       rxd
    );

    reg     [7:0]       r_data_out;

    reg     [7:0]       r_tx_buffer [0:31];     // 0x00 - 0x1f write only
    reg     [7:0]       r_rx_buffer [0:31];     // 0x20 - 0x3f read only
    
    // bit 4-0: tx_buffer ptr
    reg     [7:0]       r_tx_ctrl = 0;

    // bit 4-0: rx_buffer ptr
    reg     [7:0]       r_rx_ctrl = 0;

    // bit 7: tx_buffer reset,self clear
    reg     [7:0]       r_tx_rst = 0;

    // bit 7: rx_buffer reset,self clear
    reg     [7:0]       r_rx_rst = 0;

    // bit 7: operate start,self clear
    reg     [7:0]       r_op_startup = 0;

    // bit 0: operate busy flag,read only
    reg     [7:0]       r_op_status = 0;

    // bit 7-0: operate length
    reg     [7:0]       r_op_length = 0;

    // bit 7-0: chip select control
    reg     [7:0]       r_cs_control = 0;

    reg     [7:0]       r_reserve_0 = 0;
    reg     [7:0]       r_reserve_1 = 0;
    reg     [7:0]       r_reserve_2 = 0;
    reg     [7:0]       r_reserve_3 = 0;
    reg     [7:0]       r_reserve_4 = 0;
    reg     [7:0]       r_reserve_5 = 0;
    reg     [7:0]       r_reserve_6 = 0;
    reg     [7:0]       r_reserve_7 = 0;        // 0x40 - 0x4f

    reg     [7:0]       start_cnt;
    reg     [7:0]       txrst_cnt;
    reg     [7:0]       rxrst_cnt;

    ass
### FPGASPI 协议的实现 #### 1. SPI 基本概念 SPI 是一种同步串行数据链路标准,通常由主机(Master)和从机(Slave)组成。它通过四条主要信号线完成通信:MOSI(主发从收)、MISO(主收从发)、SCLK(时钟信号)以及 SS/CS(片选信号)。在 FPGA 设计中,可以通过硬件描述语言 Verilog 或 VHDL 来实现 SPI 主机或从机的功能[^1]。 #### 2. 使用 Verilog 实现 SPI 接口 以下是基于 Verilog 的简单 SPI Master 控制器的设计框架: ```verilog module spi_master ( input wire clk, // 系统时钟 input wire reset_n, // 复位信号,低电平有效 output reg sclk, // SPI 时钟输出 output reg mosi, // 数据发送到 Slave input wire miso, // 数据接收自 Slave output reg ss_n, // 片选信号,低电平有效 input wire start, // 开始传输标志 input wire [7:0] data_out, // 发送的数据 output reg [7:0] data_in // 接收到的数据 ); // 定义状态机变量和其他控制信号 reg [3:0] bit_count; // 记录当前传输的比特数 reg [7:0] shift_reg; // 移位寄存器用于存储待发送和接收到的数据 reg transfer_done; // 转移完成标志 always @(posedge clk or negedge reset_n) begin if (!reset_n) begin sclk <= 1'b0; mosi <= 1'b0; ss_n <= 1'b1; bit_count <= 4'd0; shift_reg <= 8'd0; transfer_done <= 1'b0; end else begin if (start && !transfer_done) begin ss_n <= 1'b0; // 激活片选信号 bit_count <= 4'd0; shift_reg <= data_out; transfer_done <= 1'b0; // SCLK 和 MOSI 更新逻辑 sclk <= ~sclk; if (sclk == 1'b1) begin mosi <= shift_reg[7]; shift_reg <= {shift_reg[6:0], miso}; bit_count <= bit_count + 1; if (bit_count == 4'd8) begin ss_n <= 1'b1; // 取消片选信号 transfer_done <= 1'b1; data_in <= shift_reg; end end end end end endmodule ``` 上述代码展示了如何利用有限状态机来管理 SPI 数据传输过程中的各个阶段,并实现了简单的单字节数据交换功能。 #### 3. 配置 DAC 模块作为 SPI 外设的例子 当使用 FPGA 进行 ADC/DAC 的 SPI 配置时,一般遵循以下流程: - 初始化 SPI 总线并设置正确的波特率及时序参数; - 向目标设备写入命令帧以指定操作模式或者地址偏移量; - 将实际数值打包成二进制形式并通过 MOSI 输出给 DAC 输入端口;最后确认事务结束再释放 CS# 引脚回到高阻态[^3]。 例如,在某些型号的 DAC 上可能只需要连续不断地更新其内部缓冲区即可改变模拟电压输出水平,则可以简化为仅需周期性调用一次 `spi_write` 函数向该器件推送新值而无需额外读取反馈信息回来验证结果准确性。 #### 4. 注意事项 为了确保可靠性和兼容性,在开发过程中还需要注意一些细节问题比如时序约束定义、噪声抑制措施应用等方面的内容[^2]。
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值