IcingTomato's Archive A Very Simple Knowledge Archive

The Tutorial for FPGA Player - Episode 2: Combinatorial Logic

在进行了两期的背景介绍后,本期也是终于是进入正题了。如果你之前有了解过数字电路,大概就听说过数字电路大致可以分为组合逻辑电路和时序逻辑电路,而大部分的电路都是这两者的结合。本期所要介绍的也就是这其中的前者:组合逻辑。顺便,上期之后,大概部分玩家购买的74芯片或者FPGA开发板也已经到货了吧?本期也会附带关于如何用74或者FPGA实现这些实验的说明。 第一个例子还是从第一期举过的例子开始好了。想要设计一个电路,里面有两个开关和一个灯泡,希望实现两个开关任意一个打开的时候灯泡点亮。当然,一个显而易见的解决方案就是把两个开关并联,如下图所示:

但是如果我们用像一般考虑单片机电路一样的思路去考虑,那么就可以把两个开关看作是两个输入口,而灯泡是一个输出口,那么这个电路就会变成这样:

而这个盒子里面所包括的就是需要实现的电路。这个电路可以是一块74,也可以是一块单片机,或者只是像图1一样简单的连接起来而已。这里就来考虑下,如果要用单片机来实现要怎么做吧。很简单,一句if语句的事情,直接翻译要求, **if ((a == 1)   (b == 1)) c = 1; else c = 0;** 可以看见逻辑运算已经出现了,输入1为高或者输入2为高时,输出1为高,否则输出1为低,这样就是这个简单例子的逻辑。另外等于1 可以省略,就变成了 **if (a   b) c = 1; else c = 0;** ,再考虑到因为逻辑运算的结果本身就是1或者0,这里甚至不需要if,只需要 **c = (a   b)** 即可。 只有一个简单的 **“或”运算(   , OR)** 的关系,如果要用图示描述出来的话:

中间这个像箭头一样的东西也就是“或”运算的符号了。除了或之外,当然还有其它的符号,以下是6个最常用的逻辑运算的符号:

现在再来一个例子吧,假设还是两个输入,现在希望有且仅有一个开关打开(也就是需要有一个开关打开,但是不能两个都打开)的时候让灯泡点亮,那要怎么实现呢?一种实现思路是,如果第一个开关打开且第二个开关没有打开,或者第二个开关打开且第一个开关没有打开,这两种情况下让灯泡点亮,否则就熄灭。用C语言的逻辑表达式表述的话大致是 **c = (((a == 1)&&(b == 0))   ((b == 1)&&(a == 0)));** 如果试图用和第一个例子一样的图示来表示逻辑关系的话,就会遇到问题:这里出现了一些新的运算。比如 “与”运算(&&, AND) ,这个在上面的常用表里面有。而 “等于” 运算虽然第一个例子里也出现了,但是被省略掉了。然而这次除了等于1之外还出现了等于0的运算,这就没法省略了。那么有没有等于运算的符号呢?回答是肯定的,然而等于并不是一个基础运算,这里并不使用。一个代替等于0运算的方法就是,先做否运算,再判断等于1,即把 (a == 0) 改写成 (!a == 1) ,这个写法可能有些奇怪,毕竟一般不会这么写,但是确实是可行的。整个逻辑运算也就变成了 **c =(a&&(!b))   (b&&(!a))** ,这样就能把图画出来了:

然而实现这个逻辑的方法并不是唯一的,如果调转一下思路,这个逻辑也可以表达为:如果有两个开关有任意一个打开,且两个开关有任意一个没有打开,那就能说明有且只有一个开关被打开了。表达式为 **c = (a   b)&&((~a)   (~b))** ,示意图如下:

虽然确实换了一种方法,但是似乎使用的逻辑门数量并没有变少啊?确实,但是如果简单看下的话,这个里面的两个非门和一个或门可以合并成一个与非(’NAND’)门,或者,其实整个逻辑可以只用一个门来完成:

有且只有一个输入为高时输出高,这其实就是 异或(XOR)门 的功能了。举上面这个例子是为了说明一个道理:使用逻辑表达式或者原理图示来说明一个逻辑,其实可能并不是最优的方案。同一个逻辑可能存在多种不同的表达方式。为了解决这个问题,可以使用 真值表(Truth table) 来表示逻辑:

