硬件描述语言Verilog设计经验总结

一、硬件描述语言Verilog
粗略地看Verilog与C语言有许多相似之处。分号用于结束每个语句,注释符也是相同的(/* ... */和// 都是熟悉的),运算符“==”也用来测试相等性。Verilog的if..then..else语法与C语言的也非常相似,只是Verilog用关键字 begin和end代替了C的大括号。事实上,关键字begin和end对于单语句块来说是可有可无的,就与C中的大括号用法一样。Verilog和C都对大小写敏感。 
当然,硬件和软件的一个重要区别是它们的“运行”方式。硬件设计中用到的许多单元都是并行工作的。一旦设备电源开启,硬件的每个单元就会一直处于运行状态。虽然根据具体的控制逻辑和数据输入,设备的一些单元可能不会改变它们的输出信号,但它们还是一直在“运行”中。相反,在同一时刻整个软件设计中只有一小部分(即使是多软件任务也只有一个任务)在执行。如果只有一个处理器,同一时间点只能有一条指令在执行。软件的其它部分可以被认为处于休眠状态,这与硬件有很大的不同。变量可能以一个有效值而存在,但大多数时间里它们都不在使用状态。
软硬件的不同行为会直接导致硬件和软件代码编程方式的不同。软件是串行执行的,每一行代码的执行都要等到前一行代码执行完毕后才能进行(中断的非线性或操作系统的命令除外)。
二、模块(module)
一个Verilog模块的开头是关键字module,紧跟其后的是模块名称和端口列表,端口列表列出了该模块用到的所有输入输出名称。接下来是端口声明部分。注意:所有的输入输出既出现在模块第一行的端口列表中,也会出现在端口声明(declaration)部分中。
以下三个模块分别以∶ 结构式(structural)、数据流式(data-flow)及行为式(behavioral)来描述一个二输入与门。
//structural
module AND2 (in1,in2,out);
input in1;
input in2;
output out;
wire in1,in2,out;
and u1 (out,in1,in2);
endmodule  
//data flow
module AND2 (in1,in2,out);
input in1;
input in2;
output out;
wire in1,in2,out;
assign out=in1&in2;
endmodule
//behavioral
module AND2 (in1,in2,out);
input in1;
input in2;
output out;
wire in1,in2;
reg out;
always@(in1 or in2);
out=in1 & in2;
endmodule
结构式的描述∶在这层次中模块是由逻辑闸(Gate Level)连接而成,在这层次的设计工作就好像以前用描绘逻辑闸来设计线路一样。例1中的AND是Verilog的基本元件 (primitive),是Verilog语言预先定义好的函式。
数据流式的描述∶ 它是一种模拟组合函式的方法。当任何输入有所改变时,输出会被重新计算而跟著改变。数据流式只能用来实践组合函式。在这个层次中,要说明数据如何在暂存器中储存与传送,如何处理数据。例2数据流式使用关键字assign进行描述。
行为式的描述∶它是一种使用高阶语言来描述硬件的方式。因Verilog提供很普遍及功能强大的描述方式。在这个层次的设计工作就好像写C语言一样,使用行为式所描述的模块其描述与真实的电路可能毫无关连。甚至某些行为式模块可能硬件实践。这种特性是Verilog的优点也是缺点。 例3中的行为式描述有一个使用关键字always进行描述。
三、Verilog中端口的描述
1、端口的位宽最好定义在I/O说明中,不要放在数据类型定义中;
Example1:
module test(addr,read,write,datain,dataout)
input[7:0]  datain;
input[15:0] addr;
input       read,write;
output[7:0] dataout;  //要这样定义端口的位宽!
wire addr,read,write,datain;
reg  dataout;
Example2:
module test(addr,read,write,datain,dataout)
input  datain,addr,read,write;
output dataout;
wire[15:0] addr;
wire[7:0]  datain;
wire       read,write;
reg[7:0]   dataout;   // 不要这样定义端口的位宽!!
2、端口的I/O与数据类型的关系:
module内部     module外部
input              wire          wire或reg
output           wire或reg         wire
inout              wire            wire
3、assign语句的左端变量必须是wire;直接用"="给变量赋值时左端变量必须是reg!
Example:
assign a=b; //a必须被定义为wire!!
********
begin
a=b; //a必须被定义为reg!
end
在Verilog中有二种类型的内部信号用得比较多,它们是reg和wire。它们具有不同的功能。wire是线网形变量,它不能存储值,必须受到驱动器或者连续赋值语句的驱动。reg是数据存储单元的抽象,通过赋值语句可以改变寄存器存储的值,其作用与改变触发器存储的值相当。所有端口都有一个名称相同且声明为wire的信号。因此连线line被声明为wire不是必要的。reg会保持上次的赋值,因此不需要每次都进行驱动。wire型信号用于异步逻辑,有时也用来连接信号。因为 reg可以保持上次的值,因此输入不能被声明为reg类型。在Verilog模块中可以在任何时候异步地将输入改变为任何事件。reg和wire的主要区别是,reg类型的信号只能在过程块(后面会谈到)中赋值,而wire类型的信号只能在过程块外赋值。这两种信号类型都可以出现在过程块内部和外部的赋值运算符右边。 使用关键字reg并不一定意味着编译器会创建一个寄存器,理解这一点是非常重要的。
四、敏感变量的描述完备性
Verilog中,用always块设计组合逻辑电路时,在赋值表达式右端参与赋值的所有信号都必须在 always @(敏感电平列表)中列出,always中if语句的判断表达式必须在敏感电平列表中列出。如果在赋值表达式右端引用了敏感电平列表中没有列出的信号,在综合时将会为没有列出的信号隐含地产生一个透明锁存器。这是因为该信号的变化不会立刻引起所赋值的变化,而必须等到敏感电平列表中的某一个信号变化时,它的作用才表现出来,即相当于存在一个透明锁存器,把该信号的变化暂存起来,待敏感电平列表中的某一个信号变化时再起作用,纯组合逻辑电路不可能作到这一点。综合器会发出警告。
Example1:
input a,b,c;
reg e,d;
always @(a or b or c)
begin
e=d&a&b; /*d没有在敏感电平列表中,d变化时e不会立刻变化,直到a,b,c中某一个变化*/
d=e|c;
end
Example2:
input a,b,c;
reg e,d;
always @(a or b or c or d)
begin
e=d&a&b; /*d在敏感电平列表中,d变化时e立刻变化*/
d=e |c;
end
Verilog中用于上升沿和下降沿的关键字分别是posedge和negedge。这二个关键字经常被用于敏感列表。
五、条件的描述完备性
如果if语句和case语句的条件描述不完备,也会造成不必要的锁存器。
Example1:
if (a==1'b1)
q=1'b1;//如果a==1'b0,q=? q将保持原值不变, 生成锁存器!
Example2:
if (a==1'b1)
q=1'b1;
else        
q=1'b0;//q有明确的值, 不会生成锁存器!
Example3:
reg[1:0] a,q;
....
case (a)
2'b00 : q=2'b00;
2'b01 : q=2'b11;//如果a==2'b10或a==2'b11,q=? q将保持原值不变, 锁存器!
endcase
Example4:
reg[1:0] a,q;
....
case (a)
2'b00 : q=2'b00;
2'b01 : q=2'b11;
default: q=2'b00;//q有明确的值. 不会生成锁存器!

endcase
六、描述的规范性
以触发器为例说明描述的规范性
1、无置位/清零的时序逻辑
always @( posedge CLK)
begin
Q<=D;
end
2、有异步置位/清零的时序逻辑
异步置位/清零是与时钟无关的,当异步置位/清零信号到来时,触发器的输出立即被置为1或0,不需要等到时钟沿到来才置位/清零。所以,必须要把置位/清零信号   列入always块的事件控制表达式。
always @( posedge CLK or negedge RESET)
begin
if (!RESET)
Q=0;
else
Q<=D;
end
3、有同步置位/清零的时序逻辑
同步置位/清零是指只有在时钟的有效跳变时刻置位/清零,才能使触发器的输出分别转换为1或0。所以,不要把置位/清零信号列入always块的事件控制表达式。但是必须在always块中首先检查置位/清零信号的电平。
always @( posedge CLK )
begin
if (!RESET)
Q=0;
else
Q<=D;
end
七、非阻塞赋值和阻塞赋值
always块中的赋值运算符与以关键字assign开头的连续赋值语句中用到的运算符不一样。"<="运算符用于非阻塞性(nonblocking)赋值,而"="运算符用于阻塞性(blocking)赋值。在一组阻塞性赋值语句中,在下一个阻塞性赋值语句执行前需要计算并赋值第一个赋值语句。这一过程就象C语言中语句的顺序执行。而非阻塞语句在执行时,所有赋值语句的右边被同时计算和赋值,在always块结束后才完成赋值操作。连续赋值语句必须使用阻塞赋值语句(否则编译器会报错)。
为了减少代码出错的概率,建议在顺序逻辑(例如希望以寄存器方式实现的逻辑)always块中的所有赋值语句使用非阻塞性赋值语句。大多数always块应该使用非阻塞性赋值语句。如果always块都是组合逻辑,那么就需要使用阻塞性赋值语句。 
现列举八条“非阻塞赋值”和“阻塞赋值”指导方针,谨遵这些方针可以帮助Verilog设计者减少所遇到的90-100%的Verilog竞争:
1: 当为时序逻辑建模,使用“非阻塞赋值”。
2: 当为锁存器(latch)建模,使用“非阻塞赋值”。
3: 当用always块为组合逻辑建模,使用“阻塞赋值”
4: 当在同一个always块里面既为组合逻辑又为时序逻辑建模,使用“非阻塞赋值”。
5: 不要在同一个always块里面混合使用“阻塞赋值”和“非阻塞赋值”。
6: 不要在两个或两个以上always块里面对同一个变量进行赋值。
7: 使用$strobe以显示已被“非阻塞赋值”的值。
8: 不要使用#0延迟的赋值。
八、其他一些设计规范和原则
1:不使用初始化语句,用复位脉冲初始化信号和变量。
2:不使用延时语句;
3:不使用循环次数不确定的语句,如:forever,while等;
4:尽量采用同步方式设计电路;
5:尽量采用行为语句完成设计;
6:always过程块描述组合逻辑,应在敏感信号表中列出所有的输入信号;
7:所有的内部寄存器都应该可以被复位;避免使用内部生成的异步置位/清零信号,内部生成的置位/清零信号会引起测试问题。使某些输出信号被置位或清零,无法正常测试。
8:用户自定义原件(UDP元件)是不能被综合的。
9: if...else if ... else 语句是有优先级的,一般说来第一个if的优先级最高,最后一个else的优先级最低。 而case语句是"平行"的结构,所有的case的条件和执行都没有“优先级”。而建立优先级结构会消耗大量的组合逻辑,所以如果能够使用case语句的地方,尽量使用case替换if...else结构。
10: 状态机的一般设计原则,Biary, gray-code 编码使用最少的触发器,较多的组合逻辑。而one-hot编码反之。所以CPLD多使用GRAY-CODE, 而FPGA多使用ONE-HOT编码。另一方面,小型设计使用GRAY-CODE和BINARY编码更有效,而大型状态机使用ONE-HOT更有效。
11:fpga设计中不要使用门时钟,内部生成的时钟称为门生时钟(gated clock)。时钟信号必须连接到全局时钟管脚上。
12:不要使用内部三态信号,否则增加功耗。
13:避免使用负延触发的双稳态多谐振荡器(flip flop)。
14:不要在代码中使用buffer 类型的端口读取输出数据;要使用out 类型,再增加另外变量或信号,以获取输出值。这是因为buffer 类型的端口不能连接到其他类型的端口上,因此buffer 类型就会在整个设计的端口中传播下去。
15:对变量要先读后写;如果先写后读,就会产生长的组合逻辑和锁存器(或寄存器)。这是因为变量值是立即获取的。

原文地址:https://www.cnblogs.com/aikimi7/p/3352474.html