实验一 选择器

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\)

../_images/mux01.png

Fig. 1 2选1选择器模块和真值表

下图是2选1选择器的卡诺图,根据卡诺图可以得出2选1选择器的的表达式为 \(y=(\sim s\&a)|(s\&b)\)

../_images/mux02.png

Fig. 2 2选1选择器的卡诺图

根据表达式画出其逻辑电路如下图所示。

../_images/mux03.png

Fig. 3 2选1选择器的逻辑电路

数据流建模

数据流建模主要是通过连续赋值语句 assign 来描述电路的功能。 根据这一逻辑电路图,利用Verilog HDL实现2选1选择器的逻辑电路,示例如下:

Listing 1 用数据流建模方式描述 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

设计一个激励代码,对设计的选择器的功能进行仿真:

Listing 2 2 选 1 选择器仿真代码
#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\) 的值。

../_images/muxtest.png

Fig. 4 2选1选择器仿真结果

结构化建模

结构化建模主要通过逐层实例化子模块的方式来描述电路的功能。用结构化建模方式来设计选择器的程序清单如下所示:

Listing 3 用结构化建模方式描述 2 选 1 选择器代码
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语句来设计选择器的程序清单如下所示:

Listing 4 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

Verilog语句的执行

在Verilog中,各语句是并发执行的,模块中所有的assign语句、always语句块和实例化语句,其执行顺序不分先后。而if语句是顺序执行的语句,其执行过程中必须先判断if后的条件,如果满足条件则执行if后的语句,否则执行else后的语句。Verilog语法规定,顺序执行的语句必须包含中always块中,always块中的语句按照它们中代码中出现的顺序执行。

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语句的功能语句只有一条,如果有多条功能语句,要把这些语句用关键词 beginend 将其括起来。如:

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\) 中的一个作为输出,具体请见真值表。

../_images/mux41.png

Fig. 5 4选1选择器模块及真值表

Verilog语言中的case语句可以综合出“多路选择器”的电路,它的可读性非常强。如下所示的是用case语句实现4选1多路选择器的方法:

Listing 5 4选1选择器case语句实现
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

上述设计的测试代码如下所示

Listing 6 4选1选择器激励代码
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();
}

上述程序的仿真结果如下图所示

../_images/mux41test.png

Fig. 6 4选1选择器仿真图

case 语句的使用

case语句是以关键字case和一个被括起来的“选择表达式”开头,表达式的结果表示一个整数。下面是case选项,每个选项由选择列表和过程语句构成,选择列表可以是一个整数值,也可以是多个整数值,多个整数值之间以逗号分开,选择列表和过程语句之间以冒号连接,如 0,1: y = a[0];

case语句的执行过程是这样的:先计算出选择表达式的值,在case选项中找到和选择表达式值相同的第一个选择,然后执行此选择值后面的过程语句。

case语句列出的选择列表,有时候不能全部包含选择表达式所有的可能值,这时关键词default就要被作为case语句的最后一个选项,它表示表达式中那些未被选择列表覆盖的所有其他值。一般情况下即使选择列表列出了选择表达式的所有选项,还是建议保留default这一选项。如果选择列表中没有包含选择表达式的所有选项,而此时又没有default选项的话,综合器会综合出一个锁存器以保存未被覆盖的情况下输出的过去值。这一般是不希望出现的情况,所以在case语句中建议无论如何保留default选项。

如果在满足某个表达式值时要执行多条语句,也要用关键词 beginend 将这些语句其括起来。

同样地,我们建议初学者不要使用case语句

因为使用case语句描述电路属于行为建模方式。随着电路变得越来越复杂,你可能会写出case语句中包含if语句,if语句中由嵌套case语句的代码,但你很可能已经理解不了它描述的电路是什么样的了。

一个通用的选择器模板

我们向大家提供一个经过泛化的选择器模板,它可以很方便地替代case语句的功能。这个选择器模板的Verilog代码如下:

Listing 7 选择器模板
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 的数据,则 out0 。特别地, MuxKeyWithDefault 模块可以提供一个默认值 default_out ,当列表中不存在键值为 key 的数据,则 outdefault_out 。实例化这两个模块时需要注意如下两点:

  • 需要使用者提供键值对的数量 NR_KEY,键值的位宽 KEY_LEN 以及数据的位宽 DATA_LEN 这三个参数,并保证端口的信号宽度与提供的参数一致,否则将会输出错误的结果

  • 若列表中存在多个键值为 key 的数据,则 out 的值是未定义的,需要使用者来保证列表中的键值互不相同

MuxKeyInternal 模块的实现中用到了很多高级的功能,如 generatefor 循环等,为了方便编写还使用了行为建模方式,在这里我们不展开介绍,通过结构化建模的抽象,使用者可以无需关心这些细节。

以下代码通过使用选择器模板来分别实现2选1多路选择器和4选1多路选择器:

Listing 8 使用选择器模板实现选择器
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;以此类推。

../_images/mux241.png

Fig. 7 2位4选1选择器

选择开发板上的SW0和SW1作为控制端Y,SW2—SW9作为四个两位数据输入端X0–X3,将两位的输出端F接到发光二极管LEDR0和LEDR1上显示输出,完成设计,对自己的设计进行功能仿真,并下载到开发板上验证电路性能。

在线测试

实现一个简单的二位四选一选择器。