这个实现是照着高等教育出版社《电子系统设计与实战》10.4关于SPI的章节给的示例写的,SPI协议应该是4根线,这本书上给的例子是五根线,片选信号(从器件使能信号)变成了数据片选和命令片选两个。这里FPGA显然是作从器件用的,FPGA自己再分多少模块、用哪个模块不应该是SPI模块操心的事情,书上这番解释确实有些牵强,不过在实际中如果要用SPI协议以8位帧的方式传多个字节,多一条命令线算是方便一些吧。
一般的SPI协议还是按标准的4根线吧,后面贴的代码是包括命令片选的。其实你可以把这两根片选线看成对应的FPGA是两个从器件,功能上其实完全一致,忽略掉一个就好。
另外书中的例子给SCLK加了两级缓存用来消除毛刺并用来检测边沿。但是数据却没给缓存,我要是想给发送数据,使能片选之后还得等两个CLK(这个CLK是从器件自己的系统时钟!)才能发送。这意味着从器件SPI的系统时钟的频率必须远大于SCLK……
SPI的工作方式,SPI协议一般需要4根线,
通信设备要分为主设备和从设备,主设备提供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