卷携着黑夜,
我闭上了眼。
站在你的窗前,
一切清晰可见,恍如昨日。
在这琉璃光影背后,
我能辨明世上的一切,
唯独没有看清自己。
我只有将其打碎,
碎如今夜星空。
这是一条没有灯火的路,
我在夜寂中惶急地奔跑,
猝地,我磕倒了,
拾起一块碎片。
残缺的窗,
用血将其填补。
——頠宸
能够被中诗协认可出席了去年的中国诗酒大会,感觉还是很幸运而不好意思的。包括百余位嘉宾们当时就餐闲谈之时,也有前辈提到我尽管文字清新淡雅,但阅历深度尤显不足,问题还是蛮多的(卑微)。顺便吐槽一下,人家连我在诗歌网、qq空间和朋友圈动态里面的诗都给强行参赛作品了一波,可见这些传统艺能在当前这个快节奏社会下被透支的是多么厉害。
言归正传。。我们先来谈一谈为什么要学习或者设计FIFO?
FIFO(First In First Out ) 先进先出是一个常用于数据缓存的一个数据缓冲器。主要有WRREQ(写信号)WRclk(写时钟)data(写数据)wrfull(写满标志)wrempty(写空标志)wrusedw(告知里面还有多少数据)Rdreq(读信号)rdclk(读时钟)rdfull(读满标志)rdempty(读空标志)rdusedw(告知里面数据个数)。这些信号都是高电平有效。
在项目设计中,我们通常需要在两个模块之间传输数据,如果两个模块的数据处理速率相同,那么自然没有什么问题,直接进行数据之间的对接就可以,可若是两个模块的数据处理速度不同呢?如果说数据接收模块和数据发送模块的速度不一致的话,必然会导致采集数据出现遗漏的现象,那么又该如何解决这一问题呢?
答案就是引用FIFO(先进先出)数据缓冲器,所有数据都先经过缓存器去缓存,然后再输入数据接收模块。这样就通过一个数据缓存的方法解决了速度不一致而导致的遗漏数据的问题。
所以说,FIFO的准确定义可以描述为:使用在需要产生数据接口的部分,用来存储、缓冲在两个异步时钟之间的数据传输。在异步电路中,由于时钟之间周期和相位完全独立,因此数据丢失概率不为零。使用FIFO可以在两个不同时钟域系统之间快速而方便地传输实时数据。
FIFO这个东西既可以手写,也可以使用QuartusⅡ中已经集成好的FIFO ip核,以下我们通过Robei_fifo实例和操作图例两种方法分别介绍手写和ip核调用的FIFO。
一、借助Robei手写FIFO:
1、设计原理与设计要求(摘自FPGA攻城狮的原文):
通过分析,我们看到图5-1-1中有一个具有独立的读端口和独立的写端口的 RAM存储器。这样选择是为了分析方便。如果是一个单端口的存储器,还应包含一个仲裁器,保证同一时刻只能进行一项操作(读或写),我们选择双口RAM(无需真正的双口RAM,因为我们只是希望有一个简单的相互独立的读写端口)是因为这些实例非常接近实际情况。读、写端口拥有两个计数器(wr_ptr、rd_ptr)产生的互相独立的读、写地址。计数器的值在读写使能信号来临时传递给“读指针”(rd)和“写指针”(wr)。写指针指向下一个将要写入的位置,读指针指向下一个将要读取的位置。每次写操作使写指针加1,读操作使读指针加1。左右两侧的模块为读写指针与满空信号产生模块。这两个模块的任务是给 FIFO提供“空”(empty)和“满”(full)信号。这些信号告诉外部电路FIFO已经达到了临界条件:如果出现“满”信号,那么FIFO为写操作的临界状态,如果出现“空”信号,则FIFO为读操作的临界状态。写操作的临界状态表示FIFO已经没有空间来存储更多的数据,读操作的临界表示FIFO没有更多的数据可以读出。读写指针与满空信号产生模块还可告诉FIFO中“满”或“空”位置的数值。这是由指针的算术运算来完成了。实际的“满”或“空”位置计算并不是为FIFO自身提供的。它是作为一个报告机构给外部电路用的。但是,“满”和“空”信号在FIFO中却扮演着非常重要的角色,它为了能实现读与写操作各自的独立运行而阻塞性的管理数据的存取。这种阻塞性管理的重要性不是将数据复写(或重读),而是指针位置可以控制整个FIFO,并且使读、写操作改变着指针数值。如果我们不阻止指针在临界状态下改变状态,FIFO还能都一边“吃”着数据一边“产生”数据,这简直是不可能的。
从功能上看,FIFO的工作原理如下所述:复位时,读、写指针均为0。这是FIFO的空状态,空标志(empty)为高电平,此时满标志(full)为低电平。当FIFO出现空标志(empty)时,不允许读操作,只能允许写操作。写操作写入到位置0,并使写指针加1。此时,空标志(empty)变为低电平。假设没有发生读操作且随后的一段时间中FIFO只有写操作,一定时间后,写指针的值等于7。这就意味着在存储器中,要写入数据的最后一个位置就是下一个位置。在这种情况下,写操作将写指针变为0,并将输出满标志(full)。
为了更好地判断空状态和满状态,这里设置一个四位的计数器(fifo_cnt),代表存储器(mem)中写入但还未读取的数据个数。当FIFO未进行任何读写操作时,计数器保持不变;当进行写操作时,计数器加1;当进行读操作时,计数器减1;当同时进行写操作和读操作时,计数器值保持不变。这样就可以根据计数器中的值来判断状态的空与满,即:当计时器fifo_cnt=0时,表示存储器处于空状态,输出空标志(empty);当计数器fifo_cnt=8时,表示存储器处于满状态,输出满标志(full)。
读写指针都指向一个内存的初始位置,每进行一次读写操作,相应的指针就递增一次,指向下一个内存位置。当指针移动到了内存的最后一个位置时,它又重新跳回初始位置。在FIFO非满或非空的情况下,这个过程将随着读写控制信号的变化一直进行下去。如果FIFO处于空的状态,下一个读动作将会导致向下溢(underflow),一个无效的数据被读入;同样,对于一个满了的FIFO,进行写动作将会导致向上溢出(overflow),一个有用的数据被新写入的数据覆盖。这两种情况都属于误动作,因此需要设置满和空两个信号,对满信号置位表示FIFO处于满状态,对满信号复位表示FIFO非满,还有空间可以写入数据;对空信号置位表示FIFO处于空状态,对空信号复位表示FIFO非空,还有有效的数据可以读出。
2、具体Robei代码实现+仿真+工程下载:
软件的操作步骤都已经在前面的文章中提到了。当前我们需要做的就是将module和testbench代码给填入并且仿真波形检查是否有误。官网上(教材上相同)给出的代码是有一些错误的,在经过了短时间的debug后,以下是我调试好的确认能运行的代码版本,可供参考。
//fifo模块代码已修正
module fifo(
clk,
rst,
data_in,
wr,
rd,
full,
empty,
data_out,
fifo_cnt);
//---Ports declearation: generated by Robei---
input clk;
input rst;
input [3:0] data_in;
input wr;
input rd;
output full;
output empty;
output [3:0] data_out;
output [3:0] fifo_cnt;
wire clk;
wire rst;
wire [3:0] data_in;
wire wr;
wire rd;
wire full;
wire empty;
reg [3:0] data_out;
reg [3:0] fifo_cnt;
//----Code starts here: integrated by Robei-----
reg [7:0] fifo_ram[0:7];
reg [2:0] rd_ptr, wr_ptr;
assign empty = (fifo_cnt==0);
assign full = (fifo_cnt==8);
always @( posedge clk )
begin: write
if(wr && !full)
fifo_ram[wr_ptr] <= data_in;
else if(wr && rd)
fifo_ram[wr_ptr] <= data_in;
end
always @( posedge clk )
begin: read
if( rst )
data_out<=0;
else
begin
if(rd && !empty)
data_out <= fifo_ram[rd_ptr];
else if(rd && wr)
data_out <= fifo_ram[rd_ptr];
end
end
always @( posedge clk )
begin: pointer
if( !rst )
begin
wr_ptr <= 0;
rd_ptr <= 0;
end
else
begin
wr_ptr <= ((wr && !full) || (wr && rd)) ? wr_ptr+1 : wr_ptr;
rd_ptr <= ((rd && !empty) || (wr && rd)) ? rd_ptr+1 : rd_ptr;
end
end
always @( posedge clk )
begin: count
if( !rst )
fifo_cnt <= 0;
else
begin
case ({wr,rd})
2'b00 : fifo_cnt <= fifo_cnt;
2'b01 : fifo_cnt <= (fifo_cnt==0) ? 0 : fifo_cnt-1;
2'b10 : fifo_cnt <= (fifo_cnt==8) ? 8 : fifo_cnt+1;
2'b11 : fifo_cnt<=fifo_cnt;
default: fifo_cnt <= fifo_cnt;
endcase
end
end
endmodule //fifo
testbench代码也不能正常执行,对照上述给出的module代码,修正后可运行的版本如下:
//tb代码修正后
module fifo_tb();
reg clk;
reg rst;
reg [3:0] data_in;
reg wr;
reg rd;
wire full;
wire empty;
wire [3:0] data_out;
wire [3:0] fifo_cnt;
//----Code starts here: integrated by Robei-----
initial
begin
rst=0;
clk=0;
wr=0;
rd=0;
data_in=0;
#1 rst=1;
#5 rst=0;
#3 wr=1;
#5 rd=1;
#5 rd=0;
#5 wr=0;
#5 wr=1;
#10 rd=1;
#10 rd=0;
#14 $finish;
end
always
begin
#2 clk=~clk;
end
always@(posedge clk or negedge rst)
begin
if(!rst)
begin
data_in<=0;
wr<=0;
rd<=0;
end
else
data_in<=$random;
end
initial begin
$dumpfile ("D:/Robei/RobeiJoey_Project/fifo/fifo_tb.vcd");
$dumpvars;
end
//---Module instantiation---
fifo fifo1(
.clk(clk),
.rst(rst),
.data_in(data_in),
.wr(wr),
.rd(rd),
.full(full),
.empty(empty),
.data_out(data_out),
.fifo_cnt(fifo_cnt));
endmodule //fifo_tb
将工程创建完成,便可以进行仿真波形查看,确认无误后,复制到QuartusⅡ,进行分析、管脚约束、综合、下载的工作即可在FPGA开发板上进行实验了。
二、借助QuartusⅡ内含FIFO_ip核:
1、fifo_ip core的调用、生成工作:
在IP核搜索区找到fifo选项
然后写入IP核的名字,点击NEXT就可以进入配置页面
在这里可以定义位宽和数据深度,因为同步FIFO用的不多,所以主要说一下异步FIFO(上面的手写版本也是异步FIFO)。在下方的图片中可以定义写空写满和读空读满信号以及写使能和读使能等等。
其余几个页面不需要配置什么直接点击NEXT就可以。
另外需要注意的是: 读端口和写端口的输出会有几个时间差,这是由FIFO内部的结构导致的。
2、读写控制信号的生成与fifo的应用:
三个输入线和一个输出线
总共需要三个模块和一个顶层连线
由上述简易版非ip核部分,我们知道FIFO需要用到读指针模块,写指针模块,顶层模块和testbench仿真文件。以下四个代码分别对应这四个部分,并且得到期望的仿真波形。
//写指针
module FIFO_wr(
input wire wclk,
input wire rst_n,
input wire wrfull,
input wire wrempty,
output reg [7:0]wrdata,
output reg wrreq
);
reg [7:0] state;
always @(posedge wclk or negedge rst_n)
begin
if(!rst_n)
begin
wrdata<=0;
wrreq<=0;
state<=0;
end
else
begin
case(state)
0:begin
if(wrempty)
begin
wrreq<=1 ;
wrdata<=0;
state<=1 ;
end
else
state<=0;
end
1:begin
if(wrfull)
begin
state<=0;
wrreq<=0;
wrdata<=0;
end
else
begin
wrreq<=1;
wrdata<=wrdata+1'b1;
end
end
default: state<=0;
endcase
end
end
endmodule
//读指针
module FIFO_rd(
input wire rdclk,
input wire rst_n,
input wire rdempty,
input wire rdfull,
output reg rdreq
);
reg[2:0]state;
always@(posedge rdclk or negedge rst_n)
begin
if(!rst_n)
begin
state<=0;
rdreq<=0;
end
else
begin
case (state)
0:begin
if(rdfull)
begin
state<=1;
rdreq<=1;
end
else
state <=0;
end
1:begin
if(rdempty==0)
begin
state<=1;
rdreq<=1;
end
else
begin
rdreq<=0;
state<=0;
end
end
default: state<=0;
endcase
end
end
endmodule
//顶层模块
module fifo_top(
input wire wrclk,
input wire rst_n,
input wire rdclk,
output wire [7:0]rdata
);
wire wrfull;
wire wrreq;
wire [7:0]wrdata;
wire wrempty;
wire rdfull;
wire rdempty;
wire rdreq;
FIFO_wr U1 (
.wclk(wrclk) ,
.rst_n(rst_n) ,
.wrfull(wrfull) ,
.wrdata(wrdata) ,
.wrreq(wrreq) ,
.wrempty(wrempty)
);
FIFO_rd U2(
.rdclk (rdclk) ,
.rst_n (rst_n) ,
.rdempty(rdempty) ,
.rdreq (rdreq) ,
.rdfull (rdfull)
);
my_fifo U3(
.data(wrdata) ,
.rdclk(rdclk) ,
.rdreq(rdreq) ,
.wrclk(wrclk) ,
.wrreq(wrreq) ,
.q(rdata) ,
.rdempty(rdempty) ,
.rdfull(rdfull) ,
.wrempty(wrempty) ,
.wrfull(wrfull)
);
endmodule
//testbench
module fifo_top_tb;
reg wrclk;
reg rdclk;
reg rst_n;
wire [7:0] rdata;
initial
begin
wrclk=1;
rdclk=1;
rst_n=0;
#1000
rst_n=1;
#100000 $stop;
end
always #10 wrclk=~wrclk;
always #20 rdclk=~rdclk;
fifo_top U1(
.wrclk (wrclk) ,
.rst_n (rst_n) ,
.rdclk (rdclk) ,
.rdata (rdata)
);
endmodule
仿真得到:当复位结束之前,写空标志为高电平,当有第一个数据写进去的时候,写空标志拉低,当数据写完的时候,写满标志拉高,延时了几拍后,读满标志拉高,当有第一个数据读出来以后,读满标志拉低,当数据读完后,读空标志由低变高,延时几拍后会出现写空标志,进行下一循环。当读和写的时钟不一样的时候也能很方便的达到数据缓存的目的,不至于数据丢失。
此处声明:Veriolog中所谓的指针也是一个寄存器或者线网类型的存储变量,只不过是存储地址而不是具体使用的值,这与C语言中的指针本质不同。实际上C语言是用来开发单片机的,软件控制的语言,Verilog是用来开发FPGA/CPLD/ZYNQ的,硬件控制的语言,本质完全不同!这也是我们都熟悉C语言,却非得在硬件设计上重新开发重新花时间学习HDL的原因。
之后的工作便是创建工程、管脚约束、分析综合、下载验证。与之前的操作完全相同,此略。
谈完了协议,我们再来学习接口。这里以SPI总线接口为例讲解有关知识。
SPI总线是一种4线总线,因其硬件功能很强,所以与SPI有关的软件就相当简单,使中央处理器(Central Processing Unit,CPU)有更多的时间处理其他事务。正是因为这种简单易用的特性,越来越多的芯片集成了这种通信协议,比如AT91RM9200。SPI是一种高速、高效率的串行接口技术。通常由一个主模块和一个或多个从模块组成,主模块选择一个从模块进行同步通信,从而完成数据的交换。SPI是一个环形结构,通信时需要至少4根线(事实上在单向传输时3根线也可以) [1] 。
SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,它们是MISO(主设备数据输入)、MOSI(主设备数据输出)、SCLK(时钟)、CS(片选)。
(1)MISO– Master Input Slave Output,主设备数据输入,从设备数据输出;
(2)MOSI– Master Output Slave Input,主设备数据输出,从设备数据输入;
(3)SCLK – Serial Clock,时钟信号,由主设备产生;
(4)CS – Chip Select,从设备使能信号,由主设备控制。
其中,CS是从芯片是否被主芯片选中的控制信号,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),主芯片对此从芯片的操作才有效。这就使在同一条总线上连接多个SPI设备成为可能。
接下来就负责通讯的3根线了。通讯是通过数据交换完成的,这里先要知道SPI是串行通讯协议,也就是说数据是一位一位的传输的。这就是SCLK时钟线存在的原因,由SCLK提供时钟脉冲,SDI,SDO则基于此脉冲完成数据传输。数据输出通过 SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取。完成一位数据传输,输入也使用同样原理。因此,至少需要8次时钟信号的改变(上沿和下沿为一次),才能完成8位数据的传输。
SCLK信号线只由主设备控制,从设备不能控制信号线。同样,在一个基于SPI的设备中,至少有一个主控设备。这样传输的特点:这样的传输方式有一个优点,与普通的串行通讯不同,普通的串行通讯一次连续传送至少8位数据,而SPI允许数据一位一位的传送,甚至允许暂停,因为SCLK时钟线由主控设备控制,当没有时钟跳变时,从设备不采集或传送数据。也就是说,主设备通过对SCLK时钟线的控制可以完成对通讯的控制。SPI还是一个数据交换协议:因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。不同的SPI设备的实现方式不尽相同,主要是数据改变和采集的时间不同,在时钟信号上沿或下沿采集有不同定义,具体请参考相关器件的文档。
最后,SPI接口的一个缺点:没有指定的流控制,没有应答机制确认是否接收到数据。
SPI的片选可以扩充选择16个外设,这时PCS输出=NPCS,说NPCS0~3接4-16译码器,这个译码器是需要外接4-16译码器,译码器的输入为NPCS0~3,输出用于16个外设的选择。
所以SPI的传输模式大致可以归纳如下,这也是程序编写的主要思路(感谢参考@jgliu 的部分总结):
SPI总线传输一共有4中模式,这4种模式分别由时钟极性(CPOL,Clock Polarity)和时钟相位(CPHA,Clock Phase)来定义,其中CPOL参数规定了SCK时钟信号空闲状态的电平,CPHA规定了数据是在SCK时钟的上升沿被采样还是下降沿被采样。这四种模式的时序图如下图所示:
①模式0:CPOL= 0,CPHA=0。SCK串行时钟线空闲是为低电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
②模式1:CPOL= 0,CPHA=1。SCK串行时钟线空闲是为低电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
③模式2:CPOL= 1,CPHA=0。SCK串行时钟线空闲是为高电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
④模式3:CPOL= 1,CPHA=1。SCK串行时钟线空闲是为高电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
其中比较常用的模式是模式0和模式3。为了更清晰的描述SPI总线的时序,下面展现了模式0下的SPI时序图:
上图清晰的表明在模式0下,在空闲状态下,SCK串行时钟线为低电平,当SS被主机拉低以后,数据传输开始,数据线MOSI和MISO的数据切换(Toggling)发生在时钟的下降沿(上图的黑色虚线),而数据线MOSI和MISO的数据的采样(Sampling)发生在数据的正中间(上图中的灰色实线)。下图清晰的描述了其他三种模式数据线MOSI和MISO的数据切换(Toggling)位置和数据采样位置的关系图:
具体的module代码描述如下:
//SPI通信module
module spi_module
(
input I_clk , // 全局时钟50MHz
input I_rst_n , // 复位信号,低电平有效
input I_rx_en , // 读使能信号
input I_tx_en , // 发送使能信号
input [7:0] I_data_in , // 要发送的数据
output reg [7:0] O_data_out , // 接收到的数据
output reg O_tx_done , // 发送一个字节完毕标志位
output reg O_rx_done , // 接收一个字节完毕标志位
// 四线标准SPI信号定义
input I_spi_miso , // SPI串行输入,用来接收从机的数据
output reg O_spi_sck , // SPI时钟
output reg O_spi_cs , // SPI片选信号
output reg O_spi_mosi // SPI输出,用来给从机发送数据
);
reg [3:0] R_tx_state ;
reg [3:0] R_rx_state ;
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
begin
R_tx_state <= 4'd0 ;
R_rx_state <= 4'd0 ;
O_spi_cs <= 1'b1 ;
O_spi_sck <= 1'b0 ;
O_spi_mosi <= 1'b0 ;
O_tx_done <= 1'b0 ;
O_rx_done <= 1'b0 ;
O_data_out <= 8'd0 ;
end
else if(I_tx_en) // 发送使能信号打开的情况下
begin
O_spi_cs <= 1'b0 ; // 把片选CS拉低
case(R_tx_state)
4'd1, 4'd3 , 4'd5 , 4'd7 ,
4'd9, 4'd11, 4'd13, 4'd15 : //整合奇数状态
begin
O_spi_sck <= 1'b1 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd0: // 发送第7位
begin
O_spi_mosi <= I_data_in[7] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd2: // 发送第6位
begin
O_spi_mosi <= I_data_in[6] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd4: // 发送第5位
begin
O_spi_mosi <= I_data_in[5] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd6: // 发送第4位
begin
O_spi_mosi <= I_data_in[4] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd8: // 发送第3位
begin
O_spi_mosi <= I_data_in[3] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd10: // 发送第2位
begin
O_spi_mosi <= I_data_in[2] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd12: // 发送第1位
begin
O_spi_mosi <= I_data_in[1] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b0 ;
end
4'd14: // 发送第0位
begin
O_spi_mosi <= I_data_in[0] ;
O_spi_sck <= 1'b0 ;
R_tx_state <= R_tx_state + 1'b1 ;
O_tx_done <= 1'b1 ;
end
default:R_tx_state <= 4'd0 ;
endcase
end
else if(I_rx_en) // 接收使能信号打开的情况下
begin
O_spi_cs <= 1'b0 ; // 拉低片选信号CS
case(R_rx_state)
4'd0, 4'd2 , 4'd4 , 4'd6 ,
4'd8, 4'd10, 4'd12, 4'd14 : //整合偶数状态
begin
O_spi_sck <= 1'b0 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
end
4'd1: // 接收第7位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[7] <= I_spi_miso ;
end
4'd3: // 接收第6位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[6] <= I_spi_miso ;
end
4'd5: // 接收第5位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[5] <= I_spi_miso ;
end
4'd7: // 接收第4位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[4] <= I_spi_miso ;
end
4'd9: // 接收第3位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[3] <= I_spi_miso ;
end
4'd11: // 接收第2位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[2] <= I_spi_miso ;
end
4'd13: // 接收第1位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b0 ;
O_data_out[1] <= I_spi_miso ;
end
4'd15: // 接收第0位
begin
O_spi_sck <= 1'b1 ;
R_rx_state <= R_rx_state + 1'b1 ;
O_rx_done <= 1'b1 ;
O_data_out[0] <= I_spi_miso ;
end
default:R_rx_state <= 4'd0 ;
endcase
end
else
begin
R_tx_state <= 4'd0 ;
R_rx_state <= 4'd0 ;
O_tx_done <= 1'b0 ;
O_rx_done <= 1'b0 ;
O_spi_cs <= 1'b1 ;
O_spi_sck <= 1'b0 ;
O_spi_mosi <= 1'b0 ;
O_data_out <= 8'd0 ;
end
end
endmodule
testbench代码如下:
//SPI testbench代码
`timescale 1ns / 1ps
module tb_spi_module;
// Inputs
reg I_clk;
reg I_rst_n;
reg I_rx_en;
reg I_tx_en;
reg [7:0] I_data_in;
reg I_spi_miso;
// Outputs
wire [7:0] O_data_out;
wire O_tx_done;
wire O_rx_done;
wire O_spi_sck;
wire O_spi_cs;
wire O_spi_mosi;
// Instantiate the Unit Under Test (UUT)
spi_module uut (
.I_clk (I_clk ),
.I_rst_n (I_rst_n ),
.I_rx_en (I_rx_en ),
.I_tx_en (I_tx_en ),
.I_data_in (I_data_in ),
.O_data_out (O_data_out ),
.O_tx_done (O_tx_done ),
.O_rx_done (O_rx_done ),
.I_spi_miso (I_spi_miso ),
.O_spi_sck (O_spi_sck ),
.O_spi_cs (O_spi_cs ),
.O_spi_mosi (O_spi_mosi )
);
initial begin
// Initialize Inputs
I_clk = 0;
I_rst_n = 0;
I_rx_en = 0;
I_tx_en = 1;
I_data_in = 8'h00;
I_spi_miso = 0;
// Wait 100 ns for global reset to finish
#100;
I_rst_n = 1;
end
always #10 I_clk = ~I_clk ;
always @(posedge I_clk or negedge I_rst_n)
begin
if(!I_rst_n)
I_data_in <= 8'h00;
else if(I_data_in == 8'hff)
begin
I_data_in <= 8'hff;
I_tx_en <= 0;
end
else if(O_tx_done)
I_data_in <= I_data_in + 1'b1 ;
end
endmodule
利用Modelsim进行仿真,可以得到以下波形:
不管是FIFO还是SPI的学习,都为我们下一节详细分析串口通信打下了坚实的基础。