24. 快速开发的法宝 — IP核

随着CPLD/FPGA的规模越来越大,设计越来越复杂(IC的复杂度以每年55%的速率递增,而设计能力每年仅提高21%),设计者的主要任务是在规定的时间周期内完成复杂的设计。为了解决这一问题,将一些在数字电路中常用,但比较复杂的功能块,如FIR滤波器、SDRAM控制器、PCI接口等设计成可修改参数的模块。这样可以避免重复劳动,大大减轻工程师的负担,提高开发效率,大大缩短产品上市时间。

本章节笔者将带领作者学习了解IP核的相关知识,掌握常用IP核的配置核使用方法,提高工程开发效率。

24.1. 理论学习

IP(Intellectual Property)即知识产权。美国Dataquest咨询公司将半导体产业的IP定义为“用于ASIC或FPGA中的预先设计好的电路功能模块”。简而言之,这里的IP即电路功能模块。IP核在数字电路中常用于比较复杂的功能模块(如FIFO、RAM、FIR滤波器、SDRAM控制 器、PCIE接口等)设计成参数可修改的模块,让其他用户可以直接调用这些模块。随着设计规模增大,复杂度提高,使用 IP 核可以提高开发效率,减少设计和调试时间,加速开发进程,降低开发成本,是业界的发展趋势。利用IP核设计电子系统,引用方便,修改基本元件的功能容易。具有复杂功能和商业价值的IP核一般具有知识产权,尽管IP核的市场活动还不规范,但是仍有许多集成电路设计公司从事IP核的设计、开发和营销工作。

IP核有三种不同的存在形式:HDL语言形式,网表形式、版图形式。分别对应我们常说的三类IP内核:软核、固核和硬核。这种分类主要依据产品交付的方式,而这三种IP内核实现方法也各具特色。

软核是用硬件描述语言的形式功能块的行为,并不涉及用什么电路和电路元件实现这些行为,软IP通常是以硬件描述语言HDL源文件的形式出现,应用开发过程与普通的HDL设计也十分相似,大多数应用于FPGA的IP内核均为软核,软核有助于用户调节参数并增强可复用性。软核通常以加密形式提供,这样实际的 RTL对用户 是不可见的,但布局和布线灵活。在这些加密的软核中,如果对内核进行了参数化,那么用户就可通过头文件或图形用户接口(GUI)方便地对参数进行操作。软IP的设计周期短,设计投入少。由于不涉及物理实现,为后续设计留有很大的发挥空间,增大了IP的灵活性和适应性。其主要缺点是在一定程度上使后续工序无法适应整体设 计,从而需要一定程度的软IP修正,在性能上也不可能获得全面的优化。由于软核是以源代码的形式提供,尽管源代码可以采用加密方法,但其知识产权保护问题不容忽视。

固核则是软核和硬核的折衷。固核是完成了综合的功能块,有较大的设计深度,以网表的形式交给客户使用。对于那些对时序要求严格的内核(如PCIE接口内核),可预布线特定信号或分配特定的布线资源,以满足时序要求。这些内核可归类为固核,由于内核是预先设计的代码模块,因此这有可能影响包含该内核的整体设计。由于内核 的建立时间、保持时间和握手信号都可能是固定的,因此其它电路的设计时都必须考虑与该内核进行正确地接口。如果内核具有固定布局或部分固定的布局,那么这还将影响其它电路的布局。

硬核是完成提供设计的最终阶段产品——掩膜(Mask),以经过完全的布局布线的网表形式提供,这种硬核既具有可预见性,同时还可以针对特定工艺或购买商进行功耗和尺寸上的优化。尽管硬核由于缺乏灵活性而可移植性差,但由于无须提供寄存器转移级(RTL)文件,因而更易于实现IP保护。比如一些FPGA芯片内置的AR M核就是硬核。

同一事物的利弊总是共存的,IP核在拥有以上众多好处的同时也有他的巨大缺点:

1、在跨平台时,IP核往往不通用,需要重新设计。IP核都是不全透明的,是每个FPGA开发厂商根据自己芯片适配的定制IP,所以如果你之前用的Xilinx的芯片,用了一个PLL,但是因为某些原因需要将代码移植到Altera平台上,那就必须要将PLL给重新替换掉,着增加了代码移植的复杂性。

2、IP核就是个黑匣子,是不透明的,我们往往看不到其核心代码。IP核都是各大FPGA厂商专门设计的,都会进行加密,内核代码都看不到,如果你使用的这个IP核万一出现了问题或者需要知道其内部结构针对具体的应用进行定制优化时,你是无法进行修改的。以上两个问题就很棘手,所以有些公司坚持所有的可综合设计都不使 用IP核,就是为了是所有的模块都能够掌控在在自己手里。

3、有些定制的IP核由于是不通用的,往往会有较高的收费,这也是一笔巨大的开销。所以IP核在能够加快我们开发周期的情况下也存在以上三种常见的问题,这就是需要我们权衡利弊,针对具体的需求来做具体的选择。

从软 IP 到硬 IP,设计灵活性降低,设计深度提高,设计成功率提高。Altera公司提供两类功能模块:免费的LPM宏功能模块(Megafunction/LPM)和需要授权使用的IP知识产权(MegaCore),两者的实现功能上有区别,使用方法相同。从复杂性的角度看,支持Altera系列FPGA的I P核既包括注入逻辑和算术运算等简单的IP核,也包括诸如数字信号处理器、以太网MAC、PCI/PCI Express接口等比较复杂的系统级构造模块。按其功能划分,Altera IP核主要有以下几类:

1、逻辑运算IP核。包括与、或、非、异或等基本逻辑运算单元和复用器、循环移位器、三态缓存器和解码器等相对复杂的逻辑运算模块。

2、数学运算IP核。Altera的数学运算IP核分为整数运算和浮点运算两大类:(1)整数运算IP核。包括LPM库(参数化模型IP库)提供的IP核和Altera指定功能的IP核。LPM库中的IP核有加法器、减法器、乘法器、除法器、比较器、计数器和绝对值计算器;Altera指定功能的IP核包括累加器、E CC编码器/解码器、乘加器、基于存储的常系数乘法器、乘累加器、乘加器、复数乘法器和整数平方根计算器等。(2)浮点运算IP核包括浮点数加法器、浮点数减法器、浮点数乘法器、浮点数除法器、浮点数平方根计算器、浮点数指数计算器、浮点数倒数计算器、浮点数平方根倒数计算器、浮点数自然对数计算器、浮点数正弦余弦计 算器和反正切计数器、浮点数矩阵求逆和乘法器以及浮点数绝对值计算器、比较器和转换器等。

3、存储器类IP核。包括移位奇存器、触发器、锁存器等简单的存储器IP核和较为复杂的ROM、RAM、FIFO和Flash存储器等模块。另外,Altera还提供了包括RAM初始化器和针对部分FPGA系列应用的FIFO分割器等辅助存储器设计IP核。

4、数字信号处理IP核。包括有限冲激响应滤波(FIR)编译器、级联积分梳状(CIC)滤波器编译器、数控振荡器(NCO)编译器以及快速傅里叶交换(FFT)等IP核,用于数字信号系统设计。

5、数字通信IP核包括RS码编通器、用于ひ积码译码的Viterbi译码器、循环冗余校验(CRC)编译器、8B/10B编/译码器以及SONET/SDH物理层IP核等。

6、图像处理IP核。主要是实现视频和图像处理系统中常用功能的IP核,具体有2D FIR滤波器和2D中值滤波器、α混合器、视频监视器、色度重采样器、图像裁剪器、视频输入和输出模块、颜色面板序列器、颜色空间转換器、同步器、视频帧读取和缓存器、γ校正器、隔行扫描和去隔行扫描器、缩放器、切换器、测试模板生成器和视频跟踪系统模块。

7、输入/输出P核。主要包括时钟控制器、锁相环(PLL)、低电压差分信号(LVDS)收发器、双数据速率(DDR)I/O、访问外部存储器的DQ-DQS I/O、I/O缓存器等。

8、芯片接口IP核。包括用于数字视频广播(DVB)的异步串行接口(AS1)、10/100/1000Mbps以太网接口、DDR和DDR2 SDRAM控制器、存储器物理层访问接口、PCI/PCI Exprsss编译器、RapidIo和用于数字电视信号传输的串行数字接口(SDI)等。

9、设计调试IP核。包括提供设计调试功能的SignalTap逻辑分析仪、串行和并行Flash加载器、系统内的源和探测模块以及虚拟JTAG等。

10、其他IP核。还有一些针对部分Altera系列FPGA应用的专用IP核,这里不再一一列举。

本章将重点介绍几个常用的IP,如锁相环(PLL)、FIFO、RAM、ROM等,详细说明各IP核的功能以及其使用方法,通过使用这些简单的IP核来掌握所有IP核的基本使用方法,起到抛砖引玉的效果。

IP核可以通过Quartus II软件集成的MegaWizard插件管理器、SOPC构造器或DSP构造器、Qsys设计系统例化,后两者仅支持部分IP核的例化和使用,非Altera的第三方IP核以网表文件方式提供。本章主要介绍在MegaWizard插件管理器定制和例化Altera IP核的方法。

MegaWizard插件管理器可以用于创建和修改包含定制IP核的设计文件,然后在设计文件中例化IP核。在MegaWizard插件管理中可以创建、定制和例化Altera IP核、参数化模型库(LPM)模块以及在Quartus II软件、EDA设计入口和综合工具使用的IP核。MegaWizard插件管理 器自动生成可以在VHDL设计文件(.vhd)中使用的组件声明文件(.cmp)以及可以在文本设计文件(.tfd)和Verilog设计文件(.v)中使用的AHDL包食文件(.inc)。还为AHDL设计、VHDL设计和Verilog HDL设计生成后级名分别为“_inst.tdf”、“_inst.vhd”和“_inst.v”的例化模板文件。此外,MegaWizard插件管理器还为Verilog HDL设计创建例化声明文件,文件后级为“_bb.v”。例化文件包含定制IP核的模型和端口声明。使用MegaWizard插件管理器可以指定IP核的不同选项,包括设置参数值和选择可选端口,还可以为第三方综合工具生成网表文件。如图 30‑1所示,我们点击“Tools”目录下的“MegaWizard Plug-In Manager”打开MegaWizard插件管理器。

IP002

图 30‑1 创建IP核步骤(一)

如图 30‑2所示的框,如果创建一个新的IP核选择第一个“Create a new Custom megafunction variation”,如果编译已存在的IP核选择第二个“Edit an existing custom megafunction variation”,如果复制已存在的IP核选择第三个“Copy an existing custom megafunction variation”,选择后点击“Next”。

IP003

图 30‑2 创建IP核步骤(二)

如图 30‑3所示,在打开的界面由以下几个主要部分组成:

框 1 提供了一个搜索框,可以通过 IP 核名称来搜索。

框 2 为 IP 核列表,Altera 提供的 IP 核都列在其中,每个文件夹代表一类,比如 Memory Compiler 里包含了与存储器有关的 IP 核。

框 3为工程指定的 FPGA 所属的器件系列,每个器件系列能提供的 IP 核种类与数量不尽相同,所以这个地方要保持与工程创建时选择的器件系列一致,避免出现添加本器件不支持的情况,这里默认是一致的。

框 4为添加 IP核时输出文件的语言类型,这个取决于工程具体设计所使用的语言,这里选择 Verilog。

框 5 是 IP 核输出文件的保存类型及 IP 核名称,路径一般在工程文件夹中。

IP004

图 30‑3 创建IP核步骤(三)

24.2. 实战演练

在理论知识小节,我们已经对IP核的相关理论知识做了详细介绍,接下来我们将带领读者学习掌握常用IP核的配置及使用方法。

24.3. ip核之pll

24.3.1. pll-ip核简介

PLL(Phase Locked Loop,即锁相环)是最常用的IP核之一,其性能强大,可以对输入到FPGA的时钟信号进行任意分频、倍频、相位调整、占空比调整,从而输出一个期望时钟,实际上,即使不想改变输入到 FPGA 时钟的任何参数,也常常会使用 PLL,因为经过 PLL 后的时钟在抖动(Jitter)方面的性能更好一些。Altera中的PLL是模拟锁相环,和数字锁相环不同的是模拟锁相环的优点是输出的稳定度高、相位连续可调、延时连续可调;缺点是当温度过高或者电磁辐射过强时会失锁(普通环境下不考虑该问题)。

如图 30‑4所示为PLL大体的一个结构模型示意图,我们可以看出这是一个闭环反馈系统,其工作原理和过程主要如下:

1、首先需要参考时钟(ref_clk)通过鉴频(FD)鉴相器(PD)和需要比较的时钟频率进行比较,我们以频率调整为例,如果参考时钟频率等于需要比较的时钟频率则鉴频鉴相器输出为0,如果参考时钟频率大于需要比较的时钟频率则鉴频鉴相器输出一个变大的成正比的值,如果参考时钟频率小于需要比较的时钟频率则鉴频鉴 相器输出一个变小的正比的值。

2、鉴频鉴相器的输出连接到环路滤波器(LF)上,用于控制噪声的带宽,滤掉高频噪声,使之稳定在一个值,起到将带有噪声的波形变平滑的作用。如果鉴频鉴相器之前的波形抖动比较大,经过环路滤波器后抖动就会变小,趋近于信号的平均值。

3、经过环路滤波器的输出连接到压控振荡器(VCO)上,环路滤波器输出的电压可以控制VCO输出频率的大小,环路滤波器输出的电压越大VCO输出的频率越高,然后将这个频率信号连接到鉴频鉴相器作为需要比较的频率。

如果ref_clk参考时钟输入的频率和需要比较的时钟频率不相等,该系统最终实现的就是让它们逐渐相等并稳定下来。如果ref_clk参考时钟的频率是50MHz,经过整个闭环反馈系统后,锁相环对外输出的时钟频率pll_out也是50MHz。

IP005

图 30‑4 PLL结构模型示意图

那倍频是如何实现的呢?如图 30‑5所示,倍频是在VCO后直接加一级分频器,我们知道ref_clk参考时钟输入的频率和需要比较的时钟频率经过闭环反馈系统后最终会保持频率相等,而在需要比较的时钟之前加入分频器,就会使进入分频器之前的信号频率为需要比较的时钟频率的倍数,VCO后输出的pll_out信号频 率就是ref_clk参考时钟倍频后的结果。

IP006

图 30‑5 倍频实现图

分频又是如何实现的呢?如图 30‑6所示,分频是在ref_clk参考时钟后加一级分频器,这样需要比较的时钟频率就始终和ref_clk参考时钟分频后的频率相等,在VCO后输出的pll_out信号就是ref_clk参考时钟分频后的结果。

IP007

图 30‑6 分频实现图

24.3.1.1. PLL IP核配置

如图 30‑7所示,在搜索栏中搜索“pll”就会显示和PLL相关的所有IP核,这里我们选择I/O目录下的“ALTPLL”。器件选择我们使用的CycloneIV E,语言选择Verilog HDL。然后就是选择IP核存放的路径,在Quartus中配置IP核之前最好先在工程目录下再新建一个单独用于存储I P核的文件夹,这里我们将文件夹的名字命名为ipcore_dir,也可自定义为其他的名字。然后是给IP核命名,后面实例化IP核的时候都是使用的该名字,这里所取的名字最好是和该IP核相关,因为本节我们主要讲解PLL,所以给该IP核取名为pll_ip。然后点击“Next”。

