EE

【FPGA】SPI协议的Verilog实现

这个实现是照着高等教育出版社《电子系统设计与实战》10.4关于SPI的章节给的示例写的,SPI协议应该是4根线,这本书上给的例子是五根线,片选信号(从器件使能信号)变成了数据片选和命令片选两个。这里FPGA显然是作从器件用的,FPGA自己再分多少模块、用哪个模块不应该是SPI模块操心的事情,书上这番解释确实有些牵强,不过在实际中如果要用SPI协议以8位帧的方式传多个字节,多一条命令线算是方便一些吧。

一般的SPI协议还是按标准的4根线吧,后面贴的代码是包括命令片选的。其实你可以把这两根片选线看成对应的FPGA是两个从器件,功能上其实完全一致,忽略掉一个就好。

另外书中的例子给SCLK加了两级缓存用来消除毛刺并用来检测边沿。但是数据却没给缓存,我要是想给发送数据,使能片选之后还得等两个CLK(这个CLK是从器件自己的系统时钟!)才能发送。这意味着从器件SPI的系统时钟的频率必须远大于SCLK……

SPI简介

SPI的工作方式,SPI协议一般需要4根线,

  • SCLK:时钟信号,由主设备提供
  • MOSI:主设备输出,从设备输入(Master output & Slave input)
  • MISO:主设备输入,从设备输出(Master input & Slave output)
  • CS: 片选信号

通信设备要分为主设备和从设备,主设备提供SCLK时钟信号和片选信号,从设备在自己的片选信号有效时输入输出。

单方向传输时,MOSI或MISO就可以只用其一,也就是SPI最少时三根线。

所以SPI的实现就可以分两个大块:时钟/片选信号边沿检测,数据传入并出和并入串出。

这儿FPGA设计是作为从器件的实现。

因为懒得分模块,所有RTL Viewer极其混乱以致不能见光。先看完整代码吧

/**
* SPI Module
* An example of SPI from dianzixitongshejiyushizhan
* 2020/1/8
*/
module SPI(
    clk, // 系统时钟
    rst, // 重置信号
    din, // 交给SPI模块去发送的信号

    spi_sdi, // SPI输入(MOSI)
    spi_cs_cmd, // SPI命令片选
    spi_cs_data, // SPI数据片选
    spi_sclk, // SPI时钟
    spi_sdo, // SPI输出(MISO)

    dout, // 输出SPI接收的数据
    dcmd, // 输出SPI接收的命令
    cmd_done, // 标志命令接收完成的信号
    data_done // 标志数据接收完成的信号
);
parameter DATA_WIDTH = 8; // 数据帧长度,8位
parameter CMD_WIDTH  = 8; // 命令帧长度,8位

input clk, rst; 
input [DATA_WIDTH-1:0] din;
input spi_sdi, spi_cs_cmd, spi_cs_data, spi_sclk;

output [DATA_WIDTH-1:0] dout;
output [CMD_WIDTH-1:0]  dcmd;
output cmd_done, data_done, spi_sdo;

reg spi_sdo = 1;
reg [DATA_WIDTH-1:0] dout, din_reg;
reg [CMD_WIDTH-1:0]  dcmd;
reg cmd_done, data_done;

reg spi_sclk_buf_1, spi_sclk_buf_2;
reg [1:0] spi_sclk_buf, spi_sdi_buf;
reg spi_sclk_down, spi_sclk_up;
reg [1:0] spi_cs_data_buf, spi_cs_cmd_buf;

// SCLK, CS_DATA, CS_CMD buffer
// 缓存信号以备边沿检测
always@(posedge clk) begin
    if(rst) begin
        spi_sclk_buf <= 0;
        spi_cs_data_buf <= 0;
        spi_cs_cmd_buf <= 0;
        spi_sdi_buf <= 0;
    end
    else begin
        spi_sclk_buf[1:0] <= {spi_sclk_buf[0], spi_sclk};
        spi_cs_data_buf <= {spi_cs_data_buf[0], spi_cs_data};
        spi_cs_cmd_buf  <= {spi_cs_cmd_buf[0], spi_cs_cmd};
        spi_sdi_buf[1:0] <= {spi_sdi_buf[0], spi_sdi}; // buffer the received data, make it sync with sclk
    end
end

