实验一 选择器
To be, or not to be, that is the question.
—《哈姆雷特》,莎士比亚
选择器是数字逻辑系统的常用电路,是组合逻辑电路中的主要组成元件之一,它是由多路数据输入、一位或多位的选择控制端,和一路数据输出所组成的。多路选择器从多路输入中,选取其中一路将其传送到输出端,由选择控制信号决定输出的是第几路输入信号。数字电路中存在大量的并行运算,通常的设计思路是使用电路同时生成所有可能用到的数字信号,再利用选择器选择最终输出哪一路信号。
本次实验将介绍几种常用的多路选择器的设计方法;Verilog语言中的always语句块、if-else语句和case语句的使用等。最后请读者自行设计一个多路选择器,熟悉电路设计的基本流程和Quartus的使用。
2选1多路选择器
下图是2选1选择器的模块图和真值表,图中 \(a\) 和 \(b\) 为输入端; \(y\) 为输出端, \(s\) 是选择端,选择两个输入的其中一个输出。当 \(s\) 为0时, \(y\) 的输出值为 \(a\) 。当 \(s\) 为1时, \(y\) 的输出值为 \(b\) 。
下图是2选1选择器的卡诺图,根据卡诺图可以得出2选1选择器的的表达式为 \(y=(\sim s\&a)|(s\&b)\) 。
根据表达式画出其逻辑电路如下图所示。
数据流建模
数据流建模主要是通过连续赋值语句 assign
来描述电路的功能。
根据这一逻辑电路图,利用Verilog HDL实现2选1选择器的逻辑电路,示例如下:
module m_mux21(a,b,s,y);
input a,b,s; // 声明3个wire型输入变量a,b,和s,其宽度为1位。
output y; // 声明1个wire型输出变量y,其宽度为1位。
assign y = (~s&a)|(s&b); // 实现电路的逻辑功能。
endmodule
设计一个激励代码,对设计的选择器的功能进行仿真:
#include "verilated.h"
#include "verilated_vcd_c.h"
#include "obj_dir/Vmux21.h"
VerilatedContext* contextp = NULL;
VerilatedVcdC* tfp = NULL;
static Vmux21* top;
void step_and_dump_wave(){
top->eval();
contextp->timeInc(1);
tfp->dump(contextp->time());
}
void sim_init(){
contextp = new VerilatedContext;
tfp = new VerilatedVcdC;
top = new Vmux21;
contextp->traceEverOn(true);
top->trace(tfp, 0);
tfp->open("dump.vcd");
}
void sim_exit(){
step_and_dump_wave();
tfp->close();
}
int main() {
sim_init();
top->s=0; top->a=0; top->b=0; step_and_dump_wave(); // 将s,a和b均初始化为“0”
top->b=1; step_and_dump_wave(); // 将b改为“1”,s和a的值不变,继续保持“0”,
top->a=1; top->b=0; step_and_dump_wave(); // 将a,b分别改为“1”和“0”,s的值不变,
top->b=1; step_and_dump_wave(); // 将b改为“1”,s和a的值不变,维持10个时间单位
top->s=1; top->a=0; top->b=0; step_and_dump_wave(); // 将s,a,b分别变为“1,0,0”,维持10个时间单位
top->b=1; step_and_dump_wave();
top->a=1; top->b=0; step_and_dump_wave();
top->b=1; step_and_dump_wave();
sim_exit();
}
上述代码分析与综合后的仿真结果如下图所示,由图中可以看出,当 \(s=0\) 时, \(y=a\) ,即 \(y\) 随着 \(a\) 值的改变而改变,此时的 \(b\) 值无论如何改变都不影响 \(y\) 的值。当 \(s=1\) 时, \(y=b\) ,即 \(y\) 随着 \(b\) 值的改变而改变,此时的 \(a\) 值无论如何改变都不影响 \(y\) 的值。
结构化建模
结构化建模主要通过逐层实例化子模块的方式来描述电路的功能。用结构化建模方式来设计选择器的程序清单如下所示:
module my_and(a,b,c);
input a,b;
output c;
assign c = a & b;
endmodule
module my_or(a,b,c);
input a,b;
output c;
assign c = a | b;
endmodule
module my_not(a,b);
input a;
output b;
assign b = ~a;
endmodule
module mux21b(a,b,s,y);
input a,b,s;
output y;
wire l, r, s_n; // 内部网线声明
my_not i1(.a(s), .b(s_n)); // 实例化非门,实现~s
my_and i2(.a(s_n), .b(a), .c(l)); // 实例化与门,实现(~s&a)
my_and i3(.a(s), .b(b), .c(r)); // 实例化与门,实现(s&b)
my_or i4(.a(l), .b(r), .c(y)); // 实例化或门,实现(~s&a)|(s&b)
endmodule
行为建模
行为建模是通过类似面向过程的编程语言来描述电路的行为。例如,在Verilog中也可以用if语句来实现2选1多路选择器的行为。用if语句来设计选择器的程序清单如下所示:
module mux21c(a,b,s,y);
input a,b,s;
output reg y; // y在always块中被赋值,一定要声明为reg型的变量
always @ (*)
if(s==0)
y = a;
else
y = b;
endmodule
always 语句块的使用
always @(<敏感事件列表>)
各可执行的语句;
......
其中敏感事件列表中列出了所有影响always块中输出的信号清单,也就是说,如果敏感事件列表中的任何一个变量发生了变化,都要执行always语句块中的语句。如 always @ (a or b or s)
表示:只要a、b、s中的任何一个变量发生了变化,就立刻执行always语句块中的语句。
为了方便起见,敏感列表也可以用 \(*\) 代替,如 always @ (*)
,这里, \(*\) 号将自动包含always语句块中语句或条件表达式右边出现的所有信号。如上述代码第5行的always语句块,只要always语句块中表达式右边出现的变量a和b,或者条件表达式中出现的变量s,这三个变量中的任何一个变量发生了变化,就立刻执行always语句块中的语句。
always语句还有另外一种形式,即:always后面不带任何有关敏感事件列表的信息,只有 always
这个保留字,那么这个时候表明在任何情况下都执行always语句块中的语句。
另外,always块中的输出信号必须被描述成reg型,而不是默认的wire型。
关于if语句
if语句是Verilog HDL中常用的条件语句,可以和else语句配对使用,也可以单独使用。
但是,如果if语句在使用时没有else语句与其配对则会发生这样的情况:编译器判断if后面的条件表达式是否满足,如果满足则执行其后的语句,那如果条件表达式不满足呢?这时,编译器就会自动产生一个寄存器来寄存当前的值,在条件不满足时保输出的过去值。这样就会产生用户没有设计的多余的寄存器出来。因此建议读者在使用if语句的时候要加上else语句与其配对。防止产生多余的寄存器。
另外,编译器默认if语句的功能语句只有一条,如果有多条功能语句,要把这些语句用关键词 begin
和 end
将其括起来。如:
if(s==0)
y = a; x = b;
else
y = b; x = a;
是错误的写法,应改为:
if(s==0)
begin y = a; x = b; end
else
begin y = b; x = a; end
在编程中也可以用条件判断语句代替if语句,如果此时不用顺序语句就不需要always语句块,比如也可以使用 ?
来代替if语句,其用法如下:
assign y = s ? b : a;
其含义如下:如果s = 1,那么 y = b;否则y = a。 则此2选1选择器代码可另写如下:
module mux21d(a,b,s,y);
input a,b,s;
output y; // y不用声明为reg型的了。
assign y = s ? b : a;
endmodule
强烈建议初学者不要使用行为建模方式设计电路
Verilog一开始并不是为了设计可综合电路而提出的,它的本质是一门基于事件队列模型的电路建模语言。因此,行为建模很容易会让初学者偏离描述电路的初衷: 开发者需要看着电路图,心里想象电路的行为,然后转化成事件队列模型的思考方式,最后再用行为建模方式来描述电路的行为,综合器再来根据这样的描述推导出相应的电路。从这个过程来看,这不仅是没有必要的,而且还很容易引入错误:
如果开发者心里本身就已经有电路图,直接描述它是最方便的
如果开发者心里本身就已经有电路图,而开发者对行为建模方式的理解所有偏差,可能会采用了错误的描述方式,从而设计出非预期的电路
如果开发者心里没有电路图,而是期望通过行为建模方式让综合器生成某种行为的电路,这就已经偏离“描述电路”的本质了。大部分同学非常容易犯这样的错误,把行为建模当作过程式的C语言来写,尝试把任意复杂的行为描述映射到电路,最终综合器只会生成出延迟大,面积大,功耗高的低质量电路
所以,直到大家掌握“描述电路”的思维而不被行为建模误导之前,我们强烈建议初学者远离行为建模方式,仅通过数据流建模和结构化建模方式直接描述电路。例如,上文关于if和always的说法从某种程度上来说是正确的,但下面的问题可以帮助大家测试自己是否已经掌握了Verilog的本质:
在硬件描述语言中,“执行”的精确含义是什么?
是谁在执行Verilog的语句? 是电路,综合器,还是其它的?
if的条件满足,就不执行else后的语句,这里的“不执行”又是什么意思? 和描述电路有什么联系?
有“并发执行”,又有“顺序执行”,还有“任何一个变量发生变化就立即执行”,以及“在任何情况下都执行”,它们都是如何在设计出来的电路中体现的?
如果你无法对这些问题作出明确的回答,我们强烈建议你不要使用行为建模方式。如果你真的想弄懂它们,你需要阅读 Verilog标准手册 。
真正的描述电路 = 实例化 + 连线
忘记行为建模方式,就可以很容易回归到描述电路的简单本质。想象一下,你手中有一张电路图纸,如果你需要向其它人描述图纸上的内容,你将会如何描述? 你一定会说出类似“有一个A元件/模块,它的x引脚和另一个B元件/模块的y引脚相连”的描述,因为这才是描述电路的最自然的方式。用HDL设计电路,就是在用HDL来描述电路图纸,图纸上有什么,就直接描述什么。所以,用HDL描述电路,无非是做两件事情:
实例化:在电路板上放一个元件/模块,可以是一个门电路,或者是由门电路组成的模块
连线:用导线将元件/模块的引脚正确地连起来
大家可以体会一下,数据流建模和结构化建模是如何体现这两件事的,而行为建模又是如何把这两件简单的事情复杂化的。
4选1多路选择器
4选1多路选择器的模块图和真值表如下图所示, \(a_0 - a_3\) 为4个输入端, \(s_0\) 和 \(s_1\) 是选择端, \(y\) 是输出端,根据 \(s_0\) 和 \(s_1\) 值的不同, \(y\) 选择 \(a_0-a_3\) 中的一个作为输出,具体请见真值表。
Verilog语言中的case语句可以综合出“多路选择器”的电路,它的可读性非常强。如下所示的是用case语句实现4选1多路选择器的方法:
module mux41(a,s,y);
input [3:0] a; // 声明一个wire型输入变量a,其变量宽度是4位的。
input [1:0] s; // 声明一个wire型输入变量s,其变量宽度是2位的。
output reg y; // 声明一个1位reg型的输出变量y。
always @ (s or a)
case (s)
0: y = a[0];
1: y = a[1];
2: y = a[2];
3: y = a[3];
default: y = 1'b0;
endcase
endmodule
上述设计的测试代码如下所示
int main() {
sim_init();
top->s=0b00; top->a=0b1110; step_and_dump_wave();
top->a=0b0001; step_and_dump_wave();
top->s=0b01; top->a=0b1110; step_and_dump_wave();
top->a=0b0010; step_and_dump_wave();
top->s=0b10; top->a=0b1010; step_and_dump_wave();
top->a=0b0100; step_and_dump_wave();
top->s=0b11; top->a=0b0111; step_and_dump_wave();
top->a=0b1001; step_and_dump_wave();
sim_exit();
}
上述程序的仿真结果如下图所示
case 语句的使用
case语句是以关键字case和一个被括起来的“选择表达式”开头,表达式的结果表示一个整数。下面是case选项,每个选项由选择列表和过程语句构成,选择列表可以是一个整数值,也可以是多个整数值,多个整数值之间以逗号分开,选择列表和过程语句之间以冒号连接,如 0,1: y = a[0];
。
case语句的执行过程是这样的:先计算出选择表达式的值,在case选项中找到和选择表达式值相同的第一个选择,然后执行此选择值后面的过程语句。
case语句列出的选择列表,有时候不能全部包含选择表达式所有的可能值,这时关键词default就要被作为case语句的最后一个选项,它表示表达式中那些未被选择列表覆盖的所有其他值。一般情况下即使选择列表列出了选择表达式的所有选项,还是建议保留default这一选项。如果选择列表中没有包含选择表达式的所有选项,而此时又没有default选项的话,综合器会综合出一个锁存器以保存未被覆盖的情况下输出的过去值。这一般是不希望出现的情况,所以在case语句中建议无论如何保留default选项。
如果在满足某个表达式值时要执行多条语句,也要用关键词 begin
和 end
将这些语句其括起来。
同样地,我们建议初学者不要使用case语句
因为使用case语句描述电路属于行为建模方式。随着电路变得越来越复杂,你可能会写出case语句中包含if语句,if语句中由嵌套case语句的代码,但你很可能已经理解不了它描述的电路是什么样的了。
一个通用的选择器模板
我们向大家提供一个经过泛化的选择器模板,它可以很方便地替代case语句的功能。这个选择器模板的Verilog代码如下:
module MuxKeyInternal #(NR_KEY = 2, KEY_LEN = 1, DATA_LEN = 1, HAS_DEFAULT = 0) (
output reg [DATA_LEN-1:0] out,
input [KEY_LEN-1:0] key,
input [DATA_LEN-1:0] default_out,
input [NR_KEY*(KEY_LEN + DATA_LEN)-1:0] lut
);
localparam PAIR_LEN = KEY_LEN + DATA_LEN;
wire [PAIR_LEN-1:0] pair_list [NR_KEY-1:0];
wire [KEY_LEN-1:0] key_list [NR_KEY-1:0];
wire [DATA_LEN-1:0] data_list [NR_KEY-1:0];
generate
for (genvar n = 0; n < NR_KEY; n = n + 1) begin
assign pair_list[n] = lut[PAIR_LEN*(n+1)-1 : PAIR_LEN*n];
assign data_list[n] = pair_list[n][DATA_LEN-1:0];
assign key_list[n] = pair_list[n][PAIR_LEN-1:DATA_LEN];
end
endgenerate
reg [DATA_LEN-1 : 0] lut_out;
reg hit;
integer i;
always @(*) begin
lut_out = 0;
hit = 0;
for (i = 0; i < NR_KEY; i = i + 1) begin
lut_out = lut_out | ({DATA_LEN{key == key_list[i]}} & data_list[i]);
hit = hit | (key == key_list[i]);
end
if (!HAS_DEFAULT) out = lut_out;
else out = (hit ? lut_out : default_out);
end
endmodule
module MuxKey #(NR_KEY = 2, KEY_LEN = 1, DATA_LEN = 1) (
output [DATA_LEN-1:0] out,
input [KEY_LEN-1:0] key,
input [NR_KEY*(KEY_LEN + DATA_LEN)-1:0] lut
);
MuxKeyInternal #(NR_KEY, KEY_LEN, DATA_LEN, 0) i0 (out, key, {DATA_LEN{1'b0}}, lut);
endmodule
module MuxKeyWithDefault #(NR_KEY = 2, KEY_LEN = 1, DATA_LEN = 1) (
output [DATA_LEN-1:0] out,
input [KEY_LEN-1:0] key,
input [DATA_LEN-1:0] default_out,
input [NR_KEY*(KEY_LEN + DATA_LEN)-1:0] lut
);
MuxKeyInternal #(NR_KEY, KEY_LEN, DATA_LEN, 1) i0 (out, key, default_out, lut);
endmodule
MuxKey
模块实现了“键值选择”功能,即在一个 (键值,数据)
的列表 lut
中,根据给定的键值 key
,将 out
设置为与其匹配的数据。若列表中不存在键值为 key
的数据,则 out
为 0
。特别地, MuxKeyWithDefault
模块可以提供一个默认值 default_out
,当列表中不存在键值为 key
的数据,则 out
为 default_out
。实例化这两个模块时需要注意如下两点:
需要使用者提供键值对的数量
NR_KEY
,键值的位宽KEY_LEN
以及数据的位宽DATA_LEN
这三个参数,并保证端口的信号宽度与提供的参数一致,否则将会输出错误的结果若列表中存在多个键值为
key
的数据,则out
的值是未定义的,需要使用者来保证列表中的键值互不相同
MuxKeyInternal
模块的实现中用到了很多高级的功能,如 generate
和 for
循环等,为了方便编写还使用了行为建模方式,在这里我们不展开介绍,通过结构化建模的抽象,使用者可以无需关心这些细节。
以下代码通过使用选择器模板来分别实现2选1多路选择器和4选1多路选择器:
module mux21e(a,b,s,y);
input a,b,s;
output y;
MuxKey #(2, 1, 1) i0 (y, s, {
1'b0, a,
1'b1, b
});
endmodule
module mux41b(a,s,y);
input [3:0] a;
input [1:0] s;
output y;
MuxKeyWithDefault #(4, 2, 1) i0 (y, s, 1'b0, {
2'b00, a[0],
2'b01, a[1],
2'b10, a[2],
2'b11, a[3]
});
endmodule
实验验收内容
上板实验: 二位四选一选择器
用选择器模板实现一个2位4选1的选择器,如下图所示,选择器有5个2位输入端,分别为X0, X1, X2, X3和Y,输出端为F;X0, X1, X2, X3是四个2位的输入变量。输出F端受控制端Y的控制,选择其中的一个X输出,当Y = 00时,输出端输出X0,即F = X0;当Y = 01时,输出端输出X1,即F = X1;以此类推。
选择开发板上的SW0和SW1作为控制端Y,SW2—SW9作为四个两位数据输入端X0–X3,将两位的输出端F接到发光二极管LEDR0和LEDR1上显示输出,完成设计,对自己的设计进行功能仿真,并下载到开发板上验证电路性能。
在线测试
实现一个简单的二位四选一选择器。