IP008

图 30‑7 PLL核的配置步骤(一)

如图 30‑8所示,在弹出的界面中我们正式开始对将生成的 PLL 进行配置,该界面的配置内容相当于告诉MegaWizard插件管理器你提供的“材料”和你大概需要一个什么样的PLL。其中:

框 为需要配置的选项,根据其固有标号一共分为 5 类:Parameter Settings(参数设置)、PLL Reconfiguration(PLL重新配置)、Output Clocks (输出时钟设置)、EDA和Summary(总体设置),每一类又分成很多项。PLL 涉及到的参数繁多,我们无需掌握每个参数的作用,只要了解了一些经常需要配置的重要参数即可。

框 中为该PLL IP核相关的官方手册,如果感觉教程中介绍的不够详细,可以直接查阅官方文档。

框为该芯片的速度等级,如果我们在选择芯片的时候就已经确定,这里不必再进行修改。

框为我们需要修改的是输入时钟频率,将其从默认的 100MHz 变为 50MHz,如框 3 所示,可以看到框 2 的结构中显示输入时钟频率也随之变为了 50MHz,其余不变,点击 Next 到下一项。

框为PLL的类型,这里因为器件的原因我们只能默认选择。

框为PLL的四种输出模式:

In normal mode(普通模式):仅在进入管脚时和到达芯片内部第一级寄存器时的相位相同,但是输出的时钟相位无法保证相同(此模式下最好不要用作于对外输出);

In source-synchronous compensation Mode(源同步补偿模式):使得进入管脚时的数据和上升沿的相位关系与到达芯片内部第一级寄存器时数据和上升沿的相位关系保持不变(通过调整内部的布局布线延时做到的,用于数据接口,特别是高速的情况下);

In zero delay buffer mode(零延时模式):对外输出的时钟和参考时钟同相位(更适合于时钟的外部输出);

With no compensation(无任何补偿模式):因为没有任何补偿,所以会由延时产生的相移。因为没有特殊要求所以我们选择默认的普通模式即可。

框7为选择哪个输出时钟将被补偿,因为没有相关需求所以我们直接选择默认设置。该界面所有的配置项都完成后,点击“Next”。

IP009

图 30‑8 PLL核的配置步骤(二)

第一类Parameter Settings(参数设置):如图 30‑9所示,该界面主要是选择PLL IP核的输入输出端口设置,该部分就像我们在设计项目时模块的设计考虑输入输出信号有哪些。这里我们主要讲两个点:

框1是为 PLL IP核创建异步复位管脚,名为 areset,用来对PLL IP核进行异步复位。

框2是为PLL IP核创建锁定管脚,名为locked,用来检测PLL IP核是否已经锁定,只有该信号为高时输出的时钟才是稳定的。

对于一般的应用而言,可以不用添加这两个管脚,这里我们只添加上“locked”,以便在仿真时能够体现PLL的工作特点。完成后点击“Next”。

IP010

图 30‑9 PLL核的配置步骤(三)

如图 30‑10所示,该界面主要是配置扩展频谱时钟和带宽可编程功能,属于PLL IP核的高级属性。这里我们不使用,保持默认即可,直接点击“Next”。

IP011

图 30‑10 PLL核的配置步骤(四)

如图 30‑11所示,该界面用于配置时钟切换,也是PLL IP核的高级属性之一。这里我们不使用,保持默认,直接点击“Next”。

IP012

图 30‑11 PLL核的配置步骤(五)

第二类PLL Reconfiguration(PLL重新配置):如图 30‑12所示,该界面用于PLL动态重配置和动态相位重配置,同样属于PLL IP核的高级属性之。这里我们不使用,保持默认,直接点击“Next”。

IP013

图 30‑12 PLL核的配置步骤(六)

第三类Output Clocks (输出时钟设置):如图 30‑13所示为PLL IP核输出时钟的参数配置界面,该界面是对输出时钟c0进行配置,其中:

框1选项勾选是否使用当前的输出时钟,每个PLL最多有5个输出时钟,分别为c0、c1、c2、c3、c4,其配置界面都相同。我们可以根据需要选择输出时钟的数量,c0默认是选中的,保持该选项不变。

框2是配置输出时钟的频率,有两种方式:直接输入频率值(未选中的选项)和输入参数配置频率(当前选中的选项)。对于直接输入频率值的方式,直接在“Requested Settings”中输入想得到的输出频率即可;对于输入参数配置频率,需要输入倍频因子(Clock multiplication factor)和分频因子(Clock division factor),最后的输出频率计算方式为:输出频率=输入频率 *倍频因子/分频因子。另外需要注意的是:PLL IP核的输出并非随心所欲的,受输入频率等因素影响,每个PLL IP核的输出频率会有一定的范围限制。

框3为设置输出时钟相对输入时钟的相移,默认设置是0。

框4为输出时钟的占空比,默认设置是50%。

完成c0的参数配置后点击“Next”就可以进入到c1、c2、c3、c4的参数配置,直到进入第四类EDA的配置界面为止。

IP014

图 30‑13 PLL核的配置步骤(七)

为了让大家能够看到PLL IP核每种参数的设置效果,下面我们分别将c0、c1、c2、c3的输出设置为输入时钟的2倍频、输入时钟的2分频、输入时钟相移90°、输入时钟占空比变为20%,然后通过仿真进行对比。如图 30‑14所示,为输出时钟c0的配置界面,要将其配置为输入时钟的2倍频,也就是让c0输出100MHz的时钟。我们使用直接输入频率值的方式进行配置,选中“Enter output clock frequency”,在“Requested Settings”栏中输入我们要c0输出的频率100MHz即可,可以看到“Actual Settings”栏也显示的是100,说明该配置的输出频率值是可以实现的。其余的设置保持不变,然后点击“Next”。

IP015

图 30‑14 PLL核的配置步骤(八)

如图 30‑15所示,为输出时钟c1的配置界面,要将其配置为输入时钟的2分频,也就是让c1输出25MHz的时钟。我们使用输入参数配置频率的方式进行配置,选中“Enter output clock parameters”,在“Requested Settings”栏中将倍频因子和分频因子分别设置为1和2即可。其余的设置保持不变,然后点击“Next”。

IP016

图 30‑15 PLL核的配置步骤(九)

如图 30‑16所示,为输出时钟c2的配置界面,要将其配置为输入时钟相移90°。直接在“Requested Settings”栏中将值改为90即可。其余的设置保持不变,然后点击“Next”。

IP017

图 30‑16 PLL核的配置步骤(十)

如图 30‑17所示,为输出时钟c3的配置界面,要将输入时钟占空比配置为20%,也就是输出时钟c3相对于输入时钟sys_clk的周期不变,而高电平的时间占总周期时间的20%,即在一个周期时间内高电平与低电平的时间比为1:4。直接在“Requested Settings”栏中将值改为20即可。其余的设置保持不变,然后点击“Next”。

IP018

图 30‑17 PLL核的配置步骤(十一)

如图 30‑18所示,为输出时钟c4的配置界面,我们不使用,所以不用勾选“Use this clock”,直接点击“Next”。

IP019

图 30‑18 PLL核的配置步骤(十二)

第四类EDA:如图 30‑19所示,该界面没有什么要配置的参数,也不需要我们修改,可以直接点击“Next”。但是有一点要提示大家注意,这里显示了我们在仿真PLL IP核时所需要的Altera的仿真库,我们在使用NativeLink的方式联合ModelSim的仿真时不需要关心这个仿真库,设置好Nati veLink后系统会自动帮我们添加这个仿真库的,但如果使用ModelSim单独进行仿真时不添加该仿真库就会报错,而这里就恰恰提示了我们需要添加哪些库才能够满足ModelSim的单独仿真,到时候我们再具体说明如何解决该问题。

IP020

图 30‑19 PLL核的配置步骤(十三)

第五类Summary(总体设置):如图 30‑20所示,该界面显示的是配置好PLL IP核后我们要输出的文件,其中“pll_ip.v”和“pll_ip.ppf”是默认输出的,不可以取消。此外我们再将“pll_ip_inst.v”这个实例化模板文件添加上,方便我们实例化时使用,其余的文件都不要勾选。

到此为止我们的PLL IP核的配置就全部完成了,再检查下我们的配置,如果有问题可以点击“Back”返回之前的配置界面进行修改,如果确认无误后可以点击“Finish”退出配置界面,如果后面在仿真时发现PLL IP核的配置有问题也可以再进行修改。

IP021

图 30‑20 PLL核的配置步骤(十四)

第一次使用IP核功能时会弹出如图 30‑21所示的对话框,提示我们是否每次都自动将IP核的文件添加到工程中,这里我们选择打勾同意,然后点击“Yes”确定。如图 30‑22所示,如果我们不选择打勾就退出也可以手动在“Project Naigator”的“File”中像添加普通文件一样添加IP核所需要的“.qip”文件即可,其余和IP核的文件不要添加进来。

IP022

图 30‑21 PLL核的配置步骤(十五)

IP023

图 30‑22 PLL核的配置步骤(十六)

24.3.1.2. PLL IP核调用

如图 30‑23所示,我们打开工程目录下的ipcore_dir文件夹,可以看到几个和PLL IP核相关的文件。如图 30‑24所示,我们在调用PLL IP核的时候可以直接使用给好的实例化模板,打开“pll_ip_inst.v”文件,然后在此基础上修改即可。如图 30‑25所示,实例化PLL IP核时也可以复制修改“pll.v”文件中模块的端口列表。

IP024

图 30‑23 PLL核实例化步骤(一)

IP025

图 30‑24 PLL核实例化步骤(二)

IP026

图 30‑25 PLL核实例化步骤(三)

下面是实例化PLL IP核并应用的代码编写。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module pll
(
input wire sys_clk , //系统时钟50MHz

output wire clk_mul_2 , //系统时钟经过2倍频后的时钟
output wire clk_div_2 , //系统时钟经过2分频后的时钟
output wire clk_phase_90 , //系统时钟经过相移90°后的时钟
output wire clk_ducle_20 , //系统时钟变为占空比为20%的时钟
output wire locked //检测锁相环是否已经锁定,
 //只有该信号为高时输出的时钟才是稳定的
 );

 ////
 //\* Instantiation \//
 ////

 //------------------------pll_ip_inst------------------------
 pll_ip pll_ip_inst
 (
 .inclk0 (sys_clk ), //input inclk0

 .c0 (clk_mul_2 ), //output c0
 .c1 (clk_div_2 ), //output c1
 .c2 (clk_phase_90 ), //output c2
 .c3 (clk_ducle_20 ), //output c3
 .locked (locked ) //output locked
 );

 endmodule

代码编写完成后我们进行综合,然后打开顶层文件显示关系,如图 30‑26所示,可以看到“pll_ip”下已经有一个pll的子模块,我们双击这个模块就可以对该PLL IP核的配置进行修改。

IP027

图 30‑26 PLL 核子模块

根据上面RTL代码综合出的RTL视图如图 30‑27所示。

IP028

图 30‑27 RTL视图

