FPGA Verilog开发实战指南:基于Intel Cyclone IV(基础篇)
上QQ阅读APP看书,第一时间看更新

9.2 实战演练——多路选择器

9.2.1 实验目标

设计并实现二选一多路选择器,主要功能是通过选通控制信号S确定选通A路或B路作为信号输出。当选通控制信号S为1时,信号输出为A路信号;当选通控制信号S为0时,信号输出为B路信号。

9.2.2 硬件资源

我们使用开发板上的按键和LED灯进行二选一多路选择器的验证,选取KEY1、KEY2、KEY3分别作为信号A、信号B和选通信号S的信号输入;以LED灯D6作为信号输出O,如图9-1所示。

图9-1 硬件资源

征途Pro开发板的按键未按下时为高电平、按下后为低电平,LED灯则为低电平点亮,如图9-2和图9-3所示。

图9-2 按键部分原理图

图9-3 LED灯原理图

9.2.3 程序设计

1. 模块框图

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

图9-4 模块框图

端口列表与功能描述如表9-1所示。

表9-1 输入输出信号描述

2. 波形图绘制

框图结构设计完毕后就可以实现该模块的具体功能了,也就是要找到输入和输出之间的具体映射关系。输入和输出满足信号与系统中输入与响应的关系。其中输入信号的名字用绿色表示,输出信号的名字用红色表示,任意模拟输入波形,画出输出信号的波形。

经分析得知:当sel为低电平时,out的输出波形和in2相同;当sel为高电平时,out的输出波形和in1相同。根据分析出的输入输出关系,我们列出如表9-2所示的真值表,然后再根据真值表的输入与输出的对应关系画出波形图。其波形图如图9-5所示,图中深色的线代表有效信号。

表9-2 真值表

图9-5 信号波形关系图

3. 代码编写

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

(1)用if-else语句实现多路选择器

用if-else语句实现多路选择器的参考代码具体参见代码清单9-1。

代码清单9-1 if-else语句实现多路选择器(mux2 _1.v)


 1 module  mux2_1           //模块的开头以“module”开始,然后是模块名“mux2_1”
 2 (
 3     input   wire    in1, //输入端1,信号名后就是端口列表“();”(端口列表里
 4                          //面列举了该模块对外输入、输出信号的方式、类型、
 5                          //位宽、名字),该写法采用了Verilog-2001标准,这
 6                          //样更直观且实例化时也更方便,之前的Verilog-1995
 7                          //标准是将模块对外输入、输出信号的方式、类型、位
 8                          //宽都放到外面
 9 
10     input   wire    in2, //输入端2,当数据只有1bit宽时,位宽表示可以省略,
11                          //且输入只能是wire型变量
12 
13     input   wire    sel, //选择端,每行信号以“,”结束,最后一个后面不加“,”
14 
15     output  reg     out  //结果输出,输出可以是wire型变量,也可以是reg型变
16                          //量,如果输出在always块中被赋值(即在“<=”的左边)
17                          //就要用reg型变量,如果输出在assign语句中被赋值
18                          //(即在“=”的左边),就要用wire型变量,
19 );                       //端口列表括号后有个“;”,不要忘记
20 
21                          // out:组合逻辑输出sel选择的结果
22 always@(*)               //“*”为通配符,表示只要if括号中的条件或赋值号右边的变量发生变化,
23                          //则立即执行下面的代码,“(*)”在此always中等价于“(sel, in1,in2)”
                              的写法