a(输入) b(输入) c(输出)
0 0 0
0 1 1
1 0 1
1 1 0

对于上面这个逻辑,这个真值表是唯一的。看表的方法也很简单,左边两列a和b在这里是输入,右边一列c在这里是输出,比如要知道a打开(1) b没有打开(0)时c的输出,只要找到 1 0这一行,就能看见c的输出是1。如果两者都为打开(1),那么就是最后一行,c为0。

这里稍微展开一下,没有理解没有关系,不影响玩。其实我上面三种画法,第一种和第二种其实分别对应了两种常见表示方法,一种叫 SOP(Sum of Product,乘积之和) ,另外一种叫 POS(Product of Sum,和之乘积) 。这两种的取名方式是因为,通常的逻辑算式中,与预算并非用&&表示,而是用乘法的点 (·) 而或运算也并非用 **   ** ,而是用加法的加号 (+) ,非运算是在式子上面画横线。于是图4和图5的式子写法分别是:

一个是两个乘法的结果加起来,也就是乘积之和SOP,另外一个是两个加法的结果乘起来,自然就是和之乘积POS了。而且这里其实都是和真值表一一对应的。式1里面的两个乘积项其实就对应了真值表里面输出为1的两行,而式2里面的每个加法项分别也就对应了真值表里面输出为0的两行。想要用逻辑表达任意一个真值表,只需要把每一个输出为1的行用乘法 (与运算,AND,&&) 表示出来,再全部加起来 **(或运算,OR,   )** 即可;或者也可以把输出为0的行用加法加起来,最后全部乘起来。这也分别就是POS和SOP了。当然通常这样表达出来的结果并非最优,要获得优化的结果还需要使用卡诺图等等。另外用乘法和加法来表示 && 和 **   ** 也不仅仅只是写着方便,这两个运算和一般计算数字的加法和乘法存在相似性,部分数字运算规则也可以应用到逻辑算式中,用于化简逻辑代数式,或者进行其它运算等等。由于本教程只是教大家玩FPGA,而不是教大家学离散数学或者逻辑电路,这些内容也就不展开讲了。感兴趣的可以自己去搜索一下。 所以,总结一下上面的内容,对于这样的,输出信号只取决于当前输入信号的(即只需要知道当前的输入信号就可以决定输出信号的)这类电路,就称为组合逻辑数字电路。或者也可以理解为,判断的内容只有输入信号的一条if语句。不知道各位也没有看明白呢?如果是第一次接触可能确实比较费解,有必要的话建议重新再读一遍。如果觉得没问题了,来做个实例吧,也能帮助加深理解。

实例:设计一个电路,可以计算1位二进制数的加法。

这个听起来完全不像之前的例子啊,之前都是开关,灯啊什么的,这里怎么突然就开始做算术了呢?其实并没有什么区别。1位二进制数加法,也就是要把两个1位的二进制数加起来,两个数分别可以是0或者1,换句话说,也就是两个开关。而1位二进制数加法可能有下面四种情况:

  • 0+0=0
  • 0+1=1
  • 1+0=1
  • 1+1=10

如果考虑把结果像之前一样也接上灯泡的话,那就和之前的事情非常接近了,只不过为了表示两位数字的输出现在需要两盏灯。不过,两盏灯又需要怎么设计呢……?首先还是先画真值表吧,假定两个输入分别是a和b,输出则是c和d:

a(输入) b(输入) c(输出) d(输出)
0 0 0 0
0 1 0 1
1 0 0 1
1 1 1 0

不难发现这个真值表看起来和上面四种情况的算式很像,只是加上了框框,并且把十位的零都写出来了而已。确实,真值表就是这样画的,列举所有可能的情况,在表里面写上每种情况的输入和输出。

现在剩下的问题就是怎么画成之前的电路图了。其实也不难做,单独考虑每个输出对应的逻辑,最后画在一起即可。比如这里有c和d两个输出,先考虑c的情况。对于c而言,只有当a和b都为1的时候c才是1,或者说只有当两个开关都打开时灯才会点亮,其实也就是c = a&&b这么一个逻辑。而d则是之前第二个例子中相同的逻辑,只需要一个异或就能完成,表达式为d = a^b。

