LJgibbs · 2020年09月23日

HDLBits:在线学习Verilog(六 · Problem 25-29)

转载自:知乎
本系列文章将和读者一起巡礼数字逻辑在线学习网站 HDLBits 的教程与习题,并附上解答和一些作者个人的理解,相信无论是想 7 分钟精通 Verilog,还是对 Verilog 和数电知识查漏补缺的同学,都能从中有所收获。

附上传送门:Module fadd - HDLBits

Problem 25: Adder 2(Module fadd)

牛刀小试

在本题中,您将描述一个具有两级层次结构的电路。在top\_module中,实例化两个add16模块(已为您提供),每个add16中实例化16个add1实例(此模块需要您编写)。所以,您需要描述两个模块:top\_module和add1。

与Problem 24: Adder 1(Module add)一样,提供给您一个执行16bit的加法的模块。您需要实例化两个16bit加法模块来实现32bit加法器。一个add16计算加法结果的低16位,另一个计算结果的高16位。您的32位加法器同样不需要处理进位输入(假设为0)和进位输出(无需进位)信号。

如下图所示,将add16模块连接在一起,给出的add16模块如下:

module add16 ( input[15:0] a, input[15:0] b, input cin, output[15:0] sum, output cout );

在每个add16中,实例化了16个全加器(add1,未给出,需要您自己写出)去执行加法操作。您必须编写具有以下声明的完整全加器(add1):

module add1 ( input a, input b, input cin, output sum, output cout );

回忆一下,全加器计算a+b+cin(三个信号均为1bit)的结果(sum)和进位(carry-out)。

总之,本题中一共有三个模块:

1、top\_module:包含两个16位加法器的顶级模块;

2、add16(已给出):一个16bit的加法器,由16个全加器构成;

3、add(未给出):1bit全加器

注意:在您提交的代码中如果缺少add1,您将收到一条如下的错误提示:

Error (12006): Node instance "user_fadd[0].a1" instantiates undefined entity "add1".

小提示:全加器的逻辑表达式

解答与分析

