73人参与 • 2024-08-01 • fpga开发
fpga的全称是 现场可编程门阵列(field programmable gate array),简单来说,就是能用代码编程,直接修改fpga芯片中数字电路的逻辑功能。那这样就怎么了呢?因为早期芯片生产出来后,电路就固定好不会改变了,于是功能也就固定了,这种芯片就是asic(专用集成电路,application specific integrated circuit)。而要想改变电路结构就需要重新设计芯片、重新“流片”、测试等,整个过程非常的 耗钱 耗时间。那突然间fpga横空出世,支持通过修改软件代码来改变硬件电路结构,是不是就非常具有开创性!😎
还记得那一系列数电实验吗?那些手动搭建起来非常繁琐的数字电路,都可以通过简单的verilog代码直接在fpga芯片中生成。
hdl(硬件描述语言,hardware description language)是用于描述数字电路结构和功能的语言的统称。hdl所描述的电路可以通过综合工具将其转换为门级电路网表,然后将其与某种工艺的基本元件逐一对应起来,再通过布局布线工具转换为电路布线结构。
上面这一段翻译成人话就是,要编写fpga的代码(术语就叫做描述电路结构)肯定得需要一种语言吧,那c/c++、java、python等一众软件语言可以吗?那肯定不行啊,软件语言无法描述出清楚的电路结构,也没有办法约定时钟、走线、端口等,简单一句话,它们没这个实力知道吧😎,所以这时候就需要专用的 硬件描述语言hdl 了。不过不像软件语言那样枝繁叶茂,经过近三十年的发展,只有 verilog 和 vhdl 二者最终脱颖而出,成为了公认的行业标准,两者逻辑相通,但 新手建议首先学习verilog。
那怎么开始学习verilog hdl呢? verilog作为一种高级的硬件描述语言,很多语法现象与 c语言 非常相似,因此在 c语言 的编程基础上去学习 verilog 比较容易。只不过需要特别注意培养硬件设计的思想,着重理解verilog的 “并行”特性。多敲敲代码总是没错的😉。
那么为什么有了软件编程还需要fpga这种硬件编程呢?换句话说,为什么人们想要改变数字电路的逻辑功能呢,软件又不是不能跑?主要有两点:灵活、处理速度快。
当然,这并不是说软件开发就不重要了,软件开发和硬件开发各自有各自的深奥之处。但对于之前一直在使用c语言/matlab进行编程的同学来说,进行fpga开发时首先要关注的点就是并行。即,不同的always块的信号是并行处理的,在一个begin end语句内的所有信号也是并行处理的(上面提到的always
和begin end
在1.6.3小小节介绍)。也就是说,fpga编程要时刻认识到代码/框图都是与实际的硬件电路相对应的,如果没有添加额外的控制逻辑,所有模块一上电就会同时运行。
fpga开发小贴士:
- fpga和单片机
说到硬件编程,很多同学想到单片机也需要将代码下载到芯片中实现功能,那fpga和单片机又有什么区别呢?其实也就是上面提到的,单片机本质上也是一种cpu,并且市面上常见的单片机初学开发板所搭载的cpu都是arm架构。
对单片机编程并不改变其电路的内部连接结构,只是根据要求实现的功能来编写运行的程序(指令),所以单片机编程和软件编程本质上没有区别;而fpga编程每次都会改变内部的硬件电路。
芯片 | 类型 | 速度 | 结构 | 应用场景 | 开发语言 |
---|---|---|---|---|---|
单片机 | asic | 慢 | 哈佛总线 冯诺依曼架构 | 工业控制 | c语言 |
fpga | 半定制电路 | 快 | 查找表 | 算法实现ic验证 | verilog、vhdl |
- fpga、arm与dsp
- fpga与gpu
由于人工智能非常火热,所以人们常常将fpga和gpu放在一起进行讨论,看看谁更适合做一些ai的算法实现。
- 补充:fpga与cpld区别。
经过上面的探讨,简单总结一下fpga优势与局限性。
灵活性 | 并行性 | 集成性 |
---|---|---|
可重编程,可定制 | 更快的速度、更高的带宽 | 更多的接口和协议支持. |
易于维护,方便移植、升级或扩展 | 满足实时处理的要求 | 可将各种端接匹配元件整合到器件内部, |
降低nre成本,加速产品上市时间 | 有效降低bom成本 | |
支持丰富的外设接口,可根据需求配置 | 单片解决方案,可以替代很多数字芯片 | |
减少板级走线,有效降低布局布线难度 |
fpga技术的局限性
所以,基于上述fpga的优缺点,在开发目标产品时,是否需要使用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所诞生并发展的时代是一个好时代,与生俱来的一些特性也注定了它将会在这个时代的大舞台上大放光彩。
fpga设计是一种整合的技术,要求从不同的设计领域融合多种设计技能。要想成为一名资深的fpga开发工程师,开发一些复杂的fpga应用,极可能涉及到多种交叉的设计技能,需要掌握交叉学科知识、拥有丰富的技能,可能还需要来自系统、软件和硬件工程的设计技能。
领域 | 硬件/dsp设计 | 软件(hdl)设计 | 软件/dsp设计 | 系统设计 |
---|---|---|---|---|
所 需 技 能 | 板级硬件与接口设计 | hdl语言的设计输入 | 处理器代码模块的定义 | 处理器需求分析 |
dsp算法的硬件实现 | 脚本实现自动化处理 | 代码的编写和测试 | 设计数据流的定义 | |
逻辑电路设计 | 设计测试平台的开发 | dsp算法的软件实现 | 处理器架构的选择 | |
功耗与去耦设计 | hdl流程设计的配置管理 | 常规的代码调试和验证 | 硬件/软件实现的权衡 | |
硬件仿真 | 设计约束 | 在处理器上运行操作系统 | 系统级设计的层次结构定义 | |
板级引脚分配 | 支持设计复用 | 代码的配置管理 | 功能划分和模块化设计 | |
硬件模块调试 | 系统模块的集成与接口测试 | |||
i/o特性的定义 | 系统级测试,调试和验证 | |||
设计布局 | ||||
设计优化权衡 | ||||
信号完整性和终端匹配 | ||||
fpga器件和封装选择 |
当然,上述技能虽多,但不用害怕,没有人天生就懂软件/硬件开发,所有大佬都是从“hello world”/点灯开始的。下面就是特权同学总结的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开发流程都在这里完成:
在整个开发过程中,为了验证电路的正确性,还会进行仿真。从上面的示意图可以看到一共有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 |
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
那硬件开发的过程中,肯定也会出现各种各样的bug(实验现象与预期不符),在fpga开发过程中,最主要的调试方式就是看各个主要信号的波形。本小小节就来介绍使用vivado进行fpga程序调试的思路。
1) 硬件异常检测
在进行程序调试前,首先要确定硬件本身没有问题。可以按照以下步骤进行:
2) 行为仿真
确认硬件没有问题后,那么程序现象出问题就显然是软件程序的问题了。为了简单快捷,首先可以快速排查几个常见的错误:
若此时还是没有发现问题,就需要编写仿真文件,来产生设计好的激励以驱动可能有问题的模块,这些激励包括时钟、复位、控制信号,另外也可以设计一些具有规律性的假数据来进行测试。仿真文件编写完毕后,再点击“运行”,就可以看到目标信号的仿真波形了。
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的方法:
总结:
本小节来点轻松的,介绍一下fpga的发展历史以及内部结构。内部结构可能看起来有点难,但是先有个印象就行。
fpga的发展史其实也就是近几十年的事情:
在fpga刚发明的时,常以有多少“门”(与门、或门、非门等逻辑资源)来衡量一个fpga。而现在“门”的概念已经逐渐被淡化,fpga不仅强调其逻辑资源,还包括其他丰富的资源,如可编程io单元、丰富的布线资源、灵活的时钟管理单元、嵌入式块ram以及各种通用的内嵌功能单元(如内嵌乘法器、内嵌硬核ip等),很多器件还顺应市场需求内嵌专用的硬件模块。
近些年来,可编程器件的龙头老大xilinx和altera更是相继推出了硬核cpu+fpga的产品,此举大有单芯片横扫千军的架势。比如上图所示的蓝色ps部分就是cpu硬核(arm架构),剩下的黄色部分就是fpga,这样的产品包括xilinx的zynq系列和altera的soc fpga。
国际上,fpga的主要生产厂商有:
那我这一顿库库介绍fpga,其他的啥也没说。但是很多初学者(比如我)在刚学fpga找资料的时候,总是会看到什么fpga、zynq、pynq啥的,被这些不同的芯片型号整的晕头转向,那现在就来捋一捋他们的关系:
那xilinx官网上着重介绍了自家的 fpga芯片 和 zynq系列芯片,咱们就大概看看它的产品线(altera那边也差不多,就不多说了),如下图所示:
从上图可以看到主要有fpga芯片、soc(system on chip, 片上系统)两大类。这两个大类各自包含四个子系列,每个子系列又细分为多种产品线,而每一个产品线都有很多型号(图中未给出具体型号)。可以大致认为,该产品树从上到下分别对应了片上资源从少到多。
目前市面上最常见的初学者开发板,就是fpga系列中的atrix-7系列(xc7a35tfgg484/xc7a100tfgg484)、以及zynq-7000系列(7000/7020)。
本小小节介绍fpga内部的硬件结构,对于初学者来说,里面的很多图一看就非常复杂,想搞懂很困难。但是不要放弃,本篇文章只是用做科普,一些看似很难的图只需要有一个大概的印象即可,后续做实验时,涉及到相应的模块则会继续讲解。另外在基础的开发实验中,只需要了解各模块的接口,能把接口接对即可,可以暂时不用对内部结构有特别深入的理解。
下面给出了在设计数字电路的过程中,经常用到的一些基本结构。下面可能只给出一些基本的示意图,关于更详细的基本电路介绍可以在任意一门《数字电子技术基础》课程上讲到,可以在 b站/chatgpt 搜索相应关键词。
1) mos管
所有门电路的功能都可以由pmos管和nmos管(统称“晶体管”) 来实现的。下面给出了其物理结构和基本的电路符号。当然,这里仅是简单的示意图,实际上还有结型场效应管、绝缘栅型场效应管(又细分为增强型和耗尽型),具体细节可以查看任意一本《模拟电子技术基础》课本(推荐清华大学出版社 童诗白主编)。
2) 基本逻辑门电路
上图给出数字电路中最常见的基本逻辑门电路。上图所示的每一个门电路都是由最基本的 电阻、二极管、三极管 组成的。比如:
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内部是如何实现上述这些数字电路的基本结构的。
实际上,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)模式。
实际上,通过配置,上图不仅可以实现“多输入与门”电路结构,同样可以实现“多输入或门”,以至于任意多输入的组合,都可以实现。
以xilinx主流的7系列为例,一颗fpga内部通常都会有数千到数十万不等的 可配置逻辑块(configurable logic block,简称clb)。呈矩阵排布的clb就构成了最基本的fpga逻辑资源的架构,clb块之间有很丰富的布线池。xilinx7系列的可配置逻辑块可以有效的支持以下特性:
从微观角度看,clb内部主要由2个更小的单位slice所组成。两个slice之间有连线,且每个slice都有独立的高速进位链以及独立的布线通道连接到矩阵开关,通过矩阵开关可以实现slice与fpga大布线池之间的灵活编程。每个slice单元则包含了以下更小的功能块:
围绕在clb周围丰富的行、列走线称为 布线池,它用于衔接fpga的各个clb以及其它相关的资源。在fpga芯片四周的小矩形以及延伸出去的短线,则表示fpga和外部芯片接口的 io块。
从整个fpga的配置来看,不仅包含了很多clb资源,还包括以下丰富的资源:
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个不等,能够满足一般性的应用。
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包括:
1) 关于逻辑值
建议初学者将所有的信号值都规定好,也就是只使用前两个逻辑值。
2) 关于数字的表示
verilog数字进制格式包括二进制('b)、八进制('o)、十进制('d)和十六进制('h)。
一般常用的二进制、十进制和十六进制。记得一定要规定数字的位宽(这个位宽是二进制的位宽),有时候可能不规定(默认32位)也能正常运行,但不要自己埋雷💣。
小技巧:使用windows自带的计算器,打开程序员模式,就可以同时看到一个数字的四种进制的表示。对于fpga开发来说,这个工具比较方便。
3) 关于命名
标识符(identifier)就是模块名、端口名、信号名等,可以类比为软件语言中各个变量、函数的名称。verilog对命名标识符的过程做出了如下规定(必须遵守):
除此之外,广大verilog开发者还有一些约定成俗的建议:
以下是一些推荐的写法:
在 verilog 语言中,常用的主要有三大数据类型:寄存器数据类型、线网数据类型和参数数据类型。顾名思义,寄存器数据类型(reg
)描述的是一个可以存储数据的单元,而线网数据类型(wire
)则是描述一个物理连线,参数数据类型(parameter
)就是常量。下面依次介绍。
综上所述,每个信号的控制逻辑如下图所示。一直没有提到的是,时序逻辑信号会等待时钟边沿再进行变化;组合逻辑信号会立即变化。为了保证fpga具有良好的时序性,一般的信号都使用时序逻辑,只有一些控制信号、状态信号、模块之间的连接信号才使用组合逻辑,当然还需要开发者的综合考量。
注:与软件语言中直接使用等号进行赋值不同,verilog中,时序逻辑信号一般使用非阻塞赋值<=
,组合逻辑信号一般使用阻塞赋值=
。极少数情况不是这样,后续有空我再单独写文章阐述阻塞和非阻塞的原理。
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位,高位补符号位(最高位) |
注:
拼接运算符:
符号 | 使用方法 | 说明 |
---|---|---|
{ } | {a , b} | 将a、b拼接起来,作为一个新的信号 |
注:这个符号使用的非常广泛,比如一个有意思的用法就是实现 循环移位:假如有一个4位的流水灯信号reg [3:0] led
,每个时钟沿都希望得到其循环移位的结果,于是有led <= {led[2:0] , led[3]}
。
下面给出上述运算符的优先级别,下表截图自《verilog数字系统设计教程》-夏宇闻:
注:遇事不决小括号,妈妈再也不用担心我的学习,so easy!😎
verilog的注释有两种:
//
/* */
就不赘述了。
略。可自行查看书籍《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 //输出信号
);
注意,哪怕只有一个信号的名称不同/顺序不同,都只能使用显式调用,否则会报错。
。。。也许还会有更新
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论