本系列对Verilog学习网站HDLBits网页上的知识点进行提炼总结,结合前辈的中文导学部分整理,用于自我学习。
文章目录
网站链接
语言版本
HDLBits使用Verilog-2001 ANSI风格的端口声明语法,因为它更容易阅读并减少拼写错误。如果您愿意,可以使用旧的Verilog-1995语法。
例如,下面的两个模块声明是可接受的并且是等效的。
//模块一
module top_module ( zero );
output zero;
// Verilog-1995
endmodule
//模块二
module top_module ( output zero );
// Verilog-2001
endmodule
以下部分为网页中讲解部分知识点:
Getting Started
Getting Started(综合仿真)
编译(逻辑综合):你的代码会通过 Altera Quartus 的综合器综合为硬件电路。
仿真:你的综合电路会通过功能仿真来检查其功能是否正确。HDLBits 使用 ModelSim 同时仿真你的代码和参考解决方案,然后比较两者的输出。
结果状态:如果你的电路完全正确,那么你会看到 Status: Success! 当然情况也有不妙的时候:
- Compile Error — 电路综合失败
- Simulation Error — 电路综合成功,但是仿真存在错误
- Incorrect — 电路综合,仿真成功,但输出结果和参考结果不同。
Verilog Language
Basics
Simple wire
wire 的中文可以翻译为导线,但 Verilog 中的 wire 和现实中的导线不同,wire 应该理解为一个信号。wire 是 Verilog 中的一种数据类型,代表的是信号,而不是连线。
信号是有方向性的,wire 从 A 点输出,输入到 B 点和 C 点。wire 一般只有一个 source,即从某一点输出,但可以有多个 sinks,即输入到多个点。A 点通常会被称为一个驱动(driver),把某个值驱动到 wire 上。
注意 wire 是有方向的 因此 assign in = out 与assign out = in是不等价的
Verilog 中的赋值assign
是使用一条带有方向的导线连接了两个信号,所以 left_side 始终等于 right_side,随 right_side 变化而变化。而软件中的赋值是一种事件,某个时刻 left_side 的值变成了和 right_side 相同的值。
Four wires
wire 的源一般只能有一个,终点确可以有多个。一个源可以驱动多个信号。
当你使用多条 assign 语句时,他们之间的顺序是无关紧要的,这点同顺序执行的软件代码不同。事实上,大部分 Verilog 代码之间的顺序都不会对结果产生影响。
图中的绿线代表的是 wire 之间的连接,而不是 wire 本身。即 wire 是连线两端的信号,而不是连线本身。
input a 等价于input wire a
wire 是信号,而 assign 语句则建立了信号之间的连接,这种连接是有方向性。
Inverter(取反)
逻辑操作符为 ~(逐位取反),逐位取反结果的位宽和输入信号位宽相同,在每一个位上逐位(bitwise)取反。
!(逻辑取反)。逻辑取反的结果时钟只有一位。
由于我们的信号位宽为 1 位,两种取反均可使用。
AND gate
被驱动的含义可以理解为,该信号的取值取决于另一个连接到它的信号的值,该信号的值随着另一个信号的值改变而改变。
模块的输入端口 input wire 被外部连接到模块的信号所驱动。
assign 语句映射到具体的硬件上,就是产生了信号的驱动,由右值驱动左值。
一个 wire 信号不能被多个信号同时驱动(当一个信号说往东,另一个信号说往西,两个信号还要同时驱动我时,我到底该往哪?)。
一个没有驱动者(driver)的信号的值会处于未定义的状态,在综合器一般会将其信号值驱动为 0.
值得注意的是 & 和 && 的区别,& 是逐位与,而 && 是逻辑与。
gate
NOT:非,直接取反。
OR:或,有1则1,其余为0。
NOR:或非,有1则0,其余为1。
AND:与,有0则0,其余为1。
NAND:与非,有0则1,其余为0。
XOR:异或,不同为1,相同为0。
XNOR:同或,相同为1,不同为0。
^ 为逐位异或,Verilog 中不存在逻辑异或符号。
Declaring wires
信号定义语句需要放置于模块的 body 中,模块的 body 指的就是 module 和 endmodule 中间的部分。
这里建议先定义信号,再使用信号。原则上,你可以在任何位置定义你的信号,使用前使用后都可以,语句的顺序对于 Verilog 来说没有关系。但有些仿真工具需要你在使用信号之前定义信号。
Vectors
Vectors([7:0])
向量是一组 wire 信号的集合,通过赋予这一组信号的集合一个名称,以便于访问其中的wire 信号。
向量类似于总线,一般将向量视为位宽超过 1 位的 wire 信号,不是特别在意向量这个概念本身。
向量的声明将维度放在向量名称之前。但是,使用其中某一信号在向量名称之后。
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 的向量
Vectors in more detail
wire [7:0] w;
声明一个名为 W 的 8 位向量,相当于有 8 个单独的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为1bit 隐式声明并定于了 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 就产生了
通过添加 default_nettype none
宏定义会关闭隐式声明功能,那么这样一来,使用未声明的变量就会变成一个 Error 而不再只是 Warning。
unpacked vs. packed 数组
reg[n-1:0]存储器名[m-1:0];或reg [n-1:0]存储器名[m:1];
在这里,reg[n-1 :0]定义了存储器中每一个存储单元的大小,即该存储单元是一个n位的寄存器;存储器名后的[m- 1:0]或[m:1]则定义了该存储器中有多少个这样的寄存器;最后用分号结束定义语句。
下面举例说明:
reg [7:0] mema[ 255:0];
这个例子定义了一个名为mena的存储器,该存储器有256个8位的存储器。该存储器的地址范围是0到255。
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.
获取向量元素:片选
通过向量名可以获得整个向量,在下方的 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.
Bitwise operators(按位与按逻辑)
逐位逻辑运算符(&)和逻辑运算符(&&)之间的差别。
逐位逻辑运算符:对于 N 比特输入向量之间的逻辑比较,会在 N 比特上逐位进行,并产生一个 N 比特长的运算结果。
逻辑运算符:任何类型的输入都会被视作布尔值,零->假,非零->真,将布尔值进行逻辑比较后,输出一个 1 比特的结果。
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 语句实现结果和进位的赋值。
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位为未赋值状态
Vector reversal 1(向量反转)
题目:给定一个 8bit 输入向量,将其反向输出。
方法一:使用位连接符。
assign out = {in[0],in[1],in[2],in[3],in[4],in[5],in[6],in[7]};
方法二:使用 for 循环(慢慢理解)
for中间用分号隔开。
integer i;
always @(*) begin
for (i=0; i<8; i++) //Use integer for pure Verilog.
out[i] = in[8-i-1];
end
创建一个组合逻辑 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 变量。
Replication operator(重复操作符)
重复操作符语法允许你将一个向量重复多次,并将它们连接在一起,语法是这样:{ 重复次数 { 向量 } }。
重复次数必须是一个常量,而且请特别注意重复操作符有两对 { }.外层的 {} 不能少。
{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}} ,少了一对 {} 是错误的.。
Modules: Hierarchy
Modules(例化)
模块是一个电路,通过输入输出端口和外部的电路联系。无论多大,多复杂的数字电路都是由一个个模块以及其他组成部分(比如 assign 赋值语句以及 always 过程块)互相连接所构成的。在一个模块中可以例化下一级的模块,这就形成了层级的概念(hierarchy)。
模块例化的基本语法 :模块名 实例名(定义连接 port 的信号);
比如 mod_a instance1 ( wa, wb, wc );
例化了一个 mod_a 模块,将例化的实例命名为 instance1 。括号中是模块端口的连接。port 的连接有两种方式,这里根据端口的位置定义了信号的连接,wire wa,wb,wc 按照顺序连接到模块端口上。
模块的层级是通过在模块中例化下一级模块产生的。虽然不同的模块写在不同的 .v 文件中,(一般推荐一个 .v 文件中只写一个模块),但只要这些模块在 ISE/Vivado/Quartus 这些开发软件中处于一个 Project。综合器就能在你例化模块时,找到对应的模块和 .v 文件。
模块中可以例化其他模块,但在模块中不允许再定义其他模块。
模块信号连接的两种方式
在实例化模块时,有两种常用的方式来进行模块端口的信号连接:按端口顺序以及按端口名称连接端口。
按端口顺序,mod_a instance1 ( wa, wb, wc );
wa, wb, wc 分别连接到模块的 第一个端口(in1),第二个端口(in2)以及第三个端口(out)。这里所谓的端口顺序指的是模块端口的定义顺序。这种方式的弊端在于,一旦端口列表发生改变,所有模块实例化中的端口连接都需要改变。
按端口名称,mod_a instance2 ( .out(wc), .in1(wa), .in2(wb) );
在这种方式中根据端口名称指定外部信号的连接。这样一来就和端口声明的顺序完全没有关系。一旦模块出现改动,只要修改相应的部分即可。实际上,一般都使用这种方式来进行模块实例化。
Three modules(移位寄存器)
D触发器构成长度为3的移位寄存器。
Adder 2(行波进位加法器)
一位全加器
module add1 (
input a,
input b,
input cin,
output sum,
output cout
);
// Full adder module here
assign sum = a^b^cin;
assign cout = (a&b)|(a&cin)|(b&cin);
endmodule
下图的加法器叫做行波进位加法器(RCA: Ripple-Carry Adder)(也叫逐级进位,逐位进位)。这种加法器的缺点是计算进位输出的延迟是相当慢的(最坏的情况下,来自于进位输入)。并且如果前一级加法器计算完成之前,后一级加法器不能开始计算。这又使得加法器的计算延迟变大。
Carry-select adder(选择进位加法器)
一个改进型的加法器,如下图所示。第一级加法器保持不变,第二级加法器实现两个,一个假设进位为0,另一个假设进位为1。然后使用第一级结果和2选一选择器来选择哪一个结果是正确的。
如果学过数字集成电路的进位链的话应该知道这是选择进位加法器(CSA: Carry-Select Adder),相对于上一题的行波进位(也叫逐级进位,逐位进位)加法器延迟小一半左右,但是增多了50%的逻辑资源。
Adder-subtractor(加减法器)
加减法器可以由加法器来构建,可以对其中一个数取相反数(对输入数据取反,然后加1)。最终结果是一个可以执行以下两个操作的电路: a+b+0和a+~b+1 。如果您想要更详细地了解该电路的工作原理,请参阅==维基百科==。
当sub为1时,使用32位的异或门对B进行取反。(这也可以被视为b[31:0]与sub复制32次相异或,请参阅复制运算符[Replication operator](#Replication operator(重复操作符))。同时sub信号连接到加法器的进位(加一)。
知识点:由上图可以看出,异或门也可以看作是可编程的非门,其中一个输入控制是否应该反转另一个。减去一个数等于加上这个数的补码(就是题中的按位取反再加1)。
Procedures
Always blocks(combinational)
数字电路是由导线连接的逻辑门组成,因此任何电路都可以表示为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)。
关于wire与reg的注意事项:assign语句的左手边必须是net类型(例如,wire),而过程赋值的左手边(在always块中)必须是变量类型(例如reg)。这些类型(wire与reg)与合成的硬件无关,只是Verilog用作硬件模拟语言时遗留下来的语法。
Always blocks(clocked)
对于硬件综合来说,存在两种always块:
组合逻辑:always @(*)
时序逻辑:always @(posedge clk)
**时序always块也会像组合always块一样生成一系列的组合电路,但同时在组合逻辑的输出生成了一组触发器(或寄存器)。**该输出在下一个时钟上升沿(posedge clk)后可见,而不是之前的立即可见。
阻塞性赋值和非阻塞性赋值
在Verilog中有以下三种赋值方法:
连续赋值 (assign x=y;):不能在过程块内使用;
过程阻塞性赋值 (x=y;):只能在过程块中使用;
过程非阻塞性赋值 (x<=y):只能在过程块内使用。
在组合always块中,使用阻塞性赋值。在时序always块中,使用非阻塞性赋值。具体为什么对设计硬件用处不大,还需要理解Verilog模拟器如何跟踪事件。不遵循此规则会导致极难发现非确定性错误,并且在仿真和综合出来的硬件之间存在差异。
If statement
下面给出了一个基本的if语句和其综合出来的电路。
always @(*) begin
if (condition) begin
out = x;
end
else begin
out = y;
end
end
这与下面使用条件运算符连续赋值的语句是等价的:
assign out = (condition) ? x : y;
但是,过程if语句使用不当可能会引入新的错误,只有out在所有的条件下都被赋值才会生成正确的组合电路。
If statement latches
常见的错误来源:如何避免引入锁存器
在设计电路时,必须首先具体考虑电路:
1、我想实现一个逻辑门;
2、我想实现一个具有输入并产生输出的组合逻辑块;
3、我想实现一组组合逻辑,紧接着一组触发器。
不要上来就写代码,这样往往与你想象的电路相差很远。
always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end
always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
end
除了你指定的情况以外,会发生些什么,答案是什么也不会发生,输出保持不变。而这往往就导致了电路的错误,所以说语法正确的代码不一定能产生合理的电路(组合逻辑+触发器)。
输出保持不变,这就意味着电路需要记住当前状态,从而产生锁存器。组合逻辑(比如逻辑门)不能记住任何状态。
Warning (10240): ... inferring latch(es)
上述这类警告通常情况下代表错误,除非锁存器是故意生成的。组合电路输出必须在所有输入的情况下都有值。这意味着必须需要else子句或着输出默认值。
Case statement
Verilog中的case语句几乎等同于if-else if-else序列,它将一个表达式与其他表达式列表进行比较。
always @(*) begin // This is a combinational circuit
case (in)
1'b1: begin
out = 1'b1; // begin-end if >1 statement
end
1'b0: out = 1'b0;
default: out = 1'bx;
endcase
end
1、case语句以case开头,每个case项以冒号结束。
2、每个case项只执行一个语句。但这也意味着如果您需要多个语句,则必须使用begin … end。
3、case项允许重复和部分重叠,执行程序匹配到的第一个。
注意:不要生成锁存器(详见:[如何避免引入锁存器](#If statement latches))。必要时一定要用default声明一下不在case项里的输出,否则会生成不必要的寄存器,影响电路的功能。
Priority encoder(优先编码器)
优先编码器是组合电路,当给定输入时,输出输入向量中的右边第一个1的位置。例如,输入8’b10010000的,则优先编码器将输出3’d4,因为位[4]是从右数第一个1。出现的第一个数字把后面的数字屏蔽掉了,第一个数字具有较高的优先级,所以叫做优先编码器。
Priority encoder with casez
如果case语句中的case项与某些输入无关,就可以减少列出的case项(在本题中减少到9个)。这就是casez的用途:它在比较中将具有值z的位视为无关项(即输入01都会匹配到)。
例如:下面的代码就是上一个联系中的4输入优先编码器(最低位的1优先):
always @(*) begin
casez (in[3:0])
4'bzzz1: out = 0; // in[3:1]输入什么都可以
4'bzz1z: out = 1;
4'bz1zz: out = 2;
4'b1zzz: out = 3;
default: out = 0;
endcase
end
case项是按顺序检查的(实际上,它更像是生成一个巨大的真值表然后生成超大的门)。注意有输入(例如,4’b1111)匹配多个case项。选择第一个匹配(因此4’b1111匹配第一个case项,out = 0)。
还有一个类似的casex,将输入的x和z都视为无关。不认为casex比casez有什么特别的好处。(作者个人感觉没必要用casex,z和x状态的问题涉及电路的基本知识)
符号"?" 是z的同义词,所以2’bz0与2’b?0相同。
显式指定优先级行为而不是依赖于事例项的顺序可能不太容易出错。例如,如果对某些事例项重新排序,则以下项的行为方式仍相同,因为任何位模式最多只能匹配一个事例项:
casez (in[3:0])
4'bzzz1: ...
4'bzz10: ...
4'bz100: ...
4'b1000: ...
default: ...
endcase
Avoiding latches
为避免生成了不必要的锁存器,必须在所有条件下为所有的输出赋值[避免锁存器](#If statement latches)。这可能会多打很多字,使你的代码变得冗长。 一个简单的方法是在case语句之前为输出分配一个“默认值”:
always @(*) begin
up = 1'b0; down = 1'b0; left = 1'b0; right = 1'b0;
case (scancode)
... // Set to 1 as necessary.
endcase
end
除非case语句覆盖赋值,否则这种代码样式可确保在所有可能的情况下输出0。 这也意味着case的default项变得不必要。
提醒:always@(*)综合器会生成一个组合电路,其行为与代码描述的相同。硬件不会按顺序“执行”代码。
always块中在scancode每次变化时都会执行一遍,所以在case中只需赋值上下左右值为1,其余已经置为0了。
always@(*)
begin
up = 1'b0; down = 1'b0; left = 1'b0; right = 1'b0;
case(scancode)
16'he06b: left = 1'b1;
16'he072: down = 1'b1;
16'he074: right = 1'b1;
16'he075: up = 1'b1;
default: //加上default,不会出现警告
begin
up = 1'b0; down = 1'b0; left = 1'b0; right = 1'b0;
end
endcase
end
More Verilog Features
Conditional ternary operator( 三元运算符?: )
Verilog有一个三元运算符( ? : )。
condition ? if_true : if_false
这可以在一行代码上实现一个MUX,而不需要在always块中使用if-else语句。
例如:
(0 ? 3 : 5) // 输出是5,因为条件"0"始终是false的
(sel ? b : a) // 一个二选一MUX,通过sel的值选择a或者b
always @(posedge clk) // 一个T触发器
q <= toggle ? ~q : q;
always @(*) // 一输入的状态转换逻辑
case (state)
A: next = w ? B : A;
B: next = w ? A : B;
endcase
assign out = ena ? q : 1'bz; // 三态缓冲器
((sel[1:0] == 2'h0) ? a :(sel[1:0] == 2'h1) ? b : c )// 一个三选一MUX
Reduction operators(归约运算符& | ^)
前面已经讲过两个变量之间的按位运算,例如a&b或a^b。有时候,我们想要构建一个输入比较多的门,对一个向量的所有位进行操作,如(a[0]&a[1]&a[2]&a[3]…),但这对于长的标量来说,这很麻烦。
归约运算符(Reduction Operators)可以对向量的每一位位进行AND,OR和XOR,产生一位输出:
&a [3:0] // AND:a[3]&a[2]&a[1]&a [0]相当于(a[3:0]== 4'hf)
|b [3:0] // OR: b[3]|b[2]|b[1]|b [0]相当于(b[3:0]!= 4'h0)
^c [2:0] // XOR:c[2]^c[1]^c[0]
这些是只有一个操作数的一元运算符(类似于NOT运算符!和~)。也可以将这些本节课的运算符的输出反相以创建NAND,NOR和XNOR门,例如( ~&d[7:0])。
奇偶校验是检验传输数据中1的个数,当然有奇数有偶数,,这时候就需要用我们的校验位了,通过检验位将传输1的个数变成奇数就是奇校验,变成偶数就是偶校验。比如:
8'b01100100 //原数据
9'b01100100_0 //奇校验
9'b01100100_1 //偶校验
Combinational for-loop - Vector reversal 2(向量反转)
给了一个长度是100的向量,请把它翻转输出一下。
module top_module (
input [99:0] in,
output reg [99:0] out //注意这里是reg
);
always @(*) begin
for (int i=0;i<$bits(out);i++) // $bits() is a system function that returns the width of a signal.
out[i] = in[$bits(out)-i-1]; // $bits(out) is 100 because out is 100 bits wide.
end //for循环里写int i是systemverilog的写法
endmodule
Combinational for-loop - 255-bit population count(向量中1的个数)
思路:用for循环累加in[i]即可。
Generate for-loop - 100-bit binary adder 2(循环例化加法器)
提示中提到:有许多完整的加法器需要实例化。实例数组或generate语句在这里会有所帮助。实例数组或generate语句均可实现。
//实例数组实现
module top_module(
input [99:0] a, b,
input cin,
output logic [99:0] cout,
output logic [99:0] sum
);
fadd fadd_0 (a[0], b[0], cin, sum[0], cout[0]);
fadd fadd_inst[99:1] (a[99:1], b[99:1], cout[98:0], sum[99:1], cout[99:1]);
endmodule
//generate语句实现
module top_module(
input [99:0] a, b,
input cin,
output [99:0] cout,
output [99:0] sum
);
fadd fadd_0 (a[0], b[0], cin, sum[0], cout[0]);
genvar i;
generate for (i = 1; i < 100; i = i + 1)
begin: adder100
fadd fadd_i (a[i], b[i], cout[i - 1], sum[i], cout[i]);
end
endgenerate
endmodule
注意在网页中编写的代码中要包含一位全加器模块,此处仅给出实现例化的代码。
Generate for-loop - 100-digit BCD adder
什么是generate语句?
生成语句可以动态的生成verilog代码,当对矢量中的多个位进行重复操作时,或者当进行多个模块的实例引用的重复操作时,或者根据参数的定义来确定程序中是否应该包含某段Verilog代码的时候,使用生成语句能大大简化程序的编写过程。
使用关键字generate 与 endgenerate来指定范围。generate语句有generate-for、generate-if、generate-case三种语句
generate-for语句:
(1) 必须有genvar关键字定义for语句的变量。
(2)for语句的内容必须加begin和end(即使就一句)。
(3)for语句必须有个名字。
例:
//创建一个2进制转换器
Module gray2bin
#(parameter SIZE = 8)
(
input [SIZE-1:0] gray,
output [SIZE-1:0] bin
)
Genvar gi; //在generate语句中采用genvar声明
generate
for (gi=0; gi<SIZE; gi=gi+1)
begin : genbit //for语句必须有名字
assign bin[i] = ^gray[SIZE-1:gi];
end
endgenerate
endmodule