本系列文章将和读者一起巡礼数字逻辑在线学习网站 HDLBits 的教程与习题,并附上解答和一些作者个人的理解,相信无论是想 7 分钟精通 Verilog,还是对 Verilog 和数电知识查漏补缺的同学,都能从中有所收获。
首先附上传送门:
Vector0 - HDLBitshdlbits.01xz.net
Problem 10 : Vectors
什么是 Verilog 中的向量(vector)?向量是一组 wire 信号的集合,通过赋予这一组信号的集合一个名称,以便于访问其中的 wire 信号。
向量类似于总线,一般将向量视为位宽超过 1 位的 wire 信号,不是特别在意向量这个概念本身。
举个栗子
wire [7:0] w ; 声明了一个 8 bit 位宽的信号,向量名为 w,等价于 8 个 1bit 位宽的 wire 信号。
assign w_0 = w[0]; //取出了向量中最低位的 wire 信号
如果你对 C 语言的数组非常熟悉的话,请注意声明向量时,位宽位于向量名之前。但在片选向量中某个 bit 时,使用的语法同 C 语言数组中取出某个数的语法相同。
wire [99:0] my_vector; // Declare a 100-element vector
assign out = my_vector[10]; // Part-select one bit out of the vector
在同时声明多个向量时,位宽对于声明的多个向量都是起作用的,比如:
wire [7:0] x,y; //y 也被声明为位宽为 8 的向量
牛刀小试
构造一个电路,拥有 1 个 3 bit 位宽的输入端口,4 个输出端口。其中一个输出端口直接输出输入的向量,剩下 3 个输出端口分别各自输出 3 bit 中的 1 bit。
上图中,箭头上的小斜杠旁边的数字代表该向量(总线)的位宽,用于将向量同 wire 信号区别开来。
解答与分析
module top_module(
input [2:0] vec,
output [2:0] outv,
output o2,
output o1,
output o0
);
assign outv = vec;
// This is ok too: assign {o2, o1, o0} = vec;
assign o0 = vec[0];
assign o1 = vec[1];
assign o2 = vec[2];
endmodule
本题中我们练习了向量的定义以及如何片选向量中的某个 bit。
如果想要片选多个 bit,那么可以通过如下操作实现,该语法在 C 语言中不存在,但类似 Python 中的切片语法。
assign w = vec[1:0];
Problem 11 : Vector in more detail
向量组合了多个相关 wire 信号,通过向量名可以更方便地访问向量中信号。本题中我们将讨论有关向量的更多细节。
声明向量
向量在声明时,必须遵循:
type [upper:lower] vector_name;
其中 type 指定了向量的类型,一般为 wire 或者 reg 型。关于 reg 型,会在后续课程过程块的介绍中引入。如果向量为模块的输入输出端口,那么可以在 type 中添加 input/output 定义。
举一堆栗子
wire [7:0] w; // 8-bit wire
reg [4:1] x; // 4-bit reg
output reg [0:0] y; // 1-bit reg output port (但仍然是一个向量)
input wire [3:-2] z; // 6-bit wire input (在位宽中使用负数作为 index 是可以的,代表倒数第二位)
output [3:0] a; // 4-bit output wire. wire 为默认定义,在没有显式声明的情况下
wire [0:7] b; // 8-bit wire b[0]是这个向量的 最高位 MSB(most-significant bit)
这里你需要了解一个向量的比特顺序(endianness)信息,比特顺序取决于向量的 LSB 是向量的高位还是地位。比如声明为 [3:0] w 的向量,LSB 是 w[0],如果声明为 [0:3] w,那么 w[3] 是 LSB 。LSB 指的是二进制数中权值最低的一位。
在 Verilog 语法中,你可以将向量声明为 [3:0], 这种语法最为常见,但也可以将向量声明为 [0:3] 。这都是可以的,但必须在声明和使用时保持一致。如果声明为 wire [3:0] w ,但使用 w[0:3]赋值,这是不允许的。保持前后如一的比特顺序是很重要的一点,一些你挠破头都定位不了的 BUG 可能就是字节顺序不一致导致的。
变量隐式声明的危害
你知道吗,变量的隐式声明是 Verilog 中 BUG 的一大来源。
信号变量有两种声明方式,一是使用 wire 或者 assign 语句进行显示声明和定义,二是综合器的隐式声明和定义。
当你将一个未定义声明的信号连接到模块的输入输出端口时,综合器会“热心”地帮助你声明这个信号。但我可以向你保证,综合器没有厉害到能通过上下文,察言观色,“热心而正确”地帮你声明信号,它只会将其声明为 1 bit wire 型信号,当你本来需要使用一个超过 1 bit 的向量,但又忘记声明时,综合器往往就好心办坏事了。
(当然综合器会在这个生成 Warning,所以查看下 Warning 是查找 BUG 的好办法)
wire [2:0] a, c; // Two vectors
assign a = 3'b101; // a = 101
assign b = a; // b = 1 隐式声明并定于了 b
wire assign c = b; // c = 001 <-- bug 来了 b 被 coder 默认为和 a 相同的 3'b101,但其实 b 只有 1bit宽
my_module i1 (d,e); // d e 都被隐式声明为 1bit wire
//如果模块期望的是 vector 那么 BUG 就产生了
隐式声明的错误很容易在连接 IP 核的时候产生,从 IP 核模板文件复制来 IP 核模块后。往往会忘记声明连接 IP 模块之间的中间变量,而这些变量的隐式声明就可能被综合器“好心办了坏事”。
通过添加 `default_nettype none
宏定义会关闭隐式声明功能,那么这样一来,使用未声明的变量就会变成一个 Error 而不再只是 Warning。
unpacked vs. packed 数组
在声明向量时,一般向量的位宽写在向量名之前。位宽定义了向量的 packed 维度,该向量中每位信号都被视作一个块进行操作(在仿真中,硬件中有所不同)。unpacked 维度定义在向量名之后,通常用来定义向量数组。
reg [7:0] mem [255:0]; // 256 unpacked elements, each of which is a 8-bit packed vector of reg.
reg mem2 [28:0]; // 29 unpacked elements, each of which is a 1-bit reg.
(这段翻得磕绊,简单得说定义在向量名之前的是向量的位宽,定义在向量名之后的维度可以理解为向量数组的长度,同 C 语言中的数组长度概念相同,一般用来对存储器建模。)
获取向量元素:片选
通过向量名可以获得整个向量,在下方的 assign 语句中,向量名 a 代表了向量中的所有比特为信号。
assign w = a;
在 assign 赋值操作中,如果等号左右两侧信号的位宽不同,那么就会进行截断或者补零操作。
左侧信号位宽大于右侧信号位宽,右值的低位赋予左值对应的低位,左值高位的部分赋零。
左侧信号位宽小于右侧信号位宽,右值的低位赋予左值对应的低位,右值高位的部分直接被截断。即保留右值的低位。
使用 [] 可以对信号进行片选,选择信号中特定几位比特,以下是一些片选的例子。
w[3:0] // Only the lower 4 bits of w
x[1] // The lowest bit of x
x[1:1] // ...also the lowest bit of x
z[-1:-2] // Z 最低两位
b[3:0] // 如果 b 在声明时 声明为 wire [0:3] b;则不能使用 b [3:0]进行选择
b[0:3] // b的高四位.
assign w[3:0] = b[0:3]; // 将 b 的高位赋予 w 的低位 w[3]=b[0], w[2]=b[1], etc.
牛刀小试
分别输出 16 位输入信号的高 8 位 和低 8 位。
解答与分析
`default_nettype none // Disable implicit nets. Reduces some types of bugs.
module top_module(
input wire [15:0] in,
output wire [7:0] out_hi,
output wire [7:0] out_lo );
assign out_hi = in[15:8];
assign out_lo = in[7:0];
endmodule
这里使用 [ : ] 语法进行了最朴素和常用的信号片选。
Problem 12 : Vector part select
牛刀小试
一个 32 位的向量可以看做由 4 个字节组成(bits[31:24],[23:16],等等)。构建一个电路,将输入向量的字节顺序颠倒,也就是字节序大小端转换。
aaaaaaaabbbbbbbbccccccccdddddddd => ddddddddccccccccbbbbbbbbaaaaaaaa
这项操作常见于不同的大小端体系之间的数据交换,比如 x86 体系使用小端模式存储数据,而因特网协议中均使用大端模式,数据在本地和网络进行数据交换之前,均要进行大小端转换。
解答与分析
module top_module (
input [31:0] in,
output [31:0] out
);
assign out[31:24] = in[ 7: 0];
assign out[23:16] = in[15: 8];
assign out[15: 8] = in[23:16];
assign out[ 7: 0] = in[31:24];
endmodule
这里是官方的解答,其实也可以用一句 assign 语句代替,使用 {} 位拼接符,我们将在后续的课程中学到。
assign out = {in[7-:8],in[15-:8],in[23-:8],in[31-:8]};
Problem 13 : Bitwise operators
本题将关注逐位逻辑运算符(&)和逻辑运算符(&&)之间的差别
逐位逻辑运算符:对于 N 比特输入向量之间的逻辑比较,会在 N 比特上逐位进行,并产生一个 N 比特长的运算结果。
逻辑运算符:任何类型的输入都会被视作布尔值,零->假,非零->真,将布尔值进行逻辑比较后,输出一个 1 比特的结果。
牛刀小试
模块有两个 3bit 宽的输入变量 a,b ,要求输出 a,b 逐位或的,a,b 逻辑或以及 a,b 按位取反的结果,其中 b 在高位。
解答与分析
module top_module(
input [2:0] a,
input [2:0] b,
output [2:0] out_or_bitwise,
output out_or_logical,
output [5:0] out_not
);
assign out_or_bitwise = a | b;
assign out_or_logical = a || b;
assign out_not[2:0] = ~a; // Part-select on left side is o.
assign out_not[5:3] = ~b; //Assigning to [5:3] does not conflict with [2:0]
endmodule
本题分别练习了逐位逻辑比较以及逻辑操作,只从题目的角度来说,只要注意观察输出信号的位宽即可。
Problem 14 : Four-input gates
牛刀小试
分别构建一个 4 输入与门,或门以及异或门。
解答与分析
module top_module(
input [3:0] in,
output out_and,
output out_or,
output out_xor
);
assign out_and = & in;
assign out_or = | in;
assign out_xor = ^ in;
endmodule
4 输入逻辑门,转换为 Verilog 的思想是将 4 个输入变量进行逻辑操作,得到 1 比特结果,在本题中,最简单的写法是
assign out_and = in[3] & in[2] & in[1] & in[0];
解答中的写法使用了缩减运算符的语法,和将位展开的写法相同,但更加简便,我们将在后续的课程中加以展开。