【HDLBits网页知识提炼】Verilog Language部分

本系列对Verilog学习网站HDLBits网页上的知识点进行提炼总结,结合前辈的中文导学部分整理,用于自我学习。

网站链接

HDLBits官网

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值