目录
一.IP核的使用
1.PLL IP核
<1>PLL简介
<2>xilinx vivado IP核配置
<2>xilinx vivado IP核调用
2.ROM IP核
<1>xilinx vivado IP核配置
⑴创建初始化文件.coe文件
⑵单端口 ROM 的配置
⑶双端口 ROM 的配置
<2>xilinx vivado IP核调用
3.RAM IP核
<1>xilinx vivado IP核配置
⑴单端口 ROM 的配置
<2>xilinx vivado IP核配置
4.FIFO IP核
⑴SCFIFO 的配置
<1>xilinx vivado IP核配置
<2>xilinx vivado IP核调用
⑵DCFIFO 的配置
<1>xilinx vivado IP核配置
<2>xilinx vivado IP核调用
⑶注意问题
前置学习:
设计流程——FPGA学习笔记<2>
verilog语法——FPGA学习笔记<1>
参考书目:《野火FPGA Verilog 开发实战指南》
IP 核有三种不同的存在形式:HDL 语言形式,网表形式、版图形式。分别对应我们常说的三类 IP 内核:软核、固核和硬核。
软核是用硬件描述语言的形式功能块的行为,并不涉及用什么电路和电路元件实现这 些行为,软 IP 通常是以硬件描述语言 HDL 源文件的形式出现,应用开发过程与普通的 HDL 设计也十分相似,大多数应用于 FPGA 的 IP 内核均为软核,软核有助于用户调节参 数并增强可复用性。软核通常以加密形式提供,这样实际的 RTL 对用户是不可见的,但布局和布线灵活。在这些加密的软核中,如果对内核进行了参数化,那么用户就可通过头文件或图形用户接口(GUI)方便地对参数进行操作。软 IP 的设计周期短,设计投入少。由 于不涉及物理实现,为后续设计留有很大的发挥空间,增大了 IP 的灵活性和适应性。其主 要缺点是在一定程度上使后续工序无法适应整体设计,从而需要一定程度的软 IP 修正,在性能上也不可能获得全面的优化。由于软核是以源代码的形式提供,尽管源代码可以采用加密方法,但其知识产权保护问题不容忽视。
固核则是软核和硬核的折衷。固核是完成了综合的功能块,有较大的设计深度,以网表的形式交给客户使用。对于那些对时序要求严格的内核(如 PCIE 接口内核),可预布线特定信号或分配特定的布线资源,以满足时序要求。这些内核可归类为固核,由于内核 是预先设计的代码模块,因此这有可能影响包含该内核的整体设计。由于内核的建立时 间、保持时间和握手信号都可能是固定的,因此其它电路的设计时都必须考虑与该内核进 行正确地接口。如果内核具有固定布局或部分固定的布局,那么这还将影响其它电路的布局。
硬核是完成提供设计的最终阶段产品——掩膜(Mask),以经过完全的布局布线的网 表形式提供,这种硬核既具有可预见性,同时还可以针对特定工艺或购买商进行功耗和尺寸上的优化。尽管硬核由于缺乏灵活性而可移植性差,但由于无须提供寄存器转移级 (RTL)文件,因而更易于实现 IP 保护。比如一些 FPGA 芯片内置的 ARM 核就是硬核。
IP 核在拥有以上众多好处的同时也有他的巨大缺点:
1、在跨平台时,IP 核往往不通用,需要重新设计。IP 核都是不全透明的,是每个 FPGA 开发厂商根据自己芯片适配的定制 IP,所以如果你之前用的 Altera 的芯片,用了一 个 PLL,但是因为某些原因需要将代码移植到 Xilinx 平台上,那就必须要将 PLL 给重新替 换掉,着增加了代码移植的复杂性。
2、IP 核就是个黑匣子,是不透明的,我们往往看不到其核心代码。IP 核都是各大 FPGA 厂商专门设计的,都会进行加密,内核代码都看不到,如果你使用的这个 IP 核万一出现了问题或者需要知道其内部结构针对具体的应用进行定制优化时,你是无法进行修改的。以上两个问题就很棘手,所以有些公司坚持所有的可综合设计都不使用 IP 核,就是为了是所有的模块都能够掌控在在自己手里。
3、有些定制的 IP 核由于是不通用的,往往会有较高的收费,这也是一笔巨大的开 销。所以 IP 核在能够加快我们开发周期的情况下也存在以上三种常见的问题,这就是需要 我们权衡利弊,针对具体的需求来做具体的选择。
IP 核生成工具提供的 IP 核主要有以下几类:
1、数学运算模块,包括累加器、乘加器、乘累加器、计数器、加/减法器、实/复数乘 法器、除法器、CORDIC 算法器、DSP48 宏和浮点数操作器。
2、存储器构造模块,包括块存储器和分布式存储器、先入先出存储器(FIFO)和移位寄存器。 3、DSP 功能,包括直接数字频率合成(DDS)编译器、数字上变频/下变频 (DUC/DDC)编译器、有限冲激响应(FIR)滤波器、级联积分梳状(CIC)滤波器、离 散傅里叶变换(DFT)和快速傅里叶变换(FFT)。
4、信道纠错码,包括 RS 码编码器和译码器、卷积码编码器、Viterbi 译码器、Turbo 码编/译码器和低密度奇偶校验码(LDPC)编码器等。
5、网络应用,包括媒体访问控制器(MAC)、以太网物理编码子层/物理介质连接 (PCS/PMA)、网络负载统计、以太网拓展连接单元接口(XAUI)、减少引脚使用的 XAUI(RXAUI)、MAC 封装包和音/视频桥接(AVB)端点。
6、FPGA 结构属性,包括时钟向导、高速串行收发器(GTX/GTP)和系统监视向导。
7、连接器,包括标准总线接口(如 PCI/PCI-X、PCI Express、CAN)和数据接口(如 以太网、RapidIO 等)。
8、调试和验证,包括逻辑调试内核(集成控制器核(ICON)、集成逻辑分析核 (ILA)、虚拟输入/输出核(VIO)、Agilent 跟踪核(ATC2)、误比特率测试核 (IBERT)和集成总线分析核(IBA)。
9、针对不同设计方法的特殊IP核,包括用工程导航工具进行逻辑设计的IP核、用 Xilinx系统生成工具进行DSP算法设计的IP核,以及用Xilinx平台开发环境(XPS)或 PlanAhead进行嵌入式设计的IP核。
PLL(Phase Locked Loop,即锁相环)是最常用的 IP 核之一,其性能强大,可以对输入到 FPGA 的时钟信号进行任意分频、倍频、相位调整、占空比调整,从而输出一个期望时钟,实际上,即使不想改变输入到 FPGA 时钟的任何参数,也常常会使用 PLL,因为经 过 PLL 后的时钟在抖动(Jitter)方面的性能更好一些。Xilinx 中的 PLL 是模拟锁相环,和数字锁相环不同的是模拟锁相环的优点是输出的稳定度高、相位连续可调、延时连续可调;缺点是当温度过高或者电磁辐射过强时会失锁(普通环境下不考虑该问题)。
工作原理:
1、首先需要参考时钟(ref_clk)通过鉴频(FD)鉴相器(PD)和需要比较的时钟频率进行比较,我们以频率调整为例,如果参考时钟频率等于需要比较的时钟频率则鉴频鉴相器输出为 0,如果参考时钟频率大于需要比较的时钟频率则鉴频鉴相器输出一个变大的成正比的值,如果参考时钟频率小于需要比较的时钟频率则鉴频鉴相器输出一个变小的正比的值。
2、鉴频鉴相器的输出连接到环路滤波器(LF)上,用于控制噪声的带宽,滤掉高频噪声,使之稳定在一个值,起到将带有噪声的波形变平滑的作用。如果鉴频鉴相器之前的波形抖动比较大,经过环路滤波器后抖动就会变小,趋近于信号的平均值。
3、经过环路滤波器的输出连接到压控振荡器(VCO)上,环路滤波器输出的电压可以控制 VCO 输出频率的大小,环路滤波器输出的电压越大 VCO 输出的频率越高,然后将这个频率信号连接到鉴频鉴相器作为需要比较的频率。 如果 ref_clk 参考时钟输入的频率和需要比较的时钟频率不相等,该系统最终实现的就是让它们逐渐相等并稳定下来。如果 ref_clk 参考时钟的频率是 50MHz,经过整个闭环反馈系统后,锁相环对外输出的时钟频率 pll_out 也是 50MHz。
倍频实现:倍频是在 VCO 后直接加一级分频器,我们知道 ref_clk 参考时钟输入的频率和需要比较的时钟频率经过闭环反馈系统后最终会保持频率相等,而在需要比较的时钟之前加入分频器,就会使进入分频器之前的信号频率为需要比较的时钟频率的倍数,VCO 后输出的 pll_out 信号频率就是 ref_clk 参考时钟倍频后的结果。
分频实现:分频是在 ref_clk 参考时钟后加一级分频器,这样需要比较的时钟频率就始终和 ref_clk 参考时钟分频后的频率相等,在 VCO 后输 出的 pll_out 信号就是 ref_clk 参考时钟分频后的结果。
②框选项区别在于左上Resoursce栏使用资源的不同
Clocking Features选择功能,左上Switch To Defaults恢复默认
Documentation可打开官方手册
IP Location选择核存放位置
接下来配置输出:这里输出四路信号,注意各个参数(期望和实际输出、相位/占空比调整)
生成之后可以在工程中看到我们生成的 IP 核,双击 IP 核可进入配置界面,对 IP 核的相关参数进行更改。
(拷贝调用: 要求同版本开发IDE。直接复制IP核文件夹,位置在source里的ip文件夹)新工程添加设计文件,点击后缀为xci的文件。
通过实例化对IP核进行调用,可从多个文件中选择复制。如下是一种办法:
或者找到IP核下的.veo文件,其中也包含实例化模版
再编写一个顶层设计文件来例化这个 PLL IP 核:
仿真:
`timescale 1ns/1ns
module tb_pll();
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//reg define
reg sys_clk ;
//wire define
wire clk_mul_2 ;
wire clk_div_2 ;
wire clk_phase_90;
wire clk_ducle_20;
wire locked ;
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//初始化系统时钟
initial sys_clk = 1'b1;
//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//------------------------pll_inst------------------------
pll pll_inst
(
.sys_clk (sys_clk ), //input sys_clk
.clk_mul_2 (clk_mul_2 ), //output clk_mul_2
.clk_div_2 (clk_div_2 ), //output clk_div_2
.clk_phase_90 (clk_phase_90 ), //output clk_phase_90
.clk_ducle_20 (clk_ducle_20 ), //output clk_ducle_20
.locked (locked ) //output locked
);
endmodule
ROM 是只读存储器(Read-Only Memory)的简称,是一种只能读出事先所存数据的固态半导体存储器。 其特性是一旦储存资料就无法再将之改变或删除,且资料不会因为电源关闭而消失。而事实上在 FPGA 中通过 IP 核生成的 ROM 或 RAM(RAM 将在下一节为大家讲解)调用的都是 FPGA 内部的 RAM 资源,掉电内容都会丢失(这也很容易解释,FPGA 芯片内部本来 就没有掉电非易失存储器单元)。用 IP 核生成的 ROM 模块只是提前添加了数据文件 (.coe 格式)(Quartus II中是mif/nex文件),在 FPGA 运行时通过数据文件给 ROM 模块初始化,才使得 ROM 模块像个 “真正”的掉电非易失存储器;也正是这个原因,ROM 模块的内容必须提前在数据文件中 写死,无法在电路中修改。 Xilinx 推出的 ROM IP 核分为两种类型:单端口 ROM(Single-Port Rom)和双端口 ROM(Dual-Port ROM)。对于单端口 ROM 提供一个读地址端口和一个读数据端口,只能进行读操作;双端口 ROM 与单端口 ROM 类似,区别是其提供两个读地址端口和两个读数 据端口,基本上可以看做两个单口 ROM 拼接而成。下面是 ROM 不同配置模式存储器的接口信号图
该文件的格式较为简单,第一行是定义数据的格式,其中 16 表示 数据格式为 16 进制,也可将数据格式定义为二进制和八进制,只需将 16 改为 2 或 8 即 可。其中第 3 到第 18 行是 16*8bit 大小 ROM 的初始化数据。
给该 IP 核取名为rom_256x8(rom 是我们调用的 IP 核,256 是调用的 IP 核容量,8 是调用的 IP 核数据位宽。这里这样命名是为了方便识别我们创建的 IP 核类型及资源量)。
3 框中选择存储器类型,可供选择的类型有:Single Port RAM(单端口 RAM)、 Simple Dual Port RAM(简单双口 RAM)、True Dual Port RAM(真双口 RAM)、Singl Port ROM(单端口 ROM)、Doul Port ROM(双端口 ROM)。这里我们选择“Single Port Rom”单端口 ROM。
4 框在 Algorithm 一栏中可选择用于实现内存的算法,其中 Minimum Area 为最小面积 算法;Low Power 为低功耗算法;Fixed Primitives 为固定单元算法。这里我们按默认选择 Minimum Area 即可。
1 框中是设置存储数据的位宽,这里我们设置为 8 位;
2 框中是设置数据深度,所谓深度其实就是个数的选择,即设置的 ROM 可以存储多 少个 8 位宽的数据,这里我们设置为 256;这样我们设置的 ROM 和最大能存储的数据即为 256 x 8bit。(注意:设置的容量需大于我们需要写入的数据文件的数据量)
3 框中选择是否创建端口使能信号,这里我们不创建,选择“Always Enabled”始终 使能。
4 框是选择是否创建输出端口寄存器,若创建了择输出数据则会延后一个时钟输出, 这里我们不创建。
5 框是选择是否生产复位信号,这里我们不创建。
添加初始化coe文件
对端口 A 的设置跟单端口 ROM 的设置一样即可。对端口 B 的设置,这里我们只需对 B 端口的数据位宽设置即可,数据深度会根据端口 A 的位宽自动设置。例如我们端口 A 设置的数据位宽为 8bit,深度为 256;而我们 B 端 口设置的数据位宽为 16bit,则其深度即为 128,其数据总量是一样的。其余设置与端口 A 设置一样即可。
剩余操作与单端口 ROM 相同
`timescale 1ns/1ns
module rom
(
input wire sys_clk , //系统时钟,频率50MHz
input wire [7:0] addra , //输入rom地址
output wire [7:0] douta //输出rom数据
);
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//----------------rom_256x8_inst---------------
rom_256x8 rom_256x8_inst
(
.clka (sys_clk ), // input clka
.addra (addra ), // input [7 : 0] addra
.douta (douta ) // output [7 : 0] douta
);
endmodule
仿真:
`timescale 1ns/1ns
module tb_rom();
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//reg define
reg sys_clk ;
reg sys_rst_n ;
reg [7:0] addra ;
//wire define
wire [7:0] douta ;
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//对sys_clk,sys_rst赋初值,并模拟按键抖动
initial
begin
sys_clk = 1'b1 ;
sys_rst_n <= 1'b0 ;
#200 sys_rst_n <= 1'b1 ;
end
//sys_clk:模拟系统时钟,每10ns电平取反一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;
//让地址从0~255循环
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
addra <= 8'd0;
else if(addra == 8'd255)
addra <= 8'd0;
else
addra <= addra + 1'b1;
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//---------------rom_inst--------------
rom rom_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.addra (addra ), //输入rom地址
.douta (douta ) //输出rom数据
);
endmodule
通过reg变量进行手动赋值仿真,wire变量引出输出结果
RAM 是随机存取存储器(Random Access Memory)的简称,是一个易失性存储器。 RAM 工作时可以随时从任何一个指定的地址写入或读出数据,同时我们还能修改其存储的数据,即写入新的数据,这是 ROM 所并不具备的功能。在 FPGA 中这也是其与 ROM 的最大区别。ROM 是只读存储器,而 RAM 是可写可读存储器,在我们 FPGA 中使用这两个存储器主要也是要区分这一点,因为这两个存储器使用的都是我们 FPGA 内部的 RAM 资源,不同的是 ROM 是只用到了 RAM 资源的读数据端口。 Xilinx 推出的 RAM IP 核分为两种类型:单端口 RAM 和双端口 RAM。其中双端口 RAM 又分为简单双端口 RAM 和真正双端口 RAM。对于单端口 RAM,读写操作共用一组地址线,读写操作不能同时进行;对于简单双端口 RAM,读操作和写操作有专用地址端口 (一个读端口和一个写端口),即写端口只能写不能读,而读端口只能读不能写;对于真正双端口 RAM,有两个地址端口用于读写操作(两个读/写端口),即两个端口都可以进行读写。
2 框中是操作模式的选择。RAM 读写操作模式共分为三种,非别是 Write First(写优 先模式)、Read First(读优先模式)和 No Change(不变模式)。
Write First(写优先模式):若我们在在同一个时钟沿下对同一个地址进行读写,则读出的数据为写入的数据。
Read First(读优先模式):若我们在在同一个时钟沿下对同一个地址进行读写,则读出的数据为该地址写入数据前存储的数据。
No Change(不变模式):在该模式下不能同时进行读写操作,输出数据为同时读写操作前输出的数据。
加载数据文件时,同 ROM 一样,RAM 也可以加载初始 化文件,但是 RAM 是可以写入数据的,所以这里可以选择不加,该页面按默认设置即可。
注:(真)双端口 ROM 的配置类似
RTL 顶层的输入信号有:50MHz 的系统时钟 sys_clk、输入 256 个 8bit 地址 addra(值 为十进制 0~256)、输入 256 个 8bit 的数据 dina(值为十进制 0~255)和伴随该输入数据的 写使能信号 wea。这些输入信号需要在 Testbench 中产生激励。
RTL 顶层的输出信号有:从 RAM 中读取的数据 douta。
RTL 顶层代码如下所示:
`timescale 1ns/1ns
module ram
(
input wire sys_clk , //系统时钟,频率50MHz
input wire [7:0] addra , //输入ram读写地址
input wire [7:0] dina , //输入ram写入数据
input wire wea , //输入ram写使能
output wire [7:0] douta //输出读ram数据
);
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//---------------ram_256x8_inst--------------
ram_256x8 ram_256x8_inst
(
.clka (sys_clk ), //使用系统时钟作为读写时钟
.addra (addra ), //读写地址线
.dina (dina ), //输入写入RAM的数据
.wea (wea ), //写RAM使能
.douta (douta ) //输出读RAM数据
);
endmodule
仿真代码:
`timescale 1ns/1ns
module tb_ram();
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//reg define
reg sys_clk ;
reg sys_rst_n ;
reg [7:0] addra ;
reg wea ;
reg wr_flag ;
//wire define
wire [7:0] dina ;
wire [7:0] douta ;
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//对sys_clk,sys_rst赋初值,并模拟按键抖动
initial
begin
sys_clk = 1'b1 ;
sys_rst_n <= 1'b0 ;
#200 sys_rst_n <= 1'b1 ;
end
//sys_clk:模拟系统时钟,每10ns电平取反一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;
//写完成标志信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
wr_flag <= 1'b0;
else if(addra == 8'd255)
wr_flag <= 1'b1;
else
wr_flag <= wr_flag;
//wea:产生写RAM使能信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
wea <= 1'b0;
else if(wr_flag == 1'b1)
wea <= 1'b0;
else
wea <= 1'b1;
//addra:读写地址(0~255循环)
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
addra <= 8'd0;
else if(addra == 8'd255)
addra <= 8'd0;
else
addra <= addra + 1'b1;
//写使能为高时产生写数据0~255
assign dina = (wea == 1'b1) ? addra : 8'd0;
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//---------------ram_inst--------------
ram ram_inst
(
.sys_clk (sys_clk ), //系统时钟,频率50MHz
.addra (addra ), //输入ram读写地址
.dina (dina ), //输入ram写入数据
.wea (wea ), //输入ram写使能
.douta (douta ) //输出读ram数据
);
endmodule
这里我们截取了前面我们讲到的 Write First 模式以及 No Change 模式下的仿真波形图;从 Write First 模式仿真图看一看到,当我们往 RAM 里写数据时,就有数据读出了;而从 No Change 模式下的波形图可以看到,当我们往 RAM 里写数据时,是不同同时进行数据读取的,读数据端口会输出读写操作前的输出数据,这里为 0。
FIFO(First In First Out,即先入先出),是一种数据缓冲器,用来实现数据先入先出的读写方式。与 ROM 或 RAM 的按地址读写方式不同,FIFO 的读写遵循“先进先出”的原则,即数据按顺序写入 FIFO,先被写入的数据同样在读取的时候先被读出,所以 FIFO 存储器没有地址线。FIFO 有一个写端口和一个读端口外部无需使用者控制地址,使用方便。 FIFO 存储器主要是作为缓存,应用在同步时钟系统和异步时钟系统中,在很多的设计中都会使用,后面实例中如:多比特数据做跨时钟域的转换、前后带宽不同步等都用到了 FIFO。FIFO 根据读写时钟是否相同,分为 SCFIFO(同步 FIFO)和 DCFIFO(异步 FIFO),SCFIFO 的读写为同一时钟,应用在同步时钟系统中;DCFIFO 的读写时钟不同,应用在异步时钟系统中。
可参考:FPGA设计心得(11)关于FIFO IP核使用的一点注意事项
3 框是选中 FIFO 的类型,以及使用什么资源来实现。这里我们选择“Common Clock Block RAM”使用块 RAM 来实现同步 FIFO;其中 Common Clock 表示是同步 FIFO, Block RAM 表示的是块 RAM 资源。可以从界面中的表格查看各类型区别
1 框中是对模式的选择,这里我们选择标准 FIFO(Standard FIF0)即可。
若3框下选项选择异步复位(Asynchronous Reset),还会多出安全电路使能选项
若3框上选项选中输出端口寄存器,可选择相应寄存器
1 框中可勾选生成 FIFO 几乎满(Almost Full Flag),几乎空(Almost Empty Flag)输 出信号;也就是说若勾选了这两个信号,当 FIFO 存储数据快满或者快空的时候,该信号 就有效。这里大家可根据自己的实际需求进行勾选,这里我们选择都不勾选。
2 框中可勾选生成写确认标志信号,用于报告写操作成功。若勾选后可以配置为高电 平有效或低电平有效,这里我们选择不勾选。
3 框中可勾选生成溢出标志信号;该标志信号可以指示 FIFO 内存储数据是否溢出, 可以指示上一次写操作何时失败。若勾选后可以配置为高电平有效或低电平有效,这里我 们选择不勾选。
4 框中可勾选生成指示输出总线上数据何时有效的有效标志信号。若勾选后可以配置 为高电平有效或低电平有,这里我们选择不勾选。
5 框中可勾选生成下溢标志信号;该标志信号可以指示 FIFO 内存储数据空了,可以 指示上一次的读请求何时失败。若勾选后可以配置为高电平有效或低电平有效,这里我们 选择不勾选。 这些选项可以根据自己的设计需求进行勾选,这里我们按默认都不勾选,直接切换到 “Data Counts”页面。
RTL 代码顶层的输入信号有:50MHz 的系统时钟 sys_clk、输入 256 个 8bit 的数据 pi_data(值为十进制 0~255)和伴随该输入数据有效的标志信号 pi_flag、FIFO 的写请求信 号 rd_en。这些输入信号需要在 Testbench 中产生激励。
RTL 代码顶层的输出信号有:从 FIFO 中读取的数据 po_data、FIFO 空标志信号 empty、FIFO 满标志信号 full、指示 FIFO 中存在数据个数的信号 data_count。这些信号也是我们需要通过仿真 SCFIFO IP 核主要观察的信号,这些信号通过 Testbench 中给输入信号激励后产生输出。
`timescale 1ns/1ns
module fifo
(
input wire sys_clk , //系统时钟50Mhz
input wire sys_rst_n , //复位信号
input wire [7:0] pi_data , //输入顶层模块的数据
//要写入到FIFO中的数据
input wire pi_flag , //输入数据有效标志信号
//也作为FIFO的写请求信号
input wire rd_en , //FIFO读请求信号
output wire [7:0] po_data , //FIFO读出的数据
output wire empty , //FIFO空标志信号,高有效
output wire full , //FIFO满标志信号,高有效
output wire [7:0] data_count //FIFO中存在的数据个数
);
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
scfifo_256x8 scfifo_256x8_inst
(
.clk (sys_clk ), // input clk
.srst (~sys_rst_n), // input srst
.din (pi_data ), // input [7 : 0] din
.wr_en (pi_flag ), // input wr_en
.rd_en (rd_en ), // input rd_en
.dout (po_data ), // output [7 : 0] dout
.full (full ), // output full
.empty (empty ), // output empty
.data_count(data_count) // output [7 : 0] data_count
);
endmodule
仿真:
下面是 Testbench 仿真测试文件,和 SCFIFO 的仿真一样,我们也需要给输入信号测试激励,pi_flag 每 4 个时钟周期且没有读请求时产生一个数据有效标志信号也作为 FIFO 的写请求信号,因为需要 pi_data 伴随着 pi_flag 一起产生,所以每当 pi_data 检测到 pi_flag 标 志信号有效时就自加 1,其值从 0~255 循环变化,这样我们就可以在 pi_flag 标志信号有效时将 pi_data 写入到 FIFO 中。而 FIFO 的读请求信号 rd_en 当 FIFO 的满标志信号 full 有效时拉高,当 FIFO 的空标志信号 empty 有效时拉低。
`timescale 1ns/1ns
module tb_fifo();
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//reg define
reg sys_clk ;
reg [7:0] pi_data ;
reg pi_flag ;
reg rd_en ;
reg sys_rst_n ;
reg [1:0] cnt_baud ;
//wire define
wire [7:0] po_data ;
wire empty ;
wire full ;
wire [7:0] data_count ;
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//初始化系统时钟、复位
initial begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#200;
sys_rst_n <= 1'b1;
end
//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50Mhz
always #10 sys_clk = ~sys_clk;
//cnt_baud:计数从0到3的计数器,用于产生输入数据间的间隔
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_baud <= 2'b0;
else if(&cnt_baud == 1'b1)
cnt_baud <= 2'b0;
else
cnt_baud <= cnt_baud + 1'b1;
//pi_flag:输入数据有效标志信号,也作为FIFO的写请求信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
pi_flag <= 1'b0;
//每4个时钟周期且没有读请求时产生一个数据有效标志信号
else if((cnt_baud == 2'd0) && (rd_en == 1'b0))
pi_flag <= 1'b1;
else
pi_flag <= 1'b0;
//pi_data:输入顶层模块的数据,要写入到FIFO中的数据
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
pi_data <= 8'b0;
//pi_data的值为0~255依次循环
else if((pi_data == 8'd255) && (pi_flag == 1'b1))
pi_data <= 8'b0;
else if(pi_flag == 1'b1) //每当pi_flag有效时产生一个数据
pi_data <= pi_data + 1'b1;
//rd_en:FIFO读请求信号
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_en <= 1'b0;
else if(full == 1'b1) //当FIFO中的数据存满时,开始读取FIFO中的数据
rd_en <= 1'b1;
else if(empty == 1'b1) //当FIFO中的数据被读空时停止读取FIFO中的数据
rd_en <= 1'b0;
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//------------------------fifo_inst------------------------
fifo fifo_inst
(
.sys_clk (sys_clk ), //input sys_clk
.sys_rst_n (sys_rst_n ), //input sys_rst_n
.pi_data (pi_data ), //input [7:0] pi_data
.pi_flag (pi_flag ), //input pi_flag
.rd_en (rd_en ), //input rd_en
.po_data (po_data ), //output [7:0] po_data
.full (full ), //output full
.empty (empty ), //output empty
.data_count (data_count ) //output [7:0] data_count
);
endmodule
可以看到 pi_data 和 po_data 交替出现并一直循环下去,pi_flag 数据有效标志信号伴随着 pi_data 一一对应,po_data 在读请求信号 rd_en 为高时输出。其中 我们也可以看到 empty 和 full 在不同的位置均有拉高的脉冲,接下来我们将图中位置 1 和 位置 2 分别放大观察。
如图所示为位置 1 放大后的波形,有几个点需要我们重点观察:
1、full、data_count 信号的状态:我们可以看到当 pi_flag 为高且 pi_data 为 255 的同时 full 满标志信号拉高了,说明 FIFO 的存储空间已经满了,而 data_count 信号也从 255 变成 了 0,因为产生的 SCFIFO IP 核中 data_count 的位宽是 8bit 的,而十进制 256 需要 9bit 才能 完全显示,这样最高位就无法显示出来,所以 data_count 的值显示为 0。
2、FIFO 读出的数据与 FIFO 读请求的关系:因为我们这里是对普通同步 FIFO 模式进行的仿真,所以可以看到当检测到 full 满标志信号有效,rd_en 读请求信号开始拉高,FIFO 开始读数据,FIFO 读出的第一个数据为 0,可以看到数据为 0 的时间有两个时钟周期,所以第一个 0 为潜伏期导致的,第二个 0 才是我们真正读出来的数据,FIFO 中随着数据的读 出,FIFO 中的数据减少,full 满标志信号也拉低了,data_count 信号的值也随着减小。
如图所示为位置 2 放大后的波形,这里我们重点观察一下 empty 空标志信号。 因为我们使用的是标准 fifo,所以读出的数据要比读使能延后一拍,所以当读出十进制数 据 254 后 empty 空标志信号拉高,表示 FIFO 中的数据已经被读空。
与SCFIFO配置不同:
2 框中是选中 FIFO 的类型,以及使用什么资源来实现。这里我们选择“Independent Clock Block RAM”使用块 RAM 来实现异步 FIFO;其中 Independent Clock 表示是异步 FIFO,Block RAM 表示的是块 RAM 资源。
如图所示,可勾选“Write Data Count”和“Read Data Count”生成 FIFO 内剩余个数输出信号,一个是基于读时钟,一个是基于写时钟。该输出信号可根据自己设计是否需要进行生成,默认是不生成,这里我们都勾选进行生成。
RTL 代码顶层的输入信号有:50MHz 的写时钟 wr_clk、输入 256 个 8bit 的数据 pi_data (值为十进制 0~255)和伴随该输入数据有效的标志信号 pi_flag、25MHz 的读时钟 rd_clk、FIFO 的写请求信号 rd_en。这些输入信号需要在 Testbench 中产生激励。
RTL 代码顶层的输出信号有:FIFO 空标志信号 empty、FIFO 满标志信号 full、同步于 wr_clk 指示 FIFO 中存在数据个数的信号 wr_data_count、从 FIFO 中读取的数据 po_data、 同步于 rd_clk 指示 FIFO 中存在数据个数的信号 rd_data_count。这些信号也是我们需要通过仿真 DCFIFO IP 核主要观察的信号,这些信号通过 Testbench 中给输入信号激励后产生输出。
`timescale 1ns/1ns
module fifo
(
input wire wr_clk , //同步于FIFO写数据的时钟50MHz
input wire [7:0] pi_data , //输入顶层模块的数据,要写入到FIFO中
//的数据同步于wrclk时钟
input wire pi_flag , //输入数据有效标志信号,也作为FIFO的
//写请求信号,同步于wrclk时钟
input wire rd_clk , //同步于FIFO读数据的时钟25MHz
input wire rd_en , //FIFO读请求信号,同步于rdclk时钟
output wire [15:0] po_data , //FIFO读出的数据,同步于rdclk时钟
output wire empty , //空标志信号,高有效,
output wire full , //满标志信号,高有效,
output wire [6:0] rd_data_count,//FIFO读端口中存在的数据个数,
//同步于rdclk时钟
output wire [7:0] wr_data_count //FIFO写端口中存在的数据个数,
//同步于wrclk时钟
);
//----------------------dcfifo_256x8to128x16_inst-----------------------
dcfifo_256x8to128x16 dcfifo_256x8to128x16_inst
(
.din (pi_data), //input [7:0] din
.rd_clk (rd_clk ), //input rd_clk
.rd_en (rd_en ), //input rd_en
.wr_clk (wr_clk ), //input wr_clk
.wr_en (pi_flag), //input wr_en
.dout (po_data), //output [15:0] dout
.empty (empty ), //output empty
.full (full ), //output full
.rd_data_count (rd_data_count),//output [6:0] rd_data_count
.wr_data_count (wr_data_count) //output [7:0] wr_data_count
);
endmodule
仿真:
`timescale 1ns/1ns
module tb_fifo();
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//reg define
reg wr_clk ;
reg [7:0] pi_data ;
reg pi_flag ;
reg rd_clk ;
reg rd_en ;
reg sys_rst_n ;
reg [1:0] cnt_baud ;
reg full_reg0 ;
reg full_reg1 ;
//wire define
wire empty ;
wire full ;
wire [7:0] wr_data_count ;
wire [15:0] po_data ;
wire [6:0] rd_data_count ;
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//初始化时钟、复位
initial begin
wr_clk = 1'b1;
rd_clk = 1'b1;
sys_rst_n <= 1'b0;
#100;
sys_rst_n <= 1'b1;
#100000
sys_rst_n <= 1'b0;
end
//wr_clk:模拟FIFO的写时钟,每10ns电平翻转一次,周期为20ns,频率为50MHz
always #10 wr_clk = ~wr_clk;
//rd_clk:模拟FIFO的读时钟,每20ns电平翻转一次,周期为40ns,频率为25MHz
always #20 rd_clk = ~rd_clk;
//cnt_baud:计数从0到3的计数器,用于产生输入数据间的间隔
always@(posedge wr_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
cnt_baud <= 2'b0;
else if(&cnt_baud == 1'b1)
cnt_baud <= 2'b0;
else
cnt_baud <= cnt_baud + 1'b1;
//pi_flag:输入数据有效标志信号,也作为FIFO的写请求信号
always@(posedge wr_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
pi_flag <= 1'b0;
//每4个时钟周期且没有读请求时产生一个数据有效标志信号
else if((cnt_baud == 2'd0) && (rd_en == 1'b0))
pi_flag <= 1'b1;
else
pi_flag <= 1'b0;
//pi_data:输入顶层模块的数据,要写入到FIFO中的数据
always@(posedge wr_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
pi_data <= 8'b0;
pi_data的值为0~255依次循环
else if((pi_data == 8'd255) && (pi_flag == 1'b1))
pi_data <= 8'b0;
else if(pi_flag == 1'b1) //每当pi_flag有效时产生一个数据
pi_data <= pi_data + 1'b1;
//将同步于rd_clk时钟的写满标志信号full在rd_clk时钟下打两拍
always@(posedge rd_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
full_reg0 <= 1'b0;
full_reg1 <= 1'b0;
end
else
begin
full_reg0 <= full;
full_reg1 <= full_reg0;
end
//rd_en:FIFO读请求信号同步于rd_clk时钟
always@(posedge rd_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rd_en <= 1'b0;
//如果full信号有效就立刻读,则不会看到full信号拉高,
//所以此处使用full在rd_clk时钟下打两拍后的信号
else if(full_reg1 == 1'b1)
rd_en <= 1'b1;
else if(empty == 1'b1)//当FIFO中的数据被读空时停止读取FIFO中的数据
rd_en <= 1'b0;
//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//------------------------fifo_inst------------------------
fifo fifo_inst
(
.wr_clk (wr_clk ), //input wr_clk
.pi_data (pi_data ), //input [7:0] pi_data
.pi_flag (pi_flag ), //input pi_flag
.rd_clk (rd_clk ), //input rd_clk
.rd_en (rd_en ), //input rd_en
.po_data (po_data ), //output [15:0] po_data
.empty (empty ), //output empty
.full (full ), //output full
.rd_data_count(rd_data_count),//output [6:0] rd_data_count
.wr_data_count(wr_data_count) //output [7:0] wr_data_count
);
endmodule
如图所示为位置 1 放大后的波形,因为我们设置的是普通模式 FIFO,所以和 SCFIFO 的普通模式一样读出的数据存在一个时钟周期的潜伏期,另外还有几个点需要我们重点观察:
1、full 满标志信号的状态:我们可以看到当 pi_flag 为高且 pi_data 为 254 的 full 满标 志信号先拉高了,虽然我们是往里面写入数据 0~255,但是我们在配置 fifo 核时知道 fifo 的实际深度只有 255,所以这里到数据写到 254 个时,full 满信号就被拉高了。
2、wr_data_count、rd_data_count 信号的状态:我们可以看到 wr_data_count 信号计数到 255,而 rd_data_count 信号则计数到 127,这是因为输入时 8btit 的,输出是 16bit 的,刚好总数据量相等。同样 wr_data_count 信号也从 255 变成了 0、rd_data_count 信号从 127 变 成 0 的原因和 SCFIFO 中的情况一样,都是因为数据存储满了,FIFO 内部的计数器溢出所导致的。我们还可以发现读出的 16bit 数据,是输入的 8bit 数据低位在后高位在前的顺序, 如果记错了顺序在使用数据的时候会产生错误。
如图所示为位置 2 放大后的波形,我们发现,最后一个读取的数据为 16’hfcfd (实际深度为 127),同时 empty 空信号标志信号也拉高,表示 16bit 的读 FIFO 数据已空。
1、在单位时间内,写数据的总带宽一定要等于读数据的总带宽,否则会存在写满或读空的现象;
2、控制好、利用好 FIFO 的关键信号,如读写时钟、读写使能、空满标志信号;
3、根据实际的项目需求还要考虑需要多大的 FIFO,大了会浪费资源,小了则达不到需求。