下一步就是具体实现这个电路了。我这里分为三个部分,74芯片,Xilinx FPGA和Intel(Altera) FPGA。各位可以根据自己有的芯片来进行实验。如果没有的话,在以后也会讲如何使用仿真工具进行开发。

74芯片

如前面所说,这个电路需要两个门,一个与门一个异或门,对应的芯片分别为74HC08和74HC86。这两款芯片的内部连接方式可以通过查找对应的数据手册找到:

(74HC08)

(74HC86)

电路的话也并不复杂,只是把上面的示意图里面的逻辑符号替换成芯片,并增加必要的电阻罢了:

如果没有74HC86,d输出自然也可以选择像图4或者图5那样使用多个独立的门芯片来搭建,比如完全按照图4的原理图来,需要一片74HC04非门,和一片74HC32或门。

(74HC04)

(74HC32)

虽然芯片只是加了一片,不过接线变得复杂了不少,这也说明在使用74芯片搭建电路时,优化还是很重要的。

Intel FPGA

接下来讲讲如何在Intel FPGA平台上完成这个实验。就像玩单片机一样,玩FPGA需要在电脑上安装开发环境,在里面编写代码,然后再通过烧录器烧录进FPGA才能完成。通常对于单片机玩家来说,这个开发环境叫做Keil或者IAR。Intel FPGA的开发环境叫做Quartus,而且Intel提供了一个免费版本的Quartus,可以用于小规模设备的开发,通常来说只要使用免费版本的就足够了。具体的软件安装过程我这里不再赘述,如果有疑问可以在网上找到很多的帮助。 打开Quartus应用程序,在欢迎页面选择New Project Wizard或者从菜单选择File - New Project Wizard打开创建新工程的向导

自己选取要保存工程的文件夹,取个名字,Project Type选择Empty Project(空工程), 添加文件的页面直接点击下一步,直到来到这个选择器件的页面:

请按照自己开发板上的芯片型号选择。这个型号印在了FPGA上,通常也可以从开发板的用户手册、原理图钟得到。如我的是MAX10系列的10M50DAF484,就在这里选择这个型号。选择完成后可以直接点Finish完成创建。 创建完工程后还需要创建主程序文件。开发FPGA使用的并不是C语言,C语言毕竟是用于开发软件的语言,开发逻辑电路(硬件)有其专门的语言,比如本教程使用的Verilog语言。不过好在Verilog的语法和C还是较为相似的,熟悉C语言的话学习Verilog语言本身并不困难(但是适应硬件开发的思路转变就不简单了)。本期并不会系统讲解Verilog的使用,只是简单使用体验一下FPGA的使用而已。新建一个文件(不是工程),在弹出的对话框中选择Verilog HDL File。

在新的文件中输入以下代码:

module lesson3(
input  wire a,
input  wire b,
output wire c,
output wire d
);
 
assign c = a & b;
assign d = a ^ b;
 
endmodule

保存为lesson3.v。注意第一行后的lesson3需要和文件名匹配,如果你保存为了别的文件名,比如adder.v,那么第一行也应该相应的写成module adder才行。如果没有问题的话,保存后可以点击菜单栏的蓝色右箭头尝试进行综合(类似于程序 编译 的过程),应该可以顺利通过。如果没有,请检查刚刚保存的lesson3.v是否被加入了工程并设置为了顶层文件(可以使用左侧的面板查看)。

完成后可以看见左边几个任务都是绿钩子,或者可能是黄感叹号,都表示顺利通过了。在中间的Flow Summary里面可以看到一些报告信息,比如使用的逻辑数量,一共有49760个,这里只用到了3个。 不过这样还没有完成,在上面的程序中只是说了会有a b c d四条线,但是并没有说这四条线会连接到那些引脚上。接下来我们就来进行引脚分配的工作。首先第一步当然是要确定怎么连接。我的开发板(DE0-Lite)上有几个拨动开关,我决定就把a和b连接到拨动开关上,而c和d则是连接到两个LED输出上。通过翻原理图可以得知两个开关分别连接在C10和C11上,而两个LED分别连接在A8和A9上。打开Assignment菜单中的Pin Planner

