LJgibbs · 2020年09月21日

HDLBits:在线学习 Verilog (四 · Problem 15-19)

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

首先附上传送门:

https://hdlbits.01xz.net/wiki/Vector3

Vector3 - HDLBits

Vector3 - HDLBits

hdlbits.01xz.net

Problem 15 : Vector concatenation operator

片选操作符用于选择向量的一部分比特。而连接操作符 { a,b,c },将较小的向量连接在一起,用于创建更大的向量。连接操作符是一个很常用的运算符。下面举些例子:

{3'b111, 3'b000} => 6'b111000
{1'b1, 1'b0, 3'b101} => 5'b10101
{4'ha, 4'd10} => 8'b10101010     // 4'ha and 4'd10 are both 4'b1010 in binary

连接操作符的基本语法使用 { } 将较小的向量括起来,每个 { } 内的向量使用逗号作为间隔。

连接运算符中的向量务必需要标注位宽,不然综合器怎么能知道你的结果需要多宽的位宽。因此 { 1,2,3 } 这样的操作是非法的,并会产生一个 Error:unsized constants are not allowed in concatenations.

习惯上,我们会把位连接符用在赋值语句的右侧,表示将较小的向量连接成较大的向量,赋予左值。但实际上位连接符同样可以用在赋值语句左侧,比如:

assign {cout,sum} = a + b + cin;

在表示全加器时,可以使用一句 assign 语句实现结果和进位的赋值。再看一些 HDLBits 上的栗子:

input [15:0] in;
output [23:0] out;
assign {out[7:0], out[15:8]} = in;         // 连接符用于赋值语句左侧,交换了字节的顺序
assign out[15:0] = {in[7:0], in[15:8]};    // 连接符用于赋值语句右侧,交换了字节的顺序
assign out = {in[7:0], in[15:8]};       // 此语句作用上与上两句相同交换了字节顺序,但不同的是赋值语句右侧为16位
//赋予左值后,右值扩展为24位,高8位赋零,前两句中,高8位为未赋值状态 

牛刀小试

上图上方的方格表示模块 32 位输入向量,按照上下对应关系,输出为下方的 4 个 8 比特向量。

解答与分析

module top_module (
    input [4:0] a, b, c, d, e, f,
    output [7:0] w, x, y, z );//

    // assign { ... } = { ... };
    assign w = {a,b[4:2]};
    assign x = {b[1:0],c,d[4]};
    assign y = {d[3:0],e[4:1]};
    assign z = {e[0],f,2'b11};
endmodule

本题中我们练习了在赋值语句右侧使用位连接符的语法,在左侧使用位连接符的场景也很常见,比如在全加器中使用一句 assign 语句实现结果和进位的赋值。

Problem 16 : Vector reversal 1

这里直接上题:给定一个 8bit 输入向量,将其反向输出。

解答与分析

module top_module (
    input [7:0] in,
    output [7:0] out
);
    assign {out[0],out[1],out[2],out[3],out[4],out[5],out[6],out[7]} = in;    
endmodule

这里使用左侧位连接符,比较笨的方法完成了题目。好,假设输入为 1024bit 向量,那咋办?

接下来我们将讨论两种使用循环实现的方法,可能会涉及一些没有讨论过的知识点,但请不用担心,我们将在后续的文章或者题目中详细讨论。

使用 for 循环

integer i;
always @(*) begin    
     for (i=0; i<8; i++)    //Use integer for pure Verilog.
         out[i] = in[8-i-1];
end

我们可以在创建一个组合逻辑 always 块(后续文章中会详细解释什么是组合逻辑 always 块),在块中的组合逻辑将会按照一定的顺序运行。for 循环描述了电路的行为,而不是电路的结构,因此,for 循环必须置于比如 always 块这样的过程块中。(描述电路行为)

但需要强调的是,for 循环中的“循环”指的是代码层面的循环,而如你所知,电路是不存在循环这种的东西的,无论是信号而是门电路,都不存在循环一说。实际上,for 循环表示的代码将被综合器解析,for 循环将被分别解析为硬件电路。(不过在仿真中,确实按照循环处理)。

所以 for 循环可以理解为代码循环的语法,减少编码量,但真正的硬件电路不存在循环,还是该怎么样怎么样。

另请注意循环变量 i,HDLBits 上的 solution 中,i 定义于 for 循环的括号中,这在 Verilog 的语法中是不被允许的,是 SystemVerilog 的语法。笔者在 ISE 中实测了一下,综合会将其作为警告,但在默认情况下,仿真将会视其为错误。Verilog 的语法需要提前定义 integer 变量,即整形。

使用 generate 生成块

generate
    genvar i;
    for (i=0; i<8; i = i+1) begin: my_block_name
        assign out[i] = in[8-i-1];
    end
endgenerate

generate 生成块很有意思的一点是,虽然在 generate ,endgenerate 之间使用的仍然是 for 循环,但生成块的概念和上面的 for 循环完全不同

for 循环和 Verilog 中其他的几种循环语句 while ,forever,repeat 本质上都用于控制语句的执行次数。但生成块主要用于动态生成语句,例化 something(不只是例化模块),生成块与上述的过程块循环语句不同,并不是描述电路的一种行为。

生成块可以例化 assign 语句模块信号和变量的声明以及 always initial 这样的过程块。循环生成块是生成块中的一种类型,在综合过程中同样被综合器进行编译,这个过程可以看做综合过程中动态生成更多 Verilog 代码的预处理过程。在上面的例子中,generate 块在综合的过程中,综合了 8 句 assign 赋值语句。

总的来说,for 循环强调了对电路的行为描述,在综合的过程中循环展开,而生成块则用于综合过程中,动态生成代码,两者有本质上的不同。

说了这么多,读者可能还会对生成块一知半解 :( ,不过没有关系,笔者觉得要理解这个概念,还是要多写写代码,综合仿真一下。另外后续的文章或者题目中还会详细讨论这些概念。

说一点笔者在实践而不是从书本(或者知乎文章: )上得来的发现:在生成块中的 for 循环中不能像前例一样使用 integer 作为循环变量,而是必须使用 genvar 变量。

Problem 17 : Replication operator

连接操作符允许我们将短小的向量连接在一起构成更宽的向量。很方便,但有的时候需要将多个重复的向量连接在一起,诸如 assign a = {b,b,b,b,b,b}; 这样的语句写多了是非常让人忧愁的。而重复操作符语法就可以在这种情况下帮到你,允许你将一个向量重复多次,并将它们连接在一起,语法是这样:{ 重复次数 { 向量 } }。

重复次数必须是一个常量,而且请特别注意重复操作符有两对 { }.外层的 {} 不能少。

来自 HDLBits 的例子

{5{1'b1}}           // 5'b11111 (or 5'd31 or 5'h1f)
{2{a,b,c}}          // The same as {a,b,c,a,b,c}
{3'd5, {2{3'd6}}}   // 9'b101_110_110. It's a concatenation of 101 with
                    // the second vector, which is two copies of 3'b110.

如果写成 {3'd5, 2{3'd6}} ,少了一对 {} 是错误的.。

牛刀小试

重复操作符的应用场景之一是在有符号数的扩展。有符号数的扩展是将符号位填充待扩展的比特。比如要将 4bit 的 4'b0101 有符号数扩展为 8bit ,0 是符号位,那么扩展之后为 8'b0000 0101.

现在要求你构建一个电路,将一个 8bit 有符号数扩展为 32bit 数。

解答与分析

module top_module (
    input [7:0] in,
    output [31:0] out );//

    // assign out = { replicate-sign-bit , the-input };
    assign out = {{24{in[7]}},in};
endmodule

将符号位 in[7] 扩展 24 位,后接原本的 8bit 数。

Problem 18 : More Replication

牛刀小试

将 5 个 1bit 信号分别组成下图中两个 25 bit 信号,输出向量为这两个 25bit 向量的逐位操作的结果。如果两个待比较信号某比特相同,则结果向量对应的该比特位 1 。

现在要求你构建一个电路,将一个 8bit 有符号数扩展为 32bit 数。

out[24] = ~a ^ a; // a == a, so out[24] is always 1.
out[23] = ~a ^ b;
out[22] = ~a ^ c;
...
out[ 1] = ~e ^ d;
out[ 0] = ~e ^ e;

根据上图,这题使用位连接符和重复操作符是再舒服不过了。

解答与分析

module top_module (
    input a, b, c, d, e,
    output [24:0] out );//

    // The output is XNOR of two vectors created by 
    // concatenating and replicating the five inputs.
    // assign out = ~{ ... } ^ { ... };
    assign out = ~{{5{a}},{5{b}},{5{c}},{5{d}},{5{e}}} ^ {{5{a,b,c,d,e}}};
endmodule

本题的重点在于如何快速地使用连接操作符和重复操作符构建图中的两个待比较向量。

Problem 19 : Modules

本题开始,我们将讨论一早我们就见识但还没有深入了解的概念:模块。

截止目前,我们已经对 Verilog 中模块这一概念建立了初步的印象:模块是一个电路,通过输入输出端口和外部的电路联系。无论多大,多复杂的数字电路都是由一个个模块以及其他组成部分(比如 assign 赋值语句以及 always 过程块)互相连接所构成的。在一个模块中可以例化下一级的模块,这就形成了层级的概念(hierarchy)。

下图展示了一个简单的,拥有下级模块的模块。在本题的练习中,创建一个下级模块 mod\_a,将其的三个端口 in1 ,in2 ,out 按照图中的连接方式,分别连接到顶层模块的 a ,b,out 端口上。mod\_a 已经在测试代码中提供给你,你不需要自己写一个模块,只需要在你的顶层模块中,例化 mod\_a 模块即可。

模块例化的基本语法 :模块名 实例名(定义连接 port 的信号);

比如 mod_a instance1 ( wa, wb, wc ); 例化了一个 mod\_a 模块,将例化的实例命名为 instance1 。括号中是模块端口的连接。port 的连接有两种方式,这里根据端口的位置定义了信号的连接,wire wa,wb,wc 按照顺序连接到模块端口上,两种信号连接方式将在后文中具体讨论。

当你从顶层模块的角度看去,下层模块 mod\_a 对你来说就是一个黑盒子,盒子里的内容并不重要。重要的是模块的输入输出端口。

并不在乎 module body 中有什么

模块的层级是通过在模块中例化下一级模块产生的。虽然不同的模块写在不同的 .v 文件中,(一般推荐一个 .v 文件中只写一个模块),但只要这些模块在 ISE/Vivado/Quartus 这些开发软件中处于一个 Project。综合器就能在你例化模块时,找到对应的模块和 .v 文件。

模块中可以例化其他模块,但在模块中不允许再定义其他模块。这项语法规则类似于在 C 语言函数中可以调用其他函数,但不能定义其他函数。

模块信号连接的两种方式

在实例化模块时,有两种常用的方式来进行模块端口的信号连接:按端口顺序以及按端口名称连接端口。

按端口顺序mod_a instance1 ( wa, wb, wc ); wa, wb, wc 分别连接到模块的 第一个端口(in1),第二个端口(in2)以及第三个端口(out)。这里所谓的端口顺序指的是模块端口的定义顺序。这种方式的弊端在于,一旦端口列表发生改变,所有模块实例化中的端口连接都需要改变。

按端口名称mod_a instance2 ( .out(wc), .in1(wa), .in2(wb) ); 在这种方式中根据端口名称指定外部信号的连接。这样一来就和端口声明的顺序完全没有关系。一旦模块出现改动,只要修改相应的部分即可。实际上,一般都使用这种方式来进行模块实例化。

解答与分析

module top_module ( input a, input b, output out );
        mod_a U_mod_a(
          .in1(a)
        , .in2(b)
        , .out(out));
    //mod_a U_mod_a(a, .b, out); //使用按照端口顺序的方式 声明信号连接
endmodule

本题中同时使用了两种方式定义了端口的信号连接,实际上按照端口名称连接的方式用得更多,因为更加容易处理模块端口列表的变动。

值得注意的是,在实例化模块时,一般一个端口用一行表示,这样更直观一些。至于逗号放在前面还是放在后面,那倒无所谓。但我看过 NVIDIA 的开源代码将逗号放在前面之后,觉得这样挺好的,故也就这么写了。

推荐阅读

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