HDLBits第三章
1、模块(Module)
到目前为止,您已经熟悉了 a module,它是一个通过输入和输出端口与其外部交互的电路。更大、更复杂的电路是通过将更小的模块和连接在一起的其他部分(例如assign语句和always块)组成更大的模块来构建的。这形成了一个层次结构,因为模块可以包含其他模块的实例。
下图显示了一个非常简单的带有子模块的电路。在这个练习中,创建一个实例模块mod_a,模块的三个引脚(连接in1,in2和out)到顶层模块的三个端口(电线a,b和out)。该模块mod_a是为您提供的——您必须实例化它。
连接模块时,只有模块上的端口很重要。您不需要知道模块内的代码。模块的代码mod_a如下所示:
module mod_a ( input in1, input in2, output out );
// Module body
endmodule
模块的层次结构是通过在另一个模块中实例化一个模块来创建的,只要使用的所有模块都属于同一个项目(因此编译器知道在哪里可以找到该模块)。一个模块的代码没有写在另一个模块的主体中(不同模块的代码没有嵌套)。
您可以通过端口名称或端口位置将信号连接到模块。如需额外练习,请尝试两种方法。
将信号连接到模块端口
有两种常用的方法将电线连接到端口:按端口位置或按端口名称。
按端口位置
按位置将电线连接到端口的语法应该很熟悉,因为它使用类似 C 的语法。实例化模块时,端口根据模块的声明从左到右连接。例如:
mod_a instance1 ( wa, wb, wc );
此实例化类型的模块mod_a,并赋予它一个实例名“INSTANCE1”的,然后连接信号wa(新模块外部)的第一端口(in1新模块的),wb到第二端口(in2),以及wc所述第三端口(out)。这种语法的一个缺点是,如果模块的端口列表发生更改,则还需要找到并更改模块的所有实例以匹配新模块。
按端口名称
按名称将信号连接到模块的端口,即使端口列表发生变化,电线也能保持正确连接。但是,这种语法更加冗长。
mod_a instance2 ( .out(wc), .in1(wa), .in2(wb) );
上述行实例化类型的模块mod_a名为“INSTANCE2”,然后连接信号wa(模块外部)的端口命名 in1,wb到端口命名 in2,和wc到端口命名 out。注意这里的端口顺序是如何无关紧要的,因为无论它在子模块的端口列表中的位置如何,都会连接到正确的名称。另请注意此语法中紧接在端口名称之前的句点。
代码实现:
(1)按端口位置
module top_module ( input a, input b, output out );
mod_a instance1 ( a, b, out );
endmodule
(2)按端口名称
module top_module ( input a, input b, output out );
mod_a instance2 ( .out(out), .in1(a), .in2(b) );
endmodule
验证结果:
2、模块位置(Module pos)
这个问题类似于上一个(模块)。您将获得一个名为的模块mod_a,该模块按该顺序具有 2 个输出和 4 个输入。您必须按位置将 6 个端口连接到顶级模块的端口out1, out2, a, b, c, 和d 。
您将获得以下模块:
module mod_a ( output, output, input, input, input, input );
代码实现:
module top_module (
input a,
input b,
input c,
input d,
output out1,
output out2
);
mod_a ( out1, out2, a, b, c, d);
endmodule
验证结果:
3、模块名称(Module name)
这个问题类似于模块. 您将获得一个名为的模块mod_a,该模块按某种顺序具有 2 个输出和 4 个输入。您必须按名称将 6 个端口连接到顶级模块的端口:
Port | Port in mod_a | Port in top_module |
---|---|---|
output | out1 | out1 |
output | out2 | out2 |
input | in1 | a |
input | in2 | b |
input | in3 | c |
input | in4 | d |
您将获得以下模块:
module mod_a ( output out1, output out2, input in1, input in2, input in3, input in4);
代码实现:
module top_module (
input a,
input b,
input c,
input d,
output out1,
output out2
);
mod_a ( .out1(out1), .out2(out2), .in1(a), .in2(b), .in3(c), .in4(d) );
endmodule
验证结果:
4、模块转移(Module shift)
您将获得一个my_dff具有两个输入和一个输出的模块(实现 D 触发器)。实例化其中三个,然后将它们链接在一起以形成长度为 3 的移位寄存器。clk端口需要连接到所有实例。
提供给您的模块是:
module my_dff ( input clk, input d, output q );
请注意,要进行内部连接,您需要声明一些电线。命名电线和模块实例时要小心:名称必须是唯一的。
代码实现:
module top_module ( input clk, input d, output q );
wire q1,q2;
my_dff U1 ( clk, d, q1 );
my_dff U2 ( clk, q1, q2 );
my_dff U3 ( clk, q2, q );
endmodule
验证结果:
5、模块转移8(Module shift8)
本练习是module_shift的扩展. 模块端口不再只是单个引脚,我们现在有带有矢量作为端口的模块,您将在其上连接线矢量而不是普通线。与 Verilog 中的其他任何地方一样,端口的向量长度不必与连接到它的电线匹配,但这会导致向量的零填充或截断。本练习不使用矢量长度不匹配的连接。
您将获得一个my_dff8具有两个输入和一个输出的模块(实现一组 8 个 D 触发器)。实例化其中三个,然后将它们链接在一起以形成一个长度为 3 的 8 位宽移位寄存器。此外,创建一个 4 对 1 多路复用器(未提供),根据以下情况选择输出内容sel[1:0]: 输入值d,在第一个之后,在第二个之后,或者在第三个 D 触发器之后。(本质上,sel选择延迟输入的周期数,从零到三个时钟周期。)
提供给您的模块是:
module my_dff8 ( input clk, input [7:0] d, output [7:0] q );
不提供多路复用器,一种可能的写法是在always一个case语句块中。
拓展:
下面是一个2选1多路复用器。
always@(*) // always语句块,在断电或中断前,会一直执行
case(sel) // case语句,根据不同的sel值选择不同的输出值
1'b0: q = d; // 输入sel=0时,输出q=d
1'b1: q = q0; // 输入sel=1时,输出q=q0
endcase
其中,always@(*)里面的敏感变量为星号时,意思是敏感变量由综合器根据always里面的输入变量自动添加,不用自己考虑。
代码实现:
module top_module (
input clk,
input [7:0] d,
input [1:0] sel,
output [7:0] q
);
wire [7:0]q1,q2,q3;
my_dff8 ( .clk(clk), .d(d), .q(q1) );
my_dff8 ( .clk(clk), .d(q1), .q(q2) );
my_dff8 ( .clk(clk), .d(q2), .q(q3) );
always@(*)
case(sel)
2'b00: q = d;
2'b01: q = q1;
2'b10: q = q2;
2'b11: q = q3;
endcase
endmodule
验证结果:
6、加法器1(Module add)
您将获得一个add16执行 16 位加法的模块。实例化其中两个以创建 32 位加法器。在从第一个加法器接收进位后,一个 add16 模块计算加法结果的低 16 位,而第二个 add16 模块计算结果的高 16 位。您的 32 位加法器不需要处理进位(假设为 0)或进位(忽略),但内部模块需要处理才能正常工作。(换句话说,add16模块执行 16 位 a + b + cin,而您的模块执行 32 位 a + b)。
如下图所示将模块连接在一起。提供的模块add16具有以下声明:
module add16 ( input[15:0] a, input[15:0] b, input cin, output[15:0] sum, output cout );
代码实现:
module top_module(
input [31:0] a,
input [31:0] b,
output [31:0] sum
);
wire cin1;
add16 add_1( .a( a[15:0] ), .b( b[15:0] ), .cin(1'b0), .sum( sum [15:0] ), .cout(cin1) );
add16 add_2( .a( a[31:16] ), .b( b[31:16] ), .cin(cin1), .sum( sum[31:16] ), .cout() );
endmodule
验证结果:
7、加法器2(Module fadd)
在本练习中,您将创建一个具有两个层次结构的电路。您top_module将实例化add16(提供)的两个副本,每个副本将实例化add1(您必须编写)的16 个副本。因此,您必须编写两个模块:top_module和add1.
例如module_add,您将获得一个add16执行 16 位加法的模块。您必须实例化其中的两个以创建 32 位加法器。一个add16模块计算加法结果的低 16 位,而第二个add16模块计算结果的高 16 位。您的 32 位加法器不需要处理进位(假设为 0)或进位(忽略)。
add16如下图所示将模块连接在一起。提供的模块add16具有以下声明:
module add16 ( input[15:0] a, input[15:0] b, input cin, output[15:0] sum, output cout );
在每个 中add16,add1实例化了16 个全加器(模块,未提供)以实际执行加法。您必须编写具有以下声明的完整加法器模块:
module add1 ( input a, input b, input cin, output sum, output cout );
回想一下,全加器计算 a+b+cin 的总和和进位。
综上所述,本设计共有三个模块:
(1)top_module — 您的顶级模块包含两个…
(2)add16, 提供 — 1个 16 位加法器模块,由 16 个…
(3)add1 — 1 位全加器模块。
如果您提交的文件缺少module add1,您将收到一条错误消息,内容为Error (12006): Node instance “user_fadd[0].a1” instantiates undefined entity “add1”。
拓展:
全加器逻辑表达式:
sum = a ^ b ^ cin;
cout = a&b | a&cin | b&cin;
代码实现:
module top_module (
input [31:0] a,
input [31:0] b,
output [31:0] sum
);
wire cin1;
add16 add_1( .a( a[15:0] ), .b( b[15:0] ), .cin(1'b0), .sum( sum [15:0] ), .cout(cin1) );
add16 add_2( .a( a[31:16] ), .b( b[31:16] ), .cin(cin1), .sum( sum[31:16] ), .cout() );
endmodule
module add1 ( input a, input b, input cin, output sum, output cout );
assign sum = a ^ b ^ cin;
assign cout = a&b | a&cin | b&cin;
endmodule
验证结果:
8、进位选择加法器(Module cseladd)
行波进位加法器的一个缺点(参见前面的练习) 是加法器计算进位的延迟(在最坏的情况下来自进位)相当慢,并且第二级加法器在第一级加法器完成之前无法开始计算其进位. 这会使加法器变慢。一项改进是进位选择加法器,如下所示。第一级加法器和之前一样,但我们复制第二级加法器,一个假设进位=0,一个假设进位=1,然后使用一个快速的2对1多路复用器来选择哪个结果碰巧是正确的。
在本练习中,为您提供了与add16上一练习相同的模块,它将两个 16 位数字与进位相加,并产生一个进位和 16 位和。您必须使用自己的 16 位 2 对 1 多路复用器实例化其中三个以构建进位选择加法器。
如下图所示将模块连接在一起。提供的模块add16具有以下声明:
module add16 ( input[15:0] a, input[15:0] b, input cin, output[15:0] sum, output cout );
拓展:
(1)条件表达式:a ? b : c;
表示:如果a为真,则表达式值为b; 如果a为假,则表达式值为c 。
(2)case语句(条件分支语句):
case(控制表达式)
值1:语句块1;
值2:语句块2;
……
值n:语句块n;
default:语句块n+1;
endcase
代码实现:
(1)条件表达式
module top_module(
input [31:0] a,
input [31:0] b,
output [31:0] sum
);
wire sel;
wire [31:16]sum0,sum1;
add16 add_1( .a(a[15:0]), .b(b[15:0]), .cin(1'b0), .sum(sum[15:0]), .cout(sel) );
add16 add_2( .a(a[31:16]), .b(b[31:16]), .cin(1'b0), .sum(sum0), .cout() );
add16 add_3( .a(a[31:16]), .b(b[31:16]), .cin(1'b1), .sum(sum1), .cout() );
assign sum[31:16] = sel ? sum1 : sum0;
endmodule
(2)case选择语句
module top_module(
input [31:0] a,
input [31:0] b,
output [31:0] sum
);
wire cout1;
wire [31:16]sum0,sum1;
add16 add_1( .a(a[15:0]), .b(b[15:0]), .cin(1'b0), .sum(sum[15:0]), .cout(cout1) );
add16 add_2( .a(a[31:16]), .b(b[31:16]), .cin(1'b0), .sum(sum0), .cout() );
add16 add_3( .a(a[31:16]), .b(b[31:16]), .cin(1'b1), .sum(sum1), .cout() );
always@(*)
case(cout1)
1'b0:sum[31:16]=sum0;
1'b1:sum[31:16]=sum1;
endcase
endmodule
验证结果:
9、加减法(Module addsub)
加法器-减法器可以通过可选地否定其中一个输入来构建加法器-减法器,这等效于将输入取反然后加 1。最终结果是一个可以执行两种运算的电路:(a + b + 0) 和 ( a + ~b + 1)。如果您想更详细地说明该电路的工作原理,请参阅维基百科。
构建下面的加法器-减法器。
为您提供了一个 16 位加法器模块,您需要将它实例化两次:
module add16 ( input[15:0] a, input[15:0] b, input cin, output[15:0] sum, output cout );
每当sub为 1时,使用 32 位宽的 XOR 门来反转b输入。(这也可以被视为b[31:0]与 sub 复制 32 次的异或。参见复制运算符.) 还将子输入连接到加法器的进位。
代码实现:
module top_module(
input [31:0] a,
input [31:0] b,
input sub,
output [31:0] sum
);
wire cout1;
wire [31:0]b_sub;
assign b_sub = b ^ {32{sub}};
add16 add_1( .a(a[15:0]), .b(b_sub[15:0]), .cin(sub), .sum(sum[15:0]), .cout(cout1) );
add16 add_2( .a(a[31:16]), .b(b_sub[31:16]), .cin(cout1), .sum(sum[31:16]), .cout() );
endmodule
验证结果: