ZYNQ的FPGA侧基础学习_Part1
1 ZYNQ初识
1.1 PL简介
这里PL就是指的FPGA(Field Programmable Gate Array,现场可编程门阵列),一种可以通过编程来修改其逻辑功能的数字集成电路 (芯片)。
1.1.1 CLB介绍
为什么它能通过编程改变硬件电路呢,是因为它有CLB(可配置的逻辑块),如上图所示,CLB由“片0”和“片1“组成(在赛灵思中片就是Slice)。在这个FPGA芯片当中是有很多的这个CLB组成了一个二维的矩阵,图中只是其中一个CLB的一个内部结构,那么它是由两个片组成,然后一个开关矩阵进行一个连接。这个开关矩阵你可以把它理解成一个布线资源,CLB呢可以通过这个开关矩阵跟其他的这个PL端的资源进行一个连接,或者是连接CLB内部的一些资源。
这个片Slice是由四个LUT加上这八个FF组成。其中LUT(Look UP Table,查找表)它主要的功能是对一个输入的逻辑函数,我们可以通过这个查找表来去配置各种不同的功能,比方说配置成一个存储器了、或者是定时器等等。FF(Flip-Flop,触发器)这个触发器就像我们在大学里面学的数字电路里面的DQ触发器,主要是实现对单个比特数据的寄存。
总之CLB是FPGA能可编程的基础。
1.1.2 FPGA架构
可编程的IO单元
- 它主要是跟这个物理的焊盘进行连接,可以实现一些可编程的一个IO的一些特性,比方说我们可以将这个IO呢配置成输入或者输出等等。
可编程的逻辑单元
- 赛琳斯给这个可编程的逻辑单元就叫CLB。
底层的嵌入功能单元
- 一般的这个FPGA芯片里面它都会有这个单元来去实现一些特定的功能,比方说有这个PLL来去实现一个对时钟的一个倍频、分频等,或者是用DSP来去做一些算法处理和运算等。
嵌入式块RAM(BRAM)
- 顾名思义这个BRAM功能就是存储数据。
布线资源
- 布线资源这个图里面并没有给它画出来,布线资源它就是去实现各个不同的功能模块的一个连接,从而去实现我们的一个功能。
硬核
- 这个硬核呢要区别于这里的底层嵌入功能单元,像什么PLL锁相环、DSP这些FPGA芯片里面基本上都有的,但是这个硬核它里面就是存在一个这样的一个专用的电路来去实现特定的功能并不是每个FPGA芯片里面都有这些硬核;
- 比方说赛里斯的七系列的FPGA它有这个XADC这个硬核,这个硬核它可以实现什么功能呢,顾名思义ADC那就是模拟到数字的一个转换,那么它除了实现这个模拟到数字的转换之外呢它还能够监测这个芯片上面的一个啊温度、各个不同的电压等等。
- 对于7系列它是具有这个硬核,但是其他的FPGA可能就没有这个硬核了,所以说要根据FPGA手册来去查看它有哪些硬核。
上面我们是介绍了FPGA的一个通用的架构,那么我们下面呢就再来看一下针对我们所使用的这个ZYNQ系列,它的PL端的一个架构是什么样子的。下图是ZYNQ的PL端的一个架构(或者说内部结构图):
图中左上部这里缺失的一部分呢就是PS了,因为这里只介绍PL的架构,所以说这里只呈现了一个PL端的一个图。好我们就从上面往下面看吧。
首先,最上方标注了CLB,就是一个可配置的一个逻辑块,可以发现这种不同颜色的这种矩形框在整个的占比非常多,它呈现水平和垂直的一个排列,组成了一个二维的矩阵,那么这个呢也是这个FPGA它能够实现可编程电路的一个重要的构成要素,就是这个CLB。
然后,再往右侧就是一个输入输出块(Input/Output Blocks,IOB),主要跟这个物理的焊盘呢进行一个连接,其实就是对应我们芯片的引脚。这个IOB是指这种可编程的输入输出单元,其可编程的特性就体现在我们可以配置它是输入输出或者是双向的,亦或者我们配置它的一个驱动电流,包括各种不同的电平标准等等都可以来去配置,所以说它的IO非常灵活。
然后,下面有Block RAM,它就是用来实现存储数据,其实在这个它的开发工具当中呢会提供好多XXX(没听清视频说的什么),比方说提供了这个RAM、ROM或者是FIFO,这些我们后面在开发的时候可能会频繁用到,那这些的一个存储相关的XXX(没听清视频说的什么)它底层使用的基本上都是这个BRAM。它非常适合存储一些比较大的一些数据量。
然后,BRAM的左侧有一个时钟的一个单元,主要是对时钟进行一个管理用的,比方说我们就可以通过这个时钟单元对输入的时钟进行一个倍频和分频等等,包括呢调整它的相位,都可以通过这个时钟单元的来去实现
然后,图中Block RAM的右侧的浅蓝色的矩形框就是指DSP,可以发现这个DSP跟Block RAM是紧挨着的,那这个DSP呢主要适合做一些啊这个算法类的,就是做一些相对来说复杂一点的算法,你要做各种算法计算,那么肯定是需要这个存储资源来做一个缓存的,所以说这俩呢是紧密连在一起。那这个DSP呢它主要是做一些算法的运算,但其实我们也可以通过可编程的逻辑块实现一个计算,只不过呢嗯这个可编程的这个逻辑块适合做一些这种简单点的计算,比方说你的这个这个字节比较比较小,或者说你的数据量比较小。但是如果说你的这个算法比较复杂,那么用这个可编程的逻辑块实现的话可能会占用非常多的资源,所以说这种的话呢就适合用这种DSP呢来去实现,并且DSP它的这个运算的一个性能呢也比较强。
接着,我们再来看一下这个硬核——就是这个XADC,那么这个ZYNQ的PL端它是具有这个XADC的硬核的,它可以实现一个模拟到数字量的一个转换。但这个自带的XADC可能转换速率不高。
除了具有这个XADC硬核之外,它还有这个GTX和PCI这样的一些功能模块,这个主要是做一些高速的一个串行收发了。因为是硬核,并不是所有的GPGA都具有这些硬核。对于ZYNQ7010和7020来说它是没有这个GTX和PCIE这个模块的,只有这个XADC硬核。
小结:针对这里的这个PL端的一个架构,就介绍完了。那么最后呢再来补充一点,就是说我们这里呢只是说简单了解一下它的一个结构,不是说我们在写代码的时候就特别要关注它的这个底层结构,我们了解它的这个底层结构之后呢,对我们后面对芯片一个资源的评估呢,比较有好处。比方说这个FPGA它的资源毕竟是有限的,但是我们要实现不同的功能都是通过它的这个可配置的逻辑块来去实现,那么了解了这个底层结构之后,有助于我们去实现某个功能,对它的一个资源消耗的一个评估。
其实我们真正在写代码的时候,开发工具它会帮我们去映射,它需要哪些LVT、需要多少DSP等等,那么工具呢会帮我们来完成。
1.2 PS简介
1.2.1 处理器系统
我们上面讲过,ZYNQ实际上是由PS+PL组成的。这个ZYNQ芯片内部有一个实实在在的ARM硬处理器。对于这个ZYNQ来说呢,它可以通过这个可编程逻辑来去搭建一个micro blaze处理器(软处理器)。
ZYNQ虽然说集成了这个arm处理器,也丝毫不影响通过它的可编程逻辑来去搭建一个软核的处理器。这个软核的处理器可以让它做一些简单的场景、简单的一些应用,而更复杂的应用通过这个ARM处理器去完成。
那下面详细的去看一下ZYNQ它究竟是由哪几部分构成。首先下面给出了一张图,图中是将这个PS和PL在垂直方向把它剥离出来。
可以发现PS(Processing System)里面是由好多个不同的电路来构成的。大家需要注意的是:虽然说我们说这个PS里面它有这个ARM处理器,但它肯定不仅仅只有ARM处理器,它是以这个ARM处理器为核心,然后它还有很多其他的外围电路,比方说像什么扩展外设了、一些互联接口、包括这个存储器接口等等,它有很多资源,这么多资源就构成了这个PS。可以发现PS有个核心——它就是一个Application Processing Unit (APU),表示一个应用处理单元,APU它实际上是包含了这个ARM处理器以及其他的一些外围电路、比方说像什么一级缓存、二级缓存,包括还有一个内存管理单元等等,那么这些构成了这个APU应用处理单元。
在PS和PL之间有一个高速的互联接口叫做AXI Interfaces,这个接口它提供了一个低延迟、高带宽的性能。
1.2.2 PS部分
首先,可以发现右上角的部分有一个被红框标起来了,它就叫APU,也就是前面所提到过的应用处理单元。然后图中EMIO和ACP上方这条黑色的线段,那么这个黑色线上方就是PS,下方呢就是PL。
下面详细的先来介绍一下这个APU它里面所包含了一些功能单元。上面的图画得不够直观,我们先来看一下另外一幅图:
首先,可以直观地发现这个ZYNQ7000包含了两个ARM处理器,我们知道这个ARN的商业模式就是它只做内核的设计以及IP的授权,ARM公司是不设计和生产这个芯片的。那么他会把这个授权给这个第三方的一个原始制造商,比如说赛灵思。赛灵思拿到了这个ARM的一个内核之后呢,赛灵思去定制一个外电路,从而得到他自己的一个芯片,那么它的最核心的地方就是这2个ARM处理器。
在这个ARM处理器的外围,有一个MMU(内存管理单元)主要是实现这个虚拟地址到实际的物理地址的映射。上面还有一个NEON——实际上是一个媒体处理引擎,可以实现一个单指令多周期这样的一个任务,所以它就比较适合这种数据量比较大的一个场景,比方说针对这个图像和视频的一个处理,它可以实现同时对多组的数据进行一个并行的处理,当然它也适合做一些这种算法类的一个计算,比方说DFT、FFT、FIR滤波。NEON旁边有一个FPU,它是叫浮点运算单元,可以对这个浮点类的运算做一个加速。
接着,在下方可以发现有一个L1(D)和L1(I),是一级Cache的储存器或者缓存器,D是数据缓存,I是指令缓存,这个缓存器的大小是32KB,这个缓存器能够起到很大的作用,可以缓存本地的指令和数据,从而能够降低ARM处理器的一个访问的时间,提高整体的性能,可以发现它离ARM处理器很近。那么为什么叫一级缓存呢,与之对应的还有一个二级缓存。注意:一级缓存每个ARM处理器都有,是相互独立的,但是这个二级缓存它就是两个ARM处理器所共享的。
下面还有一个OCM,大小是256KB,OCM本质就是一个片上RAM,位于PS端可以用来缓存数据
图的中间有一个一致性控制单元(SCU),从这个示意图可以发现,SCU就是用来作为ARM处理器和二级缓存以及OCM做一个桥的连接。除此之外它还可以去初始化以及去访问这个二级缓存。两个ARM处理器要同时访问这个二级缓存的时候,需要这个一致性控制单元(SCU)来进行一个仲裁。除此之外,这个SCU它其实还有一个跟PL端有一个交互的功能,可以管理PS和PL两侧的交互,但图中没有画出来。
好,上面就是根据简图对这个APU的介绍。
回看上面复杂的那个图,可以发现APU里还有一个GIC,GIC是一个通用的中断控制器,可以管理中断。如果大家之前有玩过STM32就会对这个中断比较熟悉(NVIC)。看指向GIC的箭头会发现:这个中断它可以来自于一些IO口外设,也可以来自PL侧IRQ,此外还有没画出来的ARM核内部的中断。
那再往左边有一个SWDT,这是一个系统看门狗,就是当这个系统处于一个异常的时候呢,那么我们可以来就控制这个系统的进行复位。
下面TTC就是一个定时器。
再下面,System Level Control Regs是系统级的一个控制,那么它一般就是用在这个IO外设的一个状态控制。
那再往下面有一个8通道的DMA(直接内存访问),它可以直接访问内存,做一些大量数据搬移的重复性操作。
🔺🔻🔺🔻
红框APU之外,还有其他一些功能模块,简单看一下。
Central Interconnect(中央互联),我们可以把它理解成一个开关,可以管理和直接进行接口通信,那这些互联的一个接口通信呢,它其实都是AXI接口,也就是说不仅仅是PL跟PS之间的接口是AXI接口,在PS内部它很多的接口也都是AXI接口。
那除了这里有一个互联之外,我们可以发现在PC端还有其他的一些互联,比方说这里有一个PL的存储互联(Programmable Logic to Memory Interconnect),还有一个OCM互联(OCM Interconnect),他们其实都类似于开关,可以来管理AXI的通信。
图的上方有一个时钟产生的一个电路(Clock Generation),PS端输入的晶振的频率可能比较低,但是PS端它需要好多不同的时钟,比如说ARM处理器运行需要时钟吧、这些不同的外设运行也需要时钟吧,那么他们很多的时钟都可以通过这个时钟产生电路来输出不同的时钟。
时钟产生电路旁边还有一个复位电路(Reset),这个复位电路肯定就是管理复位了嘛。这个复位可能来自于上电复位、或者是看门狗的复位等等。
然后再往左边,就是一个IO的外设(I/O Peripherals)。PS端供的IO外设非常丰富,可以发现,这里面包含很多,比如USB、GigE、GPIO、CAN、I2C等,可以把这些理解成是一个控制器,它们可以连接外部的外设,比方说像这里的USB控制器,有了这个USB控制器之后呢,我们的开发板就可以实现USB通信。USB下面是两个千兆以太网的一个控制器GigE,所以说PS侧实现千兆网的通信是比较简单的,因为自带了这个千兆网的控制器。再往下面的SD SDIO这个就是一个SD或者是SDIO的一个控制器,那它就可以去连接这个SD外设,对它提供一个驱动,比方说TF卡。再往下面是GPIO,表示一个通用用途的IO,主要是实现一些低速的通信,比方说控制按键啦、控制灯等等。再往下面常用的UART控制器、CAN控制器、I2C控制器、SPI控制器。
I/O Peripherals的左边连接了这个MIO,这个M就是指”负重“的意思,顾名思义,这是一个负重的IO,MIO对外连接到PS端的一个引脚了。通过原理图发现从MIO0一直到MIO53,也即总共提供了54个引脚。这个PS端引脚的灵活性没有PL端那么高。为什么这么说呢?比方说要做一个UART通信,我们把它连接到这个引脚上面去,但是你可能只能选择几组固定的引脚,并不是任意去分配的。
MIO有54个引脚,但有的时候这个引脚不够用怎么办呢?还有一种方法,看图所示可以发现有个向下箭头指向了EMIO,E是扩展的意思,也就是扩展的MIO。若54个引脚不够了,那就通过这个EMIO进行一个扩展,也就是它连接到这个PL端的引脚上面去。注意:就是这个EMIO它过来之后,可以直接连接PL端的一个引脚,它也可以去连接PL端的一些内部模块上。
I/O Peripherals的下面是存储类接口Memory Interfaces,包括SRAM/NOR、ONFI、Q-SPI等可以去连接存储类的一个外设,左侧的SRAM实际上是一个静态RAM。与之对应的,图中右边还有一个Memory Interfaces,这是动态的RAM,或者说是一个动态的存储器接口,它可以连接DDR2或DDR3的一个芯片,那它其实是一个控制器可以去驱动这个动态类的DRAM,所以说这个ZYNQ的7010或7020来说呢,它的DDR3就是位于PS端的引脚,因为它这里有一个DDR的控制器。
图中下部有个小矩形框,里面是DeVC,它其实相当于是一个设备配置电路,它可以对这个PS和PL进行初始化配置,向上就是对PS做配置,向下对PL做配置。然后Config AES/SHA这里除了这个配置接口之外呢,它还可以来去做一些加密,比方说做一个AES或者SHA的加密。这个ZYNQ在启动的时候,它是PS先启动,PS启动之后,它再去配置这个PL端的一个比特流。
基本上这个PS端的一个内部的一些不同的模块呢基本上都介绍完了。
那下面呢我们就来看一下这个PS和PL它都有哪些交互的接口,因为这个ZYNQ它提供了一个无与伦比的性能,除了这个PS本身和PL本身的性能之外呢,那么还需要依赖于他俩之间的一个高速的接口。
那么这个PS和PL交互,除了我们前面所提到的那个AXI接口之外,它可以通过EMIO交互,因为通过EMIO过来之后,它可以连接到PL端的引脚,也可以连接到PL端的一个模块上面。
EMIO右侧有一个XADC,顾名思义就是一个模拟到数字的转换,它可以采集外部的模拟电压,同时它还可以采集这个芯片内部的一些电源,当然它也可以去采集片上温度。PS端是能够方便的去获取XADC的采集值的。
接着还有GP接口(General-Purpose Ports)表示一个通用用途的端口,它可能主要是用于一些中低端的通信,也就说通信速率不是很高的一个场景。
还有一个DMI Sync来去将这个PL端大量的数据搬移到PS。
有个中断IRQ我们前面讲过了。
然后再往右边呢,这有一个HP接口(High-Perormance Ports), 顾名思义就是一个高性能的端口,所以说当有大量数据需要快速搬运的时候就需要用到这个HP接口了。
再往右边还有一个ACP接口,它是加速一致性端口,科以发现它是直接连接到这个SCU,PL端可以通过这个接口直接去访问这个APU里面的缓存。
1.3 PL和PS数据的交互接口
ZYNQ它提供了一个无与伦比的性能,这个性能不仅仅取决于PS和PL本身的性能,还取决于它俩之间的一个交互的接口,如果说交互的接口成为瓶颈的话,那么整体性能也会大打折扣。下面来看一下PL和PS数据的交互接口,当然,这里所说的交互接口主要是指这个AXI的接口。
1.3.1 AXI组成
从上面1.2.2节的那个复杂的图中,其实AXI接口就是指GP接口(General-Purpose Ports)和HP接口(High-Perormance Ports),外加一个ACP接口,AXI主要就是指这三个接口。下面是一个简化的示意图:
左边是PS右边是PL。从这个图中能够看到PS和PL之间的AXI接口。
首先第一个是GP接口,它是一个通用用途接口。可以发现图中上下有两个GP接口,一个是M_AXI_GP,另一个是S_AXI_GP,其中M表示Master,S表示Slave,这里的M和S它是相对于PS来说的,所以说上面的这个Master它就是表示PS作为主机PL作为从机,下面的Slave它是表示PS作为一个从机PL作为一个主机。我们知道一般一个通信接口作为主机和作为从句的区别是什么——主机发起和控制通信总线而从机机呢一般就是被动地响应。总共有4个GP接口。GP接口的数据总线是32位的,一般的应用场景包括中低速的一些场景上。
当我有一个大量数据搬移的情况时,该使用什么接口。这里提供了一个更高性能的接口,就是下面的HP接口,可以看到它是个S表示一个从机,HP接口总共有四个,这四个HP接口都是PS作为从机,PL作为主机。这个HP接口跟上面两个GB接口最大的一个差异在哪里呀——是不是可以发现有个FIFO,用来缓存数据,所以说PS、PL之间在高速传输数据的时候会经过FIFO进行一个缓存,实现一个大量数据的传输。
比方说我们举个例子,有个摄像头位于PL端,我们知道不同的摄像头,它输出的帧率不一样、分辨率不一样,一般摄像头它是每秒钟源源不断地输出大量的数据,而我们要通过LCD屏或者是HTMI屏显示这个摄像头数据的话呢,我们一般是需要对这个数据做一个缓存,也就是说把这个图像数据缓存到内存当中也就是DDR3当中,那对于我们所使用的ZYNQ来说呢,它的这个DDR3是位于PS端的,所以说是需要把这个图像数据搬移到这个PS内的DDR3当中,那么由于摄像头的数据量很大,所以说我们就可以通过这个HP接口进行一个传输。
若传输少量数据,可以通过GP接口进行传输。
这里还有一个接口,就是这个ACP接口,那这个S也是表示PS作为从机PL作为主机。可以明显发现这个ACP接口跟GP和HP最大的区别是什么——可以发现GP、HP其实都是连接到了一个互联Interconnect模块上面去,我们知道这个互联它其实是相当于一个开关,来去控制/管理这个接口上面的数据传输。而ACP接口它是直接连接到APU里面的SCU上,上一小节讲过,这个SCU是一致性控制单元,可以作为这个arm处理器和二级缓存以及OCM的一个桥梁,所以说SCU呢它是比较方便的去访问这个缓存,也就意味着PL端可以直接去访问这个APU里面的缓存。所以说当我们的这个PS端他有一个CPU计算了结果之后呢它可以快速的将这个结果通过ACP接口呢送到PL端,而同时当PL端做一个硬件加速的时候,ACP可以把他的这个加速的结果快速地发送到这个ARM处理器。
下面给出了一个总结的表格
1.3.2 AXI接口介绍
AXI它究竟是什么呢?下面就来介绍一下这个接口。这个AXI它首先是这个AMBA这个接口总线的一部分
AMBA它最早是由ARM开发的,主要是用在这个单片机上面,后面对这个总线进行了扩展和修订,那么现在一般就认为在这个片上系统也就是SOC,就是以这个ZYNQ为代表的SCO上面。除此之外,它其实也应用在SOPC也就是纯FPGA的一个芯片上面。
这个AMBA包含多个接口总线,包括APB、AHB、AXI、ATB。AMBA不断升级,现在是到了4.0版本。那AXI也升级到了AXI4。AXI是一个高级可扩展接口或者说是总线。
AXI4-Full表示一个全功能的接口,它比较适合用在一个中高速的一个通信,支持突发传输,比方说主机在发起这个传输的时候,只需要指定一个起始地址,在指定好起始地址之后,后面的数据就可以源源不断的传输,从机它会自动对这个数据放到相应的地址进行累加,所以说它支持的一个传输速度就比较快。
AXI4-Light表示一个轻量级的接口,那它支持的传输速率就比较低了,不支持突发传输,但占用的资源相对较小。
这里所说的这两个接口AXI4-Full和AXI4-Light它其实都是属于存储器映射接口(什么叫存储器映射接口呢,就说你在发起传输的时候,你需要指定地址)。
AXI4-Stream是一个流的传输,它是不要求指定地址的,所以说是比较适合大量数据的一个高速传输。
1.4 ZYNQ芯片资源介绍
前面学习了ZYNQ7000的PL简介和PS的简介,ZYNQ系列其实是包含很多不同的芯片的,比方说7010、7020等等。那这些不同的芯片,它们之间都有什么样的一个差异呢,本节介绍一下。
1.4.1 Xilinx产品线
好在正式介绍ZNYQ7000系列芯片之前,梳理一下这个赛灵思的产品线,了解了他的一个产品线之后呢,对以后做一个芯片选型的时候还是非常有帮助的。
赛里斯的产品主要分为两类,第一类呢是FPGA,第二类是SOC,它是在这个FPGA基础上加了一个ARM硬核。比方说ZYNQ700系列就属于这个SOC。
1.4.2 ZYNQ核心板
针对ZYNQ7020核心版,它就是一个8Gb的一个DDR3,是通过两个4GB的DDR3组成一个8Gb的DDR3。ZYNQ芯片在设计电路的时候,一般都会有这个DDR3(没有也行,但是很少不配置DDR3的),因为ZYNQ芯片本身它比较强大,不配上大容量DDR3,其芯片内自带的内存相对较小,很多大程序、大数据跑不了,无法发挥芯片本身的强答,比如在ZYNQ上跑Liunx系统的时候,肯定要在这个DDR3当中进行运行的。
上面有一个32MB的一个QSPI Flash,那么这个DDR3、flash、左侧8GB的eMMC,它其实都是连接到这个ZYNQ的PS端的引脚。那么这个Flash它是一个非易失性存储器,就说用来存储数据或者是存储程序用的。下载程序到板子里面之后,断掉电源后程序就丢失了,所以这个程序需要存储到Flash当中,才能够上电之后让程序继续运行。所以说这Flash所干的活,就基本上就是来就存储我们的程序,让它去掉电保存。可以发现这个Flash的大小只有32MB,如果说你运行比较复杂了一个系统,那么存储在Flash可能就不够了。而且这个ZYNQ芯片它所支持的Flash也是有容量限制的。
那么这个时候怎么办呢,我们可以依赖于这个eMMC或者是这个TF卡,像这个TF卡可能就是位于我们的一个底板,我们的核心版呢它是有一个8GB的一个eMMC。对于ZYNQ来说,TF卡和这个Flash可以作为它的一个第一启动,但是eMMC不能作为它的第一启动。比方说你要跑Liunx系统,那么你可以把根文件系统放在这个eMMC当中,然后你的这个启动文件放到这个Flash当中就可以了。其实这个eMMC它的特性跟TF卡非常像,包括他们的一个接口时序、引脚等等。像我们的这个这个ZYNQ驱动eMMC或者是TF卡,它其实都是用的同一个SDIO控制器。那么如果你硬要说TF卡跟这个eMMC有什么差异,可能就是eMMC它是焊接在这个板子上面的(eMMC因为它是一个芯片,它是焊接在板上面的)它更牢固。TF卡它是一个可插拔的,所以说它就没有这个eMMC那么牢固,但TF卡更加灵活。
右侧是一个千兆以太网PHY芯片,是PS端的,这个千兆以太网PHY芯片能够提供一个千兆的以太网的通信,当然他是一个自适应的速率。它是需要接一个RJ45座子的,从而连接网口,那么这个座子是位于底板上面,我们可以通过它去连接其他的带网口的一些设备,做一个千兆网的通信。
除了图中的下载口,底板其实也有一个下载口,就是一个Jtag下载口,这个Jtag的下载口是一个2×7的引脚,虽然说是2×7的引脚,但它有好多引脚都是没有用到的,实际用到的就是6个引脚(TCK、TMS、TDO、TDI、VCC、GND),图中的6Pin下载口它其实是跟这个底板上面的Jtag接口上面的是六个引脚是连接的,你可以通过这个底板上面的Jtag接口给板子下载程序,也可以使用这六个引脚给板子下载程序。
那这里呢就是我们的led灯,各自连接到PS和PL端,无论是连接到PS还是PL,它都是高电平点亮低电平熄灭,控制的比较简单。
再往右边是一个电源的一个led指示灯,板子上电之后这个电源指示灯就会亮的,那么如果不亮的话呢,那么一方面可能是前面供电有问题,另一方面可能就板卡有问题了。
PS和PL端各有一个晶振,提供时钟。
下面有一个PL端的一个配置完成指示灯,注意它是PL配置完成指示灯,当你烧录完PL的程序之后,这个灯就会点亮,所以说可以通过这个灯的状态去判断PL的程序有没有下载成功。
那最上面这里就是PS的一个复位按键,注意它是连接到PS的复位按键,那么PL的复位按键在底板上面。
1.5 ZYNQ开发软件安装
① Vivado软件;
② Modelsim软件;
2 FPGA开发初体验
2.1 FPGA开发流程
2.2 FPGA工程文件管理
以一个简单的Led工程为例,如下图所示:
这里是将整个文件夹分为四个文件夹。最外层的文件夹Led就是我们要做的一个项目的名称,本次实验呢要实现点亮灯这样的一个功能,所以说这个大的文件夹就叫Led。
那它里面会包含四个子的文件夹,第一个是DOC,从它的名称就能够知道这个doc文件夹存放的是一些文档,比如可以放项目需求书、一些芯片的数据手册,包括后面要绘制波形图、绘制的一个系统框图等,都可以放在这呢doc文件夹下面。如果说你有其他的一些说明文档啦什么的,也可以放在这个doc下面。
第二个RTL,是一个寄存器传输级,其实就是放我们编写的Verilog代码的,因为Verilog代码它描述的就是寄存器传输级的电路,所以说给它起名叫RTL,就是要存放编写好的代码。
第三个Sim,是用来仿真的,做仿真的时候是需要编写test bench文件的(是一个激励文件),后面会进一步说明。把我们的激励文件,包括仿真过程当中所产生的一些中间文件,包括一些波形文件都会放在这个sim文件夹下面。
第四个Prj,是工程的意思,就存放跟我们的开发工具所相关的一些文件,比方说我们创建工程会有一个文件,然后编译也会产生一些文件,包括一些约束相关的文件,都会放到这个Prj中。
如果个人有一些其他需求,可以自行定制。
2.3 手把手教你点亮Led灯
2.3.1 需求分析
”点亮LED灯“实验是使用开发板上的按键KEY来控制LED的亮灭。当按键没有被按下时,LED灯保持常灭;当按下按键时,LED灯被点亮。
我们现在的这个需求应该是明确了,可能还要去明确一下板子上它可能有很多不同的按键和灯,那如果说你是这个核心板加底板的一个结构,你可以用底板的这个KEY0和LED0。
2.3.2 系统设计
因为这个项目太简单了,所以没有必要进行系统设计。
2.3.3 硬件选型
用自己所使用的卡板。因为本次实验它涉及到硬件电路部分可能就是这个按键和灯,那么这个按键和灯它会影响到我们后面编写代码的逻辑,比方说这个按键是按下设为高电平还是松开为高电平,包括灯是输出高电平点亮还是低电平点亮,这些都会影响到编写代码,所以说有必要看一下开发板和板载的按键和灯的一个原理图的:
根据原理图可以看到,按键KEY0没有按下的时候是被上拉成一个高电平,然后当这个按键按下之后,那么就会被GND拉到低电平。
下面是Led灯的原理图:
2.3.4 绘制系统框图
绘制系统框图也是整个实验任务的一个梳理的过程,要划分为哪几个模块,模块有哪些端口,这些端口它是怎样的一个流向,怎样的一个关系等等。
首先,打卡思维导图的软件梳理整个过程。
然后,打开Visio图绘制系统框图、功能示意图。
2.3.5 绘制波形图
同样,使用Visio绘制波形图
2.3.6 编写RTL代码
编写RTL程序,保存到RTL文件夹下。
1 | // 文件名:led.v |
编写testbench,因为与仿真有关,所以放在sim文件夹下。
1 | // 文件名:tb_led.v |
2.3.7 软件仿真
使用Modelsim对代码进行仿真。
第9.3讲点亮LED灯- Modelsim仿真 - 正点原子 - 哔哩哔哩
2.3.8 新建工程
使用Vivado软件。
(一)新建工程
增添编写好的rtl文件:
添加约束文件:
器件选型,我使用的板卡为:ZYNQ-7020:XC7Z020CLG400-2
点击Next后,再点击Finish就完成新工程的创建了,进入下面的页面:
(二)分析
分析结果如下:
(三)工程约束
工程约束分两部分:一是引脚约束,二是时序约束。
- 引脚约束
首先,引脚约束简单说就是:把代码的输入输出端口与芯片的实际引脚绑定。如下图进入I/O Planning界面:
生成的约束语句:
- 时序约束
由于本次简单实验不涉及时序,所以就先不演示了。
为什么要做时序束呢?因为程序它毕竟跟硬件电路是相关的,所以说要考虑到整个电路的延迟,包括布局布线等等,所以说是需要做一个时序约束的。如果不做时序约束,那么有可能即使你仿真是对的,但是结果不一定对。
(四)综合
上图中,法2相当于直接去生成比特流文件,也就是我们的下载文件,那么它会去执行综合的过程和实现的过程。那么它现在处于的一个阶段,可以发现就是一个综合的阶段,软件界面会有一个转圈动画,表示它正在执行这个综合。虽然说这个代码比较简单,但其实它编译呢还是比较慢的。
完成后有一个弹窗,就是选择我们下一步要做的一个操作:你是选择打开实现后的设计呢还是选择打开这个硬件管理。我们可以先取消不选,因为我们后面还可以打开。
完成后,可以打开这个综合后的设计来看一下,如下图:
图中可以看到,输入有一个IBuffer,输出有一个OBuffer。这里解释一下,其实这个引脚它进入到FPGA,它是要先经过一个IBuffer的,这个是不用我们代码里面去编写,软件会自动帮我们添加。我们可以把这个IBuffer理解成是这个输入的端口进入FPGA内部的第一道门,所以说每个输入信号都是要加这个IBuffer的,工具会自动帮我们添加。那么这里OBuffer也是同理。
(五)实现
我们再来看一下这个实现后的一个设计。
(六)上板验证和调试
现在已经生成了比特流,那么接下来就可以进行上板验证和调试。拿出我们的开发版,就先以领航者的卡板为例呢来去演示一下,参考下面的视频连接:
需要注意的是,程序断电之后再上电程序就丢失了,这个是为什么呢?因为程序你要想断电保存,那么它肯定是要存储在非易失性存储器当中,比方说Flash、SD卡类似于这种。那么ZYNQ它的这些非易失性的存储器它是连接到了PS的引脚,所以说对这个FPGA设计来说呢,是没有办法单独的去固化这个比特流,必须要通过PS才行。那关于这个固化,大家可以参考ZYNQ的ARM裸机相关的一个开发指南。
总之,ZYNQ是没有办法单独固化PL端的程序的,这个需要注意。
3 Verilog语言
3.1 Verilog简介
3.1.1 什么是FPGA
什么是FPGA,可能有的人说这个问题非常简单,FPGA就是现场可编程逻辑门阵列,实际上这个回答对于初学者来说呢,更加的不知所云。所以先从一个简单的一个角度去回答这个问题。
(一)数字电路基础知识
- 模拟电路
- 工作在模拟信号下的电子电路;
- 模拟信号在时间和数量上的变化都是“连续”的。
- 数字电路
- 工作在数字信号下的电子电路;
- 数字信号在时间和数量上的变化都是“离散”的;
- 迄今为止,大多数的大规模和超大规模集成电路都属于数字电路。
(二)可编程逻辑器件
早期生产的数字集成电路逻辑功能都是固定不变的。要想改变它的逻辑功能,就必须改变内部各单元电路之间的连接,而这种连接在集成电路制作过程中已经固定下来了。
可编程逻辑器件(Programmable Logic Device,PLD):允许用户自行修改内部连接的集成电路。
PLD内部的电路结构可以通过写入编程数据来设置,写入PLD的编程数据还可以擦除重写,PLD的逻辑功能可以由使用者通过编程来设定(比如说今天把它配置成一个音频的编解码芯片,明天再把它配置成图像的编解码芯片)。
常用的可编程逻辑器件:
- CPLD:复杂可编程逻辑器件(Complex Programmable Logic Device)
- FPGA:现场可编程门阵列(Fiel Programmable Gate Array)
- 二者本质差异:电路结构不同——CPLD是基于“乘积项”的与或逻辑阵列,FPGA是基于“查找表”的CLB阵列。
(三)什么是FPGA
一种可通过编程来修改其逻辑功能的数字集成电路(芯片)
与单片机的区别?对单片机编程并不改变其电路的内部连接结构,只是根据要求实现的功能来编写运行的程序(指令)。而我们对FPGA进行编程的时候,实际上是在配置FPGA的内部硬件电路。
3.1.2 什么是硬件描述语言HDL
上图中,综合工具指的就是EDA软件工具。
3.1.3 什么是Verilog
(一)常见的硬件描述语言
主要就两种:Verilog HDL和VHDL。
美国的逻辑电路设计和制造厂家大都以Verilog HDL为主,在中国很多高校和集成电路设计公司也都采用Verilog HDL。我们推荐大家学习Verilog。
(二)Verilog简介
Verilog的发展简史
- Verilog语言最初是于1983年由Gateway Design Automation公司为其模拟器产品开发的硬件建模语言。Verilog语言于1995年成为IEEE标准,称为IEEE Std1364-1995,也就是通常所说的Verilog-95。Verilog-2001是对Verilog-95的一个重大改进版本。
Verilog和C的区别
- Verilog是硬件描述语言,在编译下载到FPGA之后,会生成电路,所以Verilog是并行运行的。
- C语言是软件编程语言,编译下载到单片机之后,是存储器中的一组指令。而单片机处理软件指令需要取指、译码、执行,这个过程是串行执行的。
- Verilog和C的区别也是FPGA和单片机/CPU的区别。FPGA由于全并行处理,处理速度非常快,这个是FPGA的最大优势,这一点是单片机/CPU替代不了的。
- Verilog学习方法
- Verilog HDL作为一种高级的硬件描述语言,它的很多语法现象与C语言非常相似,因此在C语言的编程基础上去学习Verilog非常容易。
- 但在学习时中要培养硬件设计的思想,把新概念与硬件结构联系起来,着重理解Verilog“并行”特性。
3.2 Verilog基础语法
3.2.1 基础知识
(一)逻辑值
- 逻辑0:表示低电平,也就对应我们电路GND;
- 逻辑1:表示高电平,也就是对应我们电路的VCC;
- 逻辑X:表示未知,有可能是高电平,也有可能是低电平;
- 逻辑Z:表示高阻态,外部没有激励信号,是一个悬空状态。
(二)数字进制格式
Verilog数字进制格式包括二进制、八进制、十进制和十六进制。一般常用的为二进制、十进制和十六进制。
- 二进制表示:
4'b0101表示4位二进制数字0101; - 十进制表示:
4'd2表示4位十进制数字2(二进制0010),注意前面的4不是指4个十进制数; - 十六进制表示:
4'ha表示4位十六进制数字a(二进制1010),注意前面的4不是指4个十六进制数;
举例:16'b1001_1010_1010_1001=16'h9AA9。
注意:如果直接给出一个数字,比如100,则表示32位的十进制数字100。
(三)标识符
标识符(identifier)类似于C语言的变量名,在Verilog中的标识符不仅可以用于定义变量,还用于定义模块名、端口名、信号名等。
标识符可以是任意一组字母、数字、$符号和(下划线)符号的组合,但标识符的第一个字符必须是字母或者下划线;标识符是区分大小写的。
- 标识符推荐写法
- 建议1:不建议大小写混合使用;
- 建议2:普通内部信号建议全部小写;
- 建议3:信号命名最好体现信号的含义,简洁、清晰、易懂;
- 以下是一些推荐的写法:
- 1、用有意义的有效的名字如 sum、cpuaddr等;
- 2、用下划线区分词,如cpu_addr;
- 3、采用一些前缀或后缀 比如时钟采用clk前缀clk_50,cIk_cpu。
3.2.2 数据类型
在Verilog语言中,主要有三大类数据类型:寄存器数据类型、线网数据类型和参数数据类型。从名称可以看出,真正在数字电路中起作用的数据类型应该是寄存器数据类型和线网数据类型。第三个参数数据类型实际上是给编译器识别的。
(一)寄存器数据类型
寄存器表示一个抽象的数据存储单元,通过赋值语句可以改变寄存器储存的值。
寄存器数据类型的关键字是reg,reg类型数据的默认初始值为不定值x
1 | reg[31:0] delay_count; // 延时计数,定义了一个名delay_count的位宽是32位的寄存器 |
reg类型的数据只能在always语句和initial语句中被赋值- 若该过程语句描述的是时序逻辑(即always语句带有时钟信号),则该寄存器变量对应为触发器;
- 若该过程语句描述的是组合逻辑(即always语句不带有时钟信号),则该寄存器变量对应为硬件连线。
(二)线网数据类型
线网数据类型表示结构实体(例如门)之间的物理连线。不同于寄存器数据类型的变量,线网类型的变量不能储存值,它的值是由驱动它的元件所决定的。
驱动线网类型变量的元件有门、连续赋值语句、assign等。如果没有驱动元件连接到线网类型的变量上,则该变量就是高阻的,即其值为Z。
线网数据类型包括wire型和tri型,其中最常用的就是wire类型。
1 | wire key_flag_1; // 定义了一个1位(默认值)的线网数据类型 |
(三)参数数据类型
参数其实就是一个常量,在Verilog HDL中用parameter定义常量。我们可以一次定义多个参数,参数与参数之间需要用逗号隔开每个参数定义的右边必须是一个常数表达式。
个人感觉,有点类似于c语言的”宏定义“。
1 | //parameter define 4.3RGB LCD |
参数型数据常用于定义状态机的状态,数据位宽和延迟大小等。
采用标识符来代表一个常量可以提高程序的可读性和可维护性。在模块调用时,可通过参数传递来改变被调用模块中已定义的参数(类似于c语言函数调用的时候参数传递的概念)。
3.2.3 运算符
- Verilog中的操作符按照功能可以分为下述类型:
- 算术运算符:
+、-、*、/、% - 关系运算符:
>、>=、<、<=、==、!= - 逻辑运算符:
!、&&、|| - 条件运算符:
?:(c语言中的三目运算符) - 位运算符:
~、&、|、^ - 移位运算符:
<<、>> - 拼接运算符:
{},例如:c={a, b[3:0]}把a和b的3到4位拼接一起,付给c。
- 算术运算符:
3.3 Verilog 程序框架
3.3.1 Verilog注释
Verilog有两种注释方式,分别是//和/**/,和c语言完全一样。
3.3.2 Verilog关键字
- 关键字大全
- 常用关键字
3.3.3 Verilog程序框架
(一)模块的结构
Verilog的基本设计单元是”模块”(block),一个模块是由两部分组成的:一部分描述接口,另一部分描述逻辑功能。
对比C语言,C语言中基本的功能单元叫做函数,也就是说C语言是由一系列函数搭建起来的。与此类似,Verilog的基本单元叫做模块。
上图中的左侧图就表示了这个模块的一个接口,可以看到左边是这个模块的一个输入接口(a和b),右边有这个模块的输出接口(c和d)。
然后看一下右侧这个图,它代表了这个模块的功能定义部分,如果说这个模块是一个可综合的,那么它就会综合出来这样的一个电路结构。在这里呢,我们提到“可综合”的一个概念,对于这个Verilog来讲,它作为硬件描述语言,“可综合”就代表着所写的一个Verilog语句能综合得到一个类似于右侧图这种的一个由门级结构组成的电路网表。也就是说可综合的模块最终是能够生成一个物理的电路的。
有可综合的模块,就有不可综合的模块也即不能够对应实际的电路。比如说给模块做一个测试文件,也就是我们常说的仿真文件,在仿真文件里面,就可以用一些不可综合的语句。虽然是不可综合,但是仿真工具是能够识别它的。
上图是可综合的,在这里这个模块它会综合出来右侧图这样的一个电路结构,在这个电路结构里面可以看到:首先这个输入端是a和b,输出端是c和d。这个电路就是在内部生成了两个门电路,第一个是一个或门,是把a和b进行一个或的操作,最终结果输出给c。第二个是一个与门,就是把a和b进行相与,结果输出给d。
从这里也可以看出来,这个模块分成了两个部分,一部分描述接口,另外一部分描述功能。这个就是Verilog的一个基本单元模块的一个示例。
(二)程序框架
下面来看一下完整的一个Verilog程序,下图流水灯实验的一个程序,借这个程序介绍一下Verilog的程序结构。
首先同样是这个model和endmodel之间它是这个模块的开头和结尾,然后在这个model后面,紧跟的是流水灯模块,名叫做flow_led。
接下是这个模块的端口定义,在这个模块里面系统时钟信号clock还有系统复位信号reset都是输入信号。然后有一个输出信号叫做led,指定了这个输出信号的位宽是四位。要注意,这里还有一个reg,表示这个输出端口它是一个寄存器类型的一个端口,在前面讲了,如果不指定数据类型的话,默认这个端口类型都是wire,也就是线网类型的。但在这里,若需要把它设置成一个reg类型的,就需要在它的位宽前面,给它添加一个关键字reg就可以把它定义成一个寄存器类型的。
在端口定义完之后,有一个变量的声明(Verilog程序除了端口定义、IO说明以及功能定义之外,它还有一个叫做内部信号声明的部分,这个内部信号指的就是我们内部所使用的一些变量),比如说在这里就使用了一个变量,名字叫做counter,顾名思义counter也就是计数器,它的位宽是24位,它的数据类型是reg型的,
接下来是这个程序的主体部分,也即这个程序的功能部分,首先可以看到它分成了两部分,叫做always语句,图中可以看到这个always的语句前面有一个注释——用以说明这个语句的功能是利用计数器来对这个系统时钟计数,从而达到计时0.2秒的目的。(这个always的语句这里先不给大家介绍)
然后来看第二个always语句,同样给出了一个注释——说通过移位寄存器来控制IO口的高低电平,从而来改变led的显示状态。
这两个always语句就是实现这个流水灯的功能,需要注意的是,在这个Verilog模块里,定义一个功能有三种方法:① assign语句,也就是前面所讲的功能描述方式,注意assign语句描述的是组合逻辑;②always 语句,也就是这个流水灯模块里面中用到一个语句,always 语句它既可以用来描述组合逻辑,也可以用来描述时序逻辑。③ 可以通过例化一些实例的原件,图中右侧举的例子and #2 u1(q,a,b),and是Verilog的关键字之一,它表示Verilog的一个“与门”实例原件,可以通过例化它实际的一个元件,从而来实现功能,比如说这里它就例化了一个与门,这个与门的输入输出分别在这个括号里面给列出来了。
不论用什么功能的定义方法,不同功能它们之间是并行的。所谓的并行是什么意思呢?并行是我们在学习Verilog语言的时候需要特别关注的一点,图中左边流水灯模块里有两个always 块,它们在代码中的先后顺序是无所谓的。
- 对比:
- 在C语言里面,程序是顺序执行的,如果有两个语句,那么这两个语句谁在前谁先执行,谁在后呢谁后执行;
- 但是对于Verilog是不一样的,两个
always块是并行的,没有先后顺序。- 注意:
- 在
always块里面,逻辑是顺序执行的,多个always块之间是并行的。
(三)模块的调用
什么叫模块的调用?有点类似于c语言里面函数的调用。在模块调用的时候,信号可以通过模块的端口,在模块之间传递。
举一个静态数码管的例子,如下图所示:
静态数码管有一个顶层模块,图中可以看到顶层模块名后面有个后缀top,就表示它是一个顶层模块,在这个顶层模块里面就调用了其他的模块。这种模块的调用也把它叫做例化,就是说我们会例化一个其他模块,比如说在这个代码里面例化了一个叫做time_count这样一个模块。
模块的例化非常简单,首先来看一下time_count这个模块它的端口定义,图中右上部分子图列出了time_count这个模块的端口以及里面的一些变量。这个模块有两个输入信号和一个输出信号,里面还定义了一个parameter参数类型的一个常量,在调用这个模块的时候就首先要把这个模块的模块名给列出来。然后要给这个例化的模块重新起一个名字,叫做u_time_count,可以同时例化多个相同的模块,在多个相同的模块之间就要分别给它们不同的名称,图中只例化了一个。
插一嘴,讲到这里,感觉越来越想面向对象编程中的实例化一个对象。
接下来,还需要把模块的端口给连接起来,可以看到,在u_time_count下面有一个点加一个信号的一个方式,比如.clk、.rst_n、.flag,这个点就代表端口的含义,而这个信号呢就是被例化模块里的一个端口信号。然后需要把这个端口呢一一对应地连接起来,也就是说可以通过这种方式,把这个底层模块的信号传递到上层去,也可以把上层的信号传递到底层模块里面去,对于.clk、.rst_n来讲,因为它们是time_count模块的一个输入信号,所以把它们与上层模块(也即顶层模块)的时钟sys_clk和复位sys_rst_n连接起来。
这就是信号通过模块的端口在模块之间传递的一种方法,时钟和复位是输入信号,而time_count还有一个输出信号,也就是flag,那么要把这个输出信号传递到顶层模块里面,用了一个变量add_flag,也就是说把这个模块的一个输出信号连接到了上层模块的一个wire类型的一个变量。
注意点1,在模块的输入端,所传递进去的信号可以是
wire类型的,也可以是reg类型的。但这个模块的输出端,必须把它连接到一个wire类型的一个变量上面去,也就是说这里如果这个add_flag它是一个reg类型的变量,那么这个编译器就会报错。注意点2,在模块调用的时候,用于传递信号的这些变量的位宽必须保持一致,比如说这里这个
flag它的位宽1,那么传递出来的变量add_flag也是1位。
除此之外,在图中右下角,列出了另外一种端口连接方式,在这个连接方式里面并没有用点加一个信号名的方法去连接端口信号,而是直接把上层的信号列到了这个模块的那个端口名里面去。
这里要注意:如果使用这种方法,它必须严格按照这个被调用模块的一个信号名进行排列。如果这个顺序搞反了的话呢,那么这个模块它的一个端口连接就连接错了。
另外还要注意一点,可以看到在这个模块调用的时候呢,这里有一个#号这种写法,然后#号后面它有一个点加一个参数,后面也有一个参数。这个实际上是一种参数传递的一种方法。首先这个点(.MAX_NUM)在这里并不是说是一个端口,而是一个参数。因为可以看到在这个time_count子模块里面,定义了一个变量叫做MAX_NUM,那么在调用这个模块的时候,可以给这个参数传递一个新的值,而传递这个值的方法就是通过这种点加一个参数名的一个方法,而这个右边那个括号就是传递给它的一个新的值。
最后来看一下这个数码管静态显示模块生成的电路的原理图:
这个图最左边是顶层模块的输入信号,最右边是顶层模块的输出信号。然后在这个顶层模块里面,可以看到例化了两个子模块,第一个子模块是time_count,第二个子模块是seg_led_static(没有给出代码)。
从图中还能看出模块之间的信号连接,比如说time_count模块的输出信号flag就连接到了seg_led_static模块的add_flag端口。通过这样的一个图可以很方便地看出模块之间的信号传递关系,包括模块之间调用的一个关系。
3.3.4 Verilog高级知识点
(一)结构语句
- initial语句介绍
测试文件也就是常说的仿真文件,更常见的叫法是test bench。右侧子图是一个仿真文件。
这个begin和end相当于给C语言中大括号的形式,就把很多语句组合成一个大的语句。首先使用<=非阻塞赋值给时钟信号sys_clk赋了一个初值,然后给复位信号sys_rest_n赋了一个初值,这两个初值都是零,接着给touch_key信号赋了一个零的初值。在上电之后会立刻执行这三个语句,因为这三个语句之间没有延时,所以这三个语句是同时执行的。
接下来可以看到这里呢有个#20,这就表示延时20个单位的时间,延时之后把这个sys_rest_n信号给赋值为1。接下来也都是延时赋值的操作。这个目的就是为了产生下面的仿真信号。下面的波形图是Modelsim软件仿真过程中所截下来的一个图。前面这三个是模块的输入信号,最后led是输出信号,仿真的目的就是利用仿真文件,也就是利用这个test bench,去产生这个模块所需要的一个输入,然后去观察这个模块的一个输出,是不是按照我们所描述的一个功能来的。
- always语句介绍
然后呢再来看一下下面一个always语句,这个英文单词的含义大家应该比较熟,就是“一直”的意思,就代表着这个语句它是一直在不断的重复活动,也正是因为always语句是一直在重复活动,所以它只有跟一定的时间控制结合起来才能有用。
什么叫做时间控制呢?图中有个语句always #10 sys_clk <= ~sys_clk,这个always语句反复对时钟信号进行取反。在前面可以看到有一个延时十纳秒,这个延时就把它叫做“时间控制”。有了这个延时,这个语句才有一个真正的一个含义,就是说延迟十纳秒之后,这个sys_clk它取一次反,然后再延迟十纳秒,再一次取反,一直不断。这样才能生成在这个仿真波形图里面看到的时钟信号。
若没有这样的一个延时的话呢,写一个always它一直等于它的取反,这样的一个语句是没有任何含义的。在任何一个时刻呢一直在取反,那么它就会陷入一个死循环里面去。
接下来,再来看一下always语句更复杂的一个用法。
首先,always语句它的时间控制有两种,① 可以是边沿触发,② 也可以是电平触发。举个例子,就比如说在下图中,有一个always语句,这个奥拉瓦的语句中就用的是一个边沿触发的方式。
图有一个关键字posedge代表这个上升沿,后面还有一个negedge代表下降沿。看到同时有上升沿和下降沿的触发方式。那么在这个两个触发信号之间呢,它就需要用一个关键字用or来进行连接。由关键词or连接的多个事件名或信号名组成的列表称为“敏感列表“。只有这个括号里面的触发条件被满足,这个always后面所跟的这个语句才会执行(也就是begin和end之间)。
边沿触发的always块常常描述时序逻辑行为。
要注意,initial和always这两个关键字后面所跟的块语句,是按顺序进行执行的,又称为”顺序块语句“,还被称为”过程块”。
电平触发的always 块常常描述组合逻辑行为。
如果组合逻辑块语句的输入变量很多,那么编写敏感列表会很烦琐并且容易出错。如下图所示,推荐使用@(*)的方式,@(*)表示对后面语句块中所有输入变量的变化都是敏感的。
临时插入知识点:组合逻辑电路 VS 时序逻辑电路
- 组合逻辑电路:任意时刻的输出仅仅取决于该时刻的输入,与电路原来的状态无关。
- 时序逻辑电路:任一时刻的输出不仅取决于当时的输入信号,而且还取决于电路原来的状态。或者说还与以前的输入有关,因此时序逻辑必须具备记忆功能,也就是说时序逻辑电路中必须有存储功能的电路。
(二)赋值语句
Verilog中赋值有2种方式:阻塞赋值=和非阻塞赋值<=。
- 阻塞赋值
RHS是指=右侧的表达式,LHS是指=左侧的表达式。
- 非阻塞赋值
通过对比两幅图下面的波形图可以发现两种赋值方式的差别。注意图中说的“非阻塞赋值只能用于对寄存器类型的变量赋值”。
小结:
阻塞赋值用=顺序执行,非阻塞赋值用<=并行执行,前者适合组合逻辑,后者适合时序逻辑。
- 符号和执行方式有啥不同:
- 阻塞赋值:用等号=表示,语句按代码顺序一条一条执行,当前语句执行完才执行下一条。
- 非阻塞赋值:用小于等于号<=表示,所有语句同时计算右侧值,在当前时间步结束时一起更新左侧变量。
- 分别用在啥场景:
- 组合逻辑电路:用阻塞赋值
=,信号传播是即时的,不需要时钟沿触发。 - 时序逻辑电路:用非阻塞赋值
<=,状态变化在时钟沿控制下同步进行,能避免竞争冒险问题。
- 组合逻辑电路:用阻塞赋值
- 混用会有啥问题:
- 同一个 always 块里不要混用两种赋值方式,否则时序不容易控制,仿真和综合结果可能不一致。
- 不允许在多个always块种对同一个变量进行赋值。
- 简单记法:时钟沿触发的 always 块用
<=,电平敏感的 always 块用=。
(三)条件语句
if-else语句
1 | if(条件表达式) |
条件语句必须在过程块中使用。过程块语句是指由initial和always语句引导的块语句。
if和else后面的操作语句可以用begin和end包含多个语句(简单说,begin和end相当于c语言中的大括号)。
允许if语句的嵌套。
case语句
3.3.5 状态机
(一)状态机概念
在介绍这个状态机的概念之前呢,首先要知道一点,就是在什么情况下要用状态机。来看一个例子,比如说现在手上有个项目,这个项目就是用FPGA去实现一个电子门锁,输入密码xxxx(8023),然后门才能打开。这是一个功能非常简单的一个小项目,那么用Verilog应该如何实现它呢?
可能有的同学会说这个简单嘛,只需要用if else先判断第一个输入的是8,然后再判断第二个是0,这样依次判断就可以实现这四个数的一个检测了。但实际上这个说起来容易,做出了就不是想象中的了。用if else实现的这个功能抽象出来实际上就是一个序列检测器,就说需要检测8023这样的一个数字序列,如果用if else的话呢,这个if else呢将会非常长(据说这是一个非常长的一个语句),可能if else之间的嵌套会把我们搞的这个晕头转向,同时这个代码的可读性也不是很强。
那么这时候就可以尝试用状态机这样的一个概念去完成这个功能。状态机非常适合在Verilog里面实现一些顺序逻辑,所谓的顺序逻辑就像密码锁这样的按顺序依次检测的情况。
在这种顺序执行的这个应用里面呢,Verilog其实是不是很擅长的,我们知道这个C语言是顺序执行的,C语言是非常擅长这种顺序操作的。但是对于Verilog并行语言来讲,他在执行这个顺序执行的功能的时候就有一点这个力不从心。
不过在这个时候,就可以借助状态机去完成这样的一个功能。
来看一下上图,这个图是我们根据要求抽象出来的一个图,在这里呢定义了一些状态,然后通过这个逻辑转换图展示设计的思路。从这个图里面可以非常清晰明了地观察出这个设计的思路非常简单,就是说在各个状态之间根据这个按下按键进行跳转。
那么这个状态跳转所需要的条件是什么呢?首先它要判断外部的一个输入,就是按下的按键值,其次还要根据当前的一个状态综合进行判断。也就是说这个状态的跳转它不仅与输入有关,它还与当前的状态有关。这个图叫做状态转换图。
从这个小项目的一个分析里面总结出状态机的概念:状态机是指在有限个状态之间按照一定规律转换的时序电路。必须强调它是有限个状态,而且它是按照一定的规律转换,最后呢它是一个时序电路,这个时序电路就把它叫做状态机(State Machine)。因为它是在有限个状态之间进行跳转的,所以把它叫做有限状态机(Finite State Machine),常见的是其缩写——FSM,见到FSM的话,它指的就是我们常说的有限状态机。
(二)状态机模型
什么叫状态机的模型呢?状态机的模型指的就是这个状态机抽象出来的结构,看一下下面这个图:
在这个图中可以看到有三个方框,其中两个是灰色的代表组合逻辑,第一个组合逻辑把它命名为F,第二个组合逻辑呢把它命名成G。
然后中间有一个黄色的区域,它是一个状态寄存器。状态寄存器是由一组触发器构成的,用来记忆状态机所处的一个状态。前面举例的密码锁的5个状态,这5个状态实际上就是由一组状态寄存器来存储的。把这个状态寄存器叫做状态寄存器。而状态的改变只发生在这个时钟的一个跳变沿,因为这是一个触发器或者说应该是一个D触发器,所以这个状态寄存器它的值的改变只有在时钟跳变沿出现的时候这个状态寄存器。
状态寄存器的输入是什么呢?图中可以看到输入就是激励信号,这个激励信号是由组合逻辑F产生的一个信号,而这个组合逻辑F的功能就是用来产生下一个状态的,图中有两个箭头指向F,上面是外部的输入,下面是当前的状态。状态是否改变以及状态如何改变,都取决于这个组合逻辑F的一个输出,而这个F就是当前状态以及输入信号的一个函数。
还有一个组合逻辑G,它有一个输出,这个输出就是这个状态机最终的一个输出结果。状态机的输出是由这个输出组合逻辑G来提供的。G也是当前状态和这个输入信号的一个函数。但是G的输出也可以只由当前状态决定。这样就根据G的输入,把状态机划分成了2种:
实际上,Moore状态机它是Mealy状态机的一个特例。
(三)状态机设计
设计装载机的方法做四段论。有点类似古代的八股文的一个写法,就说严格按照一个流程就可以写出来一个完整的一个装载机。
简单说四段论就是:首先要定义一个状态空间,然后要看一下不同状态是如何跳转的,再接着要判断下一个状态,最后是各个状态下的动作。
- 状态空间的定义
举一个例子,我们把一天的生活分为四个部分,睡觉SLEEP、学习STUDY、吃饭EAT、娱乐AMUSE。所有这些状态的一个集合就叫做状态空间。那么就需要对四个状态进行编码,因为Verilog只能处理二进制数,所以要把这四个状态用二进制数量去表示,这就是所谓编码。
对于这个例子来讲,这个状态空间就只有四个状态,接下来看一下这个状态空间它是如何进行定义的。
首先用parameter这个关键词去定义一个参数,这个参数的名字尽量有实际意义。在参数定义的右边,用一个二进制数表示这个参数,比如说这里把SLEEP定义成0,STUDY编码为1,EAT是2,ANUSE是3。
在这个定义完这些参数之后呢,又定义了两个寄存器类型变量。第一个寄存器叫做current_state,是存储当前的状态,第一个寄存器叫做next_state,是指下一个状态。也就是说在状态空间定义的时候呢,不仅要定义参数,而且需要定义两个寄存器,分别用来保存当前状态以及下一个状态。
图中右侧提供另外一种编码的方式——独热编码。独热码它的一个好处是它的译码逻辑比较简单,而对于Verilog来讲,它最终生成的一个电路结构也会比较简单,所以推荐在状态机里面使用这种编码方式。
这里再提一下,就说用了独热码,那么表示四个状态它就需要用一个四位的二进制数,那么与此同时定义的这个状态寄存器,包括current_state、next_state的位宽同样是需要与这个状态空间的编码方式保持一致的,也就是4位。
- 状态跳转(时序逻辑)
状态的跳转这部分代码非常简单,如下图所示:
这里只有一个always语句,这是一个典型的时序逻辑。前面的图中有两个灰色的组合逻辑合一个黄色的时序逻辑,所以编写的这个状态机也应该由两个组合逻辑部分和一个时序逻辑部分所组成的。状态的跳转这部分它属于时序逻辑。
这个时序逻辑里面首先它的这个敏感列表里面有一个时钟信号还有一个复位信号,一旦时钟的上升沿到达或者复位信号的下降沿到达,那么就要执行一次下面的代码。
注意:reset_n是一个复位信号,后面的n表示这个复位是低电平有效。
一旦reset_n为零的时候,也就是说满足if语句是第一个条件,这个系统处于一个复位状态,在复位的时候要给这个current_state寄存器赋一个初始值,对于我们这个例子来讲,这个初始值应该是睡觉。然后在其他的时候就要把这个next_state也就是下一个状态复制给current_state。
注意:在这个赋值过程中,要使用非阻塞赋值。
- 下个状态判断(组合逻辑)
下个状态判断属于组合逻辑,对应的是第一个灰色区域组合逻辑F。
首先既然作为一个组合逻辑,那么它的敏感列表里就应该是电平触发的方式。
这里来看一下下一个状态是如何进行判断的。在这里最重要的就是case语句。这里这个case语句它的判断条件是这个current_state,然后在前面讲过,下一个状态是由当前状态合外部输入信号共同决定的,所以这个case语句就首先判断了当前处于哪一个状态,比如说当前如果处于睡觉状态,那接下来还需要根据外部输入信号去判断下一个状态应该是什么,这里这个外部的输入就是闹钟叫做clock_alarm,也就是说一旦闹钟信号响了,下一个状态就应该是学习状态,如果闹钟没响,下一个状态就还是接着睡觉。
这就是下一个状态的一个判断,这是一个组合逻辑的写法,所以用的是阻塞赋值。
注意:if else要配对,所谓的配对也就是说有一个if语句就应该有一个对应的else语句。它这样的好处是可以避免锁存器(latch)的产生。在这个数字电路里面,latch它是一个电平触发的存储器,而触发器是一个边沿触发的一个存储器。对于这个Verilog来讲呢,在编写程序的时候一定要避免产生无谓的锁存器。
首先这个锁存器它只在组合逻辑里面产生,这个锁存器它会造成最后生成电路毛刺比较多,还会影响对整个电路的时序分析,因此就是说,我们在编写这个Verilog代码的时候,就尽量避免产生无用的锁存器。
那么在什么样的条件下,会产生这样的一个锁存器呢?
- 在组合逻辑里面,如果有if语句但没有相应的else语句,它就有可能会产生一个锁存器;
- 对于case语句来讲,如果这个case的表达式没有给完全,比如说用了一个三位的状态寄存器,但是只有四个状态,也就是说还有几个状态没有用到,那么就需要给一个default,否则这个case语句也是会生成一个锁存器的。
- 各个状态下的动作
这个动作也是由组合逻辑来生成的,就是结构图里面最后一部分产生输出的一个组合逻辑G。
这个输出的组合逻辑可以用两种表达方式:第一种呢是用assign的赋值语句,可以描述一个组合逻辑;第二种是用always语句。
(1)用assign的赋值
1 | // action |
比如说我们定义了一个read_book输出信号,那么当当前的状态是学习的时候,就要给读书这个信号高电平,如果当前的状态不在学习状态下,那么读书这个信号就给低电平。
当然有一些比较复杂的一个输出,比如这里不仅有这个读书信号的输出,还有吃饭以及睡觉状态下的信号输出,那么用这种assign赋值语句可能表示起来就比较麻烦。在输出信号比较复杂的时候可以用下面一种表达方式。
(2)用always语句
always语句不仅可以表示时序逻辑,还可以表示组合逻辑。在下面的程序里就是用always的语句描述的一个组合逻辑:
1 | always @(curretn_state) begin |
在这个语句里,它的功能跟我们上面一个语句是完全一致的。首先它的一个触发条件是curretn_state,也就是说它会根据当前状态来决定输出的信号。然后在这个语句里面如果判断当前的状态是学习,那么read_book就赋值为1。因为这个地方它是一个组合逻辑,所以这里要使用一个非阻塞赋值。
到这里,整个状态机设计的一个四段论就讲解完了。按照这种四段论写法写的状态机叫做三段式状态机。下面是一个三段式状态机的一个完整例子:
上图显示的状态机最后一段用的时序逻辑,不同于前面讲的组合逻辑,它也有自己的一个优点,下面是这种状态机的一个结构图:
前面讲的状态机直接就由这个组合逻辑G就输出信号,但这里可以看到后面它还有一个寄存器,这个寄存器叫做输出寄存器,也就是说这个状态机结构它最后还使用了一个时序逻辑,接下来看一下这样的状态机它的一个目的是什么?
三段式状态机它可以在组合逻辑后面再加一级寄存器来实现这个时序逻辑输出,而我们用那个时序逻辑输出的一个目的就是它可以有效地滤掉这个组合逻辑它的一个毛刺。如果我们在组合逻辑G直接输出的话呢,也是可行的,但是我们知道这个组合逻辑它容易产生毛刺,所以在后面再给它加一级寄存器,这样可以过滤掉组合逻辑的毛刺。
然后第二个它可以可以有效地进行时序计算与约束;这个时序约束部分呢会在后面进行讲解,简单说就是如果加一级寄存器的话,它这个时序路径呢就比较容易计算。
最后一点就是说对于总线形式的输出信号来说,它容易使这个总线数据对齐。什么叫做总线形式的输出信号,总线形式的就是说前面的这个输出是一个1位的信号,也就是它的位宽只有一位,那假设这个地方是一个八位的一个信号呢,它就属于一种总线形式的输出信号。那么对于这种总线形式的输出信号,如果用寄存器寄存一下的话(???没听清视频说的什么),它就可以减少总线数据之间的一个偏移。这个偏移指的是什么呢?比如说我们有一个八位的数据,那么如果在经过一系列的组合逻辑之后,因为它们的走线不太一致,会导致经过这个组合逻辑之后,可能这个这一组总线里面有些信号先到达,有些信号后到达,这个就称之为总线信号之间的一个偏移。那么如果我们用这个寄存器,重新给它们寄存一下,就可以减小这个总线数据之间的一个偏移。
































































