1 初识FPGA
注:笔记主要参考:
- B站 正点原子 教学视频“正点原子手把手教你学FPGA-基于达芬奇Pro开发板 Artix-7 XC7A35T/XC7A100T”。
- 小梅哥爱漂流 教学视频“【零基础轻松学习FPGA】小梅哥Xilinx FPGA基础入门到项目应用培训教程”。
- B站搬运 “特权同学2020版《深入浅出玩转FPGA视频教程》 Xilinx Artix-7 FPGA快速入门、技巧与实例”。
注:工程及代码文件放在了本人的Github仓库。
1.1 基本认知
1.1.1 什么是FPGA?
FPGA的全称是 现场可编程门阵列(Field Programmable Gate Array),简单来说,就是能用代码编程,直接修改FPGA芯片中数字电路的逻辑功能。那这样就怎么了呢?因为早期芯片生产出来后,电路就固定好不会改变了,于是功能也就固定了,这种芯片就是ASIC(专用集成电路,Application Specific Integrated Circuit)。而要想改变电路结构就需要重新设计芯片、重新“流片”、测试等,整个过程非常的 耗钱 耗时间。那突然间FPGA横空出世,支持通过修改软件代码来改变硬件电路结构,是不是就非常具有开创性!😎
还记得那一系列数电实验吗?那些手动搭建起来非常繁琐的数字电路,都可以通过简单的verilog代码直接在FPGA芯片中生成。
- B站视频“花式PCB设计展”,给出了许多奇奇怪怪的PCB设计,其中不乏“杂草式”接线技术,展示了手动搭建大规模数字电路的繁杂性。
1.1.2 什么是HDL?什么是Verilog?
HDL(硬件描述语言,Hardware Description Language)是用于描述数字电路结构和功能的语言的统称。HDL所描述的电路可以通过综合工具将其转换为门级电路网表,然后将其与某种工艺的基本元件逐一对应起来,再通过布局布线工具转换为电路布线结构。
上面这一段翻译成人话就是,要编写FPGA的代码(术语就叫做描述电路结构)肯定得需要一种语言吧,那C/C++、Java、Python等一众软件语言可以吗?那肯定不行啊,软件语言无法描述出清楚的电路结构,也没有办法约定时钟、走线、端口等,简单一句话,它们没这个实力知道吧😎,所以这时候就需要专用的 硬件描述语言HDL 了。不过不像软件语言那样枝繁叶茂,经过近三十年的发展,只有 Verilog 和 VHDL 二者最终脱颖而出,成为了公认的行业标准,两者逻辑相通,但 新手建议首先学习verilog。
- VHDL由美国军方研发,1987年就成为IEEE标准,1993年做过版本修订。语法严谨。
- Verilog起源于民间,1995年正式成为IEEE标准,2001和2005年分别做过版本修订。语法相对自由,类似C语言,易于上手。
那怎么开始学习verilog HDL呢? Verilog作为一种高级的硬件描述语言,很多语法现象与 C语言 非常相似,因此在 C语言 的编程基础上去学习 Verilog 比较容易。只不过需要特别注意培养硬件设计的思想,着重理解Verilog的 “并行”特性。多敲敲代码总是没错的😉。
1.1.3 硬件开发与软件开发
那么为什么有了软件编程还需要FPGA这种硬件编程呢?换句话说,为什么人们想要改变数字电路的逻辑功能呢,软件又不是不能跑?主要有两点:灵活、处理速度快。
灵活性:使用5G基站举例子,目前一般采用FPGA芯片,这样当有技术需要更新迭代时,只需要更新verilog代码,就可以相应的改变硬件电路的结构,而不需要再更换整个基板(因为芯片是焊接在板子上的),这样就大大节省了成本,体现出“灵活性”。
速度快:由于verilog编程与实际的电路结构紧密相连,天然具有并行处理数据的能力;而CPU(单片机也是一种CPU)则是采用“指令集”的形式实现编程,单个CPU核心只能串行的处理数据,通用性好,但当数据量较大、重复性较高时,处理速度明显不如FPGA。
举个例子,现在要求随机生成1000个不同频率不同相位的正弦波,然后输出所有正弦波相加的和。软件编程就只能先将所有的正弦波都生成出来,然后再相加。FPGA则可以同时生成1000个正弦波发生器,然后直接通过级联的加法器输出结果。所以显然FPGA的速度更快。
当然,这并不是说软件开发就不重要了,软件开发和硬件开发各自有各自的深奥之处。但对于之前一直在使用C语言/MATLAB进行编程的同学来说,进行FPGA开发时首先要关注的点就是并行。即,不同的always块的信号是并行处理的,在一个begin end语句内的所有信号也是并行处理的(上面提到的always
和begin end
在1.6.3小小节介绍)。也就是说,FPGA编程要时刻认识到代码/框图都是与实际的硬件电路相对应的,如果没有添加额外的控制逻辑,所有模块一上电就会同时运行。
FPGA开发小贴士:
- 顶层module中的输入输出信号需要对应实际的硬件端口。
- 所有信号都要设定位宽,处理信号时要时时刻刻记住每一个信号的位宽是多少。
1.1.4 FPGA与其他硬件的对比
- FPGA和单片机
说到硬件编程,很多同学想到单片机也需要将代码下载到芯片中实现功能,那FPGA和单片机又有什么区别呢?其实也就是上面提到的,单片机本质上也是一种CPU,并且市面上常见的单片机初学开发板所搭载的CPU都是ARM架构。
对单片机编程并不改变其电路的内部连接结构,只是根据要求实现的功能来编写运行的程序(指令),所以单片机编程和软件编程本质上没有区别;而FPGA编程每次都会改变内部的硬件电路。
芯片 | 类型 | 速度 | 结构 | 应用场景 | 开发语言 |
---|---|---|---|---|---|
单片机 | ASIC | 慢 | 哈佛总线 冯诺依曼架构 | 工业控制 | C语言 |
FPGA | 半定制电路 | 快 | 查找表 | 算法实现IC验证 | Verilog、VHDL |
- FPGA、ARM与DSP
- ARM:侧重于控制和传输,通常也包含丰富的人机交互功能的处理器。
- DSP:为高速数据的实时采集和传输、复杂的运算处理应用而进行定制化的处理器。
- FPGA:覆盖各种ARM和DSP无法满足的应用,更趋向于小批量、定制化、实时性要求又很高的应用。
三者某种意义上是互补的,甚至一些芯片上存在二者共生的现象,比如Xilinx的Zynq中有ARM也有FPGA,TI的达芬奇芯片中有DSP也有ARM。
- FPGA与GPU
由于人工智能非常火热,所以人们常常将FPGA和GPU放在一起进行讨论,看看谁更适合做一些AI的算法实现。
峰值性能:GPU远高于FPGA。
GPU能够集成上千个内核,架构高度优化,关键路径可手动调整,绝对性能可接近工艺极限。
FPGA设计资源受限,时钟频率受制于目标设计的布局布线,绝对性能有一定瓶颈,不及GPU。灵活性:FPGA远好于GPU。
GPU产品化之后,硬件架构固定,无法满足一些小众算法或新生算法对硬件架构的最优化“调整”。
FPGA的可编程特性,使各种不同的硬件架构最优化需求都能得到满足,一定程度上弥补绝对性能的不足。功耗:GPU的绝对功耗(200W)远大于FPGA(10W)。
在同等效率情况下,经过架构优化的FPGA功耗也远低于GPU。
- 补充:FPGA与CPLD区别。
1.1.5 FPGA优势与局限性
经过上面的探讨,简单总结一下FPGA优势与局限性。
灵活性 | 并行性 | 集成性 |
---|---|---|
可重编程,可定制 | 更快的速度、更高的带宽 | 更多的接口和协议支持. |
易于维护,方便移植、升级或扩展 | 满足实时处理的要求 | 可将各种端接匹配元件整合到器件内部, |
降低NRE成本,加速产品上市时间 | 有效降低BOM成本 | |
支持丰富的外设接口,可根据需求配置 | 单片解决方案,可以替代很多数字芯片 | |
减少板级走线,有效降低布局布线难度 |
FPGA技术的局限性
- 绝对性能受限:在某些性能上,FPGA可能比不上专用芯片;或者至少在稳定性方面,FPGA可能要逊色一些。
- 灵活性是否适用:如果设计不需要太多的灵活性,FPGA的灵活性反而是一种浪费,会潜在的增加产品的成本。
- 功耗相对较高:相比特定功能、应用集中的ASIC,使用FPGA实现相同功能可能产生更高的功耗(相比GPU,则单位性能功耗要低很多)。
- 设计复杂性高:在FPGA中除了实现ASIC所具有的复杂功能,还得添加一些额外的功能,实属一大挑战。FPGA的设计复杂性和难度可能会给产品的开发带来一场噩梦。
所以,基于上述FPGA的优缺点,在开发目标产品时,是否需要使用FPGA就需要考虑以下几点:
- 可升级性:若不需要,那么FPGA的意义就失去大半。
- 开发周期:相比于ASIC,FPGA开发的复杂度和开发周期会更高,要看项目是否允许。
- 产品性能:FPGA的绝对性能比不上专用芯片,要衡量项目是否可以接受绝对性能的降低。
- 实现成本:FPGA相对于ASIC的成本可能会高一些,要看用户是否可接受。
- 可用性
- 其它限制因素
1.1.6 FPGA的应用
FPGA的应用领域非常广泛,下面举几个例子。
- 逻辑粘合
如一些嵌入式处理常常需要地址或外设扩展,CPLD器件尤其适合。在FPGA被发明的早期,使用FPGA做的很大一部分工作就是逻辑粘合。今天已经少有项目会选择一颗FPGA器件专门用于逻辑粘合的应用,但是在使用FPGA做一些大规模处理的同时,顺便做些逻辑粘合的工作倒是非常普遍。
如上图中,左侧是一颗DSP,由于EMIF接口数量不够,可能需要拓展,就使用FPGA接了三个双口RAM。
- 实时控制
如液晶屏或电机等设备的驱动控制,此类应用都具有很强的实时性但相对简单,所以主要使用CPLD或低端FPGA。
- 高速信号采集和处理
如高速AD前端或图像前端的采集和预处理,近年来持续升温的机器视觉应用也几乎是无一例外的都使用了FPGA器件。
如今很多机器视觉的应用也采用了类似的方式,只不过前端的AD会被替换成Math Sensor。
- 协议实现
如更新较快的各种有线和无线通信标准、广播视频及其编解码算法、各种加密算法等,诸如此类小批量、定制化、更新换代频繁的应用使用,FPGA比ASIC更有竞争力。如下图所示的“SD/HD/3G SDI的协议”或者无线通信基站之间的协议实现。
- 各种原型系统验证
FPGA支持丰富的接口协议标准,可定制性强,芯片原厂使用FPGA搭建芯片的验证系统。
- 并行计算(算法实现)
传统的CPU计算受限于其串行顺序处理的架构,已经很难适应今天的云计算和数据中心对大数据运算的需求。而FPGA与生俱来的并行性与灵活可编程特性是其进入高速运算领域的一大优势。GPU虽然一直是并行处理的主流方案,但也受限于极高的成本和功耗代价。相比之下,单位功耗性能是GPU的3~4倍的FPGA则大有取而代之的趋势。
- 片上系统SoC
目前在一些单芯片SoC产品,既有成熟的ARM硬核处理器,又有丰富的FPGA资源(IO资源、RAM资源、乘法器资源等)。这种SoC集成度高,布板面积可以做到最小化,并且内部灵活和高吞吐量的高速互联。如Intel-Altera公司的Soc FPGA和AMD-Xilinx公司的ZYNQ。
- 从行业看FPGA的应用
最后总结一下,FPGA在通信领域、数字信号处理领域、视频图像处理领域、高速接口设计领域、人工智能领域、IC验证领域以及各种定制设计中都有涉猎。
FPGA所诞生并发展的时代是一个好时代,与生俱来的一些特性也注定了它将会在这个时代的大舞台上大放光彩。
1.1.7 FPGA的学习之路
FPGA设计是一种整合的技术,要求从不同的设计领域融合多种设计技能。要想成为一名资深的FPGA开发工程师,开发一些复杂的FPGA应用,极可能涉及到多种交叉的设计技能,需要掌握交叉学科知识、拥有丰富的技能,可能还需要来自系统、软件和硬件工程的设计技能。
领域 | 硬件/DSP设计 | 软件(HDL)设计 | 软件/DSP设计 | 系统设计 |
---|---|---|---|---|
所 需 技 能 | 板级硬件与接口设计 | HDL语言的设计输入 | 处理器代码模块的定义 | 处理器需求分析 |
DSP算法的硬件实现 | 脚本实现自动化处理 | 代码的编写和测试 | 设计数据流的定义 | |
逻辑电路设计 | 设计测试平台的开发 | DSP算法的软件实现 | 处理器架构的选择 | |
功耗与去耦设计 | HDL流程设计的配置管理 | 常规的代码调试和验证 | 硬件/软件实现的权衡 | |
硬件仿真 | 设计约束 | 在处理器上运行操作系统 | 系统级设计的层次结构定义 | |
板级引脚分配 | 支持设计复用 | 代码的配置管理 | 功能划分和模块化设计 | |
硬件模块调试 | 系统模块的集成与接口测试 | |||
I/O特性的定义 | 系统级测试,调试和验证 | |||
设计布局 | ||||
设计优化权衡 | ||||
信号完整性和终端匹配 | ||||
FPGA器件和封装选择 |
当然,上述技能虽多,但不用害怕,没有人天生就懂软件/硬件开发,所有大佬都是从“Hello World”/点灯开始的。下面就是特权同学总结的FPGA三阶段:
1. 入门阶段
从无到有的阶段,初识FPGA,是不折不扣的“菜鸟”。
要初步了解FPGA是什么、能做什么等基本的理论。
要学会HDL语言,能够使用EDA工具完成FPGA的代码设计、仿真验证、时序设计、综合和映射。
能够在开发板上下载并跑例程,进行初步的板级调试。
这一阶段的目标是"熟练"。2. 精通阶段
提高自己的设计和调试能力。
掌握如何用合适的HDL语法风格设计出最优化的电路;让EDA工具的不同设置功能服务于具体的设计优化。
掌握不同的板级调试手段并能熟练应用。
这一阶段的目标是“精通"。3. 从业阶段
以FPGA产品开发作为自己的职业。
让FPGA技术以最优的方式服务于产品。
有挑战也有成就,最长、最难的阶段。
这一阶段的目标是“专业”。
1.2 FPGA开发流程
1.2.1 一般性的FPGA开发流程
- 设计输入:创建FPGA工程,添加设计源文件,比如HDL文件、EDIF或NGC网表文件、原理图、IP核模块、嵌入式处> 理器以及数字信号处理器模块等。
- 设计综合: FPGA开发工具的综合引擎将编译整个设计,并将HDL源文件转译为特定结构的设计网表
- 约束输入:指定时序、布局布线或者其它的设计要求。如时序约束、I/O弓|脚约束和布局布线约束等
- 设计仿真:使用仿真I具对FPGA工程进行功能或时序验证。
- 设计实现:将逻辑设计进一步转译为可以被下载烧录到目标FPGA器件中的特定物理文件格式。
- 分析实现结果:对设计约束、器件资源占用率、实现结果以及功耗等设计性能进行分析
- 设计优化:分析当前设计结果,对设计源文件、编译属性或设计约束进行修改,然后重新综合、实现以达到设计最优化。FPGA的设计有很多迭代的过程。
- 板级调试:生成比特流并下载到开发板上,对FPGA器件进行板级的调试。FPGA有非常丰富的板级调试手段,比如在线逻辑分析仪可以直接查看FPGA内部引脚、接口、走线的信号变化,可以有效提升板级调试效率。
- 概念阶段:或称架构阶段,完成项目前期的立项准备,如需求的定义和分析、各个设计模块的划分。
- 设计实现阶段:或称详细设计阶段,包括编写RTL代码、并对其进行初步的功能验证、逻辑综合和布局布线、时序验证。
- FPGA器件实现阶段:除了器件烧录和板级调试外,也应该包括设计实现阶段的布局布线和时序验证,因为这两个步骤也都是和FPGA器件紧密相关的。
1.2.2 利用Vivado开发FPGA
Vivado就是用来进行FPGA开发的一个工具,可以编译硬件代码,完成上面所说的整个FPGA开发流程。市面上主流的FPGA开发工具只有两款,一个是Xilinx芯片(被AMD收购)专用的 Vivado,另一款则是Altera芯片(被Intel收购)专用的 Quatrus,两者原理相同,但建议先熟练掌握一个工具而不是混着学。本教程选用正点原子达芬奇PRO开发板,搭载了Xilinx的xc7a100tfgg484芯片,所以本教程都是用Vivado进行开发。
另外,Vivado集成了 HLS( High Level Synthesis) 工具,可以实现直接使用C、C++以及System C语言对Xilinx的FPGA器件进行编程。用户无需手动创建RTL,通过高层次综合生成HDL级的IP核,从而加速IP创建。在某些使用verilog语言描述异常复杂的场景中(如卷积神经网络),使用HLS将高级语言转化为对应的HDL级的IP核,就会非常方便。这种功能会在ZYNQ系列芯片的学习过程中进行介绍,本教程不涉及。
下面是Vivado左侧的导航窗格,上一节所述的FPGA开发流程都在这里完成:
- 创建源文件:用户有两种方式可以创建源文件,以描述电路结构与功能,分别是编写“HDL代码”、创建BD(框图,Block Diagram)。
- RTL分析:vivado将上述源文件转换为逻辑门电路。
- 此时可进行“行为仿真Behavioral Simulation”。
- 综合:vivado将经过“RTL分析”后的门电路映射为FPGA器件内部的物理结构。所以“综合”电路中看不到任何的“门”。“综合”的结果是所使用的特定FPGA器件中实际存在着的物理结构,如“输入缓冲”、“查找表”、“触发器”和“输出缓冲”等。
- 此时可进行“综合后功能仿真”。
- 布局布线:需要用户指定时序、布局布线或者其它的设计要求,如时序约束、I/O引脚约束和布局布线约束等。Vivado会自动根据这些约束,将“综合”给出HDL代码与实际FPGA器件进行映射,物理视图与具体的芯片一致。
- 此时可以进行所有类型的仿真。
- 生成比特流、下载到板材上。
注:使用vivado进行后续操作时,会自动检查前面的步骤是否已经完成,比如直接“综合”会自动先进行“RTL分析”、直接“生成比特流”则会自动进行整个过程,若有那个环节有错,vivado会自动停止并给出提示。
在整个开发过程中,为了验证电路的正确性,还会进行仿真。从上面的示意图可以看到一共有5种仿真类型。主要分为行为仿真/功能仿真(Behavioral Simulation / Functional Simulation)、时序仿真(Timing Simulation)两大类:
- 功能仿真也称为行为仿真,主旨在于验证电路的功能是否符合设计要求,其特点是不考虑电路门延迟与线延迟,主要是验证电路与理想情况是否一致。
- 时序仿真也称为布局布线后仿真,是指电路已经映射到特定的工艺环境以后,综合考虑电路的路径延迟与门延迟的影响,验证电路能否在一定时序条件下满足设计构想的过程,能较好地反映芯片的实际工作情况。但仿真耗时很长。
仿真是FPGA在板级调试前非常重要的验证手段。 仿真可以在RTL分析前进行,但是为了提高仿真的有效性,需要先进行RTL分析、综合。另外,布局布线后也可以进行更精确的仿真,但是一般不做。在对时序要求不高的场景中,可以认为时钟的轻微延迟对电路基本没有影响,所以为了提高开发效率,一般只进行行为仿真(Behavioral Simulation)。后续还有bug会使用ILA或VIO看波形异常,实在是找不出来问题,才会进行布局布线后时序仿真。
下面给出一个简单的开发实例:假如现在我们要开发一个电路,有三个输入信号A/B/C,输出为 AB+C,下面给出其真值表:
cina | cinb | cinc | cout |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 0 | 0 |
1 | 0 | 0 | 0 |
1 | 1 | 0 | 1 |
0 | 0 | 1 | 1 |
0 | 1 | 1 | 1 |
1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 |
- 创建源文件,编写以下verilog代码描述上述真值表所给出的逻辑功能。
module design_1(
clk,rst_p,
cina,cinb,cinc,
cout
);
//定义输入输出接口
input clk, rst_p; //时钟及复位信号
input cina, cinb, cinc; //输入的三个数据信号
output reg cout; //输出数据信号
//实现功能
always@(posedge clk or posedge rst_p)
if(rst_p)
cout <= 1'b0;
else
cout <= (cina & cinb) | cinc;
endmodule
- 进行RTL分析(Run Analysis),点击“Open Elaborated Design” → “Schematic”,即可看到RTL门级电路:
- 进行综合(Run Synthesis),将上面的门电路映射为FPGA芯片上实际的硬件电路,点击“Open Synthesized Design” → “Schematic”查看:
- 添加硬件约束(也就是引脚分配),通过查看PYNQ-Z2原理图可以发现系统时钟SYSCLK引脚为H16;四个按下为高电平的按键开关BTN3~BTN0的硬件端口依次为L19、L20、D20、D19;LED0端口为R14。设置BTN3为复位按键,BTN0~BTN2分别为cina/cinb/cinc,LED0为输出,于是分配硬件端口:
- 生成比特流,下载到开发板,就可以看到相应的实验现象了。
1.2.3 硬件调试与仿真(ILA核/VIO核)
那硬件开发的过程中,肯定也会出现各种各样的bug(实验现象与预期不符),在FPGA开发过程中,最主要的调试方式就是看各个主要信号的波形。本小小节就来介绍使用Vivado进行FPGA程序调试的思路。
1) 硬件异常检测
在进行程序调试前,首先要确定硬件本身没有问题。可以按照以下步骤进行:
- 下载一个验证好的例程,测试硬件是否能正常工作。
- 假如例程无法运行,排查接口连接、管脚分配。比如按照从电源到各个模块的顺序,测量各个模块的电压是否正常。
- 如果例程还是无法正常运行,交给硬件工程师/开发板卖家。
2) 行为仿真
确认硬件没有问题后,那么程序现象出问题就显然是软件程序的问题了。为了简单快捷,首先可以快速排查几个常见的错误:
- 检查 时钟、复位 是否接好。
- 检查每一个模块的接口是否正确。比如涉及到通信的问题,检查主控制Master和从控制器Slave是否建立的联系(两者之间的valid、ready信号)。
- 查看状态机的状态是否正常、查看数据是否有问题、查看触发条件是否有问题。(“状态机”后续会进行介绍)
注:任意查看“RTL分析”、“综合”、“布局布线”后的三种原理图,可以快速的查看电路连接状态。
小技巧:
若此时还是没有发现问题,就需要编写仿真文件,来产生设计好的激励以驱动可能有问题的模块,这些激励包括时钟、复位、控制信号,另外也可以设计一些具有规律性的假数据来进行测试。仿真文件编写完毕后,再点击“运行”,就可以看到目标信号的仿真波形了。
3) ILA核与VIO核
一般通过上述“行为仿真”就能找出相应的bug了。但有些实验涉及到与外部芯片进行通信,导致不是所有的信号都是由FPGA内部控制,比如I2C通信读写EEPROM、DDR3读写实验等。比较幸运时,可以找到该芯片对应的仿真代码,比如EEPROM就有对应的仿真代码,可以直接在行为仿真中完全模拟出芯片信号的变化。但有些芯片可能没有对应的仿真代码;或者说有对应的仿真代码但实在是过于复杂,进行一次有效的行为仿真需要几十分钟,那这个时候就需要硬件电路的帮忙。换句话说,我想直接在有问题的硬件电路上,直接抓取信号看波形。
要想实现上面这个目的,首先想到的是使用示波器,但是示波器只能看到物理存在的接口。那想查看芯片内部信号的波形该怎么办呢?就需要使用Vivado内置的IP核:ILA核(Integrated Logic Analyzer)、VIO核(Virtual Input/Output)。下面依次介绍。
Vivado的LIA/VIO通过将Probe与相应的IP核相连,来监控逻辑内部信号和端口信号,最终将数据通过下载器(JTAG接口)传输到PC。简单来说,就是将数据发送到PC端(vivado软件)进行显示。此外,VIO还可以接收电脑数据,用来驱动内部逻辑信号或提供触发信号。
上图给出了添加ILA/VIO的方法:
- HDL【常用】:生成ILA的IP核,然后在代码中进行例化。优点是方便,缺点是要修改代码。
- 信号列表(Hierarchy)或原理图(Schematic):对代码综合之后才能添加。优点是不用修改原代码,缺点是综合可能会优化掉待测信号(添加属性可以解决)。
- IP Integrator(IP集成器):在IP Integrator中添加信号,但是只有在添加硬核或软核才会用到。
注:添加成功后,下载比特流后会在软件窗口界面显示波形等设置。
总结:
- ILA核:监控信号。可以将波形存储在ROM之中,但会消耗FPGA内部的逻辑资源和存储资源。
- VIO核:监控和驱动信号。只能显示实时波形,但还可以通过电脑端手动更改程序的某些触发信号。
1.3 FPGA芯片介绍
本小节来点轻松的,介绍一下FPGA的发展历史以及内部结构。内部结构可能看起来有点难,但是先有个印象就行。
1.3.1 FPGA的发展史
FPGA的发展史其实也就是近几十年的事情:
- 1970年:PLD 是第一款进入人们视野的可编程逻辑器件,采用PROM结构,输入接口少。
- 1978年:可编程逻辑阵列PAL 和 通用阵列逻辑GAL 采用了反熔丝技术、EPROM和EEPROM技术,结构功能仍然相对简单。
- 1984年:Altera公司于发明了基于CMOS和EPROM技术相结合的 CPLD,可以胜任复杂性较高、速度也较快的逻辑功能。
- 1985年:Xilinx创始人之一Ross Freeman发明了 现场可编程门阵列FPGA,开启了可编程逻辑的“高速”发展时代。Freeman先生发明的FPGA是一块全部由“开放式门”组成的计算机芯片。可灵活编程,添加各种新功能,以满足不断发展的协议标准或规范,甚至可以在设计的最后阶段对它进行修改和升级。
在FPGA刚发明的时,常以有多少“门”(与门、或门、非门等逻辑资源)来衡量一个FPGA。而现在“门”的概念已经逐渐被淡化,FPGA不仅强调其逻辑资源,还包括其他丰富的资源,如可编程IO单元、丰富的布线资源、灵活的时钟管理单元、嵌入式块RAM以及各种通用的内嵌功能单元(如内嵌乘法器、内嵌硬核IP等),很多器件还顺应市场需求内嵌专用的硬件模块。
注:FPGA从高到低分为好几个级别:系统级、算法级、寄存器级(RTL级、行为级)、门级(布局布线后,有延迟)、开关级。
近些年来,可编程器件的龙头老大Xilinx和Altera更是相继推出了硬核CPU+FPGA的产品,此举大有单芯片横扫千军的架势。比如上图所示的蓝色PS部分就是CPU硬核(ARM架构),剩下的黄色部分就是FPGA,这样的产品包括Xilinx的ZYNQ系列和Altera的SoC FPGA。
1.3.2 FPGA厂商及型号
国际上,FPGA的主要生产厂商有:
- Altera(已被Intel收购)与Xilinx(已被AMD收购):把持中高端市场,长年占据超70%的市场份额。
- Microsemi(收购Actel):高度聚焦于FPGA的安全应用领域,如国防和航空。
- Lattice:在中低端市场有一定市场份额。
- QuickLogic公司:主要从事客户定制化标准产品。
注:正所谓集群效应,主流的FPGA供应商几乎都来自美国加利福尼亚。
那我这一顿库库介绍FPGA,其他的啥也没说。但是很多初学者(比如我)在刚学FPGA找资料的时候,总是会看到什么FPGA、ZYNQ、PYNQ啥的,被这些不同的芯片型号整的晕头转向,那现在就来捋一捋他们的关系:
- 首先,FPGA、ZYNQ、PYNQ本质上都是FPGA芯片,“最纯净”的可编程逻辑器件就是FPGA;
- 那后来呢,大家发现FPGA这个东西头脑简单、四肢发达(就是虽然数据并行计算超快,但是编写verilog还是有点费时间的),有些事情还是用软件语言处理效率更方便,于是就可以使用Vivado的 HLS( High Level Synthesis) 工具,将C语言代码编译成一个的IP核(软核,可以调用的硬件电路模块),就可以在电路中调用该模块了;
- 那对于用软件语言这事,芯片制造商也不能闲着,于是它们就把一个ARM芯片和一个FPGA芯片组合起来,推出了ZYNQ系列芯片。其中的ARM芯片就是硬核,可以使用C/C++编程,编程效率大大提升;
- 再后来呢,大家发现C/C++对于机器学习的支持不如Python好(虽然机器学习的底层代码是使用C/C++编写的),因为Python里面有很多现成的库,直接调用现有算法非常方便,于是进一步改良,使得ZYNQ中的ARM芯片也支持Python,于是就推出了PYNQ系列。
那Xilinx官网上着重介绍了自家的 FPGA芯片 和 ZYNQ系列芯片,咱们就大概看看它的产品线(Altera那边也差不多,就不多说了),如下图所示:
从上图可以看到主要有FPGA芯片、SoC(System on Chip, 片上系统)两大类。这两个大类各自包含四个子系列,每个子系列又细分为多种产品线,而每一个产品线都有很多型号(图中未给出具体型号)。可以大致认为,该产品树从上到下分别对应了片上资源从少到多。
目前市面上最常见的初学者开发板,就是FPGA系列中的Atrix-7系列(xc7a35tfgg484/xc7a100tfgg484)、以及Zynq-7000系列(7000/7020)。
1.3.3 FPGA硬件结构
本小小节介绍FPGA内部的硬件结构,对于初学者来说,里面的很多图一看就非常复杂,想搞懂很困难。但是不要放弃,本篇文章只是用做科普,一些看似很难的图只需要有一个大概的印象即可,后续做实验时,涉及到相应的模块则会继续讲解。另外在基础的开发实验中,只需要了解各模块的接口,能把接口接对即可,可以暂时不用对内部结构有特别深入的理解。
1.3.3.1 数字电路基本结构
下面给出了在设计数字电路的过程中,经常用到的一些基本结构。下面可能只给出一些基本的示意图,关于更详细的基本电路介绍可以在任意一门《数字电子技术基础》课程上讲到,可以在 B站/chatgpt 搜索相应关键词。
1) MOS管
所有门电路的功能都可以由PMOS管和NMOS管(统称“晶体管”) 来实现的。下面给出了其物理结构和基本的电路符号。当然,这里仅是简单的示意图,实际上还有结型场效应管、绝缘栅型场效应管(又细分为增强型和耗尽型),具体细节可以查看任意一本《模拟电子技术基础》课本(推荐清华大学出版社 童诗白主编)。
- NMOS管在Gate端输入为0时,D端–>S端断开;在GS端输入为1时,D端–>S端导通。
- PMOS管在Gate端输入为0时,D端–>S端导通;在GS端输入为1时,D端–>S端断开。
2) 基本逻辑门电路
上图给出数字电路中最常见的基本逻辑门电路。上图所示的每一个门电路都是由最基本的 电阻、二极管、三极管 组成的。比如:
- 1个NMOS管+1个PMOS管可以实现 非门 电路。
- 2个PMOS管+2个NMOS管可以实现 与非门 电路。
- 2个PMOS管+2个NMOS管可以实现 或非门 电路。
- 3个PMOS管+3个NMOS管可以实现 与门 电路。
- 3个PMOS管+3个NMOS管可以实现 或门 电路。
注:具体结构可以查看《数字电子技术基础》。
3) 触发器
通过将门电路进行组合,就可以得到更加复杂的“触发器”。触发器一般都需要时钟输入,作用一般是锁存数据,或者多个触发器和外围电路配合形成计数器。而多个计数器级联,就可以产生任意周期的脉冲信号。触发器的种类多种多样,比如RS触发器、D触发器、JK触发器、T触发器、T’触发器等,下图以D触发器为例。
可以看到D触发器由基本的非门、与非门构成。比如 R ˉ d \bar{R}_d Rˉd、 S ˉ d \bar{S}_d Sˉd 均设置为高电平时,输出 Q Q Q 就会在 时钟 C P CP CP 高电平时同步更新为 输入 D D D。
4) 组合逻辑和时序逻辑
那有了上面的门电路、触发器等数字电路的基本元器件,就可以搭建各种各样的数字电路了。但是注意数字电路根据是否需要时钟又分成 组合逻辑 和 时序逻辑。下面是其示意图:
- 组合逻辑的输出只与当前输入有关,时序逻辑的输出与当前输出和过去的状态都有关。
- 组合逻辑立即反应当前输入状态,时序逻辑还必须在时钟边沿触发后输出新值。
- 组合逻辑容易出现竞争、冒险现象,时序逻辑一般不会出现竞争、冒险现象。
- 组合逻辑的时序较难保证,时序逻辑更容易达到时序收敛,时序逻辑更可控。
- 组合逻辑只适合简单的电路,时序逻辑能够胜任大规模的逻辑电路。
简单一句话:组合逻辑立即变化,时序逻辑随着时钟变化。
那下面就来一步步介绍FPGA内部是如何实现上述这些数字电路的基本结构的。
1.3.3.2 LUT查找表
实际上,FPGA通过 LUT(Look-up table,查找表)结构 来实现可编程电路。LUT就是一张已经设计好的结果已知的查找表,这个表存储着不同的输入对应的所有可能的结果,这些结果对应LUT中一个唯一的地址。而组成LUT的RAM本质上也由晶体管组成,所以 LUT本质上也是晶体管的组合。
【例】2输入LUT实现的与非门电路,可以用4个预存储的数据实现既定功能。输入x和y的4种不同组合可以看出是0~3的4个不同地址,每个地址都对应一个输出结果。
当然实际的FPGA当中,使用的并不是这么简单的2输入查找表。具体到 Xilinx Artix7系列FPGA器件,它使用的是 6输入LUT结构。这6个独立的输入(称为A1~A6)可以配置为单输出(O6)模式和双输出(O5和O6)模式。
- 6输入1输出的LUT内部对应着1个64个地址的存储单元。
- 5输入2输出的LUT内部对应着2个独立的32个地址的存储单元。
实际上,通过配置,上图不仅可以实现“多输入与门”电路结构,同样可以实现“多输入或门”,以至于任意多输入的组合,都可以实现。
1.3.3.3 可配置逻辑块CLB
以Xilinx主流的7系列为例,一颗FPGA内部通常都会有数千到数十万不等的 可配置逻辑块(Configurable Logic Block,简称CLB)。呈矩阵排布的CLB就构成了最基本的FPGA逻辑资源的架构,CLB块之间有很丰富的布线池。Xilinx7系列的可配置逻辑块可以有效的支持以下特性:
- 使用6输入查找表技术;
- 可选的2个5输入查找表功能;
- 可实现分布式RAM和移位寄存器功能;
- 用于运算功能的专用高速进位链;
- 支持资源高利用率的丰富复用开关。
从微观角度看,CLB内部主要由2个更小的单位Slice所组成。两个Slice之间有连线,且每个Slice都有独立的高速进位链以及独立的布线通道连接到矩阵开关,通过矩阵开关可以实现Slice与FPGA大布线池之间的灵活编程。每个slice单元则包含了以下更小的功能块:
- 4个逻辑功能发生器(或查找表);
- 8个存储单元(或触发器);
- 功能丰富的复用开关;
- 进位链。
围绕在CLB周围丰富的行、列走线称为 布线池,它用于衔接FPGA的各个CLB以及其它相关的资源。在FPGA芯片四周的小矩形以及延伸出去的短线,则表示FPGA和外部芯片接口的 IO块。
- 布线池:类似于PCB板材上的走线,用于衔接FPGA上的CLB等资源。总线池越大,就意味着可以走更多的信号。
- IO块:FPGA和外部其他芯片进行数据交互的接口。所以FPGA也必须有很丰富的IO块。
1.3.3.4 其他丰富的FPGA资源
从整个FPGA的配置来看,不仅包含了很多CLB资源,还包括以下丰富的资源:
- 以成块出现的FPGA内嵌存储器(块RAM)。
- 用于产生不同时钟频率的锁相环( PLL时钟发生器)以及相应的时钟布线资源。
- 高速串行收发器。
- 外部存储器控制器(硬核IP)。
- 用于实现数字信号处理的乘累加模块(DSP Slice)。
- 模拟数字转换模块(Xilinx FPGA器件特有的ADC,简称XADC):支持最多16个可复用通道,对于一些工业应用领域(如电压监控、小信号采集)来说,非常实用。
1) 块RAM
块RAM就是以成块出现的FPGA内嵌存储器。因为在一些高速的数据处理、缓存、算法实现、信号处理等场合,需要内部的一个紧耦合的RAM来实现对数据的高速的缓存和读写,所以块RAM应运而生。从上图所示的FPGA内部的块RAM接口框图来看,块RAM本质上都是36kb大小的双输入双输出:左侧有DIA和DIB两个独立的数据输入,右侧也分别是DOA和DOB两个独立的输出,上下则是用于级联的接口。但当然也可以根据需求,配置成简单的单输入单输出,这个后续在学习“RAM IP核”时再具体介绍。
实际上,FPGA内嵌的存储器单元包括块RAM(BRAM) 和 分布式RAM。BRAM 就是上一段介绍的结构,分布式RAM 则是基于CLB的查找表。这些存储器单元都可以配置为随机存取存储器(RAM)、只读存储器(ROM)、FIFO或移位寄存器,非常灵活。
2) 时钟资源(PLL时钟发生器)
1) 块RAM
FPGA内部充斥着各种各样的连线,如果我们把这些逻辑间的互连线比喻为大城市里面密密麻麻的街道和马路,那么专为快速布线而定制的 时钟布线资源 则是城市里的快速路。FPGA内部的 时钟布线池 也是横平竖直的矩阵式排布,意图让每一条“小路”能够尽快地找到可以就近“上高速”的“匝道”。换回到FPGA内部电路来说,就是希望时钟源产生的时钟信号,可以通过尽可能小的延迟,快速地、同步地 达到各个资源,以驱动电路各模块正常协调工作。注意时钟信号的最大延迟,限制了FPGA的时钟频率。
Xilinx FPGA内部会将时钟布线资源划分到不同的“时钟区”中,每个时钟区对应一定数量的IO口数量、逻辑资源、存储器资源或DSP slice资源,同时也会有一个CMT(Clock management tiles)相对应。于是,由 PLL时钟发生器 产生的不同时钟频率的锁相环以及相应的时钟布线资源,就可以被输送到不同的“时钟区”。
3) 数字信号处理块
数字信号处理(Digital Signal Processing,简称DSP)块是Xilinx FPGA内部最复杂的运算单元。DSP块是内嵌到FPGA中的算术逻辑单元(ALU),它由3个不同的链路块组成:DSP块的算术链路由一个加减器连接到乘法器,再连接到一个乘累加器所组成。DSP用于实现数字信号处理的乘累加模块(DSP Slice)。可用于一些算法实现和运算功能。
当然在一些简单的应用中,无需对DSP内部的结构做如此深入的理解,只要会用即可。
4) 高速串行收发器
FPGA支持各种高速差分对,从几百MHz的普通LVDS接口到上GHz或数十GHz的Gbit串行收发器,可以满足各种高速数据传输的需求。通常在FPGA器件内部提供高速的串化器和解串器,以及低时延、高速率的时钟处理单元。普通的LVDS接口,小规模的FPGA器件中也能够提供多达几十对的差分接口,通常既可以作为LVDS接口,也可以复用为一般的IO引脚使用。在Artix7系列FPGA器件中达到6.6Gb/s的GTP Transceivers有2到16个不等,能够满足一般性的应用。
注:
- GTP Transceiver是FPGA里一种线速度达500Mb/sà6.6Gb/s的收发器,利用FPGA内部可编程资源可对其进行灵活地配置,使其适合不同的需要如以太网、SATA1.0接口等,它的作用是各种高速串行接口的物理层。
- LVDS(Low-Voltage Differential Signaling) 低电压差分信号,是一种低功耗、低误码率、低串扰和低辐射的差分信号技术,这种传输技术可以达到155Mbps以上。
5) 外部存储器控制器
外部存储器控制器(Memory Interface Generator)通常是硬核IP。在进行高速数据采集、缓存、处理等场合,可以大大方便FPGA和高速Memory之间的交互。由于FPGA的片内存储器(如BRAM)容量受限,所以对DDR3/DDR4等外部高速存储器的支持也成为了中高端FPGA器件必备的资源。FPGA器件内部往往内嵌了一个或多个DDR3/DDR4控制器硬核IP,包括用户接口(User Interface)模块、存储器控制器(Memory Controller)模块、初始化和校准(Initialization/Calibration)模块、物理层模块等。
这部分的原理非常重要,基本上能够独立完成“DDR读写实验”,就算是从“FPGA一窍不通”进阶成“FPGA熟练掌握”了,属于是大多数人学习FPGA的分水岭,这部分的实验后续会详细介绍。
6) 模拟数字转换模块
Xilinx FPGA器件特有的ADC,简称XADC,将模拟信号处理混合到FPGA器件。XADC包括:
- 2个ADC,具有12bit位宽和1MSPS采样速率,可以外接精密基准电压源作为参考电压。
- 专门的温度传感器(±4°C 最大误差)和电压传感器(±1% 最大误差),用于监控FPGA器件本身的工作状态。
- 提供了多达16个差分通道可复用的模拟电压采集接口,对于很多工业应用(如电压监控、小信号量采集等)来说非常实用。
- 专门的控制接口,可以和FPGA逻辑互连,便于编程控制。
1.4 Verilog基础语法
1.4.1 基础知识
1) 关于逻辑值
- 逻辑 0:表示低电平,也就对应电路GND;
- 逻辑 1:表示高电平,也就是对应我们电路的VCC;
- 逻辑 X:表示不定态,可能是高电平,也可能是低电平;
- 逻辑 Z:表示高阻态,外部没有激励信号,是一个悬空状态。
建议初学者将所有的信号值都规定好,也就是只使用前两个逻辑值。
2) 关于数字的表示
Verilog数字进制格式包括二进制('b)、八进制('o)、十进制('d)和十六进制('h)。
一般常用的二进制、十进制和十六进制。记得一定要规定数字的位宽(这个位宽是二进制的位宽),有时候可能不规定(默认32位)也能正常运行,但不要自己埋雷💣。
举个例子,现在要表示十进制数255:
二进制 表示为:8’b1111_1111。
十进制 表示为:8’d255。
十六进制表示为:8’hff。
注:数字分隔符(也就是下划线)可以随便加,自己看得舒服就行。
小技巧:使用windows自带的计算器,打开程序员模式,就可以同时看到一个数字的四种进制的表示。对于FPGA开发来说,这个工具比较方便。
3) 关于命名
标识符(identifier)就是模块名、端口名、信号名等,可以类比为软件语言中各个变量、函数的名称。verilog对命名标识符的过程做出了如下规定(必须遵守):
1.标识符可以是任意一组字母、数字、$(dollar符号)和_(下划线)符号的组合。
2.标识符的第一个字符必须是字母或者下划线。
3.标识符是区分大小写的。
除此之外,广大verilog开发者还有一些约定成俗的建议:
1.不建议大小写混合使用,也就是全部大写或全部小写。
2.模块名、普通内部信号建议全部小写,常量建议全部大写。
3.信号命名最好体现信号的含义,简洁、清晰、易懂;
以下是一些推荐的写法:
1.用有意义的有效的名字如 clk_count、MAX_ADDR 等。
2.用下划线区分词,如cpu_addr。
3.采用一些前缀或后缀,比如时钟采用clk前缀:clk_50,clk_cpu。
1.4.2 数据类型
在 Verilog 语言中,常用的主要有三大数据类型:寄存器数据类型、线网数据类型和参数数据类型。顾名思义,寄存器数据类型(reg
)描述的是一个可以存储数据的单元,而线网数据类型(wire
)则是描述一个物理连线,参数数据类型(parameter
)就是常量。下面依次介绍。
reg类型
的数据只能在 always 语句和 initial 语句(只出现再仿真中,不会被综合)中被赋值,默认初始值为不定值x。若always块带有时钟信号(时序逻辑),则该寄存器变量对应为触发器;若always块不带有时钟信号(组合逻辑),则该寄存器变量对应为硬件连线。wire类型
表示物理连线,所以线网类型的变量不能储存值,它的值是由驱动它的元件所决定的,且一旦有变化就会立即变化(组合逻辑)。若没有元件驱动到wire型信号上,则默认为高阻值z。驱动线网类型变量的元件有门、连续赋值语句、assign
等。
注(可能要删):线网数据类型包括 wire 型和 tri 型,两者区别不大,但一般只使用 wire 类型。parameter类型
用来定义常量,一次(只写一次parameter)可以定义多个参数,参数与参数之间需要用逗号隔开。每个参数定义的右边必须是一个常数表达式。paramter型数据常用于定义状态机的状态、数据位宽、延迟大小等(一些有意义的常数),可以提高程序的可读性和可维护性。
综上所述,每个信号的控制逻辑如下图所示。一直没有提到的是,时序逻辑信号会等待时钟边沿再进行变化;组合逻辑信号会立即变化。为了保证FPGA具有良好的时序性,一般的信号都使用时序逻辑,只有一些控制信号、状态信号、模块之间的连接信号才使用组合逻辑,当然还需要开发者的综合考量。
注:与软件语言中直接使用等号进行赋值不同,verilog中,时序逻辑信号一般使用非阻塞赋值<=
,组合逻辑信号一般使用阻塞赋值=
。极少数情况不是这样,后续有空我再单独写文章阐述阻塞和非阻塞的原理。
1.4.3 运算符
Verilog中的操作符按照功能可以分为下述7种类型:
算术运算符:
符号 | 使用方法 | 说明 |
---|---|---|
+ | a + b | 加法 |
- | a - b | 减法 |
* | a * b | 乘法 |
/ | a / b | 去a/b的商 |
% | a % b | 取a/b的余数 |
注:无特殊规定时,都是二进制的运算。
关系运算符:
符号 | 使用方法 | 说明 |
---|---|---|
> | a > b | a大于b |
< | a < b | a小于b |
>= | a >= b | a大于等于b |
<= | a <= b | a小于等于b |
== | a == b | a等于b |
!= | a != b | a不等于b |
注:上面的小于等于符号与 非阻塞赋值 符号相同,区别是关系运算符只用于if-else的控制逻辑中,而非阻塞赋值则是用于always块中的赋值。
逻辑运算符:
符号 | 使用方法 | 说明 |
---|---|---|
&& | a && b | a整体和b整体的与 |
|| | a || b | a整体和b整体的或 |
! | a ! b | a整体和b整体的非 |
注:整体的意思就是看这个数是否为0,不为0这个数就表示逻辑1,为0就表示逻辑0。
条件操作符:
符号 | 使用方法 | 说明 |
---|---|---|
? : | a ? b : c | a为真就选择b,a为假就选择c |
注:当if-else的控制逻辑比较简单时,就用条件操作符,增加可读性。
位运算符:
符号 | 使用方法 | 说明 |
---|---|---|
~ | ~a | a按位取反 |
& | a & b | a与b按位与 |
| | a | b | a与b按位或 |
^ | a ^ b | a与b按位异或 |
注:上述的按位操作的a、b要求位数相同,且返回的结果位数与它们相同。
移位运算符:
符号 | 使用方法 | 说明 |
---|---|---|
<< | a << b | 逻辑左移,a左移b位,低位补0 |
>> | a >> b | 逻辑右移,a右移b位,高位补0 |
<<< | a <<< b | 算数左移,a左移b位,低位补0 |
>>> | a >>> b | 算术右移,a右移b位,高位补符号位(最高位) |
注:
- 逻辑移位是针对无符号数使用;算术移位是针对有符号整数使用,可以快速满足有符号数的乘以/除以2的整数次幂运算。
- 左移位宽增加,右移位宽不变。
拼接运算符:
符号 | 使用方法 | 说明 |
---|---|---|
{ } | {a , b} | 将a、b拼接起来,作为一个新的信号 |
注:这个符号使用的非常广泛,比如一个有意思的用法就是实现 循环移位:假如有一个4位的流水灯信号reg [3:0] led
,每个时钟沿都希望得到其循环移位的结果,于是有led <= {led[2:0] , led[3]}
。
下面给出上述运算符的优先级别,下表截图自《Verilog数字系统设计教程》-夏宇闻:
注:遇事不决小括号,妈妈再也不用担心我的学习,so easy!😎
1.5 Verilog程序框架
1.5.1 Verilog注释
Verilog的注释有两种:
- 行注释:
//
- 块注释:
/* */
就不赘述了。
1.5.2 Verilog关键字
略。可自行查看书籍《Verilog数字系统设计教程》-夏宇闻。
1.5.3 Verilog程序框架
1. 模块module
module block(a,b,c);
input a, b; //模块的输入不需要规定类型
output wire c; //模块的输出一定要规定类型
wire d; //定义内部信号
assign d = a | b; //组合逻辑输出
assign c = d ? a : b;//组合逻辑输出
endmodule
根据上述代码,可以看出每个Verilog模块所包括4个主要的部分:端口定义、IO说明、内部信号声明、功能定义。
注:显然都采用组合逻辑,c信号可能会发生竞争冒险等现象,出现不稳定的毛刺,强烈不推荐这样的代码逻辑。使用组合逻辑则会消除这种现象,下一小节介绍。
2. always块
module block(
clk, //系统时钟信号
reset_n, //复位信号,n表示低电平复位
in_sig1, //输入信号1
in_sig2, //输入信号2
out_sig //输出信号
);
input clk, reset_n;
input in_sig1, in_sig2;
output reg [1:0] out_sig;
parameter CONST = 16'haaff;//这个常量仅作展示使用
always@(posedge clk or negedge reset_n)
if(!reset_n)
out_sig <= 2'b00;//这里也可以写2'd0
else
out_sig <= {in_sig1, in_sig2};
endmodule
上述代码的功能为:在系统不复位时,将两个输入信号拼接在一起输出出去。
可以发现使用了always块
增强程序的时序性能,@
表示监测后面括号里的内容,posedge
表示上升沿,negedge
表示下降沿。这样就保证了always@块
里面的所有信号的变化都只在规定的边沿发生,也就是时序逻辑。
若always@块
里面没有时钟,而是写做always@(*)
,则代表always@块
里面的信号会在任意一个信号发生变化时变化,也就是组合逻辑。
3. begin end
在一个module-endmodule
中,可以定义多个always@块
,它们都是同时运行的(并行)。但是在一个always@块
中,通常使用if-else if- ... -else
来控制,这个逻辑的控制是串行运行的,也就是电路会执行第一个符合条件的分支。
假如某一个分支包含多条语句,如同C语言中的大括号一样,就需要begin-end语句将这些语句括起来,如下所示:
always@(posedge clk or negedge reset_n)
if(!reset_n) begin
out_sig1 <= 2'b00;
out_sig2 <= 2'b00;
end
else begin
out_sig1 <= {in_sig1, in_sig2};
out_sig2 <= {in_sig2, in_sig1};
end
4. 模块的调用
那软件语言中有子函数的概念,指直接调用一些封装好的函数。在verilog编程中,这个子函数就变成了一个封装好的芯片(包含输入输出)。模块的调用(例化)有两种方法,下面以第2小节的block模块
举例:
显式调用:
block another_name(
.clk(clk), //系统时钟信号
.reset_n(reset_n), //复位信号,n表示低电平复位
.in_sig1(in_sig1), //输入信号1
.in_sig2(in_sig2), //输入信号2
.out_sig(out_sig) //输出信号
);
可以看到,上面的调用中,首先写出要调用模块的名字(block),然后再起一个别名(another_name),当然这个别名可以和模块名称相同;但假如多次调用这个模块,就需要有所区分。里面所有的输入输出也可以使用别的信号进行代替,只需要修改括号内的名称即可。里面的信号顺序也可以随意更换,也可以不写(如果不影响功能的话)。
当然,假如想更改模块中的常量,可以按照如下方式,注意加#号。
block #(
.CONST(CONST) //重新定义模块中的常量值
)another_name(
.clk(clk), //系统时钟信号
.reset_n(reset_n), //复位信号,n表示低电平复位
.in_sig1(in_sig1), //输入信号1
.in_sig2(in_sig2), //输入信号2
.out_sig(out_sig) //输出信号
);
隐式调用:
假如有时候上层模块定义的信号与要调用的模块内的输入输出信号名称完全相同,并且顺序完全相同,就可以省略一些麻烦,直接隐式调用:
block #(
.CONST(CONST) //重新定义模块中的常量值
)another_name(
clk, //系统时钟信号
reset_n, //复位信号,n表示低电平复位
in_sig1, //输入信号1
in_sig2, //输入信号2
out_sig //输出信号
);
注意,哪怕只有一个信号的名称不同/顺序不同,都只能使用显式调用,否则会报错。
。。。也许还会有更新