SPI在FPGA上的实现(verilog)
首先SPI是一种同步串行接口。它用于CPU或者FPGA与各种外围期间进行全双工,同步串行数据通信。Spi通信接口简单,只需要4根线。传输顺序是高位先传,低位后传。SPI接口是以主从方式工作的,这种模式通常有一个主器件和一个或多个从器件,其接口包括以下四种信号
clk:同步时钟信号
mosi:主端输出/从端输入信号
miso:主端输入/从端输出信号
cs:从端片选信号,从机使能
四种SPI通讯模式:不同的spi模式是由开始数据采样的边沿及时钟的空闲状态决定的。时钟的空闲状态有两种,高电平和低电平,分别对应CPOL=1与CPOL=0,
时钟的空闲状态为低:CPOL=0
时钟的空闲状态为高:CPOL=1
发送与接收数据的时钟边沿情况也有2种:数据采样发生在时钟的第一个边沿和数据采样发生在时钟的第二个边沿分别对应CPHA=0和CPHA=1.
CPHA=0的情况下的发送和采样时序图
CPHA=1情况下的数据发送和时序采样图
根据CPOL和CPHA的值,SPI的传输就可分为4种模式:
模式0:CPOL=0,CPHA=0.
模式1:CPOL=1,CPHA=0.
模式2:CPOL=1,CPHA=1.
模式3:CPOL=1,CPHA=1.
在这采用模式0
在片选信号下降沿时有效时开始发送最高位数据,其余的数据在时钟信号的下降沿发送,最后一个时钟下降沿不发送数据。数据采样在时钟的上升沿。
SPI又分主端和从端,从端被动的接受数据通信的接受和发送。从端的通信是和时钟信号与片选信号的边沿有关的。当片选信号的下降沿有效时发送最高位数据,当时钟信号上升沿有效时进行数据采样接受数据
我这首先是从端,但是从端不能主动数据通信(就是没有一个主机与其通信)为了方便些。先是用一个成功的从端代码然后例化。添加到主端的工程中。
首先从端代码如下
module
spi_slave
(
input i_rst_n,
input i_clk,
input [7 : 0] i_tx_data,
output o_tx_done ,
output [7 : 0] o_rx_data, 声明输入输出
output o_rx_done,
input i_spi_cs,
input i_spi_clk,
input i_spi_mosi,
output o_spi_miso
)
reg [7 : 0] r_rx_data;
reg [7 : 0] r_tx_shift; 定义寄存器
reg [7 : 0] r_rx_shift;
reg r_spi_clk_buf;
reg r_spi_clk_rising;
reg r_spi_clk_falling;
reg r_spi_cs_buf; 定义reg型变量,各信号的状态
reg r_spi_cs_rising;
reg r_spi_cs_falling;
reg r_spi_cs_rising_buf;
assign o_spi_miso =
(i_spi_cs == 1’b0)?
r_tx_shift[7] : 1’bz;
assign o_rx_done = r_spi_cs_rising_buf; 赋值,从端输出信号(片选低,寄存器中数据,或是高阻(无数据))
assign o_tx_done = r_spi_cs_rising_buf; 数据发送和接受完成和发送数据所对应的寄存器和状态
assign
o_rx_data = r_rx_data;
always @(posedge i_clk,
negedge i_rst_n)
begin
if(1’b0 == i_rst_n)
begin
r_spi_cs_buf <= 1'b0;
r_spi_cs_rising <= 1'b0;
r_spi_cs_falling <= 1'b0;
r_spi_cs_rising_buf <= 1'b0;
end
提取片选信号
else
的上升沿和下
begin 下降沿
r_spi_cs_buf <= i_spi_cs;
r_spi_cs_rising
<= i_spi_cs & (~r_spi_cs_buf);
r_spi_cs_falling <= (~i_spi_cs)
&r_spi_cs_buf;
r_spi_cs_rising_buf <= r_spi_cs_rising;
end
end
always @(posedge i_clk, negedge
i_rst_n)
begin
if(1'b0 == i_rst_n)
begin
r_spi_clk_buf <= 1'b0;
r_spi_clk_rising <= 1'b0;
r_spi_clk_falling <= 1'b0; 提取时钟信
end
号的上升沿
else 与下降沿
begin
r_spi_clk_buf <= i_spi_clk;
r_spi_clk_rising <= i_spi_clk &
(~r_spi_clk_buf);
r_spi_clk_falling <= (~i_spi_clk) &
r_spi_clk_buf;
end
end
always @(posedge i_clk, negedge i_rst_n)
begin
if(1’b0 == i_rst_n)
begin
r_tx_shift <= 8'b0;
end
else 从端在时钟的下降
begin 沿发送数据
if(1’b1 == r_spi_cs_falling)
r_tx_shift <= i_tx_data;
else if(1'b1 == r_spi_clk_falling)
r_tx_shift <= {r_tx_shift[6 : 0],
1’b0};
end
end
always @(posedge i_clk, negedge i_rst_n)
begin
if(1'b0 == i_rst_n)
r_rx_shift <= 8'b0; 从端在时钟的上
else 升沿接受数据
if(1'b1 == r_spi_clk_rising)
r_rx_shift <= {r_rx_shift[6 : 0],
i_spi_mosi};
end
always @(posedge i_clk, negedge i_rst_n)
begin
if(1’b0 == i_rst_n)
r_rx_data <= 8'b0; 从端在片选信号
else
的上升沿跟新数
if(1'b1 == r_spi_cs_rising) 据
r_rx_data <= r_rx_shift;
end
endmodule
主端代码
主端和从端不同的是,需要产生时钟信号和片选信号,可以又模式0的时序产生相应的片选信号和时钟信号。
module spi_master
( input i_rst_n,
input i_clk,
input i_spi_tx_rx,
input [7 : 0] i_tx_data,
output o_tx_done,
output [7 : 0] o_rx_data, 声明输入输出
output o_rx_done,
output o_spi_cs,
output o_spi_clk,
input i_spi_miso,
output o_spi_mosi,
output o_spi_busy
);
reg r_spi_cs;
reg r_spi_clk;
reg r_spi_busy;
reg r_spi_tx_rx_buf; 定义reg型变量各信号状态
reg r_spi_tx_rx_rising;
reg [4 : 0] r_spi_cs_cnt;
reg [4 : 0] r_div_cnt;
reg r_div_clk;
assign o_spi_cs = r_spi_cs;
assign o_spi_clk = r_spi_clk;
赋值,方便例化
assign o_spi_busy = r_spi_busy;
spi_slave I1_spi_slave 实例化从端模块代码
(
.i_rst_n(i_rst_n),
.i_clk(i_clk),
.i_tx_data(i_stx_data),
.o_tx_done(o_tx_done),
.o_rx_data(o_srx_data),
.o_rx_done(o_rx_done),
.o_spi_cs(r_spi_cs),
.i_spi_clk(r_spi_clk),
.i_spi_mosi(i_spi_miso),
.o_spi_miso(o_spi_mosi)
);
always @(posedge i_clk, negedge i_rst_n)
begin
if(1’b0 == i_rst_n)
begin
r_div_cnt <= 4'd0;
r_div_clk <= 1'b0;
end
产生分频时钟
else
begin
r_div_cnt <= r_div_cnt + 4'd1;
r_div_clk <= r_div_cnt[3];
end
end
always @(posedge r_div_clk, negedge
i_rst_n)
begin
if(1’b0 == i_rst_n)
begin
r_spi_tx_rx_buf <= 1’b0;
r_spi_tx_rx_rising <= 1’b0; 捕捉spi
end
主端数据
else 信号的上
begin 升沿
r_spi_tx_rx_buf <= i_spi_tx_rx;
r_spi_tx_rx_rising <= i_spi_tx_rx & (~r_spi_tx_rx_buf);
end
end
always
@(posedge r_div_clk, negedge i_rst_n) //产生spi主端数据忙信号
begin
if(1’b0 == i_rst_n)
begin
r_spi_busy <= 1'b0;
end
else
begin
if(1'b1 == r_spi_tx_rx_rising)
r_spi_busy <= 1'b1;
else if(5'd16 <= r_spi_cs_cnt)
r_spi_busy <= 1'b0;
end
end
always @(posedge r_div_clk, negedge i_rst_n) //忙信号计数器
begin
if(1’b0 == i_rst_n)
begin
r_spi_cs_cnt <= 5'b0;
end
else
begin
if(1'b1 == r_spi_busy)
r_spi_cs_cnt <= r_spi_cs_cnt
-
5’b1;
else r_spi_cs_cnt <= 5'b0;
end
end
always @(posedge r_div_clk, negedge i_rst_n) //产生spi时钟信号
begin
if(1’b0 == i_rst_n)
begin
r_spi_clk <= 1'b1;
end
else
begin
if(1'b1 == r_spi_busy)
r_spi_clk <=
r_spi_cs_cnt[0];
else
r_spi_clk <= 1'b1;
end
end
always @(posedge r_div_clk, negedge i_rst_n)//产生spi片选信号
begin
if(1’b0 == i_rst_n)
begin
r_spi_cs <= 1'b1;
end
else
begin
r_spi_cs <= ~r_spi_busy;
end
end
endmodule
验证spi主端模块通信
写测试代码。
测试代码前面是对一些信号的说明,因为是仿真,所以需要产生时钟,产生所需要通信的数据。并且要例化spi主端模块代码和从端模块代码。
调用modelsim仿真的结果图如下
由图中可以看出,spi实现了其数据通信功能 主端发送的数据可以在8个时钟后在从端接收到。
还是由于上面所说,使用实物主从通信,实现较繁琐。所以我们在这用示波器测量FPGA所对spi的四个引脚,如图
图中黄色的是数据输出mosi,;绿色的是时钟。因为用的是模式0.mosi在时钟的上升沿开始传输。
总结
刚开始的时候做,没有一个正确的方法,导致偏路很多。等仿真和实际测试都出来之后。重点细读spi的代码。因为代码是东拼西凑出来的。在细读的时候,发现代码大多数都是根据时序图来写的。看明白时序很重要。学习FPGA,最主要的是要思想上需要做出改变,改变原来写C时顺序操作。往往就是跳不出原有思维,导致一团迷糊。通过这次学习,对开发流程有了详细的了解。对verilog语言的规范也有了一定的掌握。对一个工程的建立,所涉及的细节方面,知道怎么处理。Spi成功之后,才发现代码不是那么难,spi也不是那么难。最主要的事就是没有一个基础,就去学习spi,实在是啥也模不到门路。