【FPGA】SPI协议的Verilog实现

Posted by 橙叶 on Wed, Jan 20, 2021

这个实现是照着高等教育出版社《电子系统设计与实战》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 &lt;= 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&#039;b00) begin // cmd cs signal effect
        if(spi_sclk_up)     // at the posedge of sclk, recieve data
            dcmd[CMD_WIDTH-1:0] &lt;= {dcmd[CMD_WIDTH-2:0], spi_sdi_buf[1]}; // dout &lt;= sdi
        else begin  // standby
            dcmd[CMD_WIDTH-1:0] &lt;= dcmd[CMD_WIDTH-1:0]; // why not ` dout &lt;= dout ` ?
        end
    end
    else if(spi_cs_cmd_buf==2&#039;b10) begin // cs_cmd goto effective, prepare for recieving cmd
        dcmd &lt;= 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&#039;b01) begin // cs_cmd goto not effective
        cmd_done &lt;= 1; end
    else begin
        cmd_done &lt;= 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&#039;b00) begin // cmd cs signal effect
        if(spi_sclk_up)     // at the posedge of sclk, recieve data
            dcmd[CMD_WIDTH-1:0] &lt;= {dcmd[CMD_WIDTH-2:0], spi_sdi_buf[1]}; // dout &lt;= sdi
        else begin  // standby
            dcmd[CMD_WIDTH-1:0] &lt;= dcmd[CMD_WIDTH-1:0]; // why not ` dout &lt;= dout ` ?
        end
    end
    else if(spi_cs_cmd_buf==2&#039;b10) begin // cs_cmd goto effective, prepare for recieving cmd
        dcmd &lt;= 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&#039;b01) begin // cs_cmd goto not effective
        cmd_done &lt;= 1; end
    else begin
        cmd_done &lt;= 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


comments powered by Disqus