// SCLK edge detect
always@(posedge clk) begin
    if(rst) begin
        spi_sclk_down <= 0;
        spi_sclk_up <= 0;
    end
    else begin
        // detect posedge, compare spi_sclk_buf_1 with spi_sclk_buf_2
        if(spi_sclk_buf==2'b01) begin
            spi_sclk_up <= 1;
        end
        else begin
            spi_sclk_up <= 0;
        end
        // detect negedge, compare spi_sclk_buf_1 with spi_sclk_buf_2
        if(spi_sclk_buf==2'b10) begin
            spi_sclk_down <= 1;
        end
        else spi_sclk_down <= 0;  
    end
end

// receive data or cmd
always@(posedge clk) begin
    if(rst) begin
        dout <= 0;
        dcmd <= 0;
    end
    else begin
        if(spi_cs_data_buf==2'b00) begin // data cs signal effect
            if(spi_sclk_up)     // at the posedge of sclk, recieve data
                dout[DATA_WIDTH-1:0] <= {dout[DATA_WIDTH-2:0], spi_sdi_buf[1]}; // dout <= sdi
            else begin  // standby
                dout[DATA_WIDTH-1:0] <= dout[DATA_WIDTH-1:0]; // why not ` dout <= dout ` ?
            end
        end
        else if(spi_cs_data_buf==2'b10) begin // cs_data goto effective, prepare for recieving data
            dout <= 0; // clear dout
        end
        else dout <= dout; // nothing happend, keep dout

        if(spi_cs_cmd_buf==2'b00) begin // cmd cs signal effect
            if(spi_sclk_up)     // at the posedge of sclk, recieve data
                dcmd[CMD_WIDTH-1:0] <= {dcmd[CMD_WIDTH-2:0], spi_sdi_buf[1]}; // dout <= sdi
            else begin  // standby
                dcmd[CMD_WIDTH-1:0] <= dcmd[CMD_WIDTH-1:0]; // why not ` dout <= dout ` ?
            end
        end
        else if(spi_cs_cmd_buf==2'b10) begin // cs_cmd goto effective, prepare for recieving cmd
            dcmd <= 0; // clear dc,d
        end

    end                                                                                                                                                                    
end

// finish receiving data
always@(posedge clk) begin
    if(rst) begin
        data_done <= 0;
        cmd_done <= 0;
    end
    else begin
        if(spi_cs_data_buf==2'b01) begin // cs_data goto not effective
            data_done <= 1; end
        else begin
            data_done <= 0;
        end

        if(spi_cs_cmd_buf==2'b01) begin // cs_cmd goto not effective
            cmd_done <= 1; end
        else begin
            cmd_done <= 0;
        end
    end
end

// send data
// revice cmd before send data
always@(posedge clk) begin
    if(rst) begin
        spi_sdo <= 0;
        din_reg <= 0;
    end
    else begin
        if(spi_cs_data_buf==2'b00) begin // cs data available
            if(spi_sclk_down) begin
                spi_sdo <= din_reg[DATA_WIDTH-1];
                din_reg[DATA_WIDTH-1:0] <= {din_reg[DATA_WIDTH-2:0], 1'b0};
            end
            else begin
                spi_sdo <= spi_sdo;
                din_reg[DATA_WIDTH-1:0] <= din_reg[DATA_WIDTH-1:0];
            end
        end else if(spi_cs_data_buf==2'b10) begin // get data before output
            spi_sdo <= spi_sdo;
            din_reg[DATA_WIDTH-1:0] <= din[DATA_WIDTH-1:0];
        end else begin
            spi_sdo <= spi_sdo;
            din_reg[DATA_WIDTH-1:0] <= din_reg[DATA_WIDTH-1:0];
        end
    end
end

endmodule   

边沿检测

边沿检测的方法:把输入信号串入到一个两位的移位寄存器里,当寄存器为"01"(假设信号从左传到右)时为下降沿,"10"时为上升沿。

边沿检测就是判断一下上述串行寄存器的状态。判断标志放到spi_sclk_down和spi_sclk_up。

// 串入寄存器
always@(posedge clk) begin
    if(rst) begin
        spi_sclk_buf <= 0;
        spi_cs_data_buf <= 0;
        spi_cs_cmd_buf <= 0;
        spi_sdi_buf <= 0;
    end
    else begin
        spi_sclk_buf[1:0] <= {spi_sclk_buf[0], spi_sclk};
        spi_cs_data_buf <= {spi_cs_data_buf[0], spi_cs_data};
        spi_cs_cmd_buf  <= {spi_cs_cmd_buf[0], spi_cs_cmd};
        spi_sdi_buf[1:0] <= {spi_sdi_buf[0], spi_sdi}; // buffer the received data, make it sync with sclk
    end
end

// SCLK 边沿检测
always@(posedge clk) begin
    if(rst) begin
        spi_sclk_down <= 0;
        spi_sclk_up <= 0;
    end
    else begin
        // detect posedge, compare spi_sclk_buf_1 with spi_sclk_buf_2
        if(spi_sclk_buf==2'b01) begin
            spi_sclk_up <= 1;
        end
        else begin
            spi_sclk_up <= 0;
        end
        // detect negedge, compare spi_sclk_buf_1 with spi_sclk_buf_2
        if(spi_sclk_buf==2'b10) begin
            spi_sclk_down <= 1;
        end
        else spi_sclk_down <= 0;  
    end
end

数据接收

片选信号的检测,只需要知道数据传输何时开始即可。这里的设计中片选信号为低则有效,当片选信号出现下降沿时(转为有效),清空并出的寄存器,准备接收数据。

// receive data or cmd
always@(posedge clk) begin
    if(rst) begin
        dout <= 0;
        dcmd <= 0;
    end
    else begin
        if(spi_cs_data_buf==2'b00) begin // data cs signal effect
            if(spi_sclk_up)     // at the posedge of sclk, recieve data
                dout[DATA_WIDTH-1:0] <= {dout[DATA_WIDTH-2:0], spi_sdi_buf[1]}; // dout <= sdi
            else begin  // standby
                dout[DATA_WIDTH-1:0] <= dout[DATA_WIDTH-1:0]; // why not ` dout <= dout ` ?
            end
        end
        else if(spi_cs_data_buf==2'b10) begin // cs_data goto effective, prepare for recieving data
            dout <= 0; // clear dout
        end
        else dout <= dout; // nothing happend, keep dout

        if(spi_cs_cmd_buf==2'b00) begin // cmd cs signal effect
            if(spi_sclk_up)     // at the posedge of sclk, recieve data
                dcmd[CMD_WIDTH-1:0] <= {dcmd[CMD_WIDTH-2:0], spi_sdi_buf[1]}; // dout <= sdi
            else begin  // standby
                dcmd[CMD_WIDTH-1:0] <= dcmd[CMD_WIDTH-1:0]; // why not ` dout <= dout ` ?
            end
        end
        else if(spi_cs_cmd_buf==2'b10) begin // cs_cmd goto effective, prepare for recieving cmd
            dcmd <= 0; // clear dc,d
        end

    end                                                                                                                                                                    
end

数据传输完成后(判断方法是看片选信号转为无效),让data_done和cmd_done有效,告诉其他模块可以取数据了。

// finish receiving data
always@(posedge clk) begin
    if(rst) begin
        data_done <= 0;
        cmd_done <= 0;
    end
    else begin
        if(spi_cs_data_buf==2'b01) begin // cs_data goto not effective
            data_done <= 1; end
        else begin
            data_done <= 0;
        end

        if(spi_cs_cmd_buf==2'b01) begin // cs_cmd goto not effective
            cmd_done <= 1; end
        else begin
            cmd_done <= 0;
        end
    end
end

可以看一下signaltap里的一个连续接收的效果

数据发送

// send data
// revice cmd before send data
always@(posedge clk) begin
    if(rst) begin
        spi_sdo <= 0;
        din_reg <= 0;
    end
    else begin
        if(spi_cs_data_buf==2'b00) begin // cs data available
            if(spi_sclk_down) begin
                spi_sdo <= din_reg[DATA_WIDTH-1];
                din_reg[DATA_WIDTH-1:0] <= {din_reg[DATA_WIDTH-2:0], 1'b0};
            end
            else begin
                spi_sdo <= spi_sdo;
                din_reg[DATA_WIDTH-1:0] <= din_reg[DATA_WIDTH-1:0];
            end
        end else if(spi_cs_data_buf==2'b10) begin // get data before output
            spi_sdo <= spi_sdo;
            din_reg[DATA_WIDTH-1:0] <= din[DATA_WIDTH-1:0];
        end else begin
            spi_sdo <= spi_sdo;
            din_reg[DATA_WIDTH-1:0] <= din_reg[DATA_WIDTH-1:0];
        end
    end
end
(1)

本文由 橙叶博客 作者:FrankGreg 发表,转载请注明来源!

热评文章

发表评论