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所示。

mux002

图 10‑1 硬件资源

由原理图可知,征途Pro开发板的按键未按下时为高电平、按下后为低电平;LED灯则为低电平点亮。如图 10‑2、图 10‑3所示。

mux003

图 10‑2 按键部分原理图

mux004

图 10‑3 LED灯原理图

4.3. 程序设计

4.3.1. 模块框图

根据功能分析,该工程只需实现一个2选1多路选择器的功能,所以设计成一个模块即可。模块命名mux2_1,模块的输入有三个1bit信号,两个名为in1和in2的数据输入信号和一个名为sel的选通控制信号,输出为1bit名为out的数据输出信号。根据上面的分析设计出的Visio框图如图 10‑4所示。

mux005

图 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

mux006

图 10‑5 信号波形关系图

4.3.1.2. 代码编写

实现2选1多路选择器功能的Verilog代码形式有很多种,我们这里主要列举三种实现方法,这三种方法对应的核心语法各不相同,后面我们还会经常用到。

  1. 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所示。

mux007

图 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所示

mux008

图 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)的方式来实现。

mux009

图 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的值,完全符合我们代码中的逻辑设计。

mux010

图 10‑9 仿真波形图

下面我们通过观察“Transcript”界面(如图 10‑10所示)打印的结果再进行验证(如果打开的界面找不到Wave或Transcript窗口可以点击“Tool”下面的列表进行添加,如图 10‑11所示)。

mux011

图 10‑10 打印结果

mux012

图 10‑11 添加打印结果

我们通过观察“Transcript”界面(如图 10‑12所示)中打印的结果发现红色小框组成的结果即为out输出的结果,我们可以发现这个打印信息和真值表的样式几乎是一模一样,在组合逻辑中,因为不考虑延时的问题,所以一行有效数据对应的就是独立的一行,清晰直观,将打印信息与前面绘制的真值表进行比对,能够更加快速验证结果的正确性。

mux013

图 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灯

mux014

图 10‑13 引脚配置图

4.4.1.1. 结果验证

如图 10‑14所示,开发板连接12V直流电源和USB-Blaster下载器JTAG端口。线路正确连接后,打开开关为板卡上电。

mux015

图 10‑14 程序下载连线图

如图 10‑15所示,使用“Programmer”为开发板下载程序。

mux016

图 10‑15 程序下载窗口

程序下载完毕后,开始进行结果验证。如图 10‑16、图 10‑17所示,当按键KEY3未被按下时,sel输出为高电平,输出信号为in1;按键KEY1未按下,in1输出高电平,led灯未被点亮;按键KEY1按下,in1输出低电平,led灯点亮。如图 10‑18所示,当按键KEY3按下时,sel输出为低电平,输出信号为in2;按键KEY2按下,in2输出低电平,led灯点亮。

mux017

图 10‑16 结果验证(一)

mux018

图 10‑17 结果验证(二)

mux019

图 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中使用的更多)

8、#延时(不可综合,但允许在可综合的模块中使用,其延时单位仍由可综合模块中的`timescale决定,但是综合时被其延时时间被综合器忽略)
9、`timescale(配合“#”允许在可综合模块中使用)

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进行验证,以达到强化熟悉语法、模块结构和软件操作流程的目的。