本文主要描述SPI在FPGA上的应用实现,描述SPI时序在FPGA上的具体实现方式
SPI协议
SPI(Serial Peripheral Interface,串行外围设备接口),是Motorola公司提出的一种同步串行接口技术,三线或四线接口,收发独立。在使用中常用四线接口,SCLK决定了SPI协议的通信速率,一般在1~20M之间。SPI以主从模式进行通信,缺省数据应答,通常为一主一从或者一主多从的模式连接,连接示图如下:
SPI协议因空闲状态的SCLK电平极性(CPOL)和时钟采集相位(CPHA)的不同,分成了四种模式,四种模式的时序如下图所示:
模式0:CPOL = 0,CPHA = 0。SCLK串行时钟在空闲是为低电平,数据再SCLK的上升沿采样,下降沿切换。
模式1:CPOL = 0,CPHA = 1。SCLK串行时钟在空闲是为低电平,数据再SCLK的下降沿采样,上升沿切换。
模式2:CPOL = 1,CPHA = 0。SCLK串行时钟在空闲是为高电平,数据再SCLK的上升沿采样,下降沿切换。
模式3:CPOL = 1,CPHA = 1。SCLK串行时钟在空闲是为高电平,数据再SCLK的下降沿采样,上升沿切换。
FPGA实现
下面以模式0为例,进行SPI协议在FPGA一主一从的实现。
首先是SPI协议的发送模块,此模块中,输入为系统时钟、复位、发送信号使能位、发送的数据(8bit)、SPI的主接口端口,输出为SPI的片选、SPI时钟以及SPI的主发送端,模型如下:
此模块中,当send信号来临,表示需要发送当前tx_data中的数据,此时cs信号拉低,并且通过一个时钟节拍,抓取cs信号的下降沿和上升沿,代码如下:
reg tmp_cs;
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
spi_cs <= 1'b1;
else if(send_en == 1'b1)
spi_cs <= 1'b0;
else if(send_cnt == 4'd8)
spi_cs <= 1'b1;
else
spi_cs <= spi_cs;
end
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
tmp_cs <= 1'b1;
else
tmp_cs <= spi_cs;
end
wire cs_rsing;
wire cs_falling;
assign cs_rsing = (spi_cs & (~tmp_cs));
assign cs_falling = ((~spi_cs) & tmp_cs);
在完成cs的输出信号处理后,则需要根据实际需要,进行clk信号的发送,此模块的输入时钟需要高于SPI的发送时钟的4倍,便于能够稳定的输出clk时钟,此时计算出spi_clk与fclk的计数值,在根绝计数值,进行spi_clk的输出,代码实现如下:
localparam CLK_HZ = 135_000_000; //clk frequency;
localparam SCK_HZ = 10_000_000; //sck frequency;
localparam DIV_NUMBER = CLK_HZ / SCK_HZ;
localparam CNT_MAX = (DIV_NUMBER >>1) - 1'b1;
/**************生成SPI时钟*************/
reg [4:0]spi_div_cnt;
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
spi_div_cnt <= 4'd0;
else if(spi_cs == 1'b1)
spi_div_cnt <= CNT_MAX;
else if(spi_div_cnt == CNT_MAX)
spi_div_cnt <= 4'd0;
else
spi_div_cnt <= spi_div_cnt + 1'b1;
end
reg tmp_sck;
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
tmp_sck <= 1'b0;
else if(spi_cs == 1'b1)
tmp_sck <= 1'b0;
else if(send_cnt == 4'd8)
tmp_sck <= 1'b0;
else if(spi_div_cnt == CNT_MAX)
tmp_sck <= ~tmp_sck;
else
tmp_sck <= tmp_sck;
end
always@(posedge pclk or negedge rst_n)begin
if(!rst_n) begin
spi_clk <= 1'b0;
end
else begin
spi_clk <= tmp_sck;
end
end
在完成clk时钟的输出后,需要进行时钟边沿判断,以此来进行数据的采集及变换,时钟边沿提取如下:
wire sck_rsing;
wire sck_falling;
assign sck_rsing = (tmp_sck & (~spi_clk));
assign sck_falling = ((~tmp_sck) & spi_clk);
完成spi_clk的边沿判断之后,则进行数据的发送,按照标准的单字节数据发送为例,当发送8个spi_clk时钟之后,则完成当前时钟的发送,此时发送计数模块如下:
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
send_cnt <= 4'd0;
else if(spi_cs == 1'b1)
send_cnt <= 4'd0;
else if(send_cnt == 4'd8)
send_cnt <= 4'd0;
else if(sck_falling == 1'b1)
send_cnt <= send_cnt + 1'b1;
else
send_cnt <= send_cnt;
end
最后,则是spi_mosi信号的输出,在spi协议中,mosi接口始终先发送tx_data的高位,再依次移位发送,在发送完成后,保持当前的电平,spi_mosi的发送模块如下:
reg [7:0]tmp_tx_data;
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
tmp_tx_data <= 8'b0;
else if(cs_falling == 1'b1)
tmp_tx_data <= spi_tx_data;
else if(sck_rsing == 1'b1)
tmp_tx_data <= {tmp_tx_data[6:0],1'b0};
else
tmp_tx_data <= tmp_tx_data;
end
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
spi_mosi <= 1'b1;
else if(spi_cs == 1'b1)
spi_mosi <= 1'b1;
else if(cs_falling == 1'b1)
spi_mosi <= spi_tx_data[7];
else if(sck_falling == 1'b1)
spi_mosi <= tmp_tx_data[7];
else
spi_mosi <= spi_mosi;
end
以上,则是完成了SPI的发送模块在FPGA上的实现。接下来则是SPI的接收模块在FPGA上的实现。
在SPI的接收模块中,输入为系统时钟、复位、SPI片选、SPI的主发送基于SPI时钟,输出为SPI接收数据、SPI接收完成信号以及SPI的从发送端,模块模型如下:
在此模块中,输入的系统时钟依旧需要是SPI时钟的4倍频以上,才能够正常的抓取到spi_clk和spi_cs的边沿信号。首先需要抓取cs的边沿信号,代码如下:
reg tmp_spi_cs;
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
tmp_spi_cs <= 1'b0;
else
tmp_spi_cs <= spi_cs;
end
wire spi_cs_falling;
wire spi_cs_rising;
reg tmp_spi_cs_rising;
assign spi_cs_rising = spi_cs & (~tmp_spi_cs);
assign spi_cs_falling = (~spi_cs) & tmp_spi_cs;
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
tmp_spi_cs_rising <= 1'b0;
else
tmp_spi_cs_rising <= spi_cs_rising;
end
之后,则是进行spi_clk信号的抓取:
reg tmp_spi_clk;
wire spi_clk_rising;
wire spi_clk_falling;
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
tmp_spi_clk <= 1'b0;
else
tmp_spi_clk <= spi_clk;
end
assign spi_clk_rising = spi_clk & (~tmp_spi_clk);
assign spi_clk_falling = (~spi_clk) & tmp_spi_clk;
最后则是对spi_mosi信号的抓取,通过clk的边沿信号,进行rx_data的数据移位,代码如下:
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
spi_rx_data <= 8'b0;
else if(spi_cs ==1'b1)
spi_rx_data <= spi_rx_data;
else if(spi_clk_rising == 1'b1)
spi_rx_data <= {spi_rx_data[6:0],spi_mosi};
else
spi_rx_data <= spi_rx_data;
end
在此模块中,over信号则是完成了一字节数据接收之后,cs的上升沿信号作为接收完成的信号,代码如下:
assign transfer_over = tmp_spi_cs_rising;
在此模块中,未进行spi_miso的从发送信号的处理,如果从接收端需要反馈数据,则在slave模块中添加需要反馈的tx_data数据,处理代码如下:
always@(posedge pclk or negedge rst_n)begin
if(!rst_n)
spi_tx_shift <= 8'b0;
else if(spi_cs_falling == 1'b1)
spi_tx_shift <= spi_tx_data;
else if(spi_clk_falling == 1'b1)
spi_tx_shift <= {spi_tx_shift[6:0],1'b0}; //发送最高位
else
spi_tx_shift <= spi_tx_shift;
end
assign spi_miso = (spi_cs == 1'b0)?spi_tx_shift[7]:1'bz; //
最后则是进行两个模块的通信仿真,仿真结果如下:
从上图可以看出,clk_135m表示内部倍频后的时钟,输入到spi的master和slave模块,作为模块的时钟,最后spi_rx_data表示为接受到的数据。可以看到,发送的数据为0x12,接受的数据也是0x12,在slave_transfor_over之后,可从模块中获取到spi接收到的数据。