4. 简单组合逻辑—–多路选择器¶
在上一章节,我们以点亮LED灯的实验为例,为读者详细讲解了FPGA开发的正确流程、Quartus软件的使用、程序的下载与固化,读者务必理解掌握;在本章节,我们用Verilog语言描述一个具有多路选择器功能的电路,使读者能够掌握新的语法知识和基本的框图、波形、代码设计方法,最后通过仿真来验证设计的正确 性。
4.1. 理论学习¶
多路选择器是数据选择器的别称。在多路数据传送过程中,能够根据需要将其中任意一路选出来的电路,叫做数据选择器,也称多路选择器或多路开关。在选择变量控制下,从多路数据输入中某一路数据送至输出端。对于一个具有2^n个输入和1个输出的多路选择器,有n个选择变量。多路选择器也是FPGA内部的一个基本资源,主要 用于内部信号的选通。简单的多路选择器还可以通过级联生成更大的多路选择器。
4.2. 实战演练¶
4.2.1. 实验目标¶
设计并实现2选1多路选择器,主要功能是通过选通控制信号S确定选通A路或B路作为信号输出。当选通控制信号S为1时,信号输出为A路信号;当选通控制信号S为0时,信号输出为B路信号。
4.2.2. 硬件资源¶
我们使用开发板上的按键和LED灯进行2选1多路选择器的验证,选取KEY1、KEY2、KEY3分别作为信号A、信号B和选通信号S的信号输入;以LED灯D6作为信号输出O,如图 10‑1所示。
图 10‑1 硬件资源
由原理图可知,征途Pro开发板的按键未按下时为高电平、按下后为低电平;LED灯则为低电平点亮。如图 10‑2、图 10‑3所示。
图 10‑2 按键部分原理图
图 10‑3 LED灯原理图
4.3. 程序设计¶
4.3.1. 模块框图¶
根据功能分析,该工程只需实现一个2选1多路选择器的功能,所以设计成一个模块即可。模块命名mux2_1,模块的输入有三个1bit信号,两个名为in1和in2的数据输入信号和一个名为sel的选通控制信号,输出为1bit名为out的数据输出信号。根据上面的分析设计出的Visio框图如图 10‑4所示。
图 10‑4 模块框图
端口列表与功能总结如表格 10‑1所示。
表格 10‑1 输入输出信号描述
信号 |
位宽 |
类型 |
功能描述 |
---|---|---|---|
in1 |
1Bit |
Input |
输入信号1 |
in2 |
1Bit |
Input |
输入信号2 |
sel |
1Bit |
Input |
选通信号 |
out |
8Bit |
Output |
输出信号 |
4.3.1.1. 波形图绘制¶
框图结构设计完毕后就可以实现该模块的具体功能了,也就是要找到输入和输出之间具体的映射关系。输入和输出满足信号与系统中输入与响应的关系。其中输入信号的名字用绿色表示,输出信号的名字用红色表示,任意模拟输入波形,画出输出信号的波形。
经分析得,当sel为低电平时,out的输出波形和in2相同;当sel为高电平时,out的输出波形和in1相同。根据分析的输入输出关系,我们列出如表格 10‑2所示的真值表,然后再根据真值表的输入与输出的对应关系画出波形图。其波形图如图 10‑5所示,图中蓝色的线代表有效信号。
表格 10‑2 真值表
输入(input) |
输出(output) |
||
---|---|---|---|
in1 |
in2 |
sel |
out |
0 |
1 |
0 |
1 |
1 |
0 |
0 |
0 |
0 |
1 |
1 |
0 |
1 |
0 |
1 |
1 |
图 10‑5 信号波形关系图
4.3.1.2. 代码编写¶
实现2选1多路选择器功能的Verilog代码形式有很多种,我们这里主要列举三种实现方法,这三种方法对应的核心语法各不相同,后面我们还会经常用到。
always中if-else实现方法
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 | module mux2_1 //模块的开头以“module”开始,然后是模块名“mux2_1”
(
input wire in1, //输入端1,信号名后就是端口列表“();”(端口列表里
//面列举了该模块对外输入、输出信号的方式、类型、
//位宽、名字),该写法采用了Verilog-2001标准,这
//样更直观且实例化时也更方便,之前的Verilog-1995
//标准是将模块对外输入、输出信号的方式、类型、位
//宽都放到外面
input wire in2, //输入端2,当数据只有一位宽时位宽表示可以省略
//且输入只能是wire型变量
input wire sel, //选择端,每行信号以“,”结束,最后一个后面不加“,”
output reg out //结果输出,输出可以是wire型变量也可以是reg型变
//量如果输出在always块中被赋值(即在“<=”的左边)
//就要用reg型变量,如果输出在assign语句中被赋值
//(即在“=”的左边)就要用wire型变量
); //端口列表括号后有个“;”不要忘记
//out:组合逻辑输出sel选择的结果
always@(*)//“*”为通配符,表示只要if括号中的条件或赋值号右边的变量发生变化
//则立即执行下面的代码,“(*)”在此always中等价于“(sel, in1, in2)”写法
if(sel == 1'b1)//当“if...else...”中只有一个变量时不需要加“begin...end”
//也显得整个代码更加简洁
out = in1; //always块中如果表达的是组合逻辑关系时使用“=”进行赋值
//每句赋值以“;”结束
else
out = in2;
//模块的结尾以“endmodule”结束
//每个模块只能有一组“module”和“endmodule”,所有的代码都要在它们中间编写
endmodule
|
根据上面RTL代码综合出的RTL视图如图 10‑6所示。
图 10‑6 RTL视图(一)
有人可能会有稍稍的疑问,就是为什么always块中被赋值的一定要是reg型变量,他并没有生成寄存器而是实现的的组合逻辑的功能?因为在Verilog语言中,寄存器的特点是,它需要在仿真运行器件中保存其值,也就是说这个变量在仿真时需要占据内存空间,而上面的always块只对sel、in1、in2三个变量 的输入敏感,如果没有这三个变量的变化事件,则out变量将需要保存其值,因此它们必须被定义为reg型变量,但是在综合之后,并不对应硬件锁存器或者触发器(后面会讲到什么时候会出现综合成这两种的情况)。
(2)always中case实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | module mux2_1
(
input wire in1, //输入端1
input wire in2, //输入端2
input wire sel, //选择端
output reg out //结果输出
);
//out:组合逻辑输出选择结果
always@(*)
case(sel)
1'b1 : out = in1;
1'b0 : out = in2;
//如果sel不能列举出所有的情况一定要加default
//此处sel只有两种情况,并且完全列举了,所以default可以省略
default : out = in1;
endcase
endmodule
|
根据上面RTL代码综合出的RTL视图如图 10‑7所示
图 10‑7 RTL视图(二)
(3)assign中条件运算符(三元运算符)实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | module mux2_1
(
input wire in1, //输入端1
input wire in2, //输入端2
input wire sel, //选择端
output wire out //结果输出
);
//out:组合逻辑输出选择结果
//此处使用的是条件运算符(三元运算符),当括号里面的条件成立时
//执行"?”后面的结果;如果括号里面的条件不成立时,执行“:”后面的结果
assign out = (sel == 1'b1) ? in1 : in2;
endmodule
|
根据上面RTL代码综合出的RTL视图如图 10‑8所示,我们发现这并不是最基本的门电路,而是一个多路器的符号,之前不是说数字电路不都是由最基本的门电路构成的吗,这个为什么不是呀?因为我们描述的角度不同,我们是从寄存器传输级这个层次来描述的,最基本的单元可能就是这些寄存器、多路器、译码器、比较器、加法 器等等,这些基本的单元再往底层划分还是可以由其他的门电路构成的,所以在描述这些电路功能时我们也可以用最基本的门电路来描述,那我们最后看到的RTL视图就是由门电路构成的了,其缺点就是效率太低。既然我们可以从更高的层次描述实现的功能来提高效率,为什么还要用低层次的描述方式呢,所以基于门级的描述我们很少用 ,大家在看其他资料的时候有很多都是将这两者混在一起讲的,这样是让初学者感觉迷惑的地方。那就有人问了还有没有更高层次的描述方法?当然有,比寄存器传输级还高的描述方式有算法级和系统级,将会使用到更高级的语言,如System Verilog和Ssytem C,也可以使用C和C++再通过高层次综合(High-level Synthesis,HLS)的方式来实现。
图 10‑8 RTL视图(三)
通过以上三种不同的代码编写方式,我们首先可以了解到一个最基本模块的书写格式和方法,还知道Veriolg语言和C语言相似的地方就是实现相同功能,其代码方式是多种多样的,所以大家在代码的实现上就有很多的选择,看到别人不同的写法也不要大惊小怪,我们要关注的是最后的功能,在不考虑资源使用的情况下只要功能满足 要求,代码的灵活性可以随意控制。通过对比发现以上三种不同代码方式实现的2选1多路选择器对应综合出的RTL视图虽有所差别,但综合工具在布局布线和最后映射FPGA资源时会自动优化,使最终的功能和占用的逻辑资源都是相同的。
4.3.1.3. 仿真验证¶
仿真文件编写
仿真文件的代码如下所示
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 | \`timescale 1ns/1ns //时间尺度、精度单位定义,决定“#(不可被综合,但在可
//综合代码中也可以写,只是会在仿真时表达效果,而综合
//时会自动被综合器优化掉)”后面的数字表示的时间尺度和
//精度,具体表达含义为:“时间尺度/时间精度”。为了以后
//编写方便我们将该句放在所有“.v”文件的开头,后面的代
//码示例将不再显示该句
module tb_mux2_1();//testbench的格式和待测试RTL模块的格式相同
//也是以“module”开始以“endmodule”结束,所有的代码都要
//在它们中间编写。不同的是在testbench中端口列表为空
//因为testbench不对外进行信号的输入输出,只是自己产生
//激励信号提供给内部实例化待测RTL模块使用,所以端口列表
//中没有内容,只是列出“()”,当然可以将“()”省略,括号
//后有个“;”不要忘记
//要在initial块和always块中被赋值的变量一定要是reg型
//在testbench中待测试RTL模块的输入永远是reg型变量
reg in1;
reg in2;
reg sel;
//输出信号,我们直接观察,也不用在任何地方进行赋值
//所以是wire型变量(在testbench中待测试RTL模块的输出永远是wire型变量)
wire out;
//initial语句是可以被综合的,一般只在testbench中表达而不在RTL代码中表达
//initial块中的语句上电后只执行一次,主要用于初始化仿真中要输入的信号
//初始化值在没有特殊要求的情况下给0或1都可以。如果不赋初值,仿真时信号
//会显示为不定态(ModelSim中的波形显示红色)
initial
begin //在仿真中begin...end块中的内容都是顺序执行的,
//在没有延时的情况下几乎没有差别,看上去是同时执行的,
//如果有延时才能表达的比较明了;
//而在rtl代码中begin...end相当于括号的作用,
//在同一个always块中给多个变量赋值的时候要加上
in1 <= 1'b0;
in2 <= 1'b0;
sel <= 1'b0;
end
//in1:产生输入随机数,模拟输入端1的输入情况
always #10 in1 <= {$random} % 2;//取模求余数,产生随机数1'b0、1'b1
//每隔10ns产生一次随机数
//in2:产生输入随机数,模拟输入端2的输入情况
always #10 in2 <= {$random} % 2;
//sel:产生输入随机数,模拟选择端的输入情况
always #10 sel <= {$random} % 2;
//下面的语句是为了在ModelSim仿真中直接打印出来信息便于观察信号变化的状态
//也可以不使用下面的语句而直接观察仿真出的波形
//------------------------------------------------------------
initial begin
$timeformat(-9, 0, "ns", 6);//设置显示的时间格式,此处表示的是(打印时间单
//位为纳秒,小数点后打印的小数位为0位,时间值
//后打印的字符串为“ns”,打印的最小数量字符为6个)
//只要监测的变量(时间、in1, in2, sel, out)发生变化,就会打印出相应的信息
$monitor("@time %t:in1=%b in2=%b sel=%b out=%b",$time,in1,in2,sel,out);
end
//------------------------------------------------------------
//待测试RTL模块的实例化,相当于将待测试模块放到测试模块中,并将输入输出对应连接上
//测试模块中产生激励信号给待测试模块的输入,以观察待测试模块的输出信号是否正确
//------------------------mux2_1_inst------------------------
mux2_1 mux2_1_inst //第一个是被实例化模块的名子,第二个是我们自己定义的在另一个
//模块中实例化后的名字。同一个模块可以在另一个模块中或不同的
//另外模块中被多次实例化,第一个名字相同,第二个名字不同
(
//前面的“in1”表示被实例化模块中的信号,后面的“in1”表示实例化该模块并要和这个
//模块的该信号相连接的信号(可以取名不同,一般取名相同,方便连接和观察)
//“.”可以理解为将这两个信号连接在一起
.in1(in1), //input in1
.in2(in2), //input in2
.sel(sel), //inputsel
.out(out) //output out
);
endmodule
|
注:上面用到了2个initial块和4个always块,上电后这6个模块同时执行,也就是所谓的“并行”执行,在RTL代码中也是同样的。
仿真波形分析
在验证RTL逻辑时,我们不用关心内部结构是如何实现的,只需达到被验证的“黑盒子”模块需要什么激励才能够比较完全的达到验证功能正确性的目的,根据此需求来提供相应的输入激励,观察输出是否为我们最初设计的结果。这个模块的输入信号只有两个,因为是组合逻辑,输入信号的时序关系也很简单,只需要给不同的输入输出值 就可以了,我们在testbench中使用随机数函数生成随机变化的0、1给输入端口,先通过ModelSim仿真出的波形验证RTL逻辑是否正确,再通过观察“Transcript”中打印的信息进行验证。
根据在QuartusII中的设置,ModelSim打开后仿真波形自动运行的时间为1us,这里我们不需要观察这么多时间。先清空波形,然后重新设置仿真时间为500ns,运行后即可验证结果的正确性(在某些情况下仿真波形运行1u后仍不能观察到所需要验证的结果,此时可以再重新设置仿真时间,该时间也不宜设置太久 ,否则会使会导致运行的时间过长且运行后占用较大的电脑内存空间,总之以适度原则为主,或者用修改参数的方法同比例缩小必要仿真时间)。
通过图 10‑9所示的波形我们可以观察到,当sel为高电平时,out输出为in1的值;当sel为低电平时,out输出为in2的值,完全符合我们代码中的逻辑设计。
图 10‑9 仿真波形图
下面我们通过观察“Transcript”界面(如图 10‑10所示)打印的结果再进行验证(如果打开的界面找不到Wave或Transcript窗口可以点击“Tool”下面的列表进行添加,如图 10‑11所示)。
图 10‑10 打印结果
图 10‑11 添加打印结果
我们通过观察“Transcript”界面(如图 10‑12所示)中打印的结果发现红色小框组成的结果即为out输出的结果,我们可以发现这个打印信息和真值表的样式几乎是一模一样,在组合逻辑中,因为不考虑延时的问题,所以一行有效数据对应的就是独立的一行,清晰直观,将打印信息与前面绘制的真值表进行比对,能够更加快速验证结果的正确性。
图 10‑12 Transcript界面图
4.4. 上板验证¶
4.4.1. 引脚约束¶
仿真验证通过后,准备上板验证,上板验证之前先要进行引脚约束。工程中各输入输出信号与开发板引脚对应关系如表格 10‑3所示。引脚配置如图 10‑13所示。
表格 10‑3 引脚分配表
信号名 |
信号类型 |
对应引脚 |
备注 |
---|---|---|---|
in1 |
Input |
M2 |
按键 |
in2 |
Input |
M1 |
按键 |
sel |
Input |
E15 |
按键 |
out |
Output |
L7 |
LED灯 |
图 10‑13 引脚配置图
4.4.1.1. 结果验证¶
如图 10‑14所示,开发板连接12V直流电源和USB-Blaster下载器JTAG端口。线路正确连接后,打开开关为板卡上电。
图 10‑14 程序下载连线图
如图 10‑15所示,使用“Programmer”为开发板下载程序。
图 10‑15 程序下载窗口
程序下载完毕后,开始进行结果验证。如图 10‑16、图 10‑17所示,当按键KEY3未被按下时,sel输出为高电平,输出信号为in1;按键KEY1未按下,in1输出高电平,led灯未被点亮;按键KEY1按下,in1输出低电平,led灯点亮。如图 10‑18所示,当按键KEY3按下时,sel输出为低电平,输出信号为in2;按键KEY2按下,in2输出低电平,led灯点亮。
图 10‑16 结果验证(一)
图 10‑17 结果验证(二)
图 10‑18 结果验证(三)
4.5. 章末总结¶
本章通过2选1多路选择器介绍了如何编写一个最简单的RTL逻辑功能模块以及对应的仿真的代码如何编写,并进行了仿真验证。其中介绍了很多语法的实际应用和需要注意事项,希望读者能够掌握。
新语法总结
重点掌握
1、always块描述组合逻辑的用法
2、assign语句的用法
3、initial的用法(不可综合,常用于Testbech中初始化信号,但也可以在可综合的模块中用于初始化寄存器)
4、if-else的用法
5、case的用法
6、条件运算符(三元运算符)的用法
7、begin…end(对多条语句赋值时使用,因为我们设计RTL代码的原则是一个always块中最好只有一个变量,所以begin…end在RTL代码中几乎很少使用,而在Tetbench中使用的更多)
10、=(赋值号的一种,阻塞赋值,在可综合的模块中表达组合逻辑的语句时使用)
11、==(常用的比较运算符)
12、//(注释一行代码时使用)
一般掌握
1、$timeformat在Testbench中的用法(不可综合)
2、$monitor在Testbench中的用法(不可综合)
3、$time在Testbench中的用法(不可综合)
知识点总结
1、功能模块的书写结构、格式(端口列表推荐使用Verilog-2001标准)
2、仿真模块的书写结构、格式(端口列表中没有任何信号)
3、如何进行实例化调用(信号名的对应关系和连线)
4、在ModelSim中通过观察波形和查看“Transcript”界面中打印的信息对设计的代码进行功能验证(如果有语法或者仿真错误也会在该界面中提示)
4.6. 拓展训练¶
完成本章实例程后,希望大家可以自己多动手练习,并分别用本章中提到的三种方式分别编写一个8选1多路选择器,并编写对应的Testbench,且使用ModelSim进行验证,以达到强化熟悉语法、模块结构和软件操作流程的目的。