24 
25     if(sel == 1'b1)      //当“if...else...”中只有一个变量时不需要加“begin...end”,
26                          //显得整个代码更加简洁
27 
28         out = in1;       // always块中如果表达的是组合逻辑关系时,使用“=”进行赋值,
29                          //每句赋值以“;”结束
30     else
31         out = in2;
32 
33                          //模块的结尾以“endmodule”结束
34                          //每个模块只能有一组“module”和“endmodule”,所有的代码都要
                              在它们中间编写
35 endmodule

根据上面RTL代码综合出的RTL视图如图9-6所示。

图9-6 用if-else实现方法生成的RTL视图

有的读者可能会有疑问:为什么always块中被赋值的一定要为reg型变量,它并没有生成寄存器,而是实现了组合逻辑的功能。因为在Verilog语言中,寄存器的特点是需要在仿真运行器件中保存其值,也就是说这个变量在仿真时需要占据内存空间,而上面的always块只对sel、in1、in2三个变量的输入敏感,如果没有这三个变量的变化事件,则out变量将需要保存其值,因此它们必须被定义为reg型变量,但是在综合之后,并不对应硬件锁存器或者触发器(后面会讲到什么时候会出现综合成硬件锁存器或触发器的情况)。

(2)用case语句实现多路选择器

用case语句实现多路选择器的参考代码具体参见代码清单9-2。

代码清单9-2 用case语句实现多路选择器


 1 module  mux2_1
 2 (
 3     input   wire    in1,    //输入端1
 4     input   wire    in2,    //输入端2
 5     input   wire    sel,    //选择端
 6         
 7     output  reg     out     //结果输出
 8 );
 9 
10                             // out:组合逻辑输出选择结果
11 always@(*)
12     case(sel)
13         1'b1  : out = in1;
14         1'b0  : out = in2;
15 //如果sel不能列举出所有的情况,则一定要加default
16 //此处sel只有两种情况,并且完全列举了,所以default可以省略
17         default : out = in1;
18     endcase
19 
20 endmodule

根据上面RTL代码综合出的RTL视图如图9-7所示。

图9-7 用case实现方法生成的RTL视图

(3)用三目运算符实现多路选择器

用三目运算符实现多路选择器的参考代码具体参见代码清单9-3。

代码清单9-3 三目运算符实现多路选择器(mux2_1.v)


 1 module  mux2_1
 2 (
 3     input   wire    in1,    //输入端1
 4     input   wire    in2,    //输入端2
 5     input   wire    sel,    //选择端
 6 
 7     output  wire    out     //结果输出
 8 );
 9 
10 //out:组合逻辑输出选择结果
11 //此处使用的是条件运算符(三元运算符),当括号里面的条件成立时,
12 //执行“?”后面的结果;当括号里面的条件不成立时,执行“:”后面的结果
13 assign out = (sel == 1'b1) ? in1 : in2;
14 
15 endmodule

根据上面RTL代码综合出的RTL视图与图9-6相同,这并不是最基本的门电路,而是一个多路器的符号,但根据前面的介绍,数字电路不都是由最基本的门电路构成的吗,这个为什么不是呀?因为描述的角度不同,我们是从寄存器传输级这个层次来描述的,最基本的单元可能就是这些寄存器、多路器、译码器、比较器、加法器等,这些基本的单元再往底层划分还是可以由其他的门电路构成的,所以在描述这些电路功能时,我们也可以用最基本的门电路来描述,那我们最后看到的RTL视图就是由门电路构成的了,其缺点就是效率太低。既然我们可以从更高的层次描述实现的功能来提高效率,为什么还要用低层次的描述方式呢?所以基于门级的描述我们很少用,其他资料中有很多都是将这两者混在一起介绍的,这也是让初学者感到迷惑的地方。那还有没有更高层次的描述方法?当然有,比寄存器传输级还高的描述方法有算法级和系统级,将会用到更高级的语言,如System Verilog和System C,也可以使用C和C++通过高层次综合(High-level Synthesis,HLS)的方式来实现。

通过以上三种不同的代码编写方式,我们首先可以了解到一个最基本模块的书写格式和方法,还知道Veriolg语言和C语言的相似之处就是实现相同的功能,其代码方式是多种多样的,所以大家在代码的实现上有很多选择,看到不同的写法也不必奇怪,我们关注的是最后的功能,在不考虑资源使用的情况下,只要功能能够达到要求,对代码的灵活性可以随意控制。通过对比,我们发现用以上三种方式实现的二选一多路选择器对应综合出的RTL视图虽然有所差别,但综合工具在布局布线和最后映射FPGA资源时会自动优化,使最终的功能和占用的逻辑资源都是相同的。

4. 仿真验证

(1)仿真文件编写

多路选择器仿真文件的参考代码如代码清单9-4所示。

代码清单9-4 mux2_1模块仿真(tb_mux2_1.v)


 1 `timescale  1ns/1ns //时间尺度、精度单位定义,决定“#(不可被综合,但在可
 2                     //综合代码中也可以写,只是会在仿真时表达效果,而综合
 3                     //时会自动被综合器优化掉)”后面的数字表示的时间尺度和
 4                     //精度,具体含义为“时间尺度/时间精度”。为了以后
 5                     //编写方便,我们将该句放在所有“.v”文件的开头,后面的代
 6                     //码示例将不再显示该句
 7 
 8 module  tb_mux2_1();// testbench的格式和待测试RTL模块的格式相同,
 9                     //也是以“module”开始,以“endmodule”结束,所有的代码都要
10                     //在它们中间编写。不同的是在testbench中端口列表为空,
11                     //因为testbench不对外进行信号的输入输出,只是自己产生
12                     //激励信号,提供给内部实例化待测RTL模块使用,所以端口列表
13                     //中没有内容,只是列出“()”,当然可以将“()”省略,不要忘记括号
14                     //后有个“;”
15 
16 //在initial块和always块中被赋值的变量一定要为reg型
17 //在testbench中待测试RTL模块的输入永远是reg型变量
18 reg     in1;
19 reg     in2;
20 reg     sel;
21 
22 //输出信号,我们直接观察,也不用在任何地方进行赋值,
23 //所以是wire型变量(在testbench中待测试RTL模块的输出永远是wire型变量)
24 wire    out;
25 
26 // initial语句是可以被综合的,一般只在testbench中表达,而不在RTL代码中表达
27 // initial块中的语句上电后只执行一次,主要用于初始化仿真中要输入的信号
28 //初始化值在没有特殊要求的情况下给0或1都可以。如果不赋初值,仿真时信号
29 //会显示为不定态(ModelSim中的波形显示为红色)
30 initial     
31     begin   //在仿真中begin...end块中的内容都是顺序执行的,
32             //在没有延时的情况下几乎没有差别,看上去是同时执行的,
33             //有延时时才能表达得比较明了;
34             //而在rtl代码中,begin...end的作用相当于括号,
35             //在同一个always块中给多个变量赋值时要加上
36         in1 <= 1'b0;
37         in2 <= 1'b0;
38         sel <= 1'b0;
39     end 
40 
41 // in1:产生输入随机数,模拟输入端1的输入情况
42 always #10 in1 <= {$random} % 2; //取模求余数,产生随机数1'b0、1'b1
43                                  //每隔10ns产生一次随机数
44 
45 // in2:产生输入随机数,模拟输入端2的输入情况
46 always #10 in2 <= {$random} % 2;
47 
48 // sel:产生输入随机数,模拟选择端的输入情况
49 always #10 sel <= {$random} % 2;
50 
51 //下面的语句是为了在ModelSim仿真中直接打印出信息,以便于观察信号的状态变化
52 //也可以不使用下面的语句而直接观察仿真出的波形
53 // ------------------------------------------------------------
54 initial begin
55    $timeformat(-9, 0, "ns", 6); //设置显示的时间格式,此处表示的是(打印时间单
56                                  //位为ns,小数点后打印的小数位为0位,时间值
57                                  //后打印的字符串为“ns”,打印的最小数量字符为6个)
58 
59     //只要监测的变量(时间、in1, in2, sel, out)发生变化,就会打印出相应的信息
60    $monitor("@time %t:in1=%b in2=%b sel=%b out=%b",$time,in1,in2,sel,out);
61 end
62 //------------------------------------------------------------
63 
64 //待测试RTL模块的实例化,相当于将待测试模块放到测试模块中,并将输入输出对应连接上
65 //测试模块中产生激励信号给待测试模块的输入,以观察待测试模块的输出信号是否正确
66 //------------------------mux2_1_inst------------------------
67 mux2_1  mux2_1_inst //第一个是被实例化模块的名字,第二个是我们自己定义的在另一个
68                     //模块中实例化后的名字。同一个模块可以在另一个模块中或不同的
69                     //另外模块中被多次实例化,第一个名字相同,第二个名字不同
70 (
71 //前面的“in1”表示被实例化模块中的信号,后面的“in1”表示实例化该模块以及要和这个
72 //模块的该信号相连接的信号(名字可以不同,但为了便于连接和观察,一般让名字相同)
73 // “.”可以理解为将这两个信号连接在一起
74     .in1(in1),  // input in1
75     .in2(in2),  // input in2
76     .sel(sel),  // inputsel
77     
78     .out(out)   // output    out
79 );
80 
81 endmodule

注意:上面用到了2个initial块和4个always块,上电后这6个模块同时执行,也就是所谓的“并行”执行,在RTL代码中也是这样。

(2)仿真波形分析

在验证RTL逻辑时,我们不用关心内部结构是如何实现的,只需要明确被验证的“黑盒子”模块需要什么激励才能够比较完全地验证功能正确性,根据此需求来提供相应的输入激励,观察输出是否为我们最初设计的结果即可。这个模块的输入信号只有两个,因为是组合逻辑,所以输入信号的时序关系也很简单,只需要给不同的输入输出赋值就可以了。在Testbench中使用随机数函数生成随机变化的0、1给输入端口,先通过ModelSim仿真出的波形验证RTL逻辑是否正确,再通过观察“Transcript”中打印的信息进行验证。

根据Quartus II中的设置,ModelSim打开后,仿真波形自动运行的时间为1μs,这里我们不需要观察这么长时间。先清空波形,然后重新设置仿真时间为500ns,运行后即可验证结果的正确性(在某些情况下,当仿真波形运行1μs后,仍无法观察到所需要验证的结果,此时可以重新设置仿真时间,该时间也不宜设置得太长,否则会导致运行的时间过长,且运行后占用较大的计算机内存空间。总之,要以适度为主,或者用修改参数的方法同比例缩小必要仿真时间)。

通过图9-8所示的波形可以发现:当sel为高电平时,out输出为in1的值;当sel为低电平时,out输出为in2的值。这完全符合我们代码中的逻辑设计。

图9-8 仿真波形图

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

图9-9 打印结果

图9-10 添加打印结果

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

图9-11 Transcript界面图

5. 上板验证

仿真验证通过后,绑定引脚,对工程进行重新编译。将开发板连接12V直流电源和USB-Blaster下载器的JTAG端口,线路连接正确后,打开开关为板卡上电,随后为开发板下载程序。

程序下载完毕后,开始进行结果验证。如图9-12和图9-13所示,当按键KEY3未被按下时,sel输出为高电平,输出信号为in1。按键KEY1未按下,in1输出高电平,LED灯未被点亮;按键KEY1按下,in1输出低电平,LED灯点亮。如图9-14所示,当按键KEY3按下时,sel输出为低电平,输出信号为in2;当按键KEY2按下时,in2输出低电平,LED灯点亮。

图9-12 结果验证(一)

图9-13 结果验证(二)

图9-14 结果验证(三)