24.3.1.3. PLL IP核仿真

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
\`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

将编写好仿真文件添加到工程中,最好NativeLink的设置后打开ModelSim执行仿真。仿真出来的波形如图 30‑28所示,我们让仿真运行了500ns,可以看到所有的输出时钟都是在locked信号为高电平之后开始有效,因为没有复位,所以locked信号为高之前输出时钟都是不定态。我们分别将参考线锁定到输入时钟sys_clk一个周期的开始和结束位置,显示的频率为50MHz。

IP029

图 30‑28 仿真波形图(一)

图 30‑29为输出时钟c0的波形,分别将参考线锁定到输出时钟c0一个周期的开始和结束位置,显示的频率为100MHz。

IP030

图 30‑29 仿真波形图(二)

图 30‑30为输出时钟c1的波形,分别将参考线锁定到输出时钟c1一个周期的开始和结束位置,显示的频率为25MHz。

IP031

图 30‑30 仿真波形图(三)

图 30‑31为输出时钟c2的波形,分别将参考线锁定到输入时钟sys_clk一个周期的开始位置和输出时钟c2一个周期的开始位置,并设置为显示时间,结果为5ns,也就是输出时钟c2相对于输入时钟sys_clk相移了90°。

IP032

图 30‑31 仿真波形图(四)

图 30‑32为输出时钟c3的波形,分别测量输入时钟c3一个周期内的高电平时间和低电平时间,发现高电平时间为4ns,低电平时间为16ns,也就是输出时钟c3相对于输入时钟sys_clk的周期不变,而高电平的时间占总周期时间的20%,即在一个周期时间内高电平与低电平的时间比为1:4。

IP033

图 30‑32 仿真波形图(五)

以上所有仿真验证结果均和配置的PLL IP核的结果一致,验证正确。对于使用IP核的设计,我们发现在实例化IP核的时候不必关心IP核的内部结构和代码是怎样设计的,只需要配置好其中可以配置的参数、端口信号、了解输入什么、输出什么就可以正确的使用IP 核了。这样我们就只需要关心自己的核心设计即可,极大的加快了我们的整体设计进程。

24.4. ip核之rom

24.4.1. rom-ip核简介

本小节为大家介绍一种较为常用的存储类IP核——ROM的使用方法。ROM是只读存储器(Read-Only Memory)的简称,是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除,且资料不会因为电源关闭而消失。而事实上在 FPGA 中通过 IP 核生成的 ROM 或 RAM(RAM将在下一节为大家讲解)调用的都是 FPGA 内部的 RAM 资源,掉电内容都会丢失(这也很容易解释,FPGA芯片内部本来就没有掉电非易失存储器单元)。用 IP 核生成的 ROM 模块只是提前添加了数据文件(.mif 或.hex 格式),在 FPGA 运行时通过数据文件给 ROM 模块初始化,才使得 ROM 模块像个“真正”的掉电非易失存储器;也正是这个原因,ROM 模块的内容必须提前在数据文件中写死,无法在电路中修改。

Altera推出的ROM IP核分为两种类型:单端口ROM和双端口ROM。对于单端口ROM提供一个读地址端口和一个读数据端口,只能进行读操作;双端口ROM与单端口ROM类似,区别是其提供两个读地址端口和两个读数据端口,基本上可以看做两个单口RAM拼接而成。下面给出ROM不同配置模式存储器的接口信号图,如图 30‑33、图 30‑34所示。

IP034

图 30‑33 单端口ROM接口信号

IP035

图 30‑34 双端口ROM接口信号

上图中的接口信号我们并不是全部需要用到,因为我们配置时,有些信号是可以不创建的。所以很多接口信号我们可以不用管,只需管我们需要用的信号即可,而什么信号是我们需要用到的呢?这在我们调用完IP核后,是可以生成其例化模块的,到时候就可以看到我们需要控制的信号了。

24.4.1.1. ROM IP核配置

mif格式文件的制作

ROM作为只读存储器,在进行IP核设置时需要指定初始化文件,即写入存储器中的图片数据,图片要以规定的格式才能正确写入ROM,这种格式就是mif文件。mif是Quartus规定的一种文件格式,文件格式示意图,具体见图 30‑35。

IP036

图 30‑35 mif文件格式示意图

我们使用Quartus II制作一个mif格式文件,首先在Quartus II新建一个工程,新建工程后,选择 Quartus II 的主界面选择菜单栏的 File→New,如图 30‑36所示:

IP037

图 30‑36 制作mif格式文件步骤(一)

单击完File→New后,出现如下界面:

IP038

图 30‑37 制作mif格式文件步骤(二)

如图 30‑37所示,我们选择“Memory Initialization File”后,点击“OK”,来到下一个界面:

IP039

图 30‑38 制作mif格式文件步骤(三)

如图 30‑38所示,是设置数据容量,框是设置数据位宽,也就是设置ROM初始化存储的数据容量及数据位宽。这里我们设置容量为256,位宽为8bit,点击“OK”完成设置,来到下一界面:

IP040

图 30‑39 制作mif格式文件步骤(四)

我们右键单击框或框的行或列界面,可以改变行/列的进制显示,如下图所示:

IP041

图 30‑40 制作mif格式文件步骤(五)

如图 30‑40所示,我们保持显示10进制即可。接下来我们需要在配置文件中填充我们想要的数据,我们可以手动输入进行填充,也可以从别的文本中复制、粘贴进行填充,还可以利用软件自带的功能,直接推译出所有数据。这里我们为大家讲解一下这种软件自带的填充功能。如下图所示,右键点击任意单元格,弹出如下界面:

IP042

图 30‑41 制作mif格式文件步骤(六)

如图 30‑41所示,点击选择“Custom Fill Cells…”出现如下界面:

IP043

图 30‑42 制作mif格式文件步骤(七)

如图 30‑42所示,其中“Starting address”是设置起始地址,“Ending address”是设置截止地址,由于我们设置的文件容量是256,所以这里我们设置起始地址为0,截止地址为255 。接下是设置要填充的具体数据,在同一页面进行设置,如下图所示:

IP044

图 30‑43 制作mif格式文件步骤(八)

如图 30‑43所示,我们设置数据从0开始递增,增量为1,这样就于我们生成了数据0~255,共256个数,,其中最大255也不会超过我们设置的数据位宽8bit。设置完之后点击“OK”进入下一界面:

IP045

图 30‑44 制作mif格式文件步骤(九)

如图 30‑44所示,可以看到我们所生成的0~255数据了,接下来我们点击“Save”保存即可,如下图所示:

IP046

图 30‑45制作mif格式文件步骤(十)

IP047

图 30‑46 制作mif格式文件步骤(十一)

大家可自由选择文件的保存位置,我们一般按默认位置保存即可。

单端口ROM的配置

ROM的初始化文件创建好了之后,我们就可以开始ROM IP核的创建。在IP核简介小节已经介绍了如何进入IP核配置界面,按其中步骤我们先进入IP核配置界面,如下图所示:

IP048

图 30‑47 单端口ROM的配置步骤(一)

如图 30‑47所示:

框中可输入IP核的名称进行搜索,这里我们输入rom后,会自动出现如框所示的IP核信息。

框中“ROM:1-PORT”是单端口ROM;“ROM:2-PORT”是双端口ROM;这里我们选择单端口ROM来为大家先介绍单端口ROM的调用步骤。

框是IP核保存位置的选择,这里我们在工程目录下新建一个ip_core文件夹,将IP核保存在该文件夹下并命名为rom_256x8(rom是我们调用的IP核,256是调用的IP核容量,8是调用的IP核数据位宽。这里这样命名是为了方便识别我们创建的IP核类型及资源量,我们制作的数据文件就是容量为256,数 据位宽为8bit)。

选择完之后点击Next进入下一界面:

IP049

图 30‑48 单端口ROM的配置步骤(二)

如图 30‑48所示:

框是设置输出数据端口的位宽,由于我们生成的数据文件的数据位宽是8bit,所以这里我们设置数据位宽8bit。

框是设置存储器容量的大小,这里我们设置存储容量为256个数据。即我们设置的ROM的最大存储量为256个8bit数据。

框是存储单元类型的选择,这决定着我们使用FPGA内部的哪种存储器资源生成ROM模块,这里我们保持Auto(软件自动选择)即可。

框中是选择使用的时钟模式,可选择单时钟或双时钟。选择单时钟时用一个时钟控制存储块的所有寄存器,选择双时钟时输入时钟控制地址寄存器,输出时钟控制数据输出寄存器。ROM模式没有写使能、字节使能和数据输入寄存器。这里我们选择默认选项Single clock(单时钟)。

设置完之后点击Next进入下一界面:

IP050

图 30‑49 单端口ROM的配置步骤(三)

如图 30‑49所示:

框是选择是否输出“q”寄存器。这里我们把输出端口的寄存器去掉(如果不去掉的话,就会使输出延迟一拍。如果没有特别的需求的话我们是不需要延迟这一拍的,所以这里我们把它去掉)。

框是选择是否创建“aclr”异步复位信号以及是否创建“rden”读使能信号,大家可根据实际的设计需求进行勾选,这里我们不勾选。

设置完之后点击Next进入下一界面:

IP051

图 30‑50 单端口ROM的配置步骤(四)

如图 30‑50所示,该页面是加载数据文件,我们点击“Browse…”来添加我们之前生成的数据文件,点击后出现如下界面:

IP052

图 30‑51 单端口ROM的配置步骤(五)

如图 30‑51所示,刚进入该页面,界面上并没有我们的mif数据文件,那是因为我们搜索的数据类型是hex格式文件,我们需将搜索的数据类型切换为mif格式文件,如上图所示,切换完后出现如下界面:

IP053

图 30‑52 单端口ROM的配置步骤(六)

如图 30‑52所示,我们选择我们前面生成好的mif文件,点击“Open”后进入下一界面:

IP054

图 30‑53 单端口ROM的配置步骤(七)

如图 30‑53所示,“File name”中已经加入了我们的mif文件,点击Next进入下一界面:

IP055

图 30‑54 单端口ROM的配置步骤(八)

如图 30‑54所示,该界面没有什么要配置的参数,但显示了我们在仿真ROM IP核时所需要的Altera仿真库,这里提示了我们单独使用第三方仿真工具时需要添加名为“altera_mf”的库。这里保持默认,直接点击“Next”。

IP056

图 30‑55 单端口ROM的配置步骤(九)

如图 30‑55所示,是ROM输出的文件,除了灰色必选文件,默认还勾选上了 rom_256x8_bb.v,这里我们去掉 rom_256x8_bb.v文件,加入rom_256x8_inst.v(例化模板文件)即可。最后点击“Finish”完成整个IP核的创建。接下来Quartus II软件会在我们创建的IP核文件目录下生成ROM IP文件,若第一次使用IP核功能时会跳出一个界面,如下图所示:

IP057

图 30‑56 单端口ROM的配置步骤(十)

如图 30‑56所示的对话框,提示我们是否每次都自动将IP核的文件添加到工程中,这里我们选择打勾同意,然后点击“Yes”确定。如果我们不选择打勾就退出也可以手动在“Project Naigator”的“File”中像添加普通文件一样添加IP核所需要的“.qip”文件即可,其余和IP核的文件不要添加进来。

双端口ROM的配置

单端口ROM的配置步骤讲解完之后,为大家介绍一下双端口ROM的配置步骤。

IP058

图 30‑57 双端口ROM的配置步骤(一)

如图 30‑57所示,该步骤与单端口ROM步骤几乎是一样的,不同的是我们需要选择“ROM:2-PORT”来调用双口ROM。点击Next进入下一步:

IP059

图 30‑58 双端口ROM的配置步骤(二)

如图 30‑58所示:

该界面和单端口ROM的界面就有很明显的不同了,框中可以看到双口ROM有两组地址线和两组输出数据线。

框是设置定义ROM存储器大小的方式,“As a number of words”是按字数确定,“AS a number of bits”是按比特数确定。我们默认选择按字数确定。

框是选择ROM的容量,大家可根据设计需求进行选择,这里我们选择256个数据(注意:选择的容量需大于我们需要写入的数据文件的数据量)。

框是设置不同端口的位宽是否相同,可以选着打开或者关闭,默认是关闭,即使用相同位宽。

框是设置数据位宽,这里的数据位宽设置为我们写入数据文件的数据位宽相同即可,这里我们选择8bit。

框是存储单元类型的选择,按默认选择即可。

该页面设置完之后点击Next进入下一界面:

IP060

图 30‑59 双端口ROM的配置步骤(三)

如图 30‑59所示:

框中是对时钟的选择。

Single clock(单时钟):使用一个时钟控制。

Dual clock:use separate “input” and “output”clocks(双时钟:使用单独的输入时钟和输出时钟):输入和输出时钟分别控制存储块的数据输入和输出的相关寄存器。

Dual clock:use separate clocks for A and B ports(双时钟:端口A和端口B使用不同的独立时钟):时钟A控制端口A的所有寄存器,时钟B控制端口B的所有寄存器。每个端口也支持独立的时钟使能。大家可根据自己的需求进行选择。

框是选择是否创建‘rden_a’和‘rden_b’读使能信号,同样大家可根据设计需求进行选择。

点击Next进入下一界面:

IP061

图 30‑60 双端口ROM的配置步骤(四)

如图 30‑60所示:

框是选择是否输出‘q_a’和‘q_b’寄存器。这里把输出端口的寄存器去掉(如果不去掉的话,就会使输出延迟一拍。如果没有特别的需求的话我们是不需要延迟这一拍的,所以这里我们把它去掉)。

框是选择是否为时钟信号创建使能信号,大家可根据实际的设计需求进行创建。

框是选择是否创建“aclr”异步复位信号,大家可根据实际的设计需求进行勾选。

设置完之后点击Next进入下一界面:

IP062

图 30‑61 双端口ROM的配置步骤(五)

如图 30‑61所示,该页面是配置初始化文件,文件的调用步骤我们在单端口ROM的配置步骤中已有详细的讲解,在此不再重复讲解了,调用完之后我们点击Next进入下一界面。

IP063

图 30‑62 双端口ROM的配置步骤(六)

如图 30‑62所示,该页面与单端口ROM的设置一样,直接点击Next进入下一界面:

IP064

图 30‑63 双端口ROM的配置步骤(七)

如图 30‑63所示,该页面与单口ROM是一样的,在单口ROM中已有讲解,这里我们勾选“rom2_inst.v”后点击Next进入下一页面:

IP057

图 30‑64 双端口ROM的配置步骤(八)

第一次使用IP核功能时会弹出如图 30‑64所示的对话框,提示我们是否每次都自动将IP核的文件添加到工程中,这里我们选择打勾同意,然后点击“Yes”确定。如果我们不选择打勾就退出也可以手动在“Project Naigator”的“File”中像添加普通文件一样添加IP核所需要的“.qip”文件即可,其余和IP核的文件不要添加进来。

到这里两种类型的ROM的配置步骤我们就讲解完毕了,下面我们根据示例来为大家介绍ROM的使用。

24.4.1.2. ROM IP核的使用

实验目标

我们可以结合我们之前讲的数码管,将ROM内的数据读取出来显示在数码管上。我们可以设计这样一个实验:首先我们ROM的初始化数据是0~255,也就是存入数据0~255。然后每隔0.2s我们从0地址开始往下读取数据显示在数码管上,我们再利用两个按键信号来读取指定地址的数据,每按一个按键就读取一个地址的数据 显示在数码管上。再次按下按键后,以当前地址继续以0.2s的时间间隔往下读取数据并显示出来。

整体说明

根据我们设计的实验可知,首先我们需要一个数码管显示模块,其次是按键的消抖模块,再加上我们调用的ROM IP核,最后再编写一个ROM控制模块即可以完成我们的实验要求了,模块框图如图 30‑65所示。

IP065

图 30‑65 工程整体框图

由上图可以看到,该工程共分5个模块,其中按键消抖是一个模块,只是使用了两次而已。各模块简介见表格 30‑1。

表格 30‑1 工程模块简介

模块名称

功能描述

key_fifter

按键消抖模块

rom_ctrl

ROM控制模块

rom_256x8

ROM IP核模块

seg_595_dynamic

数码管显示模块

rom

顶层模块

下面分模块为大家讲解。

按键消抖模块

在《按键消抖模块的设计与验证》章节中我们对按键消抖模块已经有了详细的讲解,这里我们直接调用即可。

数码管显示模块

在“数码管的动态显示”章节中我们已经对数码管动态显示模块做了详细的讲解,在此就不再讲解了,在这里我们直接调用这个模块即可。需要注意的是该模块下还有子模块我们没有在整体框图中画出,该模块框图如图 30‑66所示。

IP066

图 30‑66 数码管显示模块

ROM IP核模块

这里我们调用一个单端口ROM,完全按单端口ROM的配置小节的配置步骤配置来生成一个单端口ROM IP核即可。按步骤调用生成完之后打开工程目录下的ROM IP核保存位置,如图 30‑67所示。

IP067

图 30‑67 ROM例化模板文件位置

如图 30‑67所示,我们打开框中例化模板文件,打开后如下图所示:

IP068

图 30‑68 ROM例化模板文件代码

如图 30‑68所示,该文件的内容就是ROM的例化模块,我们将其复制粘贴到顶层模块,将各对应信号连接起来即可。

ROM 控制模块

从ROM的数据手册可知,读操作是在时钟的上升沿触发的,而我们在调用ROM时是没有生成读使能的,所以在读时钟上升沿我们只要给相应的地址就能在时钟的上升沿读出该地址内的数据了。在该模块我们只需要控制生成读地址即可。

模块框图

IP069

图 30‑69 ROM控制模块框图

该模块的各个信号的简介如表格 30‑2所示。

表格 30‑2 ROM控制模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

Input

时钟信号,频率50MHz

sys_rst_n

1bit

Input

复位信号,低电平有效

key1_flag

1bit

Input

按键1消抖信号

key2_flag

1bit

Input

按键2消抖信号

addr

8bit

Output

读ROM地址

这里我们选用系统时钟作为ROM的读取时钟,复位也选用系统复位。两个按键消抖信号由按键消抖模块传来,我们通过这几个输入信号来产生我们读取的地址进行读取数据。下面通过波形图去了解其具体的控制时序。

波形图绘制

IP070

图 30‑70 ROM控制模块波形图

如图 30‑70所示,当我们在没有按下任何按键时,我们是依次读取ROM内存储的数据。我们使用的系统时钟是50MHz,一个时钟是20ns,如果我们一个时钟读取的地址就变化一次,也就是说我们读取的数据在一个时钟(20ns)变化一次再输出数据给数码管显示,这样的话我们显示的数据就是20ns变化一次,这是我 们肉眼难以捕捉的。所以在这里我们设计让地址每0.2s变化一次,这样读出的数据就是0.2s变化一次,我们肉眼就能很清晰的看到我们读出的数据变化了。所以这里我们先生成一个0.2s的计数器。

cnt_200ms:0.2s(200ms)计数器,每来一个时钟上升沿计数器加一,当加到最大值CNT_MAX(9999999)时,计数器清零开始下一轮的计数。0~9999999即10000000个系统时钟,即为0.2s(10000000*20ns=0.2s)。

当0.2s计数器产生完之后,每检测到计数器计到最大值时我们就让地址加一,这样我们就能每0.2s依次读出ROM里的数据了。

这里需要说明的是ROM并不是只能从0地址开始读取,它能读取指定的任意地址,为了验证这个功能,我们加入两个按键信号,每按一个按键就读出一个指定地址的数据,再次按下就又沿当前地址顺序读取。

key1_flag、key2_flag:按键消抖后的标志信号,具体的产生波形图大家可参考《按键消抖模块的设计与验证》章节进行了解。按键消抖后的标志信号只拉高了一个系统时钟,而我们按下按键后是要一直读取一个信号的,所以这么我们要用这个按键消抖后的标志信号去生成一个地址读取的标志信号。

addr_flag1、addr_flag2:读地址的标志信号。当按下按键1/按键2(key1_flag/key2_flag=1)时,让读地址的标志信号为高,再次按下时让读地址的标志信号为低,我们使用一个取反操作即可完成。当按键1按下后,addr_flag1为高读取地址99的数据;当按下按键2后,ad dr_flag2为高读取地址199的数据(地址大家可任意去取,只要在ROM的地址范围即可)。这里需要注意的是因为每次我们只能读取一个地址的信号,所以我们在拉高一个地址标志信号时要将另一个地址标志信号置为0,这样读取的地址才不会有冲突。

代码编写

参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 30‑1。

代码清单 30‑1 ROM控制模块参考代码(rom_ctrl.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
module rom_ctrl
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低有效
input wire key1_flag , //按键1消抖后有效信号
input wire key2_flag , //按键2消抖后有效信号

output reg [7:0] addr //输出读ROM地址

 );

 ////
 //\* Parameter and Internal Signal \//
 ////

 //parameter define
 parameter CNT_MAX = 9_999_999; //0.2s计数器最大值

 //reg define
 reg addr_flag1 ; //特定地址1标志信号
 reg addr_flag2 ; //特定地址2标志信号
 reg [23:0] cnt_200ms ; //0.2s计数器

 ////
 //\* Main Code \//
 ////

 //产生特定地址1标志信号
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 addr_flag1 <= 1'b0;
 else if(key2_flag == 1'b1)
 addr_flag1 <= 1'b0;
 else if(key1_flag == 1'b1)
 addr_flag1 <= ~addr_flag1;

 //产生特定地址2标志信号
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 addr_flag2 <= 1'b0;
 else if(key1_flag == 1'b1)
 addr_flag2 <= 1'b0;
 else if(key2_flag == 1'b1)
 addr_flag2 <= ~addr_flag2;

 //0.2s循环计数
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_200ms <= 24'd0;
 else if(cnt_200ms == CNT_MAX)
 cnt_200ms <= 24'd0;
 else
 cnt_200ms <= cnt_200ms + 1'b1;

 //让地址从0~255循环,其中两个按键控制两个特定地址的跳转
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 addr <= 8'd0;
 else if(addr == 8'd255 && cnt_200ms == CNT_MAX)
 addr <= 8'd0;
 else if(addr_flag1 == 1'b1)
 addr <= 8'd99;
 else if(addr_flag2 == 1'b1)
 addr <= 8'd199;
 else if(cnt_200ms == CNT_MAX)
 addr <= addr + 1'b1;

 endmodule

模块参考代码是参照绘制波形图进行编写的,在波形图绘制小节已经对模块各信号有了详细的说明,这里就不再过多叙述。

顶层模块

模块框图

rom顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,模块框图如图 30‑71所示。

IP071

图 30‑71 顶层模块框图

模块各输入输出信号描述如表格 30‑3所示。

表格 30‑3 rom顶层模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

input

系统时钟,50MHz

sys_rst_n

1bit

input

复位信号

key

2bit

input

按键信号

stcp

1bit

output

输出数据存储器时钟

shcp

1bit

output

移位寄存器的时钟输入

ds

1bit

output

串行数据输入

oe

1bit

output

输出使能信号

代码编写

rom顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,代码编写较为容易,无需波形图的绘制。顶层参考代码,具体见代码清单 30‑2。

代码清单 30‑2 rom顶层模块参考代码(rom.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
module rom
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire [1:0] key , //输入按键信号

output wire stcp , //输出数据存储器时钟
output wire shcp , //移位寄存器的时钟输入
output wire ds , //串行数据输入
 output wire oe //输出使能信号

 );

 ////
 //\* Parameter and Internal Signal \//
 ////

 //wire define
 wire [7:0] addr ; //地址线
 wire [7:0] rom_data ; //读出ROM数据
 wire key1_flag ; //按键1消抖信号
 wire key2_flag ; //按键2消抖信号

 ////
 //\* Instantiation \//
 ////

 //----------------rom_ctrl_inst----------------
 rom_ctrl rom_ctrl_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低有效
 .key1_flag (key1_flag ), //按键1消抖后有效信号
 .key2_flag (key2_flag ), //按键2消抖后有效信号

 .addr (addr ) //输出读ROM地址
 );

 //----------------key1_filter_inst--------------
 key_filter key1_filter_inst
 (
 .sys_clk (sys_clk ), //系统时钟50MHz
 .sys_rst_n (sys_rst_n ), //全局复位
 .key_in (key[0] ), //按键输入信号

 .key_flag (key1_flag ) //key_flag为1时表示消抖后检测到按键被按下
 //key_flag为0时表示没有检测到按键被按下
 );

 //----------------key2_filter_inst--------------
 key_filter key2_filter_inst
 (
 .sys_clk (sys_clk ), //系统时钟50MHz
 .sys_rst_n (sys_rst_n ), //全局复位
 .key_in (key[1] ), //按键输入信号

 .key_flag (key2_flag ) //key_flag为1时表示消抖后检测到按键被按下
 //key_flag为0时表示没有检测到按键被按下
 );

 //--------------seg_595_dynamic_inst-------------
 seg_595_dynamic seg_595_dynamic_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低有效
 .data ({12'd0,rom_data}), //数码管要显示的值
 .point (0 ), //小数点显示,高电平有效
 .seg_en (1'b1 ), //数码管使能信号,高电平有效
 .sign (0 ), //符号位,高电平显示负号

 .stcp (stcp ), //输出数据存储寄时钟
 .shcp (shcp ), //移位寄存器的时钟输入
 .ds (ds ), //串行数据输入
 .oe (oe ) //输出使能信号

 );

 //----------------rom_256x8_inst---------------
 rom_256x8 rom_256x8_inst
 (
 .address (addr ),
 .clock (sys_clk ),
 .q (rom_data )
 );

 endmodule

因为我们需要用到两个按键信号,所以我们需要例化两次按键消抖模块,产生两个按键有效信号。

RTL视图

顶层模块介绍完毕,使用Quartus II软件对实验工程进行编译,工程通过编译后查看实验工程RTL视图。工程RTL视图,具体见图 30‑72。由图可知,实验工程的RTL视图与实验整体框图相同,各信号线均已正确连接。

IP072

图 30‑72 RTL视图

24.4.1.3. ROM IP核仿真

仿真代码编写

顶层模块参考代码介绍完毕,开始对顶层模块进行仿真,对顶层模块的仿真就是对实验工程的整体仿真。顶层模块仿真参考代码,具体见代码清单 30‑3。

代码清单 30‑3 rom顶层模块仿真参考代码(tb_rom.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
\`timescale 1ns/1ns
module tb_rom();

////
//\* Parameter and Internal Signal \//
////

//wire define
wire stcp;
wire shcp;
wire ds ;
wire oe ;
//reg define
reg sys_clk ;
reg sys_rst_n ;
reg [1:0] key ;
////
//\* Main Code \//
////
//对sys_clk,sys_rst赋初值,并模拟按键抖动
initial
begin
sys_clk = 1'b1 ;
sys_rst_n <= 1'b0 ;
key <= 2'b11;
#200 sys_rst_n <= 1'b1 ;
//按下按键key[0]
#2000000 key[0] <= 1'b0;//按下按键
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#200 key[0] <= 1'b1;//松开按键
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
//按下按键key[1]
#2000000 key[1] <= 1'b0;//按下按键
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#200 key[1] <= 1'b1;//松开按键
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
//按下按键key[1]
#2000000 key[1] <= 1'b0;//按下按键
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#200 key[1] <= 1'b1;//松开按键
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
//按下按键key[1]
#2000000 key[1] <= 1'b0;//按下按键
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#200 key[1] <= 1'b1;//松开按键
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
//按下按键key[0]
#2000000 key[0] <= 1'b0;//按下按键
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#200 key[0] <= 1'b1;//松开按键
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
//按下按键key[0]
#2000000 key[0] <= 1'b0;//按下按键
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#200 key[0] <= 1'b1;//松开按键
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
end
//sys_clk:模拟系统时钟,每10ns电平取反一次,周期为20ns,频率为50MHz
always #10 sys_clk = ~sys_clk;

 //重新定义参数值,缩短仿真时间仿真
 defparam rom_inst.key1_filter_inst.CNT_MAX = 5 ;
 defparam rom_inst.key2_filter_inst.CNT_MAX = 5 ;
 defparam rom_inst.rom_ctrl_inst.CNT_MAX = 99;

 ////
 //\* Instantiation \//
 ////

 //---------------rom_inst--------------
 rom rom_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低电平有效
 .key (key ), //输入按键信号

 .stcp (stcp ), //输出数据存储寄时钟
 .shcp (shcp ), //移位寄存器的时钟输入
 .ds (ds ), //串行数据输入
 .oe (oe ) //输出使能信号

 );

 endmodule

仿真波形分析

使用ModelSim软件对代码进行仿真,仿真波形如下所示。

IP073

图 30‑73 rom仿真波形图(一)

图 30‑73为仿真的整体波形图,从图中大致可以看到当我们按下按键后读出的数据正是我们指定的地址存储数据。下面我们放大波形图从局部进行进一步观看。

IP074

图 30‑74 rom仿真波形图(二)

从图 30‑74中可以很清楚的看到ROM IP核地址与数据的关系。在前面我们知道我们往地址0~255存入的数据是0~255,从波形图中可以看到当时钟的上升沿采到地址时,在下一刻就会输出改地址存储的数据。

IP075

图 30‑75 rom仿真波形图(三)

如图 30‑75所示,可以看到当读地址从8’d232跳转到8’d99时,读取的数据也从8’d232跳转到8’d99,这正是验证了ROM IP核的随机读取的特性。

24.4.1.4. ROM IP核上板验证

引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 30‑4所示。

表格 30‑4 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

input

E1

时钟

sys_rst_n

input

M15

复位

key[0]

input

M2

按键

key[1]

input

M1

按键

stcp

output

K9

存储寄存器时钟

shcp

output

B1

移位寄存器时钟

ds

output

R1

串行数据

oe

output

L11

输出使能

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 30‑76所示。

IP076

图 30‑76 管脚分配

结果验证

管脚配置完成之后重新进行编译,编译完之后就可以进行下载验证了,在下载之前首先将电源与下载线与开发板连接好,连接好之后上电,如图 30‑77所示。

IP077

图 30‑77 下载连线图

打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下载,随后界面会显示下载成功,如图 30‑78所示。

IP078

图 30‑78 下载成功界面

下载成功后即可以开始验证了。按照实验目标描述进行操作,若显示结果与实验目标描述相同,则说明验证成功。

24.5. ip核之ram

24.5.1. ram-ip核简介

在上一节中我们对ROM的使用已有了详细的讲解。本小节将为大家介绍另一种存储类IP核——RAM的使用方法。

RAM是随机存取存储器(Random Access Memory)的简称,是一个易失性存储器。RAM工作时可以随时从任何一个指定的地址写入或读出数据,同时我们还能修改其存储的数据,即写入新的数据,这是ROM所并不具备的功能。在FPGA中这也是其与ROM的最大区别。ROM是只读存储器,而RAM是可写可 读存储器,在我们FPGA中使用这两个存储器主要也是要区分这一点,因为这两个存储器使用的都是我们FPGA内部的RAM资源,不同的是ROM是只用到了RAM资源的读数据端口。

Altera推出的RAM IP核分为两种类型:单端口RAM和双端口RAM。其中双端口RAM又分为简单双端口RAM和真正双端口RAM。对于单端口RAM,读写操作共用一组地址线,读写操作不能同时进行;对于简单双端口RAM,读操作和写操作有专用地址端口(一个读端口和一个写端口),即写端口只能写不能读,而读 端口只能读不能写;对于真正双端口RAM,有两个地址端口用于读写操作(两个读/写端口),即两个端口都可以进行读写。

下面给出RAM不同配置模式存储器的接口信号图,如图 30‑79、图 30‑80、图 30‑81所示。

IP079

图 30‑79 单端口RAM接口信号

IP080

图 30‑80 简单双端口RAM接口信号

IP081

图 30‑81 真正双端口RAM接口信号

各IP核端口信号将会在IP核配置页面进行讲解。

24.5.1.1. RAM IP核配置

该小节将为大家详细介绍RAM IP核的配置步骤,首先是单端口RAM的配置。

单端口RAM的配置

在IP核简介小节已经介绍了如何进入IP核配置界面,按其中步骤我们先进入IP核配置界面,如下图所示:

IP082

图 30‑82 单端口RAM的配置步骤(一)

如图 30‑82所示:

框中可输入IP核的名称进行搜索,这里我们输入ram后,往下拉可以看到我们需要的IP核信息。

框中“RAM:1-PORT”是单端口RAM;“RAM:2-PORT”是双端口RAM;这里我们选择单端口RAM来为大家先介绍单端口RAM的配置步骤。

框是IP核保存位置的选择,这里我们在工程目录下新建一个ip_core文件夹,将IP核保存在该文件夹下并命名为ram_256x8(ram是我们调用的IP核,256是我们想配置的IP核容量,8是想配置的IP核数据位宽。这里这样命名是为了方便识别我们调用的IP核类型及资源量)。

选择完之后点击Next进入下一界面:

IP083

图 30‑83 单端口RAM的配置步骤(二)

如图 30‑83所示:

框是设置输出数据端口的位宽,框是设置存储器容量的大小,这两个选项大家可根据实际的设计进行设置。这里我们设置数据位宽8bit,存储容量为256words,即我们设置的RAM的最大存储量为256个8bit数据。

框是存储单元类型的选择,这里我们保持Auto(软件自动选择)即可。

框中是选择使用的时钟模式,可选择单时钟或双时钟。选择单时钟时:用一个时钟信号控制存储块的所有寄存器。选择双时钟时:输入时钟控制地址寄存器,输出时钟控制数据输出寄存器。大家可根据设计需求进行选择,这里我们选择默认选项Single clock(单时钟)。

设置完之后点击Next进入下一界面:

IP084

图 30‑84 单端口RAM的配置步骤(三)

如图 30‑84所示:

框是选择是否输出“q”输出寄存器。这里把输出端口的寄存器去掉(如果不去掉的话,就会使输出延迟一拍。如果没有特别的需求的话我们是不需要延迟这一拍的,所以这里我们把它去掉)。

框是询问我们是否选择为时钟信号创建响应的使能信号,这里我们不需要,不勾选。

框是选择是否创建“aclr”异步复位信号以及是否创建“rden”读使能信号,大家可根据实际的设计需求进行勾选,这里我们把它们都勾选上。

设置完之后点击Next进入下一界面:

IP085

图 30‑85 单端口RAM的配置步骤(四)

如图 30‑86是进行 Read During Write Operation 项配置,是选择某个地址即将被写入数据时读该地址的数据输出类型:有 Don’t Care(不关心)、 New Data(写入的新数据)和 Old Data(原有数据),我们保持默认的 New Data 即可,也就是说,某个地址将被写入新数据时,同时进行读操作会读出新的数据。点击Next进入下一页面:

IP086

图 30‑86 单端口RAM的配置步骤(五)

如图 30‑86所示:

框是选择是否为存储器配置初始化文件,与ROM不同的是,RAM可以选择不配置初始化文件,这里我们选择不配置初始化文件。

框是选择是否允许系统内存储器内容编辑器在于与系统时钟无关的情况下捕获和更新存储器的内容,这里我们不勾选。

设置完后点击Next进入下一界面:

IP087

图 30‑87 单端口RAM的配置步骤(六)

如图 30‑87所示,该界面没有什么要配置的参数,但显示了我们在仿真ROM IP核时所需要的Altera仿真库,这里提示了我们单独使用第三方仿真工具时需要添加名为“altera_mf”的库。这里保持默认,直接点击“Next”。

IP088

图 30‑88 单端口RAM的配置步骤(七)

如图 30‑88所示,是RAM输出的文件,除了灰色必选文件,默认还勾选上了 ram_256x8_bb.v,这里我们去掉 rom_256x8_bb.v文件,加入rom_256x8_inst.v(例化模板文件)即可。最后点击“Finish”完成整个IP核的创建。接下来Quartus II软件会在我们创建的IP核文件目录下生成RAM IP文件,若第一次使用IP核功能时会跳出一个界面,如下图所示:

IP057

图 30‑89 单端口ROM的配置步骤(十)

如图 30‑89所示的对话框,提示我们是否每次都自动将IP核的文件添加到工程中,这里我们选择打勾同意,然后点击“Yes”确定。如果我们不选择打勾就退出也可以手动在“Project Naigator”的“File”中像添加普通文件一样添加IP核所需要的“.qip”文件即可,其余和IP核的文件不要添加进来。

简单双口RAM的配置

单端口RAM的配置步骤讲解完之后,为大家介绍一下简单双端口RAM的配置步骤。简单双口RAM和真正双口RAM是在双口RAM内进行设置的,所以我们首先需调用一个双口RAM。

下面开始进入双口RAM的配置界面。在IP核简介小节已经介绍了如何进入IP核配置界面,按其中步骤我们先进入IP核配置界面,如下图所示:

IP089

图 30‑90 简单双端口RAM的配置步骤(一)

如图 30‑90所示,该步骤与单端口RAM步骤几乎是一样的,不同的是我们需要选择“RAM:2-PORT”来调用双口RAM。选择完后点击Next进入下一步:

IP090

图 30‑91 简单双端口RAM的配置步骤(二)

如图 30‑91所示,一进入双口RAM的配置就会提示我们是选择简单双口RAM还是选择真双口RAM。其中是简单双口RAM,是真双口RAM,这里我们先选择简单双口RAM为大家讲解。是设置ROM存储器大小的方式,“As a number of words”是按字数确定,“AS a number of bits”是按比特数确定。我们默认选择按字数确定。设置完后点击Next进入下一界面:

IP091

图 30‑92 简单双端口RAM的配置步骤(三)

如图 30‑92所示:

框是选择存储器的容量,可根据设计需求进行设置,这里我们设置256个数据。

框是设置不同端口的位宽是否相同,可以选着打开或者关闭,默认是关闭,即使用相同位宽。

框是设置数据的位宽,可根据设计需求进行设置,这里我们选择8bit。

框是存储单元类型的选择,按默认选择即可。

该页面设置完之后点击Next进入下一界面:

IP092

图 30‑93 简单双端口RAM的配置步骤(四)

如图 30‑93所示:

框中是时钟的选择:

Single clock(单时钟):使用一个时钟控制。

Dual clock:use separate “read”and“write”clocks(双时钟:使用单独的读时钟和写时钟):写时钟控制数据输入、写地址和写使能寄存器,读时钟控制数据输出、读地址和读使能寄存器。

Dual clock:use separate “input” and “output”clocks(双时钟:使用单独的输入时钟和输出时钟):输入和输出时钟分别控制存储块的数据输入和输出的相关寄存器。这里我们选择单时钟。

框中是选择是否创建“rden”读使能信号,可根据设计需求进行设置,这里我们勾选创建一个使能信号。

框是设置是否选择字节使能信号,可根据设计需求进行设置,这里我们不勾选。

设置完成之后点击Next进入下一界面:

IP093

图 30‑94 简单双端口RAM的配置步骤(五)

如图 30‑94所示:

框是选择是寄存输出还是端口输出。若我们选中则为寄存输出,不勾选即为端口输出;寄存输出方式输出数据会延迟一个时钟,默认是寄存输出的,这里我们不勾选,选择端口输出(如果不去掉的话,就会使输出延迟一拍。如果没有特别的需求的话我们是不需要延迟这一拍的,所以这里我们把它去掉)。

框为是否为每个时钟信号创建使能信号,这里我们不需要,不勾选即可。

框为是否为寄存器端口创建“aclr”异步复位信号,大家可根据实际的设计需求进行勾选,本次我们不勾选。

设置完后点击Next进入下一界面:

IP094

图 30‑95 简单双端口RAM的配置步骤(六)

如图 30‑95所示,该页面是设置:当一个端口正在写数据到存储器某个地址时,从另外一个端口读出,则输出端口q的输出是什么?其中“Old memory contents appear”选项为输出写入新数据之前原来存储在存储器中的数据。“I do not care”(不关心)选择为根据选择实现存储器的存储块类型不同而有所不同,默认选项为不关心,这里我们默认勾选不关心即可。设置完之后点击Next进入下一界面:

IP095

图 30‑96 简单双端口RAM的配置步骤(七)

如图 30‑96所示,该页面是设置存储器的初始化内容,其中框选择保持为空;框选择使用数据文件加载,该选择与RAM的数据文件加载方法是一样的。按照RAM的数据文件加载步骤去加载即可。这里我们选择框保持为空。点击Next进入下一界面:

IP096

图 30‑97 简单双端口RAM的配置步骤(八)

如图 30‑97所示,该页面与单端口RAM的设置一样,直接点击Next进入下一界面:

IP097

图 30‑98 简单双端口RAM的配置步骤(九)

如图 30‑98所示,我们勾选rom_256x8_inst.v(例化模板文件)即可。点击“Finish”完成整个IP核的创建。接下来Quartus II软件会在我们创建的IP核文件目录下生成RAM IP文件,若第一次使用IP核功能时会跳出一个界面,如下图所示:

IP057

图 30‑99 单端口ROM的配置步骤(十)

如图 30‑99所示的对话框,提示我们是否每次都自动将IP核的文件添加到工程中,这里我们选择打勾同意,然后点击“Yes”确定。如果我们不选择打勾就退出也可以手动在“Project Naigator”的“File”中像添加普通文件一样添加IP核所需要的“.qip”文件即可,其余和IP核的文件不要添加进来。

真双端口RAM的配置

前面我们已经讲了简单双口RAM的相关配置,本小节将为大家介绍真双口RAM的相关配置。由简单双口RAM的配置小节我们知道,一进入双口RAM的配置界面就会提示我们选择双口RAM的类型,如下图所示:

IP098

图 30‑100 真双端口RAM的配置步骤(一)

如图 30‑100所示,这里我们选择真双口RAM,该页面的其余设置与简单双口RAM一样即可。点击Next进入下一界面:

IP099

图 30‑101 真双端口RAM的配置步骤(二)

如图 30‑101所示,同样该页面的设置也与简单双口的设置一样即可,点击Next进入下一界面:

IP100

图 30‑102 真双端口RAM的配置步骤(三)

如图 30‑102所示,该页面的设置与简单双口RAM的设置大致相同,不同的是框和框。框是选择为每个端口使用不同的独立时钟。框是选择是否为端口A和端口B创建读使能信号。大家可根据实际的设计需求进行勾选。点击Next进入下一界面:

IP101

图 30‑103 真双端口RAM的配置步骤(四)

如图 30‑103所示,该页面的各项设置与简单双口RAM的页面设置一样,根据设计需求进行设置即可。点击Next进入下一界面

IP102

图 30‑104 真双端口RAM的配置步骤(五)

如图 30‑104所示,该页面与简单双口RAM的页面设置一样即可。点击Next进入下一界面

IP103

图 30‑105 真双端口RAM的配置步骤(七)

如图 30‑105所示,该页面是为两个端口选择某个地址即将被写入数据时读该地址的数据输出类型:有New Data(写入的新数据)和 Old Data(原有数据),我们保持默认的 New Data 即可,也就是说,某个地址将被写入新数据时,同时进行的读操作会读出新的数据。点击Next进入下一页面:

IP104

图 30‑106 真双端口RAM的配置步骤(八)

如图 30‑106所示,该页面与简单双口RAM页面的设置是一样的。点击Next进入下一界面:

IP105

图 30‑107 真双端口RAM的配置步骤(九)

如图 30‑107,该页面也与简单双口RAM的设置一样,默认即可,点击Next进入下一界面:

IP106

图 30‑108 真双端口RAM的配置步骤(十)

如图 30‑108所示,我们勾选rom_256x8_inst.v(例化模板文件)即可。点击“Finish”完成整个IP核的创建。接下来Quartus II软件会在我们创建的IP核文件目录下生成RAM IP文件,若第一次使用IP核功能时会跳出一个界面,如下图所示:

IP057

图 30‑109 单端口ROM的配置步骤(十)

如图 30‑109所示的对话框,提示我们是否每次都自动将IP核的文件添加到工程中,这里我们选择打勾同意,然后点击“Yes”确定。如果我们不选择打勾就退出也可以手动在“Project Naigator”的“File”中像添加普通文件一样添加IP核所需要的“.qip”文件即可,其余和IP核的文件不要添加进来。

到这里RAM IP核的配置就全部讲完了,可以发现其实简单双口RAM的调用配置和真双口RAM的调用配置时大致相同的,就是真双口RAM多了一个端口而已。下面我们根据单端口RAM来为大家讲解其操作时序。

24.5.1.2. RAM IP核使用

同样的我们可以设计一个与ROM小节一样的例子,只不过是ROM是初始化数据文件,而我们这里是由我们自己写入。具体功能如下:

按下按键1时往RAM地址0~255里写入数据0~255;按下按键2时读取RAM内的数据,从地址0开始每隔0.2s地址加1往下进行读取;再次按下按键1时停止读取重新写入数据0~255;再次按下按键2时从头开始读取数据。

整体说明

根据我们设计的实验可知,首先我们需要一个数码管显示模块,其次是按键的消抖模块,再加上我们调用的RAM IP核,最后再编写一个RAM控制模块即可以完成我们的实验要求了,模块框图如所示。

IP107

图 30‑110 工程整体框图

由上图可以看到,该工程共分5个模块,其中按键消抖是一个模块,只是使用了两次而已。各模块简介见表格 30‑5。

表格 30‑5 工程模块简介

模块名称

功能描述

key_fifter

按键消抖模块

ram_ctrl

RAM控制模块

ram_256x8

RAM IP核模块

seg_595_dynamic

数码管显示模块

ram

顶层模块

下面分模块为大家讲解。

按键消抖模块

在《按键消抖模块的设计与验证》章节中我们对按键消抖模块已经有了详细的讲解,这里我们直接调用即可。

数码管显示模块

在“数码管的动态显示”章节中我们已经对数码管动态显示模块做了详细的讲解,在此就不再讲解了,在这里我们直接调用这个模块即可。需要注意的是该模块下还有子模块我们没有在整体框图中画出,该模块框图如图 30‑111所示。

IP066

图 30‑111 数码管显示模块

RAM IP核模块

这里我们调用一个单端口RAM,完全按单端口RAM的配置小节的步骤配置来生成一个单端口RAM IP核即可。按步骤调用生成完之后打开工程目录下的RAM IP核保存位置,如下图所示:

IP108

图 30‑112 RAM例化模板文件位置

如图 30‑112所示,我们打开“ram_256x8_inst.v”文件,打开后如下图所示:

IP109

图 30‑113 RAM例化模板文件代码

如图 30‑113所示为RAM IP核的例化模板,我们将其复制到顶层模块将对应的信号连接起来即可。各接口信号简介如下所示:

aclr:异步清零信号,高电平有效。该清零信号只能清楚输出端口的数据,并不会清除存储器内部存储的内容。

address:地址线,位宽为8bit。各信号的位宽可打开图 30‑112中的“ram_256x8.v”文件进行查看。由于我们调用的RAM为单口RAM,所以只有一组地址写。

clock:读写时钟。

data:写入RAM的数据,位宽为8bit。

rd_en:读使能信号,高电平有效。该信号在配置时刻可选择不生成。

wr_en:写使能信号,高电平有效。在RAM中,该信号固定存在。

q:读出RAM中的数据,位宽也是8bit。

其中“q”为输出信号,其余信号都为该模块的输入信号,需要我们产生输入。

RAM控制模块

模块框图

该模块要生成的是输入RAM的端口信号。模块框图如图 30‑114所示:

IP110

图 30‑114 RAM控制模块框图

该模块的各个信号简介如表格 30‑6所示。

表格 30‑6 RAM控制模块输入输出信号简介

信号

位宽

类型

功能描述

sys_clk

1bit

input

系统时钟,频率50MHz

sys_rst_n

1bit

input

复位信号,低电平有效

key1_flag

1bit

input

按键1消抖信号

key2_flag

1bit

input

按键2消抖信号

wr_en

1bit

output

写使能信号

addr

8bit

output

读写地址

wr_data

8bit

output

写数据

rd_en

1bit

output

读使能信号

这里我们使用系统时钟(50MHz)作为RAM的读写时钟,再输入两个按键消抖信号作为读写标志信号。下面根据波形图来讲解其具体时序。

波形图绘制

IP111

图 30‑115 RAM控制模块波形图(一)

如图 30‑115所示,当按下按键1时(key1_flag=1)说明要开始往RAM里写数据了。RAM的写时序为:当RAM写时钟上升沿采到写使能为高时,就能将该上升沿采到的数据写入该上升沿采到的地址中。所以要想往RAM里写数据必须有时钟,写使能,地址,写数据信号。其中写时钟我们直接用系统时钟即可,接下 来是写使能:

wr_en:写RAM使能信号,高有效,当其为高电平时才能往RAM里写数据。所以当检测到按键1消抖信号有效时,我们拉高写使能,开始往RAM里写数据。

addr:当写使能信号为高时,我们需要给写入RAM的地址才能往地址里写入数据。这里我们从0地址开始写入,一个时钟上升沿写一个地址,依次一直写到最后一个地址255。当写完最后一个地址时拉低写使能信号,停止写入。

wr_data:写入RAM的数据。当使能和地址信号都有了,再加上地址就能往RAM里写入数据了。这里我们让数据和地址相等即可。即往地址里写入数据0~255.

读操作与写操作的时序是一样的。都是时钟上升沿触发。RAM的读时序为:当RAM读时钟上升沿采到读使能为高时,就能读出该上升沿采到的地址中的数据。若是我们配置IP核时没有生成RAM读使能,那么RAM就能直接读出读时钟上升沿采到的地址中的数据。

rd_en:读RAM使能信号,高有效,当其为高电平时才能读出RAM里的数据。当检测到按键2消抖信号有效时,我们拉高读使能信号,开始读出RAM里的数据。由于我们读出的数据要显示在数码管上,如果读的太快,我们肉眼是看不出来的。所以这里我们设计每0.2s读一个地址。这样我们就能很清晰的看到我们读出的数据了 。

cnt_200ms:0.2s(200ms)计数器。读使能为高时开始计数。每来一个时钟上升沿计数器加一,当加到最大值CNT_MAX(9999999)时,计数器清零开始下一轮的计数。0~9999999即10000000个系统时钟,即为0.2s(10000000*20ns=0.2s)。

当0.2s计数器产生完之后,每检测到计数器计到最大值我们就让地址加一,这样我们就能每0.2s依次读出RAM里的数据了。当读完最后一个地址的数据时,我们让地址归0重新开始读取,依次循环。

如果我们读取数据时按下按键1(写按键),那么我们就停止读取(拉低读使能),再次往RAM里面写数据。如图 30‑116所示。

IP112

图 30‑116 RAM控制模块波形图(二)

若我们读取数据时按下按键2(读按键),那么我们就让地址归0从地址0开始从头读取。如图 30‑117所示。

IP113

图 30‑117 RAM控制模块波形图(三)

若我们在往RAM里写数据时,按下按键2(读按键)将不会读取RAM内的数据,即写使能为低时按下按键2(读按键)才会拉高读使能。若我们在往RAM里写数据时,按下按键1(写按键),那么我们就将地址归0,从0地址开始重新写入。

代码编写

参照绘制波形图,编写模块代码。模块参考代码,具体见代码清单 30‑4。

代码清单 30‑4 RAM控制模块参考代码(ram_ctrl.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
module ram_ctrl
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低有效
input wire key1_flag , //按键1消抖后有效信号,作为写标志信号
input wire key2_flag , //按键2消抖后有效信号,作为读标志信号

output reg wr_en , //输出写RAM使能,高点平有效
output reg rd_en , //输出读RAM使能,高电平有效
 output reg [7:0] addr , //输出读写RAM地址
 output wire [7:0] wr_data //输出写RAM数据

 );

 ////
 //\* Parameter and Internal Signal \//
 ////

 //parameter define
 parameter CNT_MAX = 9_999_999; //0.2s计数器最大值

 //reg define
 reg [23:0] cnt_200ms ; //0.2s计数器

 ////
 //\* Main Code \//
 ////

 //让写入的数据等于地址数,即写入数据0~255
 assign wr_data = (wr_en == 1'b1) ? addr : 8'd0;

 //wr_en:产生写RAM使能信号
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 wr_en <= 1'b0;
 else if(addr == 8'd255)
 wr_en <= 1'b0;
 else if(key1_flag == 1'b1)
 wr_en <= 1'b1;

 //rd_en:产生读RAM使能信号
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 rd_en <= 1'b0;
 else if(key2_flag == 1'b1 && wr_en == 1'b0)
 rd_en <= 1'b1;
 else if(key1_flag == 1'b1)
 rd_en <= 1'b0;
 else
 rd_en <= rd_en;

 //0.2s循环计数
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 cnt_200ms <= 24'd0;
 else if(cnt_200ms == CNT_MAX \|\| key2_flag == 1'b1)
 cnt_200ms <= 24'd0;
 else if(rd_en == 1'b1)
 cnt_200ms <= cnt_200ms + 1'b1;

 //写使能有效时,
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 addr <= 8'd0;
 else if((addr == 8'd255 && cnt_200ms == CNT_MAX) \|\|
 (addr == 8'd255 && wr_en == 1'b1) \|\|
 (key2_flag == 1'b1) \|\| (key1_flag == 1'b1))
 addr <= 8'd0;
 else if((wr_en == 1'b1) \|\| (rd_en == 1'b1 && cnt_200ms == CNT_MAX))
 addr <= addr + 1'b1;

 endmodule

模块参考代码是参照绘制波形图进行编写的,在波形图绘制小节已经对模块各信号有了详细的说明,本小节不再过多叙述。

顶层模块

模块框图

ram顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,模块框图如图 30‑118所示。

IP114

图 30‑118 顶层模块框图

模块各输入输出信号描述如表格 30‑3所示。

表格 30‑7 ram顶层模块输入输出信号描述

信号

位宽

类型

功能描述

sys_clk

1bit

input

系统时钟,50MHz

sys_rst_n

1bit

input

复位信号

key

2bit

input

按键信号

stcp

1bit

output

输出数据存储器时钟

shcp

1bit

output

移位寄存器的时钟输入

ds

1bit

output

串行数据输入

oe

1bit

output

输出使能信号

代码编写

rom顶层模块主要是对各个子功能模块的实例化,以及对应信号的连接,代码编写较为容易,无需波形图的绘制。顶层参考代码,具体见代码清单 30‑5。

代码清单 30‑5 ram顶层模块参考代码(ram.v)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
module ram
(
input wire sys_clk , //系统时钟,频率50MHz
input wire sys_rst_n , //复位信号,低电平有效
input wire [1:0] key , //输入按键信号

output wire stcp , //输出数据存储器时钟
output wire shcp , //移位寄存器的时钟输入
output wire ds , //串行数据输入
 output wire oe //输出使能信号

 );

 ////
 //\* Parameter and Internal Signal \//
 ////

 //wire define
 wire wr_en ; //写使能
 wire rd_en ; //读使能
 wire [7:0] addr ; //地址线
 wire [7:0] wr_data ; //写数据
 wire [7:0] rd_data ; //读出RAM数据
 wire key1_flag ; //按键1消抖信号
 wire key2_flag ; //按键2消抖信号

 ////
 //\* Instantiation \//
 ////

 //----------------ram_ctrl_inst----------------
 ram_ctrl ram_ctrl_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低有效
 .key1_flag (key1_flag ), //按键1消抖后有效信号,作为写标志信号
 .key2_flag (key2_flag ), //按键2消抖后有效信号,作为读标志信号

 .wr_en (wr_en ), //输出写RAM使能,高点平有效
 .rd_en (rd_en ), //输出读RAM使能,高电平有效
 .addr (addr ), //输出读写RAM地址
 .wr_data (wr_data ) //输出写RAM数据

 );

 //----------------key1_filter_inst----------------
 key_filter key1_filter_inst
 (
 .sys_clk (sys_clk ), //系统时钟50MHz
 .sys_rst_n (sys_rst_n ), //全局复位
 .key_in (key[0] ), //按键输入信号

 .key_flag (key1_flag ) //key_flag为1时表示消抖后检测到按键被按下
 //key_flag为0时表示没有检测到按键被按下
 );

 //----------------key2_filter_inst----------------
 key_filter key2_filter_inst
 (
 .sys_clk (sys_clk ), //系统时钟50MHz
 .sys_rst_n (sys_rst_n ), //全局复位
 .key_in (key[1] ), //按键输入信号

 .key_flag (key2_flag ) //key_flag为1时表示消抖后检测到按键被按下
 //key_flag为0时表示没有检测到按键被按下
 );

 //----------------seg_595_dynamic_inst----------------
 seg_595_dynamic seg_595_dynamic_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低有效
 .data ({12'd0,rd_data} ), //数码管要显示的值
 .point (0 ), //小数点显示,高电平有效
 .seg_en (1'b1 ), //数码管使能信号,高电平有效
 .sign (0 ), //符号位,高电平显示负号

 .stcp (stcp ), //输出数据存储寄时钟
 .shcp (shcp ), //移位寄存器的时钟输入
 .ds (ds ), //串行数据输入
 .oe (oe ) //输出使能信号

 );

 //---------------rom_256x8_inst--------------
 ram_256x8 ram_256x8_inst
 (
 .aclr (~sys_rst_n ), //异步清零信号
 .address (addr ), //读写地址线
 .clock (sys_clk ), //使用系统时钟作为读写时钟
 .data (wr_data ), //输入写入RAM的数据
 .rden (rd_en ), //读RAM使能
 .wren (wr_en ), //写RAM使能
 .q (rd_data ) //输出读RAM数据
 );

 endmodule

因为我们需要用到两个按键信号,所以我们需要例化两次按键消抖模块,产生两个按键有效信号。

RTL视图

顶层模块介绍完毕,使用Quartus II软件对实验工程进行编译,工程通过编译后查看实验工程RTL视图。工程RTL视图,具体见图 30‑119。由图可知,实验工程的RTL视图与实验整体框图相同,各信号线均已正确连接。

IP115

图 30‑119 RTL视图

24.5.1.3. RAM IP核仿真

仿真代码编写

顶层模块参考代码介绍完毕,开始对顶层模块进行仿真,对顶层模块的仿真就是对实验工程的整体仿真。顶层模块仿真参考代码,具体见代码清单 30‑6。

代码清单 30‑6 ram顶层模块仿真参考代码(tb_ram.v)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
\`timescale 1ns/1ns
module tb_ram();

////
//\* Parameter and Internal Signal \//
////

//wire define
wire stcp;
wire shcp;
wire ds ;
wire oe ;
//reg define
reg sys_clk ;
reg sys_rst_n ;
reg [1:0] key ;
////
//\* Main Code \//
////
//对sys_clk,sys_rst赋初值,并模拟按键抖动
initial
begin
sys_clk = 1'b1 ;
sys_rst_n <= 1'b0 ;
key <= 2'b11;
#200 sys_rst_n <= 1'b1 ;
//按下按键key[1]
#2000000 key[1] <= 1'b0;//按下按键
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#200 key[1] <= 1'b1;//松开按键
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
//按下按键key[0]
#2000000 key[0] <= 1'b0;//按下按键
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#200 key[0] <= 1'b1;//松开按键
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
//按下按键key[1]
#2000000 key[1] <= 1'b0;//按下按键
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#200 key[1] <= 1'b1;//松开按键
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
//按下按键key[1]
#2000000 key[1] <= 1'b0;//按下按键
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#200 key[1] <= 1'b1;//松开按键
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
//按下按键key[0]
#2000000 key[0] <= 1'b0;//按下按键
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#200 key[0] <= 1'b1;//松开按键
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
#20 key[0] <= 1'b0;//模拟抖动
#20 key[0] <= 1'b1;//模拟抖动
//按下按键key[1]
#2000000 key[1] <= 1'b0;//按下按键
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#200 key[1] <= 1'b1;//松开按键
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
#20 key[1] <= 1'b0;//模拟抖动
#20 key[1] <= 1'b1;//模拟抖动
end
//sys_clk:模拟系统时钟,每10ns电平取反一次,周期为20ns,频率为50MHz
always #10 sys_clk = ~sys_clk;

 //重新定义参数值,缩短仿真时间仿真
 defparam ram_inst.key1_filter_inst.CNT_MAX = 5 ;
 defparam ram_inst.key2_filter_inst.CNT_MAX = 5 ;
 defparam ram_inst.ram_ctrl_inst.CNT_MAX = 99;

 ////
 //\* Instantiation \//
 ////

 //---------------ram_inst--------------
 ram ram_inst
 (
 .sys_clk (sys_clk ), //系统时钟,频率50MHz
 .sys_rst_n (sys_rst_n ), //复位信号,低电平有效
 .key (key ), //输入按键信号

 .stcp (stcp ), //输出数据存储寄时钟
 .shcp (shcp ), //移位寄存器的时钟输入
 .ds (ds ), //串行数据输入
 .oe (oe ) //输出使能信号

 );

 endmodule

仿真波形分析

使用ModelSim软件对代码进行仿真,仿真波形如下所示。

IP116

图 30‑120 ram仿真波形图(一)

图 30‑120为仿真20ms的整体波形图,从波形图我们只能大致看出RAM是有数据写入和读出的,下面我们放大从局部来看看是否正确。

IP117

图 30‑121 ram仿真波形图(二)

根据仿真代码可以知道我们是先模拟按下了读RAM按键,从图 30‑121中可以看到,由于我们并没有往RAM里写入数据,所以看到此时RAM输出数据“q”并不是我们需要写入的数据0~255。

IP118

图 30‑122 ram仿真波形图(三)

IP119

图 30‑123 ram仿真波形图(四)

IP120

图 30‑124 ram仿真波形图(五)

图 30‑122、图 30‑123、图 30‑124截取的是写时序的完整、开头、结尾部分图。在RAM控制模块波形图讲解中我们讲到RAM的写时序是:当RAM写时钟上升沿采到写使能为高时,就能将该上升沿采到的数据写入该上升沿采到的地址中。所以从以上三图中我们可以看到我们往RAM中写入的数据为0~255.。

IP121

图 30‑125 ram仿真波形图(六)

如图 30‑125,当我们往RAM里写入之后再按下读取按键就能读出数据了。

IP122

图 30‑126 ram仿真波形图(七)

由图 30‑126可知,当我们再次按下读RAM按键时又从0地址开始读取数据了,这与我们的设计是完全相符的。同时可以看到时钟上升采到相应的地址后接下来就会输出该地址的数据,这与ROM的读时序是一样的。

24.5.1.4. RAM IP核上板验证

引脚约束

仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 30‑8所示。

表格 30‑8 引脚分配表

信号名

信号类型

对应引脚

备注

sys_clk

input

E1

时钟

sys_rst_n

input

M15

复位

key[0]

input

M2

按键

key[1]

input

M1

按键

stcp

output

K9

存储寄存器时钟

shcp

output

B1

移位寄存器时钟

ds

output

R1

串行数据

oe

output

L11

输出使能

下面进行管脚分配,管脚的分配方法在前面章节已有所讲解,在此就不再过多叙述,管脚的分配如下图 30‑127所示。

IP076

图 30‑127 管脚分配

结果验证

管脚配置完成之后重新进行编译,编译完之后就可以进行下载验证了,在下载之前首先将电源与下载线与开发板连接好,连接好之后上电,如图 30‑128所示。

IP077

图 30‑128 下载连线图

打开下载界面后,当检测到下载器(USB-Blaster)已连接之后,即可点击“Add File…”添加sof文件,添加好后点击“start”开始下载,随后界面会显示下载成功,如图 30‑129所示。

IP123

图 30‑129 下载成功界面

下载成功后即可以开始验证了。按照实验目标描述进行操作,若显示结果与实验目标描述相同,则说明验证成功。

24.6. ip核之fifo

24.6.1. fifo-ip核简介

FIFO(First In First Out,即先入先出),是一种数据缓冲器,用来实现数据先入先出的读写方式。与 ROM 或 RAM 的按地址读写方式不同,FIFO 的读写遵循“先进先出”的原则,即数据按顺序写入 FIFO,先被写入的数据同样在读取的时候先被读出,所以 FIFO存储器没有地址线。FIFO有一个写端口和一个读端口外部无需使用者控制地址,使用方便。

FIFO 存储器主要是作为缓存,应用在同步时钟系统和异步时钟系统中,在很多的设计中都会使用,后面实例中如:多比特数据做跨时钟域的转换、前后带宽不同步等都用到了FIFO。FIFO 根据读写时钟是否相同,分为 SCFIFO(同步 FIFO)和 DCFIFO(异步 FIFO),SCFIFO 的读写为同一时钟,应用在同步时钟系统中;DCFIFO 的读写时钟不同,应用在异步时钟系统中。

24.6.1.1. SCFIFO IP核配置

首先介绍 SCFIFO 的使用。如图 30‑130所示,在搜索栏中搜索“fifo(大小写均可)”就会显示和 FIFO 相关的所有IP核,这里我们选择Installed Plug-Ins目录下的Memory Compiler文件夹下的“FIFO”。器件选择我们使用的CycloneIV E,语言选择Verilog HDL。然后就是选择IP核存放的路径,这里我们将SCFIFO IP核放在工程目录的ipcore_dir文件夹下。然后是给IP核命名,为了能够清晰表达 FIFO 的大小和位宽,这里我们将其命名为“scfifo_256x8”,让人一看名字就知道我们调用的scfifo是256个深度8位宽的。以上选择无误后点击“Next”。

IP124

图 30‑130 同步FIFO IP核的配置步骤(一)

下面我们开始进行 FIFO 参数的配置,其中:

框 1 为 FIFO 数据位宽的选择,可以根据需求选择,这里我们使用默认的8bit。

框2为FIFO 深度的选择,所谓深度其实就是个数的选择,即设置FIFO可以存储多少个8位宽的数据,这里最小为4个,且都是2的n次方个,我们默认选择FIFO的深度为256。

框 3为 FIFO 读写时钟的配置,选择“Yes” 读写为同一个时钟,即 SCFIFO;选择 “No” 读写为不同时钟,即 DCFIFO,这里我们选择 “Yes”。

如图 30‑131所示,配置完成后点击“Next”。

IP125

图 30‑131 同步FIFO IP核的配置步骤(二)

图 30‑132为 FIFO 输入输出管脚的选择界面。其中红框中的三个输出信号默认是有效的,分别为:

full:写满标志位,有效表示 FIFO 已经存储满了,此时应该通过该信号控制写请求信号(也称为写使能信号),禁止再往FIFO中写入数据,防止数据溢出丢失。当写入数据量达到FIFO设置的最大空间时,时钟上升沿写入最后一个数据同时full拉高;读取数据时随时钟上升沿触发同时拉低。

empty:读空标志位,有效表示 FIFO 中已经没有数据了,此时应该通过该信号控制读请求信号(也称为读使能信号),禁止FIFO继续再读出数据,否则读出的将是无效数据。与full相反,写入数据同时拉低;读到最后一个数据同时拉高。

usedw:显示当前FIFO中已存数据个数,与写入数据的个数是同步的,即写第一个数据时就置1,空或满时值为0(满是因为寄存器溢出)。

剩下的信号可以选择添加也可以选择不添加,这里我们只做简单的介绍不进行添加。

almost full:几乎满标志信号,我们可以控制FIFO快要被写满的时候和full信号的作用一样。

almost empty:几乎空标志信号,我们可以控制FIFO快要被读空的时候和empty信号的作用一样。

Asynchronous clear:异步复位信号,用于清空FIFO。

Synchronous clear:同步复位信号,用于清空FIFO。

此页面我们保持默认即可,然后点击“Next”。

IP126

图 30‑132 同步FIFO IP核的配置步骤(三)

图 30‑133为设置FIFO属性和使用资源的界面,其中:

框1需要重点注意,上面的是普通同步FIFO模式,当前读请求有效的下一拍数据才出来;而下面的则是先出数据FIFO模式,读请求来到之前第一个数据就已经先出来了,使得当前的读请求有效时立刻输出数据,而不会像普通模式那样当前读请求有效后下一拍才会输出数据。后面我们会通过仿真会来对比两者的不同。这里我们选择默 认的普通模式。

框2为指定实现FIFO使用的存储块类型和FIFO的存储深度,具体可选值与使用的FPGA芯片有关,默认为Auto,我们一般使用默认值就可以了。

设置完毕后点击“Next”。

IP127

图 30‑133 同步FIFO IP核的配置步骤(四)

图 30‑134为FIFO速度(性能)选择和是禁用保护电路,其中:

框1为性能设置,选择“Yes(best speed)”将获得最大的速度(性能),但是需要牺牲更多的资源(面积);选择“No(smallest area)”将使用更少的资源(面积)但是速度(性能)可能不是最快的。这里我们没有特殊要求,默认即可。

框2为选择是否禁止上溢检测和下溢检测的保护电路(上溢检测保护电路主要是用于在FIFO存储满时禁止继续写数据;下溢检测保护电路主要是用于FIFO被读空时,禁止继续读数据)。如果需要则保持默认,如果不需要可以选择禁用下面的两个选项来提高FIFO速度(性能),这里我们没有特殊需求所以保持默认即可。

框3为选项使用逻辑单元实现FIFO存储器。这里使用默认设置,即用存储块实现FIFO。

设置完毕后点击“Next”。

IP128

图 30‑134 同步FIFO IP核的配置步骤(五)

如图 30‑135所示,该界面没有什么要配置的参数,但显示了我们在仿真SCFIFO IP核时所需要的Altera仿真库,这里提示了我们单独使用第三方仿真工具时需要添加名为“altera_mf”的库。这里保持默认,直接点击“Next”。

IP129

图 30‑135 同步FIFO IP核的配置步骤(六)

如图 30‑136所示,该界面显示的是配置好SCFIFO IP核后我们要输出的文件,其中“scfifo_256x8.v”是默认输出的,不可以取消。此外我们再将“scfifo_256x8_inst.v”这个实例化模板文件添加上,方便我们实例化时使用,其余的文件都可以不勾选。

到此为止我们的SCFIFO IP核的配置就全部完成了,再检查下我们的配置,如果有问题可以点击“Back”返回之前的配置界面进行修改,如果确认无误后可以点击“Finish”退出配置界面,如果后面在仿真时发现SCFIFO IP核的配置有问题也可以再进行修改。

IP130

图 30‑136 同步FIFO IP核的配置步骤(七)

24.6.1.2. SCFIFO IP核调用

SCFIFO IP核配置完成后我们对它进行调用,通过仿真来观察full、empty、usedw这三个信号的变化。验证的方法和例子有很多,后面我们还会在实战项目中使用到这个IP核。为了让大家能够清晰的看到SCFIFO IP核信号的变化,RTL代码的顶层只有端口列表和SCFIFO IP核的实例化,其中:

RTL代码顶层的输入信号有:50MHz的系统时钟sys_clk、输入256个8bit的数据pi_data(值为十进制0~255)和伴随该输入数据有效的标志信号pi_flag、FIFO的写请求信号rdreq。这些输入信号需要在Testbench中产生激励。

RTL代码顶层的输出信号有:从FIFO中读取的数据po_data、FIFO空标志信号empty、FIFO满标志信号full、指示FIFO中存在数据个数的信号usedw。这些信号也是我们需要通过仿真SCFIFO IP核主要观察的信号,这些信号通过Testbench中给输入信号激励后产生输出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
module fifo(
input wire sys_clk , //系统时钟50MHz
input wire [7:0] pi_data , //输入顶层模块的数据
//要写入到FIFO中的数据
input wire pi_flag , //输入数据有效标志信号
//也作为FIFO的写请求信号
input wire rdreq , //FIFO读请求信号

output wire [7:0] po_data , //FIFO读出的数据
 output wire empty , //FIFO空标志信号,高有效
 output wire full , //FIFO满标志信号,高有效
 output wire [7:0] usedw //FIFO中存在的数据个数
 );

 ////
 //\* Instantiation \//
 ////

 //---------------scfifo_256x8_inst-------------------
 scfifo_256x8 scfifo_256x8_inst(
 .clock (sys_clk ), //input clock
 .data (pi_data ), //input [7:0] data
 .rdreq (rdreq ), //input rdreq
 .wrreq (pi_flag ), //input wrreq

 .empty (empty ), //output empty
 .full (full ), //output full
 .q (po_data ), //output [7:0] q
 .usedw (usedw ) //output [7:0] usedw
 );

 endmodule

根据上面RTL代码综合出的RTL视图如图 30‑137所示。

IP131

图 30‑137 RTL视图

24.6.1.3. SCFIFO IP核仿真

下面是Testbench仿真测试文件,和SCFIFO的仿真一样,我们也需要给输入信号测试激励,pi_flag每4个时钟周期且没有读请求时产生一个数据有效标志信号也作为FIFO的写请求信号,因为需要pi_data伴随着pi_flag一起产生,所以每当pi_data检测到pi_flag标志信号有效时就自 加1,其值从0~255循环变化,这样我们就可以在pi_flag标志信号有效时将pi_data写入到FIFO中。而FIFO的读请求信号rdreq当FIFO的满标志信号full有效时拉高,当FIFO的空标志信号empty有效时拉低。

SCFIFO可以同时进行读写操作,但本例我们没有让SCFIFO同时进行读写,当我们真正使用SCFIFO IP核时一定要保证FIFO不被写满也不被读空。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
\`timescale 1ns/1ns
module tb_fifo();

////
//\* Parameter and Internal Signal \//
////

//reg define
reg sys_clk ;
 reg [7:0] pi_data ;
 reg pi_flag ;
 reg rdreq ;
 reg sys_rst_n ;
 reg [1:0] cnt_baud ;

 //wire define
 wire [7:0] po_data ;
 wire empty ;
 wire full ;
 wire [7:0] usedw ;

 ////
 //\* Main Code \//
 ////

 //初始化系统时钟、复位
 initial begin
 sys_clk = 1'b1;
 sys_rst_n <= 1'b0;
 #100;
 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) && (rdreq == 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;

 //rdreq:FIFO读请求信号
 always@(posedge sys_clk or negedge sys_rst_n)
 if(sys_rst_n == 1'b0)
 rdreq <= 1'b0;
 else if(full == 1'b1) //当FIFO中的数据存满时,开始读取FIFO中的数据
 rdreq <= 1'b1;
 else if(empty == 1'b1) //当FIFO中的数据被读空时停止读取FIFO中的数据
 rdreq <= 1'b0;

 ////
 //\* Instantiation \//
 ////

 //------------------------fifo_inst------------------------
 fifo fifo_inst(
 .sys_clk (sys_clk ), //input sys_clk
 .pi_data (pi_data ), //input [7:0] pi_data
 .pi_flag (pi_flag ), //input pi_flag
 .rdreq (rdreq ), //input rdreq

 .po_data (po_data ), //output [7:0] po_data
 .empty (empty ), //output empty
 .full (full ), //output full
 .usedw (usedw ) //output [7:0] usedw
 );

 endmodule

打开ModelSim执行仿真,普通同步FIFO模式仿真出来的波形如图 30‑138所示,我们让仿真运行了100us,可以看到pi_data和po_data交替出现并一直循环下去,pi_flag数据有效标志信号伴随着pi_data一一对应,po_data在读请求信号rdreq为高时输出。其中我们也可以 看到empty和full在不同的位置均有拉高的脉冲,接下来我们将图中位置1和位置2分别放大观察。

IP132

图 30‑138 同步FIFO仿真波形图(一)

如图 30‑139所示为位置1放大后的波形,有几个点需要我们重点观察:

1、full、usedw信号的状态:我们可以看到当pi_flag为高且pi_data为255的同时full满标志信号拉高了,说明FIFO的存储空间已经满了,而usedw信号也从255变成了0,因为产生的SCFIFO IP核中usedw的位宽是8bit的,而十进制256需要9bit才能完全显示,这样最高位就无法显示出来,所以usedw的值显示为0。

2、FIFO读出的数据与FIFO读请求的关系:因为我们这里是对普通同步FIFO模式进行的仿真,所以可以看到当检测到full满标志信号有效,rdreq读请求信号开始拉高,FIFO开始读数据,FIFO读出的第一个数据为0,可以看到数据为0的时间有两个时钟周期,所以第一个0为潜伏期导致的,第二个0才是我们 真正读出来的数据,FIFO中随着数据的读出,FIFO中的数据减少,full满标志信号也拉低了,usedw信号的值也随着减小。

IP133

图 30‑139 同步FIFO仿真波形图(二)

如图 30‑140所示为位置2放大后的波形,这里我们重点观察一下empty空标志信号。因为我们使用的是普通模式,所以读出的数据要比读使能延后一拍,所以当读出十进制数据254后empty空标志信号拉高,表示FIFO中的数据已经被读空。

IP134

图 30‑140 同步FIFO仿真波形图(三)

下面我们将FIFO的属性修改为先出数据FIFO模式,重新打开SCFIFO IP核配置界面至如图 30‑141所示页,选择框中的先出数据FIFO模式,然后一直点击“Next”到结束为止。重新综合并打开ModelSim进行仿真。

IP135

图 30‑141 同步FIFO数据先出配置

如图 30‑142所示,我们可以发现先出数据模式整体的波形没有太大的变化,和普通模式相同,变化最大的地方是数据输出部分的波形。

IP136

图 30‑142 同步FIFO仿真波形图(四)

如图 30‑143所示,因为是先出数据模式,所以当读请求信号rdreq有效时数据立刻就出来了,没有潜伏期的存在,读请求信号rdreq和数据po_data是同步的。

IP137

图 30‑143 同步FIFO仿真波形图(五)

如图 30‑144所示,我们再来观察一下empty空标志信号的情况,可以发现在读出十进制数据255后empty空标志信号拉高,表示FIFO中的数据已经被读空。

IP138

图 30‑144 同步FIFO仿真波形图(六)

SCFIFO的配置、使用、仿真验证到此就结束了,下面我们继续进行DCFIFO的学习。

24.6.1.4. DCFIFO IP核配置

首先新建一个单独的dcfifo工程。然后打开IP核配置界面,如图 30‑145所示,在搜索栏中搜索“fifo(大小写均可)”就会显示和 FIFO 相关的所有IP核,这里我们仍选择Installed Plug-Ins目录下的Memory Compiler文件夹下的“FIFO”。器件选择我们使用的CycloneIV E,语言选择Verilog HDL。然后就是选择IP核存放的路径,这里我们将DCFIFO IP核放在工程目录的ipcore_dir文件夹下。然后是给IP核命名,为了能够清晰表达 FIFO 的大小和位宽,这里我们将其命名为“dcfifo_256x8to128x16”,让人一看到名字就知道我们调用的dcfifo是输入256个深度8位宽、输出128个深度16位宽的。以上选择无误后点击“Next”。

IP139

图 30‑145 异步FIFO IP核配置步骤(一)

下面我们开始进行 FIFO 参数的配置。我们需要先选则框4中的“No”将其设置为DCFIFO后才能够继续配置其他参数。其中:

框 1 为 FIFO输入数据位宽的选择,可以根据实际需求设置大小,这里我们选择输入为8bit。

框2为FIFO输出数据位宽的选择,可以根据实际需求设置大小,这里我们选择输出为16bit。

框 3为FIFO 深度的选择,这里的深度是对FIFO输入数据来说的,即设置FIFO可以存储多少个8位宽的数据,这里最小为4个,且都是2的n次方个,我们选择FIFO的深度为256,既然输入数据的个数和位宽、输出数据的位宽都确定了,那么输出数据的个数也就相应的确定了,为128个。

如图 30‑146所示,配置完成后点击“Next”。

IP140

图 30‑146 异步FIFO IP核配置步骤(二)

如图 30‑147所示,为DCFIFO的优化选择界面,其中:

Lowest latency but requires synchronized clocks(最低延迟,但需要同步时钟):该选项使用一个同步阶段,没有亚稳态保护,适用于同步时钟。面积最小,提供良好的性能。

Minimal setting for unsynchronized clocks(最小设置且为异步时钟):该选项使用两个同步阶段,具有良好的亚稳态保护。面积中等,提供良好的性能。

Best metastability protection, best fmax and unsynchronized clocks(提供最佳的亚稳性保护,最佳的fmax且为异步时钟):该选项使用三个或更多的同步阶段,具有最好的亚稳态保护。它的面积是最大的,但给出了最好的性能。

在使用过程中如果工程对速度和稳定性要求较高,同时FPGA的资源也比较充分,那么建议选择第三个选项;如果工程资源相对紧张,可以选择使用第一个选项。这里我们选择性能和资源平衡的也是默认的第二个选项,直接点击“Next”。

IP141

图 30‑147 异步FIFO IP核配置步骤(三)

图 30‑148为 FIFO 输入输出管脚的选择界面,其中:

框1为Read-side为写端口的full、empty、usedw信号,同步于rdclk;Write-side为读端口的full、empty、usedw信号,同步于wrclk。我们都勾选上。

框2中的Add an extra MSB to usedw ports为设置将rdusedw和wrusedw数据位宽增加1位,用于保护FIFO在写满时不会翻转到0。Asynchronous clear为异步复位信号,用于清空FIFO。这里我们不勾选。

设置完成后点击“Next”。

IP142

图 30‑148 异步FIFO IP核配置步骤(四)

图 30‑149为设置FIFO属性和使用资源的界面,其中:

框1中上面的是普通同步FIFO模式,当前读请求有效的下一拍数据才出来;而下面的则是先出数据FIFO模式,读请求来到之前第一个数据就已经先出来了,使得当前的读请求有效时立刻输出数据,而不会像普通模式那样当前读请求有效后下一拍才会输出数据。后面我们会通过仿真会来对比两者的不同。这里我们选择默认的普通模式 。

框2为指定实现FIFO使用的存储块类型和FIFO的存储深度,具体可选值与使用的FPGA芯片有关,默认为Auto,我们一般使用默认值就可以了。

设置完毕后点击“Next”。

IP143

图 30‑149 异步FIFO IP核配置步骤(五)

图 30‑150为FIFO速度(性能)选择和是禁用保护电路,其中:

框1为选择是否禁止上溢检测和下溢检测的保护电路(上溢检测保护电路主要是用于在FIFO存储满时禁止继续写数据;下溢检测保护电路主要是用于FIFO被读空时,禁止继续读数据)。如果需要则保持默认,如果不需要可以选择禁用下面的两个选项来提高FIFO速度(性能),这里我们没有特殊需求所以保持默认即可。

框2为选项使用逻辑单元实现FIFO存储器。这里使用默认设置,即用存储块实现FIFO。

设置完毕后点击“Next”。

IP144

图 30‑150 异步FIFO IP核配置步骤(六)

如图 30‑151所示,该界面没有什么要配置的参数,但显示了我们在仿真DCFIFO IP核时所需要的Altera仿真库,这里提示了我们单独使用第三方仿真工具时需要添加名为“altera_mf”的库。这里保持默认,直接点击“Next”。

IP145

图 30‑151 异步FIFO IP核配置步骤(七)

如图 30‑152所示,该界面显示的是配置好DCFIFO IP核后我们要输出的文件,其中“dcfifo_256x8to128x16.v”是默认输出的,不可以取消。此外我们再将“dcfifo_256x8to128x16_inst.v”这个实例化模板文件添加上,方便我们实例化时使用,其余的文件都可以不勾选。

到此为止我们的DCFIFO IP核的配置就全部完成了,再检查下我们的配置,如果有问题可以点击“Back”返回之前的配置界面进行修改,如果确认无误后可以点击“Finish”退出配置界面,如果后面在仿真时发现DCFIFO IP核的配置有问题也可以再进行修改。

IP146

图 30‑152 异步FIFO IP核配置步骤(八)

24.6.1.5. DCFIFO IP核调用

DCFIFO IP核配置完成后我们对它进行调用,并通过仿真来观察wrfull、wrempty、wrusedw、rdfull、rdempty、rdusedw这六个信号的变化。验证的方法和例子有很多,后面我们也会在实战项目中使用到这个IP核。为了让大家能够清晰的看到DCFIFO IP核信号的变化,RTL代码的顶层只有端口列表和DCFIFO IP核的实例化,其中:

RTL代码顶层的输入信号有:50MHz的写时钟wrclk、输入256个8bit的数据pi_data(值为十进制0~255)和伴随该输入数据有效的标志信号pi_flag、25MHz的读时钟rdclk、FIFO的写请求信号rdreq。这些输入信号需要在Testbench中产生激励。

RTL代码顶层的输出信号有:同步于wrclk的FIFO空标志信号wrempty、同步于wrclk的FIFO满标志信号wrfull、同步于wrclk指示FIFO中存在数据个数的信号wrusedw、从FIFO中读取的数据po_data、同步于rdclk的FIFO空标志信号rdempty、同步于rdclk 的FIFO满标志信号rdfull、同步于rdclk指示FIFO中存在数据个数的信号rdusedw。这些信号也是我们需要通过仿真DCFIFO IP核主要观察的信号,这些信号通过Testbench中给输入信号激励后产生输出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
module fifo
(
//如果端口信号较多,我们可以将端口信号进行分组
//把相关的信号放在一起,使代码更加清晰
//FIFO写端
input wire wrclk , //同步于FIFO写数据的时钟50MHz
input wire [7:0] pi_data , //输入顶层模块的数据,要写入到FIFO中
//的数据同步于wrclk时钟
input wire pi_flag , //输入数据有效标志信号,也作为FIFO的
 //写请求信号,同步于wrclk时钟
 //FIFO读端
 input wire rdclk , //同步于FIFO读数据的时钟25MHz
 input wire rdreq , //FIFO读请求信号,同步于rdclk时钟

 //FIFO写端
 output wire wrempty , //FIFO写端口空标志信号,高有效,
 //同步于wrclk时钟
 output wire wrfull , //FIFO写端口满标志信号,高有效,
 //同步于wrclk时钟
 output wire [7:0] wrusedw , //FIFO写端口中存在的数据个数,
 //同步于wrclk时钟
 //FIFO读端
 output wire [15:0] po_data , //FIFO读出的数据,同步于rdclk时钟
 output wire rdempty , //FIFO读端口空标志信号,高有效,
 //同步于rdclk时钟
 output wire rdfull , //FIFO读端口满标志信号,高有效,
 //同步于rdclk时钟
 output wire [6:0] rdusedw //FIFO读端口中存在的数据个数,
 //同步于rdclk时钟
 );

 //----------------------dcfifo_256x8to128x16_inst-----------------------
 dcfifo_256x8to128x16 dcfifo_256x8to128x16_inst
 (
 .data (pi_data), //input [7:0] data
 .rdclk (rdclk ), //input rdclk
 .rdreq (rdreq ), //input rdreq
 .wrclk (wrclk ), //input wrclk
 .wrreq (pi_flag), //input wrreq

 .q (po_data), //output [15:0] q
 .rdempty(rdempty), //output rdempty
 .rdfull (rdfull ), //output rdfull
 .rdusedw(rdusedw), //output [6:0] rdusedw
 .wrempty(wrempty), //output wrempty
 .wrfull (wrfull ), //output wrfull
 .wrusedw(wrusedw) //output [7:0] wrusedw
 );

 endmodule

根据上面RTL代码综合出的RTL视图如图 30‑153所示。

IP147

图 30‑153 RTL视图

24.6.1.6. DCFIFO IP核仿真

下面是Testbench仿真测试文件,和SCFIFO的仿真一样,我们也需要给输入信号测试激励,pi_flag每4个时钟周期且没有读请求时产生一个数据有效标志信号也作为FIFO的写请求信号,因为需要pi_data伴随着pi_flag一起产生,所以每当pi_data检测到pi_flag标志信号有效时就自 加1,其值从0~255循环变化,这样我们就可以在pi_flag标志信号有效时将pi_data写入到FIFO中。FIFO的读请求信号rdreq在rdclk时钟条件下当FIFO的满标志信号wrfull在rdclk时钟下打两拍后有效时拉高,当FIFO的空标志信号rdempty有效时拉低。

DCFIFO也可以同时进行读写操作,但本例我们没有让DCFIFO同时进行读写,当我们真正使用DCFIFO IP核时一定要保证FIFO不被写满也不被读空。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
\`timescale 1ns/1ns
module tb_fifo();

////
//\* Parameter and Internal Signal \//
////

//reg define
reg wrclk ;
reg [7:0] pi_data ;
reg pi_flag ;
reg rdclk ;
reg rdreq ;
reg sys_rst_n ;
reg [1:0] cnt_baud ;
reg wrfull_reg0 ;
reg wrfull_reg1 ;
//wire define
wire wrempty ;
wire wrfull ;
wire [7:0] wrusedw ;
wire [15:0] po_data ;
wire rdempty ;
wire rdfull ;
wire [6:0] rdusedw ;
////
//\* Main Code \//
////
//初始化时钟、复位
initial begin
wrclk = 1'b1;
rdclk = 1'b1;
sys_rst_n <= 1'b0;
#100;
sys_rst_n <= 1'b1;
end
//wrclk:模拟FIFO的写时钟,每10ns电平翻转一次,周期为20ns,频率为50MHz
always #10 wrclk = ~wrclk;
//rdclk:模拟FIFO的读时钟,每20ns电平翻转一次,周期为40ns,频率为25MHz
always #20 rdclk = ~rdclk;
//cnt_baud:计数从0到3的计数器,用于产生输入数据间的间隔
always@(posedge wrclk 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 wrclk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
pi_flag <= 1'b0;
//每4个时钟周期且没有读请求时产生一个数据有效标志信号
else if((cnt_baud == 2'd0) && (rdreq == 1'b0))
pi_flag <= 1'b1;
else
pi_flag <= 1'b0;
//pi_data:输入顶层模块的数据,要写入到FIFO中的数据
always@(posedge wrclk 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;
//将同步于rdclk时钟的写满标志信号wrfull在rdclk时钟下打两拍
always@(posedge rdclk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
begin
wrfull_reg0 <= 1'b0;
wrfull_reg1 <= 1'b0;
end
else
begin
wrfull_reg0 <= wrfull;
wrfull_reg1 <= wrfull_reg0;
end
//rdreq:FIFO读请求信号同步于rdclk时钟
always@(posedge rdclk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
rdreq <= 1'b0;
//如果wrfull信号有效就立刻读,则不会看到rd_full信号拉高,
//所以此处使用wrfull在rdclk时钟下打两拍后的信号
else if(wrfull_reg1 == 1'b1)
rdreq <= 1'b1;
else if(rdempty == 1'b1)//当FIFO中的数据被读空时停止读取FIFO中的数据
rdreq <= 1'b0;
 ////
 //\* Instantiation \//
 ////

 //------------------------fifo_inst------------------------
 fifo fifo_inst(
 //FIFO写端
 .wrclk (wrclk ), //input wclk
 .pi_data(pi_data), //input [7:0] pi_data
 .pi_flag(pi_flag), //input pi_flag
 //FIFO读端
 .rdclk (rdclk ), //input rdclk
 .rdreq (rdreq ), //input rdreq

 //FIFO写端
 .wrempty(wrempty), //output wrempty
 .wrfull (wrfull ), //output wrfull
 .wrusedw(wrusedw), //output [7:0] wrusedw
 //FIFO读端
 .po_data(po_data), //output [15:0] po_data
 .rdempty(rdempty), //output rdempty
 .rdfull (rdfull ), //output rdfull
 .rdusedw(rdusedw) //output [6:0] rdusedw
 );

 endmodule

打开ModelSim执行仿真,普通异步FIFO模式仿真出来的波形如图 30‑154所示,我们让仿真运行了100us,可以看到pi_data和po_data交替出现并一直循环下去,pi_flag数据有效标志信号伴随着pi_data一一对应,po_data在读请求信号rdreq为高时输出。其中我们也可以 看到wrempty、wrfull、rdempty、rdfull在不同的位置均有拉高的脉冲,接下来我们将图中位置1和位置2分别放大观察。

IP148

图 30‑154 异步FIFO仿真波形图(一)

如图 30‑155所示为位置1放大后的波形,因为我们设置的是普通模式FIFO,所以和SCFIFO的普通模式一样读出的数据存在一个时钟周期的潜伏期,另外还有几个点需要我们重点观察:

1、wrfull、rdfull满标志信号的状态:我们可以看到当pi_flag为高且pi_data为255的同时wrfull满标志信号先拉高了,延后一段时间rdfull满标志信号也拉高了,说明FIFO的存储空间已经满了。因为wrfull满标志信号和rdfull满标志信号同步于不同的时钟,所以拉高的时间 不同步。

2、wrusedw、rdusedw信号的状态:我们可以看到wrusedw信号计数到,而rdusedw信号则计数到127,这是因为输入时8btit的,输出是16bit的,刚好总数据量相等。同样wrusedw信号也从255变成了0、rdusedw信号从127变成0的原因和SCFIFO中的情况一样,都是因 为数据存储满了,FIFO内部的计数器溢出所导致的。我们还可以发现读出的16bit数据,是输入的8bit数据低位在后高位在前的顺序,如果记错了顺序在使用数据的时候会产生错误。

IP149

图 30‑155 异步FIFO仿真波形图(二)

如图 30‑156所示为位置2放大后的波形,我们发现。当读出十六进制数据fdfc时rdempty空标志信号首先拉高,一段时间后wrempty空标志信号也拉高,表示FIFO中的数据已经被读空。

IP150

图 30‑156 异步FIFO仿真波形图(三)

这里我们就不再展示先出数据FIFO模式的波形了因为和SCFIFO先出数据模式类似,大家可以自己设置并仿真观察现象。到此为止DCFIFO的配置、使用、仿真验证也结束了。

SCFIFO和DCFIFO我们都讲解过了,还需要大家注意几个问题:

1、在单位时间内,写数据的总带宽一定要等于读数据的总带宽,否则一定会存在写满或读空的现象;

2、控制好、利用好FIFO的关键信号,如读写时钟、读写使能、空满标志信号;

3、根据实际的项目需求还要考虑需要多大的FIFO,大了会浪费资源,小了则达不到需求。

24.7. 章末总结

本章节我们主要讲解了Quartus软件中4中常用IP核(pll、rom、ram、fifo)的调用、配置和使用方法,读者要牢记于心,这对后面章节的学习至关重要,能大大提高开发效率。