第2节 Verilog HDL基本程序结构
Verilog HDL 程序结构 Verilog HDL基本程序结构
用Verilog HDL描述的电路设计就是该电路的Verilog HDL模型,也称为模块,是Verilog的基本描述单位。模块描述某个设计的功能或结构以及与其他模块通信的外部接口,一般来说一个文件就是一个模块,但并不绝对如此。模块是并行运行的,通常需要一个高层模块通过调用其他模块的实例来定义一个封闭的系统,包括测试数据和硬件描述。一个模块的基本架构如下:
module module_name (port_list) //声明各种变量、信号 reg //寄存器 wire//线网 parameter//参数 input//输入信号 output/输出信号 inout//输入输出信号 function//函数 task//任务 …… //程序代码 initial assignment always assignment module assignment gate assignment UDP assignment continous assignment endmodule
说明部分用于定义不同的项,例如模块描述中使用的寄存器和参数。语句用于定义设计的功能和结构。说明部分可以分散于模块的任何地方,但是变量、寄存器、线网和参数等的说明必须在使用前出现。一般的模块结构如下:
module <模块名> (<端口列表>) <定义> <模块条目>
endmodule
其中,<定义>用来指定数据对象为寄存器型、存储器型、线型以及过程块。<模块条目>可以是initial结构、always结构、连续赋值或模块实例。
下面给出一个简单的Verilog模块,实现了一个二选一选择器。
例2-1 二选一选择器(见图2-1)的Verilog实现
图2-1 例2-1所示的二选一电路
module muxtwo(out, a, b, s1); input a, b, s1; output out; reg out;
always @ (s1 or a or b) if (!s1) out = a; else out = b; endmodule
模块的名字是muxtwo,模块有4个端口:三个输入端口a、b和s1,一个输出端口out。由于没有定义端口的位数,所有端口大小都默认为1位;由于没有定义端口a, b, s1的数据类型,这3个端口都默认为线网型数据类型。输出端口out定义为reg类型。如果没有明确的说明,则端口都是线网型的,且输入端口只能是线网型的。
第3节 VerilogHDL语言的数据类型和运算符
VerilogHDL 语言 数据类型 运算符 2.3.1 标志符
标志符可以是一组字母、数字、_下划线和$符号的组合,且标志符的第一个字符必须是字母或者下划线。另外,标志符是区别大小写的。下面给出标志符的几个例子:
Clk_100MHz diag_state _ce P_o1_02
需要注意的是,Verilog HDL定义了一系列保留字,叫作关键字,具体资料可查阅相关标准。只有小写的关键字才是保留字,因此在实际开发中,建议将不确定是否是保留字的标志符首字母大写。例如:标志符if(关键字)与标志符IF是不同的。
2.3.2 数据类型
数据类型用来表示数字电路硬件中的数据存储和传送元素。Verilog HDL中总共有19种数据类型,本书只介绍4个常用的数据类型:wire型、reg型、memory型和parameter型,其他类型将在后续章节中逐步介绍。
1.wire型
wire型数据常用来表示以assign关键字指定的组合逻辑信号。Verilog程序模块中输入、输出信号类型默认为wire型。wire型信号可以用做方程式的输入,也可以用做“assign”语句或者实例元件的输出。
wire型信号的定义格式如下:
wire [n-1:0] 数据名1,数据名2,……数据名N;
这里,总共定义了N条线,每条线的位宽为n。下面给出几个例子: wire [9:0] a, b, c; // a, b, c都是位宽为10的wire型信号 wire d;
2.reg型
reg是寄存器数据类型的关键字。寄存器是数据存储单元的抽象,通过赋值语句可以改变寄存器存储的值,其作用相当于改变触发器存储器的值。reg型数据常用来表示always模块内的指定信号,代表触发器。通常在设计中要由always模块通过使用行为描述语句来表达逻辑关系。在always块内被赋值的每一个信号都必须定义为reg型,即赋值操作符的右端变量必须是reg型。
reg型信号的定义格式如下:
reg [n-1:0] 数据名1,数据名2,……数据名N;
这里,总共定义了N个寄存器变量,每条线的位宽为n。下面给出几个例子: reg [9:0] a, b, c; // a, b, c都是位宽为10的寄存器 reg d;
reg型数据的缺省值是未知的。reg型数据可以为正值或负值。但当一个reg型数据是一个表达式中的操作数时,它的值被当作无符号值,即正值。如果一个4位的reg型数据被写入-1,在表达式中运算时,其值被认为是+15。
reg型和wire型的区别在于:reg型保持最后一次的赋值,而wire型则需要持续的驱动。
3.memory型
Verilog通过对reg型变量建立数组来对存储器建模,可以描述RAM、ROM存储器和寄存器数组。数组中的每一个单元通过一个整数索引进行寻址。memory型通过扩展reg 型数据的地址范围来达到二维数组的效果,其定义的格式如下:
reg [n-1:0] 存储器名 [m-1:0];
其中,reg [n-1:0]定义了存储器中每一个存储单元的大小,即该存储器单元是一个n位位宽的寄存器;存储器后面的[m-1:0]则定义了存储器的大小,即该存储器中有多少个这样的寄存器。例如:
reg [15:0] ROMA [7:0];
这个例子定义了一个存储位宽为16位,存储深度为8的一个存储器。该存储器的地址范围是0到8。
需要注意的是:对存储器进行地址索引的表达式必须是常数表达式。
尽管memory型和reg型数据的定义比较接近,但二者还是有很大区别的。例如,一个由n个1位寄存器构成的存储器是不同于一个n位寄存器的。 reg [n-1 : 0] rega; // 一个n位的寄存器
reg memb [n-1 : 0]; // 一个由n个1位寄存器构成的存储器组
一个n位的寄存器可以在一条赋值语句中直接进行赋值,而一个完整的存储器则不行。
rega = 0; // 合法赋值 memb = 0; // 非法赋值
如果要对memory型存储单元进行读写必须要指定地址。例如: memb[0] = 1; // 将memeb中的第0个单元赋值为1。 reg [3:0] Xrom [4:1]; Xrom[1] = 4’h0; Xrom[2] = 4’ha; Xrom[3] = 4’h9; Xrom[4] = 4’hf;
4.parameter型
在Verilog HDL中用parameter来定义常量,即用parameter来定义一个标志符表示一个常数。采用该类型可以提高程序的可读性和可维护性。 parameter型信号的定义格式如下: parameter 参数名1 = 数据名1; 下面给出几个例子: parameter s1 = 1; parameter [3:0] S0=4'h0, S1=4'h1, S2=4'h2, S3=4'h3, S4=4'h4;
2.3.3 模块端口
模块端口是指模块与外界交互信息的接口,包括3种类型:
? input: 模块从外界读取数据的接口,在模块内不可写。
? output:模块往外界送出数据的接口,在模块内不可读。 ? inout:可读取数据也可以送出数据,数据可双向流动。
2.3.4 常量集合
Verilog HDL有下列4种基本的数值:
0:逻辑0或“假” 1:逻辑1或“真” x:未知 z:高阻
其中x、z是不区分大小写的。Verilog HDL中的数字由这四类基本数值表示。
Verilog HDL中的常量分为3类:整数型、实数型以及字符串型。下划线符号“_”可以随意用在整数和实数中,没有实际意义,只是为了提高可读性。例如:56等效于5_6。
1.整数
整数型可以按如下两种方式书写:简单的十进制数格式以及基数格式。
(1)简单的十进制格式
简单的十进制数格式的整数定义为带有一个“+”或“-”操作符的数字序列。下面是这种简易十进制形式整数的例子。
45 十进制数45 -46 十进制数-46
简单的十进制数格式的整数值代表一个有符号的数,其中负数可使用两种补码形式表示。例如,32在6位二进制形式中表示为100000,在7位二进制形式中为0100000,这里最高位0表示符号位;-15在5位二进制中的形式为10001,最高位1表示符号位,在6位二进制中为110001,最高位1为符号扩展位。
(2)基数表示格式
基数格式的整数格式为: [长度] '基数 数值
长度是常量的位长,基数可以是二进制、十进制、十六进制之一。数值是基于基数的数字序列,且数值不能为负数。下面是一些具体实例: 6'b9 6位二进制数 5'o9 5位八进制数 9'd6 9位十进制数
2.实数
实数可以用下列两种形式定义:
(1)十进制计数法,例如: 2.0 16539.236
(2)科学计数法
这种形式的实数举例如下,其中e与E相同。
235.12e2 其值为23512 5e-4 其值为0.0005
根据Verilog语言的定义,实数通过四舍五入隐式地转换为最相近的整数。
3.字符串
字符串是双引号内的字符序列。字符串不能分成多行书写。例如:
“counter”
用8位ASCII值表示的字符可看作是无符号整数,因此字符串是8位ASCII值的序列。为存储字符串“counter”,变量需要 位。
reg [1: 8*7] Char; Char = ''counter'';
2.3.5 运算符和表达式
在Verilog HDL语言中运算符所带的操作数是不同的,按其所带操作数的个数可以分为三种:
? 单目运算符:带一个操作数,且放在运算符的右边。 ? 双目运算符:带两个操作数,且放在运算符的两边。 ? 三目运算符:带三个操作数,且被运算符间隔开。
Verilog HDL语言参考了C语言中大多数算符的语法和句义,运算范围很广,其运算符按其功能分为下列9类:
1. 基本算术运算符
在Verilog HDL中,算术运算符又称为二进制运算符,有下列5种:
? + 加法运算符或正值运算符,如:s1+s2; +5; ? - 减法运算符或负值运算符,如:s1-s2; -5; ? * 乘法运算符,如s1*5; ? / 除法运算符,如s1/5; ? % 模运算符,如s1%2;
在进行整数除法时,结果值要略去小数部分。在取模运算时,结果的符号位和模运算第一个操作数的符号位保持一致。例如: 运算表达式 结果 说明
12.5/3 4 结果为4,小数部分省去 12%4 0 整除,余数为0
-15%2 -1 结果取第一个数的符号,所以余数为-1 13/-3 1 结果取第一个数的符号,所以余数为1
注意:在进行基本算术运算时,如果某一操作数有不确定的值X,则运算结果也是不确定值X。
2. 赋值运算符
赋值运算分为连续赋值和过程赋值两种。
(1)连续赋值
连续赋值语句和过程块一样也是一种行为描述语句,有的文献中将其称为数据流描述形式,但本书将其视为一种行为描述语句。
连续赋值语句只能用来对线网型变量进行赋值,而不能对寄存器变量进行赋值,其基本的语法格式为:
线网型变量类型 [线网型变量位宽] 线网型变量名; assign #(延时量) 线网型变量名 = 赋值表达式; 例如: wire a;
assign a = 1'b1;
一个线网型变量一旦被连续赋值语句赋值之后,赋值语句右端赋值表达式的值将持续对被赋值变量产生连续驱动。只要右端表达式任一个操作数的值发生变化,就会立即触发对被赋值变量的更新操作。
在实际使用中,连续赋值语句有下列几种应用:
? 对标量线网型赋值
wire a, b; assign a = b;
? 对矢量线网型赋值
wire [7:0] a, b; assign a = b;
? 对矢量线网型中的某一位赋值
wire [7:0] a, b; assign a[3] = b[1];
? 对矢量线网型中的某几位赋值
wire [7:0] a, b; assign a[3:0] = b[3:0];
? 对任意拼接的线网型赋值
wire a, b; wire [1:0] c; assign c ={a ,b};
(2)过程赋值
过程赋值主要用于两种结构化模块(initial模块和always模块)中的赋值语句。在过程块中只能使用过程赋值语句(不能在过程块中出现连续赋值语句),同时过程赋值语句也只能用在过程赋值模块中。
过程赋值语句的基本格式为:
<被赋值变量><赋值操作符><赋值表达式>
其中,<赋值操作符>是“=”或“<=”,它分别代表了阻塞赋值和非阻塞赋值类型。3.5.1节对阻塞赋值和非阻塞赋值操作进行了详细解释。
过程赋值语句只能对寄存器类型的变量(reg、integer、real和time)进行操作,经过赋值后,上面这些变量的取值将保持不变,直到另一条赋值语句对变量重新赋值为止。过程赋值操作的具体目标可以是:
? reg、integer、real和time型变量(矢量和标量); ? 上述变量的一位或几位;
? 上述变量用{}操作符所组成的矢量;
? 存储器类型,只能对指定地址单元的整个字进行赋值,不能对其中某些位单独赋值。
例2-2 给出一个过程赋值的例子。 reg c; always @(a) begin c = 1'b0; end
3. 关系运算符
关系运算符总共有以下8种:
? > 大于 ? >= 大于等于 ? < 小于 ? <= 小于等于
? == 逻辑相等 ? != 逻辑不相等 ? === 实例相等 ? !== 实例不相等
在进行关系运算符时,如果操作数之间的关系成立,返回值为1;关系不成立,则返回值为0;若某一个操作数的值不定,则关系是模糊的,返回的是不定值X。
实例算子“===”和“!==”可以比较含有X和Z的操作数,在模块的功能仿真中有着广泛的应用。所有的关系运算符有着相同优先级,但低于算术运算符的优先级。
4. 逻辑运算符
Verilog HDL中有3类逻辑运算符:
? && 逻辑与 ? || 逻辑或 ? ! 逻辑非
其中“&&”和“||”是二目运算符,要求有两个操作数;而“!”是单目运算符,只要求一个操作数。“&&”和“||”的优先级高于算术运算符。逻辑运算符的真值表如下表所示:
表2-2 逻辑运算符的真值表
5. 条件运算符
条件运算符的格式如下:
y = x ? a : b;
条件运算符有3个操作数,若第一个操作数y = x是True,算子返回第二个操作数a,否则返回第三个操作数b。 如: wire y;
assign y = (s1 == 1) ? a : b;
嵌套的条件运算符可以实现多路选择。如: wire [1:0] s;
assign s = (a >=2 ) ? 1 : (a < 0) ? 2: 0;
//当a >=2时,s=1;当a <0时,s=2;在其余情况,s=0。
6. 位运算符
作为一种针对数字电路的硬件描述语言,Verilog HDL用位运算来描述电路信号中的与、或以及非操作,总共有7种位逻辑运算符:
? ~ 非 ? & 与 ? | 或 ? ^ 异或 ? ^~ 同或 ? ~& 与非 ? ~| 或非
位运算符中除了“~”,都是二目运算符。位运算对其自变量的每一位进行操作,例如:s1&s2的含义就是s1和s2的对应位相与。如果两个操作数的长度不相等的话,将会对较短的数高位补零,然后进行对应位运算,使输出结果的长度与位宽较长的操作数长度保持一致。例如:
s1 = ~s1; var = ce1 & ce2;
7. 移位运算符
移位运算符只有两种:“<<”(左移)和“>>”(右移),左移一位相当于乘2,右移一位相当于除2。其使用格式为: s1 << N; 或 s1 >>N
其含义是将第一个操作数s1向左(右)移位,所移动的位数由第二个操作数N来决定,且都用0来填补移出的空位。
在实际运算中,经常通过不同移位数的组合来计算简单的乘法和除法。例如s1*20,因为20=16+4,所以可以通过s1<<4+s1<<2来实现。
8. 拼接运算符
拼接运算符可以将两个或更多个信号的某些位并接起来进行运算操作。其使用格式为:
{s1, s2, … , sn}
将某些信号的某些位详细地列出来,中间用逗号隔开,最后用一个大括号表示一个整体信号。
在工程实际中,拼接运算受到了广泛使用,特别是在描述移位寄存器时。
例2-3 给出拼接符的Verilog实例 reg [15:0] shiftreg; always @( posedge clk)
shiftreg [15:0] <= {shiftreg [14:0], data_in};
9. 一元约简运算符
一元约简运算符是单目运算符,其运算规则类似于位运算符中的与、或、非,但其运算过程不同。约简运算符对单个操作数进行运算,最后返回一位数,其运算过程为:首先将操作数的第一位和第二位进行与、或、非运算;然后再将运算结果和第三位进行与、或、非运算;依次类推直至最后一位。
常用的约简运算符的关键字和位操作符关键字一样,仅仅由单目运算和双目运算的区别。
例2-4 给出一元简约运算符的Verilog实例
reg [3:0] s1; reg s2;
s2 = &s1; //&即为一元约简运算符“与”
2.4.1 结构描述形式
通过实例进行描述的方法,将Verilog HDL预先定义的基本单元实例嵌入到代码中,监控实
例的输入。Verilog HDL中定义了26个有关门级的关键字,比较常用的有8个。在实际工程中,简单的逻辑电路由逻辑门和开关组成,通过门元语可以直观地描述其结构。
基本的门类型关键字如下所述:
? and ? nand ? nor ? or ? xor ? xnor ? buf ? not
Verilog HDL支持的基本逻辑部件是由该基本逻辑器件的原语提供的。其调用格式为:
门类型 <实例名> (输出,输入1,输入2,……,输入N)
例如,nand na01(na_out, a, b, c );
表示一个名字为na01的与非门,输出为na_out,输入为a, b, c。
例2-5 一个简单的全加器例子:
module ADD(A, B, Cin, Sum, Cout); input A, B, Cin; output Sum, Cout; // 声明变量
wire S1, T1, T2, T3;
xor X1 (S1, A, B), X2 (Sum, S1, Cin);
and A1 (T3, A, B), A2 (T2, B, Cin), A3 (T1, A, Cin);
or O1 (Cout, T1, T2, T3);
endmodule
在这一实例中,模块包含门的实例语句,也就是包含内置门xor、and和or的实例语句。门实例由线网型变量S1、T1、T2和T3互连。由于未指定顺序,门实例语句可以以任何顺序出现。
门级描述本质上也是一种结构网表。在实际中的使用方式为:先使用门逻辑构成常用的触发器、选择器、加法器等模块,再利用已经设计的模块构成更高一层的模块,依次重复几次,便可以构成一些结构复杂的电路。其缺点是:不易管理,难度较大且需要一定的资源积累。
2.4.2 数据流描述形式
数据流型描述一般都采用assign连续赋值语句来实现,主要用于实现组合功能。连续赋值语句右边所有的变量受持续监控,只要这些变量有一个发生变化,整个表达式被重新赋值给左端。这种方法只能用于实现组合逻辑电路。其格式如下:
assign L_s = R_s;
例2-6 一个利用数据流描述的移位器
module mlshift2(a, b); input a; output b;
assign b = a<<2; endmodule
在上述模块中,只要a的值发生变化,b就会被重新赋值,所赋值为a左移两位后的值。
2.4.3 行为描述形式
行为型描述主要包括过程结构、语句块、时序控制、流控制等4个方面,主要用于时序逻辑功能的实现。
1.过程结构
过程结构采用下面4种过程模块来实现,具有强的通用型和有效性。
? initial模块 ? always模块 ? 任务(task)模块 ? 函数(function)模块
一个程序可以有多个initial模块、always模块、task模块和function模块。initial模块和always模块都是同时并行执行的,区别在于initial模块只执行一次,而always模块则是不断重复地运行。另外,task模块和function模块能被多次调用,其具体使用方法可参见3.5.3节的专题。
(1)initial 模块
在进行仿真时,一个initial模块从模拟0时刻开始执行,且在仿真过程中只执行一次,在执行完一次后,该initial就被挂起,不再执行。如果仿真中有两个initial模块,则同时从0时刻开始并行执行。
initial模块是面向仿真的,是不可综合的,通常被用来描述测试模块的初始化、监视、波形生成等功能。其格式为:
initial begin/fork 块内变量说明
时序控制1 行为语句1; ……
时序控制n 行为语句n; end/join
其中,begin……end块定义语句中的语句是串行执行的,而fork……join块语句中的语句定义是并行执行的。当块内只有一条语句且不需要定义局部变量时,可以省略begin……end/ fork……join。
例2-7 下面给出一个initial模块的实例。
initial begin
// 初始化输入向量
clk = 0; ar = 0; ai = 0; br = 0; bi = 0;
// 等待100ns,全局reset信号有效 #100; ar = 20; ai = 10; br = 10; bi = 10; end
(2)always 模块
和initial模块不同,always模块是一直重复执行的,并且可被综合。always过程块由always过程语句和语句块组成的,其格式为:
always @ (敏感事件列表) begin/fork 块内变量说明
时序控制1 行为语句1; ……
时序控制n 行为语句n; end/join
其中,begin……end/fork……join的使用方法和initial模块中的一样。敏感事件列表是可选项,但在实际工程中却很常用,而且是比较容易出错的地方。敏感事件表的目的就是触发always模块的运行,而initial后面是不允许有敏感事件表的。
敏感事件表由一个或多个事件表达式构成,事件表达式就是模块启动的条件。当存在多个事件表达式时,要使用关键词or将多个触发条件结合起来。Verilog HDL的语法规定:对于这些表达式所代表的多个触发条件,只要有一个成立,就可以启动块内语句的执行。例如,在语句
always@ (a or b or c) begin …… end
中,always过程块的多个事件表达式所代表的触发条件是:只要a、b、c信号的电平有任意一个发生变化,begin……end语句就会被触发。
always模块主要是对硬件功能的的行为进行描述,可以实现锁存器和触发器,也可以用来实现组合逻辑。利用always实现组合逻辑时,要将所有的信号放进敏感列表,而实现时序逻辑时却不一定要将所有的结果放进敏感信号列表。敏感信号列表未包含所有输入的情况称为不完整事件说明,有时可能会引起综合器的误解,产生许多意想不到的结果。
例2-8 下例给出敏感事件未包含所有输入信号的情况
module and3(f, a, b, c); input a, b, c; output f; reg f;
always @(a or b )begin f = a & b & c; end endmodule
其中,由于c不在敏感变量列表中,所以当c值变化时,不会重新计算f值。所以上面的程序并不能实现3输入的与门功能行为。正确的3输入与门应当采用下面的表述形式。
module and3(f, a, b, c); input a, b, c; output f; reg f;
always @(a or b or c )begin f = a & b & c; end endmodule
2.语句块
语句块就是在initial或always模块中位于begin……end/fork……join块定义语句之间的一组行为语句。语句块可以有个名字,写在块定义语句的第一个关键字之后,即begin或fork
之后,可以唯一地标识出某一语句块。如果有了块名字,则该语句块被称为一个有名块。在有名块内部可以定义内部寄存器变量,且可以使用“disable”中断语句中断。块名提供了唯一标识寄存器的一种方法。
例2-9 语句块使用例子
always @ (a or b ) begin : adder1 c = a + b; end
定义了一个名为adder1的语句块,实现输入数据的相加。
按照界定不同分为两种:
(1)begin……end,用来组合需要顺序执行的语句,被称为串行块。例如:
parameter d = 50; reg[7:0] r;
begin //由一系列延迟产生的波形 # d r = ' h35 ; //语句1 # d r = ' hE2 ; //语句2 # d r = ' h00 ; //语句3 # d r = ' hF7 ; //语句4
# d –> end_wave; //语句5,触发事件end_wave end
串行块的执行特点如下:
? 串行块内的各条语句是按它们在块内的语句逐次逐条顺序执行的,当前一条执行完
之后,才能执行下一条。如上例中语句1至语句5是顺序执行的。
? 块内每一条语句中的延时控制都是相对于前一条语句结束时刻的延时控制。如上例
中语句2的时延为2d。
? 在进行仿真时,整个语句块总的执行时间等于所有语句执行时间之和。如上例中语
句块中总的执行时间为5d。
(2)fork……join,用来组合需要并行执行的语句,被称为并行块。例如:
parameter d = 50; reg[7:0] r;
fork //由一系列延迟产生的波形 # d r = ' h35 ; //语句1 # 2d r = ' hE2 ; //语句2 # 3d r = ' h00 ; //语句3 # 4d r = ' hF7 ; //语句4
# 5d –> end_wave; //语句5,触发事件end_wave join
并行块的执行特点为:
? 并行语句块内各条语句是各自独立地同时开始执行的,各条语句的起始执行时间都
等于程序流程进入该语句块的时间。如上例中语句2并不需要等语句1执行完才开始执行,它与语句1是同时开始的。
? 块内每一条语句中的延时控制都是相对于程序流程进入该语句块的时间而言的。如
上例中语句2的延时为2d。
? 在进行仿真时,整个语句块总的执行时间等于执行时间最长的那条语句所需要的执
行时间,如上例中整个语句块的执行时间为5d。
(3)混合使用
在分别对串行块和并行块进行了介绍之后,还需要讨论一下二者的混合使用。混合使用可以分为下面两种情况。
? 串行块和并行块分别属于不同的过程块时,串行块和并行块是并行执行的。例如一
个串行块和并行块分别存在于两个initial过程块中,由于这两个过程块是并行执行的,所以其中所包含的串行语句和并行语句也是同时并行执行的。在串行块内部,其语句是串行执行的;在并行块内部,其语句是并行执行的。
? 当串行块和并行块嵌套在同一过程块中时,内层语句可以看作是外层语句块中的一
条普通语句,内层语句块什么时候得到执行是由外层语句块的规则决定的;而在内层语句块开始执行时,其内部语句怎么执行就要遵守内层语句块的规则。
3.时序控制
Verilog HDL提供了两种类型的显示时序控制,一种是延迟控制,在这种类型的时序控制中
通过表达式定义开始遇到这一语句和真正执行这一语句之间的延迟时间。另外一种是事件控制,这种时序控制是通过表达式来完成的,只有当某一事件发生时才允许语句继续向下执行。
(1)延时控制
延时控制的语法如下: # 延时数 表达式;
延时控制表示在语句执行前的“等待时延”,下面给出一个例子: initial begin
#5 clk = ~clk; end
延时控制只能在仿真中使用,是不可综合的。在综合时,所有的延时控制都会被忽略。
(2)事件控制
事件控制分为两种:边沿触发事件控制和电平触发事件控制。
? 边沿触发事件是指指定信号的边沿信号跳变时发生指定的行为,分为信号的上升沿
和下降沿控制。上升沿用posedge关键字来描述,下降沿用negedge关键字描述。边沿触发事件控制的语法格式为: 第一种:@(<边沿触发事件>) 行为语句;
第二种:@(<边沿触发事件1> or <边沿触发事件2> or …… or <边沿触发事件n>) 行为语句;
例2-10 边沿触发事件计数器 reg [4:0] cnt;
always @(posedge clk) begin if (reset) cnt <= 0; else
cnt <= cnt +1; end
上面这个例子表明:只要clk信号有上升沿,那么cnt信号就会加1,完成计数的功能。这种边沿计数器在同步分频电路中有着广泛的应用。
? 电平敏感事件是指指定信号的电平发生变化时发生指定的行为。下面是电平触发事
件控制的语法和实例:
第一种:@(<电平触发事件>) 行为语句;
第二种:@(<电平触发事件1> or <电平触发事件2> or …… or <电平触发事件n>) 行为语句;
例2-11 电平沿触发计数器 reg [4:0] cnt;
always @(a or b or c) begin if (reset) cnt <= 0; else
cnt <= cnt +1; end
其中,只要a,b,c信号的电平有变化,信号cnt的值就会加1,这可以用于记录a,b,c变化的次数。
4.流控制
流控制语句包括3类,即跳转、分支和循环语句。
(1)if语句
if语句的语法如下:
if (条件1) 语句块1 else if (条件2) 语句块2 …… else 语句块n
如果条件1的表达式为真(或非0值),那么语句块1被执行,否则语句块不被执行,然后依次判断条件2至条件n是否满足,如果满足就执行相应的语句块,最后跳出if语句,整个模块结束。如果所有的条件都不满足,则执行最后一个else分支。在应用中,else if分支的语句数目由实际情况决定;else分支也可以缺省,但会产生一些不可预料的结果,生
成本不期望的锁存器。
例2-12 下面给出一个if语句的例子,并说明省略else分支所产生的一些结果。
always @(a1 or b1) begin
if (a1) q<= d; end
if语句只能保证当a1=1时,q才取d的值,但程序没有给出a1=0时的结果。因此在缺少else语句的情况下,即使a1=0时,q的值会保持a1=1的原值,这就综合成了一个锁存器。
如果希望a1=0时,q的值为0或者其他值,那么else分支是必不可少的。下面给出a1=0,q=0的设计:
always @(a1 or b1) begin
if (a1) q <= d; else q <= 0; end
(2)case语句
case语句是一个多路条件分支形式,其用法和C语言的csae语句是一样的。
下面给出一个case语句的例子:
reg [2:0] cnt; case (cnt)
3'b000: q = q + 1; 3'b001: q = q + 2; 3'b010: q = q + 3; 3'b011: q = q + 4; 3'b100: q = q + 5; 3'b101: q = q + 6; 3'b110: q = q + 7;
3'b111: q = q + 8; default: q <= q+ 1; endcase
需要指出的是,case语句的default分支虽然可以缺省,但是一般不要缺省,否则会和if语句中缺少else分支一样,生成锁存器。
例2-13 给出case语句的Verilog实例
always @(a1[1:0] or b1) begin
case (a1) 2'b00: q <= b'1; 2'b01: q <= b'1 + 1; end
这样就会生成锁存器。一般为了使case语句可控,都需要加上default选项。
always @(a1[1:0] or b1) begin
case (a1) 2'b00: q <= b1; 2'b01: q <= b1 + 1; default: q <= b1 + 2; end
在实际开发中,要避免生成锁存器的错误。如果用if语句,最好写上else选项;如果用case语句,最好写上default项。遵循上面两条原则,就可以避免发生这种错误,使设计者更加明确设计目标,同时也增加了Verilog程序的可读性。
此外,还需要解释在硬件语言中使用if语句和case语句的区别。在实际中如果有分支情况,尽量选择case语句。这是因为case语句的分支是并行执行的,各个分支没有优先级的区别。而if语句的选择分支是串行执行的,是按照书写的顺序逐次判断的。如果设计没有这种优先级的考虑,if语句和case语句相比,需要占用额外的硬件资源。
(3)循环语句
Verilog HDL中提供了4种循环语句:for循环、while循环、forever循环和repeat循环。其语法和用途与C语言很类似。
? for循环照指定的次数重复执行过程赋值语句。for循环的语法为:
for(表达式1; 表达式2; 表达式3) 语句
for循环语句最简单的应用形式是很容易理解的,其形式为: for(循环变量赋初值; 循环结束条件; 循环变量增值) 例:for语句的应用实例
for(bindex = 1; bindex <= size; bindex = bindex + 1) result = resul + (a <<(bindex-1));
? while循环执行过程赋值语句直到指定的条件为假。如果表达式条件在开始不为真
(包括假、x以及z),那么过程语句将永远不会被执行。while循环的语法为:
while (表达式) begin …… end
例:while语句的应用实例 while (temp) begin count = count + 1; end
? forever循环语句连续执行过程语句。为跳出这样的循环,中止语句可以与过程语
句共同使用。同时,在过程语句中必须使用某种形式的时序控制,否则forever循环将永远循环下去。forever语句必须写在initial模块中,用于产生周期性波形。forever循环的语法为 forever begin …… end
例:forever语句的应用实例 initial forever begin if(d) a = b + c; else a= 0; end
? repeat循环语句执行指定循环数,如果循环计数表达式的值不确定,即为x或z
时,那么循环次数按0处理。repeat循环语句的语法为 repeat(表达式) begin ……
end
例:repeat语句的应用实例 repeat (size) begin c = b << 1; end
2.4.4 混合设计模式
在模型中,结构描述、数据流描述和行为描述可以自由混合。也就是说,模块描述中可以包括实例化的门、模块实例化语句、连续赋值语句以及行为描述语句的混合,它们之间可以相互包含。使用always语句和initial语句(切记只有寄存器类型数据才可以在模块中赋值)来驱动门和开关,而来自于门或连续赋值语句(只能驱动线网型)的输出能够反过来用于触发always语句和initial语句。
下面给出一个混合设计方式的实例。
例2-14 用结构和行为实体描述了一个4位全加器。 module adder4(in1, in2, sum, flag); input [3:0] in1; input [3:0] in2; output [4:0] sum; output flag;
wire c0, c1, c2;
fulladd u1 (in1 [0], in2 [0], 0, sum[0], c0); fulladd u2 (in1 [1], in2 [1], c0, sum[1], c1); fulladd u3 (in1 [2], in2 [2], c1, sum[2], c2); fulladd u4 (in1 [3], in2 [3], c2, sum[3], sum[4]);
assign flag = sum ? 0 : 1; endmodule
在这个例子中,用结构化模块计数sum输出,用行为级模块输出标志位。
第5节 Verilog代码书写规范
2.5.1 信号命名规则
信号命名规则在团队开发中占据着重要地位,统一、有序的命名能大幅减少设计人员之间的冗余工作,还可便于团队成员代码的查错和验证。比较著名的信号命名规则当推Microsoft公司的“匈牙利”法,该命名规则的主要思想是“在变量和函数名中加入前缀以增进人们对程序的理解”。例如所有的字符变量均以ch为前缀,若是常数变量则追加前缀c。信号命名的整体要求为:命名字符具有一定的意义,直白易懂,且项目命名规则唯一。对于HDL设计,设计人员还需要注意以下命名规则。
1.系统级信号的命名
系统级信号指复位信号,置位信号,时钟信号等需要输送到各个模块的全局信号。系统信号以字符串sys或syn开头;时钟信号以clk开头,并在后面添加相应的频率值;复位信号一般以rst或reset开头;置位信号为st或set开头。典型的信号命名方式如下所示:
wire [7:0] sys_dout, sys_din; wire clk_32p768MHz; wire reset; wire st_counter;
2.低电平有效的信号命名
低电平有效的信号后一律加下划线和字母n。如:
wire SysRst_n; wire FifoFull_n;
3.过锁存器锁存后的信号
经过锁存器锁存后的信号,后加下划线和字母r,与锁存前的信号区别。如:
信号CpuRamRd信号,经锁存后应命名为CpuRamRd_r。 低电平有效的信号经过锁存器锁存后,其命名应在_n后加r。如: CpuRamRd_n信号,经锁存后应命名为CpuRamRd_nr
多级锁存的信号,可多加r以标明。如:
CpuRamRd信号,经两级触发器锁存后,应命名为CpuRamRd_rr。
2.5.2 模块命名规则
HDL语言的模块类似于C语言中的函数,可采用C语言函数的大多数规则。模块的命名应该尽量用英文表达出其完成的功能。遵循动宾结构的命名法则,函数名中动词在前,并在命名前加入函数的前缀,函数名的长度一般不少于2个字母。HDL模块的命名还需要考虑以下情况:
1.模块的命名规则
在系统设计阶段应该为每个模块进行命名。命名的方法是,将模块英文名称的各个单词首字母组合起来,形成3到5个字符的缩写。若模块的英文名只有一个单词,可取该单词的前3个字母。各模块的命名以3个字母为宜。例如:
Arithmatic Logical Unit模块,命名为ALU。 Data Memory Interface模块,命名为DMI。 Decoder模块,命名为DEC。
2.模块之间接口信号的命名
所有变量命名分为两个部分,第一部分表明数据方向,其中数据发出方在前,数据接收方在后,第二部分为数据名称。两部分之间用下划线隔离开。第一部分全部大写,第二部分所有具有明确意义的英文名全部拼写或缩写的第一个字母大写,其余部分小写。举例:
wire CPUMMU_WrReq;
下划线左边是第一部分,代表数据方向是从CPU模块发向存储器管理单元模块(MMU)。下划线右边Wr为Write的缩写,Req是Request的缩写。两个缩写的第一个字母都大写,便于理解。整个变量连起来的意思就是CPU发送给MMU的写请求信号。模块上下层次间信号的命名也遵循本规定。若某个信号从一个模块传递到多个模块,其命名应视信号的主要路径而定。
3.模块内部信号:
模块内部的信号由几个单词连接而成,缩写要求能基本表明本单词的含义;单词除常用的缩
写方法外(如:Clock->Clk, Write->Wr, Read->Rd等),一律取该单词的前几个字母( 如:Frequency->Freq, Variable->Var 等);每个缩写单词的第一个字母大写;若遇两个大写字母相邻,中间添加一个下划线(如DivN_Cntr);举例:
SdramWrEn_n; FlashAddrLatchEn;
2.5.3 代码格式规范
1. 分节书写格式
各节之间加1到多行空格。如每个always,initial语句都是一节。每节基本上完成一个特定的功能,即用于描述某几个信号的产生。在每节之前有几行注释对该节代码加以描述,至少列出本节中所描述信号的含义。
行首不要使用空格来对齐,而是用Tab键,Tab键的宽度设为4个字符宽度。行尾不要有多余的空格。
2. 注释的规范
使用//进行的注释行以分号结束;使用/* */进行的注释,/*和*/各占用一行,并且顶头;例如:
// Edge detector used to synchronize the input signal;
对于函数,应该从“功能”,“参数”,“返回值”、“主要思路”、“调用方法”、“日期”六个方面用如下格式注释: // 程序说明开始
// ================================================================//
// 功能: 完成两个输入数的相加。 // 参数: strByDelete,strToDelete // 输入参数 // 输出参数
// 主要思路:本算法主要采用2级流水线完成相加 // 日期:起始日期,如:2007/8/21.9:40--2007/8/23.21:45 // 版本: // 程序编写人员:
// 程序调试记录:
// ================================================================// // 模块说明结束
此外,在注释说明中,需要注意以下细节:
? 在注释中应该详细说明模块的主要实现思路,特别要注明自己的一些想法,如果有
必要则应该写明对想法产生的来由。
? 在注释中详细注明函数的适用方法,对于输入参数的要求以及输出数据的格式。 ? 在注释中要强调调用时的危险方面,可能出错的地方。
? 对日期的注释要求记录从开始编写模块到模块测试结束之间的日期。
? 对模块注释开始到模块命名之间应该有一组用来标识的特殊字符串。如果算法比较
复杂,或算法中的变量定义与位置有关,则要求对变量的定义进行图解。对难以理解的算法能图解尽量图解。
3.空格的使用:
不同变量,以及变量与符号、变量与括号之间都应当保留一个空格。Verilog关键字与其它任何字符串之间都应当保留一个空格。如:
always @ ( ...... )
使用大括号和小括号时,前括号的后边和后括号的前边应当留有一个空格。逻辑运算符、算术运算符、比较运算符等运算符的两侧各留一个空格,与变量分隔开来;单操作数运算符例外,直接位于操作数前,不使用空格。使用//进行的注释,在//后应当有一个空格;注释行的末尾不要有多余的空格。例:
assign SramAddrBus = { AddrBus[31:24], AddrBus[7:0] }; assign DivCntr[3:0] = DivCntr[3:0] + 4'b0001; assign Result = ~Operand;
4.begin…end的书写规范
同一个层次的所有语句左端对齐;initial、always等语句块的begin关键词跟在本行的末尾,相应的end关键词与initial、always对齐;这样做的好处是避免因begin独占一行而造成行数太多;如:
always @ ( posedge SysClk or negedge SysRst ) begin if( !SysRst ) DataOut <= 4'b0000; else if( LdEn ) begin DataOut <= DataIn; End else
DataOut <= DataOut + 4'b0001; end
不同层次之间的语句使用Tab键进行缩进,每加深一层缩进一个Tab;在endmodule,endtask,endcase等标记一个代码块结束的关键词后面要加上一行注释说明这个代码块的名称。
2.5.4 模块调用规范
在Verilog中,有两种模块调用的方法,一种是位置映射法,严格按照模块定义的端口顺序来连接,不用注明原模块定义时规定的端口名,其语法为:
模块名 (连接端口1信号名, 连接端口2信号名, 连接端口3信号名,…);
另一种为信号映射法,即利用“.”符号,表明原模块定义时的端口名,其语法为:
模块名 (.端口1信号名(连接端口1信号名), .端口2信号名(连接端口2信号名), .端口3信号名(连接端口3信号名),…);
显然,信号映射法同时将信号名和被引用端口名列出来,不必严格遵守端口顺序,不仅降低了代码易错性,还提高了程序的可读性和可移植性。因此,在良好的代码中,严禁使用位置调用法,全部采用信号映射法。 第6节 Verilog常用程序示例
2.6.1 Verilog基本模块
1.触发器的Verilog实现
时序电路是高速电路的主要应用类型,其特点是任意时刻电路产生的稳定输出不仅与当前的
输入有关,而且还与电路过去时刻的输入有关。时序电路的基本单元就是触发器。下面介绍几种常见同步触发器的Verilog实现。
? 同步RS触发器
RS触发器分为同步触发器和异步触发器,二者的区别在于同步触发器有一个时钟端clk,只有在时钟端的信号上升(正触发)或下降(负触发)时,触发器的输出才会发生变化。下面以正触发为例,给出其Verilog代码实现。
例2-15 正触发型同步RS触发器的Verilog实现。
module sy_rs_ff (clk, r, s, q, qb); input clk, r, s; output q, qb; reg q;
assign qb = ~ q;
always @(posedge clk) begin case({r, s})
2'b00: q <= 0; 2'b01: q <= 1; 2'b10: q <= 0; 2'b11: q <= 1'bx; endcase end endmodule
上述程序经过综合Synplify Pro后,其RTL级结构如图2-2所示。
图2-2 同步RS触发器的RTL结构图
在ModelSim 6.2b中完成仿真,其结果如图2-3所示
图2-3 同步RS触发器的仿真结果示意图
? 同步T触发器
T触发器也分为同步触发器和异步触发器,二者的区别在于同步T触发器多了一个时钟端。同步T触发器的逻辑功能为:当时钟clk沿到来时,如果T=0,则触发器状态保持不变;否则,触发器输出端反转。R为复位端,当其为高电平时,输出Q与时钟无关,Q=0。
例2-16 同步T触发器的Verilog实现。
module sy_t_ff(clk, r, t, q, qb); input clk, r, t; output q, qb; reg q;
assign qb = ~q;
always @(posedge clk) begin if(r) q <= 0; else
q <= ~q; end endmodule
上述程序经过综合Synplify Pro后,其RTL级结构如图2-4所示。
图2-4 同步T触发器电路的RTL结构图
在ModelSim 6.2b中完成仿真,其结果如图2-5所示
图2-5 同步T触发器的仿真结果示意图
? 同步D触发器
同步D触发器的功能为: D输入只能在时序信号clk的沿变化时才能被写入到存储器中,替换以前的值,常用于数据延迟以及数据存储模块中。
例2-17 同步D触发器的Verilog实现。
module sy_d_ff(clk, d, q, qb); input clk, d; output q, qb; reg q;
assign qb = ~q;
always @(posedge clk) begin q <= d; end endmodule
上述程序经过综合Synplify Pro后,其RTL级结构如图2-6所示。
图2-6 同步D触发器的RTL结构图
在ModelSim 6.2b中完成仿真,其结果如图2-7所示
图2-7 同步D触发器的仿真结果示意图
? 同步JK触发器
JK触发器是在RS触发器的基础上发展而来的,常用于实现计数器。当clk=0时,触发器不工作,处于保持状态。当时钟clk=1时,触发器的功能如下:当JK为00、01以及10时实现RS触发器的功能;当JK为11时实现T触发器的功能。
例2-18 同步JK触发器的Verilog实现。
module sy_jk_ff(clk, j, k, q, qb); input clk, i, k; output q, qb; reg q;
assign qb = ~q;
always @(posedge clk) begin case({j, k})
2'b00: q <= q; 2'b01: q <= 0; 2'b10: q <= 1; 2'b11: q <= ~q; endcase
end endmodule
上述程序经过综合Synplify Pro后,其RTL级结构如图2-8所示。
图2-8 同步JK触发器的RTL结构图
在ModelSim 6.2b中完成仿真,其结果如图2-9所示
图2-9 同步JK触发器的仿真结果示意图
2.三态缓冲器的Verilog实现
三态缓冲器也称三态门,其典型应用是双向端口,常用于双向数据总线的构建。在数字电路中,逻辑输出有两个正常态:低电平状态(对应逻辑0)和高电平状态(对应逻辑1);此外,电路还有不属于0和1状态的高组态(对应于逻辑Z)。所谓高阻,即输出端属于浮空状态,只有很小的漏电流流动,其电平随外部电平高低而定,门电平放弃对输出电路的控制。或者可以理解为输出与电路是断开的。最基本的三态缓冲器的逻辑符号如图2-10所示。
图2-10 三态缓冲器的逻辑符号图
当OE为高电平时,Dataout与Datain相连;而OE为低时,Dataout为高阻态,相当于和Datain之间的连线断开。
例2-19 使用Verilog实现三态缓冲器
inout a; wire z, b;
//当控制信号z为1时,开通三态门,b为输入端口;当z为0时,三态门为高阻, //a为输出端口
assign a = (z) ? b : 8'bz;
3.38译码器的Verilog实现
38译码器是通过3条线来达到控制8条线的状态,就是通过3条控制线不同的高低电平组合, 一共可以组合出23=8种状态。在电路中主要起到扩展IO资源的作用。当然,可根据实际需求将38译码器扩展到更高级数上。
例2-20 使用Verilog实现38译码器
module decoder3to8(din, reset, dout); input [2:0] din; input reset; output [7:0] dout;
reg [7:0] dout;
always @(din or reset) begin if(!reset)
dout = 8'b0000_0000; else
case(din)
3'b000: dout = 8'b0000_0001; 3'b001: dout = 8'b0000_0010; 3'b010: dout = 8'b0000_0100; 3'b011: dout = 8'b0000_1000; 3'b100: dout = 8'b0001_0000; 3'b101: dout = 8'b0010_0000;
3'b110: dout = 8'b0100_0000; 3'b111: dout = 8'b1000_0000; endcase end
endmodule
上述程序经过综合Synplify Pro后,其RTL级结构如图2-11所示。
图2-11 38译码器的RTL结构图
在ModelSim 6.2b中完成仿真,其结果如图2-12所示
图2-12 38译码器的仿真结果示意图
2.6.2 基本时序处理模块
1.奇、偶数分频电路
在数字逻辑电路设计中,分频器是一种基本电路。通常用来对某个给定频率进行分频,以得到所需的频率。
? 偶数分频电路
偶数倍分频是最简单的一种分频模式,完全可通过计数器计数实现。如要进行N倍偶数分频,那么可由待分频的时钟触发计数器计数,当计数器从0计数到N/2-1时,输出时钟进行翻转,并给计数器一个复位信号,使得下一个时钟从零开始计数,以此循环下去。这种方法可以实现任意的偶数分频。例2-21给出的是一个16分频电路,其它倍数的分频电路可通过修改计数器的上限值得到。
例2-21 用Verilog实现一个16分频电路。
module clk_div16(clk_in, reset, clk_out); input clk_in; input reset; output clk_out;
reg clk_out; reg [2:0] cnt;
always @(posedge clk_in) begin if(!reset) begin cnt <= 0; clk_out <= 0; end else
if(cnt == 7) begin cnt <= 0;
clk_out <= !clk_out; end else begin
cnt <= cnt + 1; clk_out <= clk_out; end end
endmodule
上述程序经过综合Synplify Pro后,其RTL级结构如图2-13所示。
图2-13 16分频电路的RTL结构图
在ModelSim 6.2b中完成仿真,其结果如图2-14所示,从中可以看出例2-21成功实现了输入时钟的16分频。
图2-14 16分频电路的仿真结果示意图
? 奇数分频电路
奇数倍分频有多种实现方法,下面介绍常用的错位“异或”法的原理。如进行三分频,通过待分频时钟上升沿触发计数器进行模三计数,当计数器计数到邻近值进行两次翻转。比如在计数器计数到1时,输出时钟进行翻转,计数到2时再次进行翻转,即在邻近的1和2时刻进行两次翻转。这样实现的三分频占空比为1/3或者2/3。如果要实现占空比为50%的三分频时钟,可以通过待分频时钟下降沿触发计数,和上升沿同样的方法计数进行三分频,然后将下降沿产生的三分频时钟和上升沿产生的时钟进行相或运算,即可得到占空比为50%的三分频时钟。
这种错位“异或”法可以推广实现任意的奇数分频:对于实现占空比为50%的N倍奇数分频,首先进行上升沿触发的模N计数,计数到某一选定值时进行输出时钟翻转,然后经过(N-1)/2再次进行翻转得到一个占空比非50%奇数N分频时钟。再者同时进行下降沿触发的模N计数,到和上升沿触发输出时钟翻转选定值相同值时,进行输出时钟时钟翻转,同样经过(N-1)/2时,输出时钟再次翻转生成占空比非50%的奇数N分频时钟。两个占空比非50%的N分频时钟相或运算,得到占空比为50%的奇数N分频时钟。
例2-22 使用Verilog实现3分频电路。
module clk_div3(clk_in, reset, clk_out); input clk_in; input reset; output clk_out;
reg [1:0] cnt, cnt1; reg clk_1to3p, clk_1to3n; always @(posedge clk_in) begin if(!reset) begin cnt <= 0; clk_1to3p <= 0; end else begin
if(cnt == 2'b10) begin cnt <= 0;
clk_1to3p <= clk_1to3p; end else begin
cnt <= cnt + 1;
clk_1to3p <= !clk_1to3p; end end end
always @(negedge clk_in) begin if(!reset) begin cnt1 <= 0; clk_1to3n <= 0; end else begin
if(cnt1 == 2'b10) begin cnt1 <= 0;
clk_1to3n <= clk_1to3n; end else begin
cnt1 <= cnt1 + 1; clk_1to3n <= !clk_1to3n; end end end
assign clk_out = clk_1to3p | clk_1to3n;
endmodule
上述程序经过综合Synplify Pro后,其RTL级结构如图2-15所示。
图2-15 3分频电路的RTL结构图
在ModelSim 6.2b中完成仿真,其结果如图2-16所示,可以看到输出时钟为占空比为50%的3分频时钟。
图2-16 3分频电路的仿真结果
2.同步采样模块
在实际应用中,外部输入的异步信号需要经过系统时钟的同步化,且将输入的异步信号整形成一个时钟长的脉冲信号,如图2-17所示。这里以例2-23来说明实现的方法。
图2-17 异步信号的同步采样示意图
例2-23 使用Verilog将外部异步信号进行同步整形。
module clk_syn(clk, reset, s_in, s_out); input clk; input reset; input s_in; output s_out;
reg s_t1, s_t2;
always @(posedge clk) begin if(!reset) begin s_t1 <= 0; s_t2 <= 0; end else begin s_t1 <= s_in; s_t2 <= s_t1; end end
assign s_out = s_t1 & (!s_t2);
endmodule
上述程序经过综合Synplify Pro后,其RTL级结构如图2-18所示。从结果上看,该电路非常简单,但需要一定的时序逻辑能力才能真正理解该段程序。
图2-18 同步电路的RTL结构示意图
在ModelSim 6.2b中完成仿真,其结果如图2-19所示,将不等长的高电平信号整形成宽度为一个时钟周期的脉冲信号。
图2-19 同步电路的仿真结果示意图
其中,如果在时钟的上升沿din=\,则x=1,y=0,dout=x and (not y)=1;如果din=\超过一个时钟宽度,则x=1,y=1,dout=x and (not y)=0。即使din在时钟周期内出现抖动,也不会影响输出结果,还是被整形成一个时钟宽度。所以不管是长周期信号还是短周期信号,经过同步采样后,有效高电平宽度都等于时钟周期。
3.同步状态机的Verilog实现
状态机一般包括组合逻辑和寄存器逻辑两部分。组合电路用于状态译码和产生输出信号,寄存器用于存储状态。状态机的下一个状态及输出不仅与输入信号有关,还与寄存器当前状态有关。根据输出信号产生方法的不同,状态机可分为米里(Mealy)型和摩尔(Moore)型。前者的输出是当前状态和输入信号的函数,后者的输出仅是当前状态的函数。在硬件设计时,根据需要决定采用哪种状态机。
? 状态编码
状态编码又称状态分配。通常有多种编码方法,编码方案选择得当,设计的电路可以简单;反之,电路会占用过多的逻辑或速度降低。设计时,须综合考虑电路复杂度和电路性能这两个因素。下面主要介绍二进制编码、格雷编码和独热码。
二进制编码和格雷码都是压缩状态编码。二进制编码的优点是使用的状态向量最少,但从一个状态转换到相邻状态时,可能有多个比特位发生变化,瞬变次数多,易产生毛刺。格雷编码在相邻状态的转换中,每次只有1个比特位发生变化,虽减少了产生毛刺和一些暂态的可能,但不适用于有很多状态跳转的情况。
独热码是指对任意给定的状态,状态向量中只有1位为1,其余位都为0。n状态的状态机需要n个触发器。这种状态机的速度与状态的数量无关,仅取决于到某特定状态的转移数量,速度很快。当状态机的状态增加时,如果使用二进制编码,那么状态机速度会明显下降。而采用独热码,虽然多用了触发器,但由于状态译码简单,节省和简化了组合逻辑电路。独热编码还具有设计简单、修改灵活、易于综合和调试等优点。对于寄存器数量多、而门逻辑相对缺乏的FPGA器件,采用独热编码可以有效提高电路的速度和可靠性,也有利于提高器件资源的利用率。独热编码有很多无效状态,应该确保状态机一旦进入无效状态时,可以立即跳转到确定的已知状态。
? 有限状态机的Verilog实现
用Verilog 语言描述有限状态机可使用多种风格,不同的风格会极大地影响电路性能。通常有3种描述方式:单always块、双always块和三always块。
单always块把组合逻辑和时序逻辑用同一个时序always块描述,其输出是寄存器输出,无毛刺。但是这种方式会产生多余的触发器,代码难于修改和调试,应该尽量避免使用。
双always块大多用于描述Mealy状态机和组合输出的Moore状态机,时序always块描述当前状态逻辑,组合逻辑always块描述次态逻辑并给输出赋值。这种方式结构清晰,综合后的面积和时间性能好。但组合逻辑输出往往会有毛刺,当输出向量作为时钟信号时,这些毛刺会对电路产生致命的影响。
三always块大多用于同步Mealy状态机,两个时序always块分别用来描述现态逻辑和对输出赋值,组合always块用于产生下一状态。这种方式的状态机也是寄存器输出,输出无毛刺,并且代码比单always块清晰易读,但是面积大于双always块。随着芯片资源和速度的提高,目前这种方式得到了广泛应用。
下面以三always块模块给出状态机的Verilog模板。
// 构成状态跳转环
always @(posedge clk or negedge rst_n) current_state <= next_state;
// 完成状态机的内部逻辑
always @ (current_state or ) begin case(current_state) S1: next_state = S2; S2: next_state = S1; default: next_state = S2; endcase end
// 完成状态机的外部逻辑
always @(current_state or ) begin case(current_state) S1: S2: default: endcase end
? 综合状态机的一般原则
在硬件描述语言中,许多基于仿真的语句虽然符合语法规则,但是不能映射到硬件逻辑电路单元,如果要最终实现硬件设计,必须写出可以综合的程序。通常,综合的原则为:
1. 综合之前一定要进行仿真,仿真会暴露逻辑错误。如果不做仿真,没有发现的逻辑
错误会进入综合器,使综合的结果产生同样的逻辑错误。
2. 每一次布线之后都要进行仿真,在器件编程或流片之前一定要进行最后的仿真。 3. 用Verilog HDL描述的异步状态机是不能综合的,应该避免用综合器来设计。在
必须设计异步状态机时,建议用电路图输入的方法
4. 状态机应该有一个异步或同步复位端,以便在通电时将硬件电路复位到有效状态。
建议使用异步复位以简化硬件开销。
5. 时序逻辑电路建模时,用非阻塞赋值。用always块写组合逻辑时,采用阻塞赋值。
不要在多个always块中为同一个变量赋值。
6. always块中应该避免组合反馈回路。在赋值表达式右端参与赋值的信号都必须出
现在敏感信号列表中,否则在综合时,会为没有列出的信号隐含地产生一个透明锁存器。