module top_module (
    input [31:0] a,
    input [31:0] b,
    output [31:0] sum
);//

    wire carry;
    
    add16 a1(a[15:0],b[15:0],1'b0,sum[15:0],carry);
    add16 a2(a[31:16],b[31:16],carry,sum[31:16],);
    
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

本体与上一题实现的功能是一样的,就是要多实现一个1bit全加器,如果不小心把16bit的全加器实现的话会提示模块声明多次的错误:

Error (10228): Verilog HDL error at tb_modules.sv(1): module "add16" cannot be declared more than once File:

Problem 26: Carry-select adder (Module cseladd)

上一个练习中(Problem 25: Adder 2(Module fadd))实现的加法器应该叫做行波进位加法器(RCA: Ripple-Carry Adder)。这种加法器的缺点是计算进位输出的延迟是相当慢的(最坏的情况下,来自于进位输入)。并且如果前一级加法器计算完成之前,后一级加法器不能开始计算。这又使得加法器的计算延迟变大。

牛刀小试

这次来实现一个改进型的加法器,如下图所示。第一级加法器保持不变,第二级加法器实现两个,一个假设进位为0,另一个假设进位为1。然后使用第一级结果和2选一选择器来选择哪一个结果是正确的。

在本题中,您将获得与上一练习相同的模块add16,它将两个16bit数和进位输入相加,并产生16bit的结果和进位输出。您必须实例化其中的三add16来构建进位选择加法器,同时实现16bit的2选1选择器来选择结果。

将模块如下图所示连在一起。提供的模块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 carry;
    wire [31:16] sum0;
    wire [31:16] sum1;
    
    add16 al(a[15:0],b[15:0],1'b0,sum[15:0],carry);
    add16 ah0(a[31:16],b[31:16],1'b0,sum0[31:16],);
    add16 ah1(a[31:16],b[31:16],1'b1,sum1[31:16],);
    
    assign sum[31:16] = carry?sum1:sum0;
    
endmodule

如果学过数字集成电路的进位链的话应该知道这是选择进位加法器(CSA: Carry-Select Adder),相对于上一题的行波进位(也叫逐级进位,逐位进位)加法器延迟小一半左右,但是比增多了50%的逻辑资源。

Problem 27: Adder–subtractor (Module addsub)

加减法器可以由加法器来构建,可以对其中一个数取相反数(对输入数据取反,然后加1)。最终结果是一个可以执行以下两个操作的电路: 和 。如果您想要更详细地了解该电路的工作原理,请参阅维基百科

牛刀小试

如下图所示构建加减法器,您需要实例化两次下面给出的16bit加法器模块:

module add16 ( input[15:0] a, input[15:0] b, input cin, output[15:0] sum, output cout );

当sub为1时,使用32位的异或门对B进行取反。(这也可以被视为b[31:0]与sub复制32次相异或,请参阅复制运算符Problem 17: Replication operator(Vector4))。同时sub信号连接到加法器的进位。

小提示:异或门也可以看作是可编程的非门,其中一个输入控制是否应该反转另一个。 以下两个电路都是异或门:

解答与分析

module top_module(
    input [31:0] a,
    input [31:0] b,
    input sub,
    output [31:0] result
);
    
    wire [31:0] b_n;
    wire carry;
    
    assign b_n = b^{32{sub}};
    
    add16 a0(a[15:0],b_n[15:0],sub,result[15:0],carry);
    add16 a1(a[31:16],b_n[31:16],carry,result[31:16],);
    
endmodule

学过数字逻辑电路的应该知道的小常识,减去一个数等于加上这个数的补码(就是题中的按位取反再加1)。

到这里关于模块层次的学习就结束了,下一题开始,我们进位过程块的学习。

Problem 28: Always blocks(combinational) (Alwaysblock1)

我们知道数字电路是由导线连接的逻辑门组成,因此任何电路都可以表示为module和assign语句的某种组合。但是,有时候这不是描述电路最简便的方法。过程块(比如always块)提供了一种用于替代assign语句描述电路的方法。

有两种always块是可以综合出电路硬件的:

综合逻辑:always @(*)
时序逻辑:always @(posedge clk)

组合always块相当于assign语句,因此组合电路存在两种表达方法。具体使用哪个主要取决于使用哪个更方便。过程块内的代码与外部的assign代码不同。过程块中可以使用更丰富的语句(比如if-then,case),但不能包含连续赋值*。但也引入了一些非直观的错误。(*过程连续赋值确实可以存在,但与连续赋值有些不同,并且不可综合)

例如,assign和组合always块描述相同的电路。两者均创造出了相同的组合逻辑电路。只要任何输入(右侧)改变值,两者都将重新计算输出。

assign out1 = a & b | c ^ d;
always @(*) out2 = a & b | c ^ d;

对于组合always块,敏感变量列表总是使用(*)。如果把所有的输入都列出来也是可以的,但容易出错的(可能少列出了一个),并且在硬件综合时会忽略您少列了一个,仍按原电路综合。 但仿真器将会按少列一个来仿真,这导致了仿真与硬件不匹配。(在SystemVerilog中,使用always\_comb)

牛刀小试

使用assign语句和组合always块来构建与门。(因为赋值语句和组合always相同,仿真器检测不出来你使用了那种方法,所以没有办法强制你使用这两种方法,但是你会这里练习的,对吧?......)(译者注:作者还是很调皮的)

解答与分析

// synthesis verilog_input_version verilog_2001
module top_module(
    input a, 
    input b,
    output wire out_assign,
    output reg out_alwaysblock
);

    assign out_assign = a & b;
    
    always@(*)
        out_alwaysblock = a & b;
    
endmodule

好像没什么分析的,照着上面的讲解抄就行。

Problem 29: Always blocks(clocked) (Alwaysblock2)

对于硬件综合来说,存在两种always块:

组合逻辑:always @(*)
时序逻辑:always @(posedge clk)

时序always块也会像组合always块一样生成一系列的组合电路,但同时在组合逻辑的输出生成了一组触发器(或寄存器)。该输出在下一个时钟上升沿(posedge clk)后可见,而不是之前的立即可见。

阻塞性赋值和非阻塞性赋值

在Verilog中有以下三种赋值方法:

连续赋值(assign x=y;):不能在过程块内使用;
过程阻塞性赋值(x=y;):只能在过程块中使用;
过程费阻塞性复制(x<=y):只能在过程块内使用。

在组合always块中,使用阻塞性赋值。在时序always块中,使用非阻塞性赋值。具体为什么对设计硬件用处不大,还需要理解Verilog模拟器如何跟踪事件(译者注:的确是这样,记住组合用阻塞性,时序用非阻塞性就可以了)。不遵循此规则会导致极难发现非确定性错误,并且在仿真和综合出来的硬件之间存在差异。

牛刀小试

使用assign语句,组合always块和时序always块这三种方式来构建异或门。 请注意,时钟always块生成了与另外两个不同的电路,多了一个触发器,因此输出会有一定的延迟。

解答与分析

module top_module(
    input clk,
    input a,
    input b,
    output wire out_assign,
    output reg out_always_comb,
    output reg out_always_ff   );

    assign out_assign = a ^ b;
    always@(*) out_always_comb = a ^ b;
    always@(posedge clk) out_always_ff <= a ^ b;
    
endmodule

从仿真的波形图可以看出,out\_always\_ff比其他两个输出延迟了一个时钟周期,这就是非阻塞性赋值带来的。

PS:本系列课程的题号是从0开始的,与网站1不符,望周知。

推荐阅读

关注此系列,请关注专栏FPGA的逻辑
推荐阅读
关注数
10514
内容数
513
FPGA Logic 二三事
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息