可以注意到a b c d已经出现在了里面,而且有一些自动定义的位置。按照之前得知的信息在Location一栏填入引脚。Fitter Location可以不用理会,IO Standard这里要按照开关和LED连接的块的电源电压填写,通常也可以在原理图里看到,我这里是3.3V,也就是LVCMOS33。

完成后直接关闭窗口。重新点击蓝色右箭头进行综合。值得一提的是,不少开发板厂家会把板上的引脚配置预先定义好,要使用时只需要导入一个定义文件即可,而不需要每次这样手动查原理图配置。以后也会讲解如何使用那种方式完成,本期这么做一来是为了让大家体验这个过程,二来也是方便使用不同开发板甚至是最小系统板的玩家可以应用到自己的板子上。 现在就可以把程序(位流)烧录进FPGA测试了。使用USB线连接FPGA开发板至电脑,使用Tools菜单中的Programmer打开烧录工具。注意首次使用可能需要为烧录器安装驱动程序。如果你的Programmer窗口中左上方显示No Hardware则需要打开设备管理器为USB Blaster安装驱动。驱动可以在Quartus的安装目录中找到,如C:\intelFPGA_lite\18.0\quartus\drivers\usb-blaster。

确认左上角显示USB Blaster之后就可以点击Start开始烧录了,烧录完成后应该就可以观察到效果了。

Xilinx FPGA

如果你选购的是Xilinx FPGA的开发板,也没关系,整体过程也是大同小异的。需要注意的是,Xilinx曾经使用ISE作为开发环境,现在新的芯片已转为使用Vivado开发。鉴于目前最常用入门的芯片Spartan6仍然需要使用ISE开发,这里也使用ISE进行演示。Xilinx也像Intel一样推出了免费版本的ISE,叫做ISE WebPACK,对于我们来说完全足够了。具体安装方法这里不进行赘述,只讲使用。 在开始菜单中打开Project Navigator(ISE的主程序)

点击左侧的New Project…或者是菜单中的File - New Project打开新建工程窗口。也是一样,输入工程名称,保存位置,顶层文件类型选择HDL,在属性页中选择自己的设备。

其它不用修改,完成创建。在左侧窗格的Hierarchy中右键选择New Source建立新文件

在向导中选择Verilog Module并在右边输入文件名。

这个项目中有4个信号,a b c d,分别为两个输入和两个输出,这里就这么填写。

建立文件后注意到框架已经在了,只需要在中间插入两行逻辑即可,整体程序应该和Intel FPGA中列出的相同。

assign c = a & b;
assign d = a ^ b;

当然也和Intel FPGA那边一样,需要定义引脚,不过不是使用图形化的工具,而是直接编辑约束文件。和上面一样打开添加新文件的窗口,在左侧选择Implementation Constraints File,并在右边输入文件名constraints.ucf

根据原理图可知我的开发板LED连接到了H18和L18上,而两个按钮则是在AJ6和AK7上。另外需要知道所连接的块(Bank)的电压,通常也可以在原理图里找到。我的板子上按钮为3.3V而LED为2.5V。其它开发板可能是不同值,通常可能为3.3V。在新的约束文件中,根据上面得到的信息写入以下内容:

NET a LOC = AJ6;
NET a IOSTANDARD = LVCMOS33;
NET b LOC = AK7;
NET b IOSTANDARD = LVCMOS33;
NET c LOC = L18;
NET c IOSTANDARD = LVCMOS25;
NET d LOC = H18;
NET d IOSTANDARD = LVCMOS25;

其中a b c d就是四个要定义的信号的名字了。如果不确定后面的IOSTANDARD,可以参考开发板提供的参考程序中的ucf文件。编辑完成后,双击左侧任务窗口中的Generate Programming File开始综合并生成编程文件。如果没有问题的话左侧应该是三个绿钩。

在右侧可以看到综合的报告,如逻辑的使用量等等。通过USB连接开发板到电脑,使用Tools - iMPACT打开编程工具。点击左侧的Boundary Scan进入设备检测界面,在右侧右键,点击Initialize Chain检测设备。

双击FPGA设备,在弹出的窗口中选择刚刚生成的文件。注意到下面的bypass已经变成了选择的文件名。选中FPGA,在左侧的任务列表里面双击Program。一段时间后程序就应该被写入了。按下按钮可以注意到LED变化。