笔记
完整的module参考模型
`define AAAA BBBB //宏定义
`include "CCCC.v" //文件包含
`timescale 1ns/1ns //时间刻度定义
//------------------------------------------------------------------------
module DDDD(完整的端口列表); //模块声明
input [宽度声明] E; //输入、输出、双向端口及宽度
output [宽度声明] F;
inout [宽度声明] G;
//------------------------------------------------------------------------
reg [宽度声明] F; //如果输出是用行为及语句描述的,记得要再声明为reg
reg [宽度声明] H; //模块内部用到的变量
wire [宽度声明] I; //模块内用到线网
integer [宽度声明] J; //整数型,记得符号
//------------------------------------------------------------------------
parameter K=数值; //参数部分声明
//------------------------------------------------------------------------
always@(posedge 信号 or negedge 信号) //对边沿信号动作敏感,体现为时序电路
begin
F<=E; //采用非阻塞赋值
end
//------------------------------------------------------------------------
always@(信号) //对边沿信号电平动作敏感,体现为组合电路
begin
F=E; //采用阻塞赋值
end
//------------------------------------------------------------------------
assign G=^E; //简单或逻辑清晰的组合逻辑可以用数据流语句
//------------------------------------------------------------------------
and and1(out,in1,in2); //可以使用门级调用
//------------------------------------------------------------------------
KKK my_KKK(端口连接); //也可以实例化其他模块,记得保证端口连接方式的正确
endmodule
第二章 Verilog 基础知识
2.1 Verilog HDL语言要素
2.1.1 空白符
空白符包括空格符(\b)、制表符(\t)、换行符和换页符。在编译和综合时,空白符被忽略。
例如:
inital begin a = 3'b100 ;end
与相当
intial
begin
a = 3'b100;
b = 3'b010;
end
空白符为了程序的易读性
2.1.2 注释符
(1)单行注释:以“//”开始,Verilog忽略从此处到行尾的内容
(2)多行注释:多行注释以“/*”开始,到“*/”结束
Note:都用英文
非法多行注释:/*注释内容/*多行注释嵌套多行注释*/注释内容*/
合法多行注释:/*注释内容//多行注释嵌套单行注释*/
2.1.3 标识符
在verilog中标识符(Identifier)被用来命名信号名、模块名、参数名等,他可以是任意一组字母、数字、$符号和 _ (下划线)符号的组合。应该注意的是标识符的字母区分大小写,并且第一位字符必须是字母或者下划线。
转义标识符:在不符合语法的标识符前加“\”,使其能够语法不出错,但是没啥卵用!
2.1.4 关键字
2.1.5 数值
verilog有四种基本的逻辑数值状态
状态 | 含义 |
---|---|
0 | 低电平,逻辑0或假 |
1 | 高电平,逻辑1或真 |
x或X | 不确定或未知的逻辑状态 |
z或Z | 高阻态 |
-
整数及其表示法
+/-'<base_format>
例如:8‘b10001101 //位宽为8位的二进制数10001101
数制 基数符号 合法标识符 *二进制 b或B 0、1、x、X、z、Z、? 八进制 0或O 0~7、x、X、z、Z、? 十进制 d或D 0~9 *十六进制 h或H 0~9、a~f、A~F、x、X、z、Z、? -
实数及其表示
(1)十进制表示法,采用十进制格式,小数点两边必须都有数字,否则为非法的表示形式。
(2)科学计数法,例如:564.2e2的值为56420.0,8.7E2的值为870.0(e不区分大小写)。
2.2 数据类型及功能
数据类型 | 功能 |
---|---|
parameter类型 | 用于参数的描述 |
wire类型 | 用于描述线网 |
reg类型 | 用于描述寄存器 |
integer类型 | 用于描述整数类型 |
time类型 | 用于描述时间类型 |
2.2.1 数字
基本表达格式:
<位宽>'<进制><数值>
- 进制字母不区分大小写;
- 如果位宽部分和数值部分的宽度不匹配的时候,位宽大的高位补零;位宽小的舍去溢出的高位,低位截取
- 树枝部分也可以出现x和z。八进制的x相当于二进制的xxx,十六进制相当于xxxx。特别的,数值的首位为x或z时,如果出现了位宽多于数值宽度的,则缺少的位分别按x或z来补齐。
2.2.2 参数
有些时候某些数字或字符需要多次使用,而且具有一定意义,此时就可以设计为参数类型(parameter),用于指代某个常用的数值、字符串或表达式等。格式如下:
parameter 参数名1=表达式1,参数名2=表达式2;
parameter size=8;
parameter a=4,b=6;
parameter clock=a+b;
- 参数一般用于定义宽度、延迟这样的表达式或不同状态,其他情况使用不多。
- 参数要定义在模块内,位置和端口声明所处的级别相同,处于第一级别(端口声明、wire或reg声明、门级调用、模块的实例化语句、连续赋值assign语句、参数声明等)
module 模块名(端口列表); output/input 输出/输入端口; wire/reg 线网/寄存器; parameter 内部参数; and and1(); //实例化语句 assign 输出=表达式; //数据流语句 endmodule
- 参数的作用范围尽在此模块内部以及实例化后的本模块,出了模块module和endmodule的边界后就不再生效
模块实例化中参数的改写
法1:
module example(A,Y);
......
//参数在module和endmodule中有效
parameter size=5,delay=6;
......
endmodule
module test;
......
//参数在模块实例化引用了example模块,可以用#()语法来改写参数
example #(6,6) t1(a1,y1); //seize和delay的值被重新赋值为6和6
example #(4) t2(a2,y2); //只有一个参数时,按顺序赋值size=4,delay不变
......
endmodule
法2:
可以用关键字defparam来改写
module example(A,Y);
......
//参数在module和endmodule中有效
parameter size=5,delay=6;
......
endmodule
module test;
......
//参数在模块实例化引用了example模块,可以用#()语法来改写参数
example #(6,6) t1(a1,y1); //seize和delay的值被重新赋值为6和6
example #(4) t2(a2,y2); //只有一个参数时,按顺序赋值size=4,delay不变
......
endmodule
module annotate;
//参数改写
defparam test.t1.size=6,test.t1.delay=6;
defparam test.t2.size=4;
endmodule
2.2.3 连线型、寄存器型和存储器型数据类型
在设计中,如何确定是使用wire或reg类型:
链接1
链接2
信号强度表示数字电路中不同强度的驱动源,用来解决不同驱动强度存在下的赋值冲突:
标记符 | 名称 | 类型 | 强弱程度 |
---|---|---|---|
supply | 电源级驱动 | 驱动 | 强 |
strong | 强驱动 | 驱动 | |
pull | 上拉级驱动 | 驱动 | |
large | 大容性 | 存储 | 到 |
weak | 弱驱动 | 驱动 | |
medium | 中性驱动 | 驱动 | |
small | 小容性 | 存储 | |
highz | 高容性 | 存储 | 弱 |
根据电流来区分驱动强度
1.连线型
连线型数据类型 | 功能说明 |
---|---|
wire,tri | 标准连线(缺省为该数据类型) |
wor,trior | 多重驱动时,具有线或特性的连线类型 |
wand,trand | 多重驱动时,具有线或特性的连线类型 |
trireg | 具有电荷保持特性的连线型数据(特例) |
tri1 | 上拉电阻 |
tri0 | 下拉电阻 |
supply1 | 电源线、用于对电源建模,为高电平1 |
supply2 | 电源线、用于对“地”建模,为低电平0 |
功能wire/tri
wire/tri | 0 | 1 | x | z |
---|---|---|---|---|
0 | 0 | x | x | 0 |
1 | x | 1 | x | 1 |
x | x | x | x | x |
z | 0 | 1 | x | z |
2.寄存器型
reg型是数据存储单元的抽象类型,其对应的硬件电路元件具有状态保持作用,能够存储数据,如触发器,锁存器等。
reg型变量常用于行为及描述,由过程赋值语句对其进行赋值。
reg型变量简单例子:
reg a; //定义一个以为的名为a的reg变量
reg [3:0]b; //定义一个4位的名为b的reg变量
reg [8:1]c,d,e //定义三个名分别为c,d,e的8位reg变量
reg变量一般为无符号数,若将一个负数赋给reg型变量,则自动转换成其二进制补码形式
reg signed[3:0]rega; rega=-2; //rega的值为1110(14),是2的补码
Note:原码、反码和补码关系
原码:是最简单的机器数表示法。用最高位表示符号位,‘1’表示负号,‘0’表示正号。其他位存放该数的二进制的绝对值。
反码:正数的反码还是等于原码,负数的反码就是他的原码除符号位外,按位取反。
补码:正数的补码等于他的原码,负数的补码等于反码+1。
3.存储类型
-
存储类型变量可以描述RAM型、ROM型存储器以及reg文件。
-
存储器变量的一般声明格式为:
reg< range1><name_of_register>< range2>;
—range1和range2都是可选项,缺省都为1.
—range1:表示存储器当中寄存器的位宽,格式为[msb:lsb]。
—range2:表示寄存器的个数,格式为[msb:lsb]即有msb-lsb+1个。
—name_of_register为变量名称列表,一次可以定义多个名称,之间用逗号分开。
例如:
reg [7:0] mem1[255:0]; //定义了一个有256个8位寄存器的存储器mem1,地址范围是0到255
reg [15:0] mem2[127:0],reg1,reg2; //定义了一个具有128个16位寄存器的存储器mem2和两个16位寄存器reg1和reg2
reg [n-1:0]a; //表示一个n位寄存器a
reg mem1[n-1:0]; //表示一个由n个1位寄存器构成的存储器mem1
2.3 操作符和表达式
2.3.1 算术操作符
-
加法、减法、乘法(*)、除法、取模(%)
如果操作数包含x,则整个结果都作为x值处理。
2.3.2 关系操作符
- 大于、小于、大于等于、小于等于
2.3.3 相等关系操作符
- 等于(==)、不等于(!=)、全等于(===)、非全等于(!==)
2.3.4 逻辑运算符
-
逻辑与“&&”、逻辑或“||”、逻辑非“!”
操作数中存在不定状态x,则逻辑运算的结果也是不定状态
2.3.5 按位操作符
-
按位取反"~“、按位与“&”、按位或“|”、按位异或“^”、按位同或“^~”
异或:Y=AB’+A’B 异则为1,同则为0
同或:Y=AB+A’B’ 同则为1,异则为0
a = 5'b101; b = 5'b11101; $display("%b",a&b); //结果为5b'00101
2.3.6 规约操作符
-
与“&”、或“|”、异或“^",以及相应的非操作
a = 6'b101011; $display("%b",&a); //结果为1b'0
2.3.7 移位操作运算符
- 左移位运算符“<<"、右移位运算符”>>“
- 算术右移“>>>”、算数左移“<<<”
算术移位多用于有符号数的位移,在一位过程中可以保留符号位,而不像逻辑移位直接丢弃值。
2.3.8 条件运算符
-
表达式如下
<条件表达式>?<表达式1><表达式2>
—表达式的计算结果有真、假和x三种状态,当条件表达式的结果为真时,执行表达式1,当条件表达式为假时执行表达式2。
module MUX2(in1,in2,sel,out); input [3:0] in1,in2; input sel; output [3:0]out; reg [3:0]out; assign out = (!sel)?in1:in2; endmodule
2.3.9 连接和复制运算符
-
连接运算符“{}”和复制运算符“{{}}”
—连接操作符
{信号1的某几位,信号2的某几位,…,信号n的某几位}
—重复操作符{{}}将一个表达式放入双重花括号内,复制因子放在第一层括号中。
module corn_rep_tb; reg[2:0]a; reg[3:0]b; reg[7:0]c; reg[4:0]d; reg[5:0]e; initial begin a=3'b101; b=4'b1110; c={a,b}; d={a[2:1],b[2:0]}; e={2{a}}; $display("%b",c); //结果为8’b01011110 $display("%b",d); //结果为5'b10110 $display("%b",e); //结果为6'b101101 end endmodule
2.4模块的基本概念
2.4.1模块的基本概念
— 模块(module)是verilog语言的基本单元,它代表一个基本的功能块,用于描述某个设计的功能或结构以及与其它模块通信的外部端口。
module name(port_list);
端口定义
.
.
.
数据类型说明
.
.
.
逻辑功能描述
.
.
.
endmodule
- 一个模块主要包括:模块的开始与结束、模块端口定义、模块数据类型说明和模块逻辑功能描述这几个基本部分。
(1)模块的开始和结束:以关键词module开始,以关键词endmodule结束的一段程序,其中模块开始语句要以分号结束。
(2)端口定义:用来定义端口列表里的变量哪些是输入(input)、输出(output)和双向端口(inout)以及位宽说明。
(3)数据类型说明:用来说明模块中所用到的内部信号,调用模块等的声明语句和功能定义语句。
(4)逻辑功能描述:用来产生各种逻辑(主要是组合逻辑和时序逻辑)
主要包括:initial语句、always语句、其他子模块实例化语句、门实例化语句、用户自定义原语(UDP)实例化语句、连续赋值语句(assign)、函数和任务。
例如:上升沿D触发器
module dff(din,clk,q);
input din,clk;
output q;
reg q;
always@(posedge clk);
q<=din;
endmodule
2.4.2 端口
-
端口的定义
—模块的端口可以是输入端口、输出端口或双向端口。例如下:
module add
(
input [3:0] a,
input [3:0] b,
output [4:0] c
);
assign c = a + b;
endmodule
-
模块引用时端口的对应方式
(1)在引用时,严格按照模块定义的端口顺序来连接,不用标明原模块定义时规定的端口名。格式如下:
模块名(连接端口1信号名,连接端口2信号名…);
wire [3:0] x1; wire [3:0] x2; wire [4:0] x3; // 希望 x3 = x1 + x2; add add_inst1 ( x1, //对应 模块本身的a x2, //对应 模块本身的b x3 //对应 模块本身的c );
(2)在引用时用“ ."表明元模块定义时规定的端口名。格式如下:
模块名(.端口1名(连接信号1名),。端口2名(连接信号2名)…);
这样表示的好处在于可以用端口名与被引用模块的端口对应,不必严格按端口顺序对应,提高了程序的可读性和可移植性。
wire [3:0] x1; wire [3:0] x2; wire [4:0] x3; add add_inst3 ( .a (x1), .b (x2), .c (x3) );
第三章 Verilog程序设计语言和描述方式
3.1 门级建模
3.2 *数据流建模
3.2.1 连续赋值语句
—连续赋值语句的目标类型主要是标量线网和向量线网两种
(1)标量线网,如:wire a,b;
(2)向量线网,如:wire [3:0]a,b
- 显示连续赋值语句:
—<net_declaration>;
—assign #=Assignment expression;
- 隐式连续赋值语句
—<net_declaration><drive_strength>#=Assignment expression;
1.<net_declaration>(连线型变量类型)
2.(变量位宽)指明了变量数据类型的宽度,格式为[msb:lsb],缺省为1
3.<drive_strength>(赋值驱动强度)是可选项,只能在“隐式连续赋值语句”格式中。他用来对连线类型变量受到的驱动强度进行指定。
wire (weak0,strong1)out = in1&in2;
4.(延迟量)这一项是可选项的。
#(delay1,delay2,delay3)
例3.1-1 显示连续赋值语句
module example1_assignment(a,b,m,n,c,y);
input[3:0] a,b,m,n;
output[3:0] c,y;
wire[3:0] a,b,m,n,c,y;
assign y=m|n;
assign #(3,2,1) c=a&b;
endmodule
例3.1-2隐式连续赋值语句
module example2_assignment(a,b,m,n,c,y);
input[3:0] a,b,m,n;
output[3:0] c,y,w;
wire[3:0] a,b,m,n;
wire[3:0] y=m|n;
wire[3:0] #(3,2,4) c=a&b;
wire(strong0,weak1)[3:0]#(2,1,3)w=(a^b)&(m^n);
endmodule
- 连续赋值语句需要注意以下几点:
- 赋值目标只能是线网类型(wire);
- 在连续赋值中,只能赋值语句右边表达式任何一个变量有变化,表达式立即被计算,计算的结果立即赋给左边信号(若没有定义延迟量)
- 连续赋值语句不能出现在过程块中(initial和always)。
- 多个连续赋值语句之间是并行语句,因此与位置无关。
- 连续赋值语句中的延时具有硬件电路中惯性延时特性,在任何小于延时的信号变化脉冲都将被过滤掉,不会体现在输出端口。
使用数据流建模时要注意,在等式的左侧出现的一定要是wire类型,即在之前的声明部分定义为wire类型网名,宽度可以使1位,也可以是多位,绝对不能是reg类型的寄存器名。
3.3 *行为级建模
- initial结构和always结构在一个module中可以出现很多次,但是不能嵌套使用。
- 一个module中的initial和always结构都是同时执行的,不以代码中出现先后顺序。
类别 | 语句 | 可综合性 |
---|---|---|
过程语句 | initial | |
always | √ | |
语句块 | 串行语句块begin-end | √ |
并行语句块fork-join | ||
赋值语句 | 过程连续赋值assign | |
过程赋值=,<= | √ | |
条件语句 | if-else | √ |
case,casez,casex | √ | |
循环语句 | repeat | |
forever |
3.3.1过程语句
1.initial过程语句
initial结构的主要功能就是进行初始化。initial结构仅在仿真开始的时候被激活一次,然后该结构中的所有语句被执行一次,执行结束后就不再执行了。
语法格式为:
initial
begin
语句1;
语句2;
...
语句n;
end
3.2-2用initial语句产生测试信号
module initial_tb2;
reg S1; //被赋值信号定义reg类型
initial
begin
S1=0;
#100 S1=1;
#200 S1=0;
#50 S1=1;
#100 $finish;
end
endmodule
2.always语句块
从语法描述角度,相对于initial过程块,always语句块的触发状态一直是存在的,只要满足always后面的敏感事件列表,就会执行过程块。
always结构的控制方式有三种:基于延迟的控制、基于事件的控制和基于电平敏感的控制
其语法格式:
always@(<敏感事件列表>)
语句块;
例如:
@(a) //当信号a的值发生变化时
@(a or b) //当信号a或b的值发生变化时
@(posedge clock) //当clock的上升沿到来时
@(posedge clk or negedge reset) //当clk上升到来或reset信号的下降沿到来时
例3.2-4用always过程语句描述异步清零计数器
module counter2(clear,clk,out);
output[7:0] out;
input clk,clear;
reg[7:0] out;
always@(posedge clk or negedge clear)
//clk上升沿或clear低电平清零有效
begin
if(clear) //异步清零
out=0;
else out=out+1;
end
endmodule
例3.2-5 用always语句描述同步置数、同步清零计数器
module counter(out,data,load,reset,clk);
input[7:0]data;
input load,clk,reset;
output[7:0]out;
reg[7:0] out;
always@(posedge clk);
begin
if(reset)out=8'b00; //同步清零,低电平有效
else if(load)out=data;
else out=out=out+1;
end
endmodule
例3.2-6用always语句描述4选1数据选择器
module mux4_1(out,in0,in1,in2,in3,sel);
output out;
input in0,in1,in2,in3;
input[1:0] sel;
reg out; //被赋值信号定义为reg类型
always@(in0 or in1 or in2 or in3 or sel)
case(sel)
2'b00 out=in0;
2'b01 out=in1;
2'b10 out=in2;
2'b11 out=in3;
endcase
endmodule
过程语句使用中需要注意的问题
在信号定义形式方面,无论是对时序逻辑还是组合逻辑描述,Verilog要求在过程语句(initial和always)中,被赋值信号必须定义为“reg"类型。
作为过程的触发条件,在verilog程序中有一定的设计要求。
(1)采用过程对组合电路进行描述时,作为全部的输入信号需要列入敏感信号列表。
(2)采用过程对时序电路进行描述时,需要把时间信号和部分输入信号列入敏感信号列表。应注意的是,不同的敏感事件列表会产生不同的电路形式。
3.3.2 语句块
-
语句块包括串行语句块(begin-end)和并行语句块(fork-join)两种
串行语句块采用关键字begin和end,其中的语句按串行方式顺序执行,可以用于可综合电路程序和仿真测试程序。其语法格式是:
begin块名(**串行按顺序执行**) fork块名*(**并行执行**)
*块内声明语句 块内声明语句*
*语句1; 语句1;*
*语句2; 语句2;*
*... ...*
*语句n; 语句n;*
end end*
begin-end:串行执行
fork-join:并行执行,只能用于仿真测试程序,不能用于可综合电路程序。
fork-join模块很容易产生竞争冒险行为,参考顺序块中一个代码:
reg a,b;
reg [1:0] c,d;
initial
fork
a=0;
b=1;
c={a,b};
d={b,a};
join
3.3.3 过程赋值语句
- 过程赋值语句有阻塞性和非阻塞性过程赋值语句两种形式。
- 无论哪一种,都必须在initial和always结构中,建立可综合模块时赋值语句左端必须要是reg类型,这是语法强制要求。
- 组合逻辑电路使用阻塞赋值来建模;
- 时序逻辑电路使用非阻塞赋值来建模。
阻塞赋值语句的操作符号为“=”,语法格式是:
变量=表达式;
阻塞赋值语句有如下特点:
(1)在串行语句块中,各条阻塞赋值语句将按照先后排列顺序依次执行;在并行语句中的各条阻塞赋值语句则同时执行,没有先后顺序。
(2)执行阻塞赋值语句的顺序是,先计算等号右端等号表达式的值,然后立刻将计算的值赋给左边的变量,与仿真时间无关。
非阻塞赋值语句的操作符号为“<=",语法格式是:
变量<=表达式;
非阻塞赋值语句的特点:
(1)在串行语句块中,各条非阻塞赋值语句的执行没有先后顺序之分,排在前面的语句不会影响后面的语句的执行,各条语句并行执行。
(2)执行非阻塞赋值语句的顺序是,先计算右端表达式的值,然后等到延迟时间结束以后,将右边的值赋给左边的变量。
例3.2-7
程序1修改后实现结构2:
module block1(din,clk,out1,out2);
input din,clk;
output out1,out2;
reg out1,out2;
always@(posedge clk)
begin
out2 = out1;
out1 = din;
end
endmodule
*流水线设计
所有的数字电路都可以划分成:寄存器+组合电路+寄存器的结构;其中电路的快慢由组合电路的最大延迟决定的,而门级电路本质是容性的
τ
=
R
C
,
τ
为延迟
\tau=RC ,\tau为延迟
τ=RC,τ为延迟
为了降低延迟,可以将组合电路分解成若干个小的组合电路+寄存器+小的组合电路的形式。
例3.2-8 试分析下面两段verilog所描述的电路结构
module block3(a,b,c,clk,sel,out);
input a,b,c,clk,sel;
output out;
reg out,temp;
always@(posedge clk)
begin
temp=a&b;
if(sel) out=temp|c;
else out=c;
end
endmodule
综合后电路
3.3.4 过程连续赋值语句
在verilog中,过程连续赋值语句有两种类型:赋值、重新赋值语句(assign、deassign)和强制、释放语句(force、release且优先级更高)。
赋值语句和重新赋值语句采用的关键字是(“assgin”和“deassgin”),语法格式分别是:
assgin<寄存器变量>=<赋值表达式>;
deassgin<寄存器变量>;
例3.2-9 使用assign和deassign设计异步清零D触发器
module assign_dff(d,clr,clk,q);
input d,clr,clk;
output q;
reg q;
always@(clr);
begin
if(clr)
assign q=0; //时钟来临时,d的变化对q无效。
else
deassign q;
end
always@(negedge clk) q=d;
endmodule
强制语句和释放语句采用的关键字“force”和“release”,可以对连线型和寄存器变量类型进行赋值操作,其语法格式如下:
force<寄存器变量或连线型变量>=<赋值表达式>;
release<寄存器变量或连线型变量>;
例3.2-10
module force_release(a,b,out);
input a,b;
output out;
wire out;
and #1(out,a,b);
initial
begin
force out=a|b;
#5;
release out;
end
endmodule
force和release在代码多用于测试仿真中某一部分有异议的情况下,将某一部分值强行赋值拉回正确位置,再观察结果。用得少!
3.3.5 条件分支语句
verilog的条件分支语句有两种:if条件语句和case条件语句。
1.if条件语句
if条件语句是判断所给的条件是否满足,然后根据判断的结果来确定下一步的操作。
形式1:
if(条件表达式)语句块;
形式2:
if(条件表达式)
语句块1;
else
语句块2;
形式3:
if(条件表达式1)
语句块1;
else if(表达式2)
语句块2;
else if(表达式3)
语句块3;
…
else
语句块n;
注意事项
- 如果待执行的语句有多条时,可以使用begin-end来进行封装。
if(a==b)
begin
out=b;
eq=1;
end
- if语句可以嵌套使用,即if语句中又包含一条或多个if语句。使用if的嵌套结构时一定要注意if和else的对应关系,建议使用begin-end进行指定,而不是格式上对齐,verilog语法不支持格式上的对应关系,else只会和最近的if配对。
if(a>0)
if(a>b)
begin
a=a-i;
b=b-i;
end
else
//报错,以下语句省略
因为if-else的配对为就近原则,所以上述else与if(a>b)配对,而非if(a>0)。故加入begin-end后调整语法为:
if(a>0)
begin
if(a>b)
begin
a=a-i;
b=b-i;
end
end
else
//报错,以下语句省略
- 对于没出现的一个if结构,都要有一个else对应条件为假的情况,基石没有实际意义,也建议添加一个空的else。
if(a>0)
out=a;
else
; //添加一个空语句
2.case语句
case语句中的关键字为case、default、endcase,基本结构如下:
case(表达式)
分支1:语句1;
分支2:语句2;
......
default:默认值;
endcase
case语句的注意事项:
- case语句中的每个分支条件必须不同,同时变量的位宽要严格相等,否则会引发逻辑混乱。
- case语句中的每个分支可以接多条待执行语句,只需要使用begin-end即可。
...... 2'b00:begin out=a-b; sum=a+b; end ......
- case语句执行有优先顺序,但在执行过程中case语句被视为并行结构,这个优先级并没有得到体现。
- case语句有且必须有一个default语句,否则综合后会生成锁存器。
3.3.6 循环语句
verilog中循环语句有四类:while、for、repeat和forever
所有循环语句必须放在initial或always块中才符合语法要求。这些语句在可综合的模块中也不建议使用,但在测试模块中无使用限制。
1. while循环
while基本结构如下:
while(判断条件)
begin
循环体语句;
end
2. for循环
for循环基本结构如下:
for(初始化条件;判断条件;变量控制)
begin //begin-end可以去除
循环体语句;
end
for循环在测试模块中尝尝用作存储器的初始化方式,如:
reg [7:0] mem [0:3]; for(i=0;i<mem_size;i=i+1) mem[i]=0;
3. repeat循环
repeat循环的功能是把循环体语句执行某个次数,其基本格式如下:
repeat (次数)
begin
循环体语句
end
例如,一个存储器的初始化可以使用repeat循环,如下:
reg [7:0] mem [0:3];
initial
begin
i=0;
repeat(4)
begin
mem[i]=0;
i=i+1;
end
end
repeat循环还可以用来指定数出确定个数的信号边沿,如:
......
repeat(8) @(posedge clock)
reset=1;
......
4.forever循环
forever循环表示永远循环,直到仿真结束为止,相当判断条件永远为真。易知其循环体需要添加时序控制,否则就会永远循环某句语句,陷入死循环。
比较常见的forever生成时钟信号,如:
initial
begin
clock=0;
forever #10 clock=~clock;
end
作为对比,使用always生成时钟信号如下:
initial clock=0;
always #10 clock=~clock
最大的区别在于always比forever高一个层次,而forever以及前面的三个循环提必须在initial或always结构中使用。
第四章 任务、函数与编译指令
在程序设计过程中,设计者经常需要在程序的许多不同地方实现相同功能,此时需要把一些公共的部分提取出来,做成子程序供重复使用,这样就可以在需要的位置直接调用这些子程序,以避免重复编程,减少工作量。任务和函数就是如此。
4.1 任务
任务的弹性程度要比函数大,因为在任务中可以调用其他任务或函数,还可以包含延迟、时间、时间控制等多种语法。从模块结构来讲,任务应该和initial、always结构处于一个层次,严格来说它也属于行为级建模,所以只要行为级可以使用的语法,在任务中都支持的,这一点与函数加以区分。
4.1.1 任务的声明和调用
任务的声明格式如下:
task 任务名称;
input [宽度声明] 输入信号名;
output [宽度声明] 输入信号名;
inout [宽度声明] 输入信号名;
reg 任务所用的变量声明;
begin
任务包含的语句
end
endtask
针对任务个事的语法要求,依次解释如下:
(1)任务声明以task开始,以endtask结束,中间部分是任务包含的语句。
(2)任务名称就是一个标识符,满足标识符语法即可。
(3)任务可以有输入信号,输出信号、双向信号和供本任务使用的变量,变量不仅包括上面写出的reg 型,行为级中支持的类型如integer、time等都可以使用。
(4)任务整体形势上和模块十分类似,但任务虽然有输出/输出信号,却没有端口列表,这点要记住!
(5)完成信号和变量声明后,可以用begin-end或fork-join来封装,但要注意此语句块之前并没有 initial或always结构。
4位全加器的任务
task add4;
input [3:0] x,y;
output [cin;
output [3:0] s;
output cout;
begin
{cout,s}=x+y+cin;
end
endtask
任务调用格式
任务名(信号对照列表);
如4位全加器调用:
add4(a,b,c,d,e); //按照add4中信号命名的顺序进行赋值,a,b赋给x,y,把c赋给cin,把s和cout赋给d和e。
对于任务调用,需要注意如下几点:
- 任务调用时要写出任务名称来进行调用,这点与模块实例化过程相似,但是任务调用不需要使用实例化名称,像add4这个任务名直接写出即可调用对应任务。
- 任务的功能藐视虽然和always、initial处于同一个层次,但是任务的调用却必须发生在initial、always、task中,注意任务中是可以再次调用任务的。
- 任务中如果有输入/输出或双向信号,按照类似实例化语句中按名称链接的方式连接信号。任务定义时是什么顺序,调用时就按什么顺序赋值,不能进行改动!
- 任务的信号连接也要遵循基本的连接要求。
- 任务调用后需要添加一个分号,作为行为及语句的一句来处理。
- 任务不能实时输出内部值,而是只能在整个任务结束时得到一个最终结果,输出的值也是这个最终的结果值。
4.1.2 自动任务
在仿真过程中,仿真器会分配给任务一个地址空间,以为所有的反正运算都需要存储起来完成,分配给任务的也是这样的一个存储空间。由于任务地址空间的分配是一个静态过程,在一次仿真中任务所得的地址空间就是一个固定范围。这样在仿真过程中如果出现多次使用同一个任务,且每次操作的值不同时,就可能出现由于地址空间相互覆盖而导致的结果错误。
在verilog中,使用自动任务来应对这种情况,在任务的时候可使用automatic声明为自动任务,如下:
task atuomatic 任务名;// 这样每次调用任务都会给任务重新分配地址空间,就不会出现重叠的情况
对于任务中出现阻塞赋值语句,无论哪次调用任务,所使用的存储器地址空间在此时就会运算结束,并返回数值,这样即使再次调用任务并给予新的值,也不会改变之前返回的数值。从存储器的角度来考虑,两次调用一先一后,结果按照先进先出的原则,后进的数据到来时,会抛弃当前数据,这样不会出现混淆。
**如果任务中包含#、@或者wait语句就会产生错误。**因为这类语法都有一个特点:不是立刻得到结果,而要等待某个条件才会继续运行。此时不采用automatic。
4.2 函数
函数与任务不同。任务可以把组合逻辑编写成任务,也可以使用时序控制等语法来完成。但是对函数来说,仅仅可以把组合逻辑编写成函数,因为函数中并不能有任何的时序语句,而且函数不能调用任务。
4.2.1 函数的声明和调用
函数的声明格式如下:
function 返回值的类型和范围 函数名;
input [端口范围] 端口声明;
reg、integer等变量声明;
begin
阻塞赋值语句块
end
endfunction
按格式声明解释如下:
1.如不指定类型,则默认为reg类型;没指定范围,则默认为1。
2. 至少需要一个输入,但无输出。
3. 函数可定义自己所需的变量。
4. begin-end前没有initial或always,且内的语句不能有任何时间相关的语法,如@何#,而且时序电路描述的非阻塞赋值也不能使用。function automactic [31:0] factorial; input [3:0] a; integer i; begin factorial=1; for(i=2;i<=a;i=i+1) factorial=i*factorial; end endfunction
函数的调用格式:
待赋值变量=函数名称(信号对照列表);
需要注意的事项:
- 单独的函数调用格式是没有左侧的待赋值变量和等号的。但是函数的调用不像任务一样可以只出现任务名,函数调用之后必须把返回值赋给某个变量,也就是说上面给出的格式每次函数出现时必须遵循。
- 信号对照列表部分也是按照函数内部声明的顺序出现的,和任务一样。
- 函数调用后也作为行为级建模的一条语句出现在initial、always、task和function结构中,即任务可以调用函数,但是函数不能调用任务。
4.2.2 自动函数
自动函数的出现和自动任务一样,可参考自动任务,其格式如下:
function automatic 类型 范围 函数名;
......
function automactic [7:0] op;
input [7:0] op_in1,op_in2;
begin
op=op_in1^op_in2;
end
......
always @(posedge clock)
out1=op(a,b);
always @(posedge clock)
out1=op(c,d);
自动函数的主要适用场合是函数的嵌套使用,例如之前的阶乘函数可以采用函数的嵌套的形式来完成
function automactic [31:0] factorial;
input [3:0] a;
begin
if(a>=2)
factorial=factorial(a-1)*a; //嵌套调用函数
else
factorial=1;
end
endfunction
4.2.3 常量函数
常量函数只对常量进行处理,如下例子:
//计算某个参数所需的宽度
module mem;
parameter mem_size=1024;
reg [c_width(mem_size)-2:0] word_line;
function interger c_width;
input integer size;
begin
for(c_width=0;size>0;c_width=c_width+1)
size=size>>1; //判断需要右移多少次某个值变成零,即此时输入值的所有有效为都右移消失
end
endfunction
endmodule
任务与函数的比较
任务和函数对照表如下:
任务task | 函数function |
---|---|
可以有0个或任意个输入信号 | 至少有1个输入信号 |
可以有0个或任意个输出信号 | 没有由output定义的输出信号 |
通过output与外界联系 | 通过默认定义的函数名返回值与外界联系 |
内部可以声明变量,但是不包含wire类型 | 内部可以声明变量,但是不包含wire类型 |
begin-end前没有initial、always结构 | begin-end前没有initial、always结构 |
begin-end内部语句没有限制,只要满足行为级要求即可 | begin-end内部只能使用阻塞赋值语句,且不能有任何与时间相关的语句 |
内部可以调用任务和函数 | 内部只能调用函数 |
调用时直接使用即可 | 调用时需要使用“=”进行赋值 |
4.3 系统任务和系统函数
所有系统任务都以“$”开头,以“;”结束。
4.3.1 限时任务$display和$write
显示任务用于信息的显示和输出,常用的是$display和$write。$display每次显示信息后自动换行,$write不会换行,而是一行显示。显示任务还能指定显示出来的信息格式,如下:
%b(o/d/h) 或%B(O/D/H) | 二(八/十/十六)进制 |
---|---|
%e 或 %E | 实数 |
%c 或 %C | 字符 |
%s 或 %S | 字符串 |
%v 或 %V | 信号强度 |
%t 或 %T | 时间 |
%m 或 %M | 层次实例 |
还有换行、制表、反斜线、引号等 |
例如:
$dispaly("x=%b",X);
显示变量和双引号区域一定要准确对应!否则会出现问题
4.3.2 探测任务$strobe
探测任务的语法和显示任务完全相同,也是把信息显示出来。
reg[3:0] test;
initial
begin
test=1;
$display("after first assignment,test has value %d",test)
$strobe("when strobe is exectued 1,test has value %d",test)
test=2;
$display("after first assignment,test has value %d",test)
$strobe("when strobe is exectued 2,test has value %d",test)
end
运行结果为:
# after first assignment,test has value 1
# after first assignment,test has value 2
# when strobe is exectued 1,test has value 1
# when strobe is exectued 1,test has value 2
两者的区别在于:$strobe命令会在当前时间步结束时完成,即发生在向下一个时间步运行之前;$display是只要被仿真器看到,就会立刻执行。所以test=1时,$display会理科吧test显示出来,而$strobe则会等到时间步结束时,即仿真零结束时刻时再执行。
4.3.3 监视任务$monitor
监视任务$monitor可以持续监控指定的变量,只要这些变量发生变化,就会立刻显示对应的输出语句。
initial
begin
X=4'b0000;Y=4'b0000;CIN=1;
#10 X=4'b0000;Y=4'b1110;CIN=1;
#10 X=4'b0101;Y=4'b1010;CIN=1;
#10 X=4'b0000;Y=4'b0000;CIN=0;
#10 X=4'b0000;Y=4'b1110;CIN=0;
#10 X=4'b0101;Y=4'b1010;CIN=0;
#10 $stop;
end
initial
begin
$monitor("x=%b,y=%b,cin=%b,cout=%b",X,Y,CIN,COUT); //%display需要六条语句才能完成
//若设计者在仿真时,不关心中间过程的信号变化
#20 $monitoroff; //在仿真第20个时间单位关闭监视任务
#20 $monitoron; //在仿真第40个时间单位打开监视任务
//此时结果会不显示20~40个时间单位之间的信号变化
end
4.3.4 仿真任务控制$stop和$finish
任务$stop是用来停止当前的仿真,而不是退出,仿真器会把反正到该语句之前的仿真运行完,然后停止仿真,等待下一步命令,此时依然停留在仿真器的仿真界面中,一些仿真窗口依然保留,仿真的结果也会保留。
任务$finish的功能则是停止仿真并退出仿真器,再退回到操作系统界面。仿真过程中的结果可以用其他方式来记录和查看。
4.3.5 仿真时间函数$time
仿真时间函数可以用来返回仿真时间,辨别当前现实的信息是在哪个仿真时间发生的。
4.3.6 随机函数$random
随机函数可以为设计者提供一些随机数,用于测试设计模块是否正确。
随机函数的语法形式如下:
$random(seed)
seed是随机函数用来生成随机数的随机种子,不同的种子会生成不同的随机数,这些随机数是一个32位的有符号的整型数值。seed可以不使用,这样生成的随机数都是一样的,改种子的值会生成不同的随机数,如果要使用种子,必须在使用前事先声明该值,可以声明为reg、integer等类型。
integer i;
reg [7:] mem [0:1023];
initial
begin
i=0;
repeat(1024)
begin
mem[i]=$random;
i=i+1;
end
end
部分结果:
0:00000000 000100010 ...10100100 //8个
8: ......
16:......
......
注意:seed生成的为32位integer型,赋值给8位寄存器型变量,此时会去低8位作为有效数据。
如果想声称某个范围内的数值可以采用:
reg [7:0] rand;
rand=$random%64;
这样会得到一个取值范围为[-63:63]的随机数,再赋给rand就能得到一个8位的数值。这个8位数值为reg型,是没有符号的。用有符号的赋给没符号数,有时候用以造成不必要的麻烦,可以修改成:
reg [7:0] rand;
rand={$random}%64;
在这个代码中进添加了{},得到的值就是一个0~63的数。因为此时{}作为拼接操作符来使用,会把32位整型数变为32位寄存器型,即变为无符号数。
4.3.7 文件控制任务
文件打开使用方法:
文件句柄=$fopen("文件名");
如果只有文件名,则表示和仿真器工作的路径相同,否则,需要使用绝对路径。
integer a,b;
initial
begin
a=$fopen("out.dat");
b=$fopen("D:\work\out.dat");
end
打开文件后就可以向文件中写入数据了。使用$fdisplay、$fwrite、$fstrobe和$fmonitor,其语法要求和原有任务完全相同,只是不在输出到仿真器的显示窗口中,而是把结果输出到打开的文件中。
文件打开需要关闭,格式如下:
$fclose(文件句柄); //注意此时不需要加双引号
另外,当设计模块中有存储器时往往需要对存储器进行初始化,使用for循环可以完成全0赋值。如果初始值不全为零,而是一些有意义的数值就可以使用$readmemb(要求文件必须是二进制)或$readmemh(要求文件必须是十六进制)把文件记录的数值读入储存器中。其语法结构如下:
$readmemb=("文件名称",储存器名);
先有一个文件“mem.dat”,内部包含如下数据:
0000_0001 0000_0011
@4
0000_1001 0000_1011
0000_1101 0000_1111
欲读入存储器的文件内部必须是数值形式,且同一位二进制或十六进制形式,每个数值之间以空格隔开。还可以用@来指定地址,如本文件中就有一个@4,表示之后的数据是从地址4开始的,这里的地址要以十六进制给出。地址2和3,地址8和9没有数据,默认为x。
除了整个读入数据,$readmemb还支持部分读入数据:
$readmemb("文件名称",存储器,起始地址);
$readmemb("文件名称",存储器,起始地址,结束地址);
4.3.8 时间检验任务
此类任务包括$setup,$hold和$width等多个任务,用来检测某个信号的建立时间、保持时间和脉冲电平的宽度:
$setup(待检验事件,参考事件,限量值);
$hold(参考事件,待检验事件,限量值);
$width(参考事件,限量值,门限数);
例子:
$setup(D,posedge,1);//检测信号D的建立时间,相对于始终信号clock的上升沿要提前一个时间单位,否则会报警
$hold(posedge clock,D,0.5);//检测信号D和保持时间,相对于时钟信号clock的上升沿要保持0.5个时间单位,小于这个数值就会报警
$width(negedge clock,10,0.2);//检测clock信号的电平宽度,参考事件是clock下降沿,所以检测的事clock低电平宽度的时间,如果clock低电平持续时间在0.2~10个时间单位就会报警,表示电平宽度不够。
4.3.9 值变转储任务
4.4 编译指令
verilog中提供了编译指令,使程序在仿真钱能够通过这些特殊命令进行预处理,然后再开始仿真。这些命令的标志是“ ` ”符号,这些编译指令的有效范围是本文件结束(而非module结束)或者出现其他编译指令替代了之前的命令。
4.4.1 `define
宏定义采用 `define来进行指定,把某个指定的标识符用来代表一个字符串,整个标识符在整个文件中的表示所指代的字符串,其语法结构如下:
`define 标识符 字符串
例如,可以使用如下的方式来定义信号的宽度:
`define width 8 //如果再出现width就表示8
reg [0:`width-1] data;
宏定义需要注意如下几个问题:
(1)宏定义中的标识符可以大写也可以小写,但是建议大写字母,这样在程序中很容易辨识,同时也是为了和模块中定义的变量名区分。
`define test 8 //这样可以,满足语法要求
`define TEST 8 //但是建议如此定义
(2)宏定义出现的位置不局限于模块外,但习惯上写在模块外,表示作用范围为整个文件。
(3)宏定义尾部不加分号。
(4)如果不想让宏定义生效,可以使用`undef指令来取消前面定义的宏。
(5)宏定义可嵌套使用。
(6)不要尝试输出宏。
parameter、`define、localparam的区别:
- `define:可以跨模块的定义,作用于整个工程;
- parameter:本module内有效的定义,可用于参数传递(可通过手段修改参数值);
- localparam:关于localparam,这个关键字书上很少会讲到。但是大公司的代码里经常会看到;本module内有效的定义,不可用于参数传递;只能通过源代码修改参数值。
4.4.2 `include
本指令的功能是在本文件中指定包含另一个文件的全部内容,相当于把两个文件都放在了一个文件中,所执行的功能可以成为文件包含或者引用,其语法如下:
`include "文件名"
`include指令文件名部分指导意见:
(1)如果能把涉及文件和仿真文件放在相同文件夹中(或相互调用的其他设计或测试文件),就可以直接把仿真器的工作路径指向同一个文件夹,此时可以不写出文件的路径,直接引用文件即可。
(2)如果不能把引用文件放在一起,建议使用绝对路径来确保文件的正确性。
设计中如果有很多宏需要定义,就可以将这些宏定义到一个专门的文件夹中
`include "constants.v" //constants.v中包含了很多宏定义
4.4.3 `timescale
时间刻度指令用来说明模块工作的时间单位和时间精度,其基本语句形式如下:
`timescale 时间单位/时间精度
其中的时间单位和时间精度可以以秒(s)、毫秒(ms)、纳秒(ns)、皮秒(ps)或飞秒(fs)作为度量,具体的数值可以选择1、10或100,如:
`timescale 10ns/1ns
仿真时间单位是10ns,仿真时间精度是1ns,语法上要求时间精度必须小于等于时间单位,即时间单位是10ns时,时间精度最大也是10ns。
`timescale 10ns/1ns
module top;
reg A,B;
initial
begin
A=0;
B=0;
#6.1 A=1;
#3.8 B=1;
#5.44 A=0;
#4.55 $stop;
end
initial $monitor($time,"A=%d,B=%d"A,B)
endmodule
本例代码中的四个时间单位都为小数,用其乘以$timescale的时间单位后,由于精度是1ns故对小数部分四舍五入得到整数61ns,38ns,54ns,46ns。
但是$monitor的返回值:
# 0A=0;B=0
# 6A=1;B=0
# 10A=1;B=1
# 15A=0;B=1
由于$time返回值是64位的无符号整数,他的参考的是以时间单位为基准的,此代码中显示的是以10ns为单位进行四舍五入的值,所以153ns被视为15,最后结束的时间199ns被视为20.
采用$realtime改写代码可以显示实际时间:
initial $monitor($realtime,"A=%d,B=%d"A,B)
此时现实的信息是:
# 0A=0;B=0
# 6.1A=1;B=0
# 9.9A=1;B=1
# 15.3A=0;B=1
但是看不到最后199ns这个时间点,故:
#4.55 $stop(2);
这样就能得到199ns时间点的显示信息。
4.4.4 `ifdef、`else和`endif
三条指令的功能是进行条件编译,即满足一定情况的时候才进行编译:
module top;
`ifdef test1 //在标志位为test1情况下执行下列语句
`define width 12
`define heigh 30
`define vec_sync 6'b00011
`else //若非test1时执行下列语句
//重新定义一系列参数,适应新的仿真情况
`define width 11
`define heigh 35
`define vec_sync 6'b01011
`endif //终止
....
endmodule
verilog中绝大部分语法都能满足条件编译,甚至是两个module。
第五章 verilog测试模块
一个测试模块的基本功能:
5.2 时钟信号
5.3 复位信号
复位信号正常维持低电平,因为在非复位信号时的信号占整个电路工作的绝大部分时间,这样电路功耗会增加且不利于电路稳定。
对复位信号最简单的复制代码如下:
reg reset1
initial
begin
reset1=1'b0;
#20 reset1=1'b1;
....
end
endmodule
但上述复位信号只用于仿真小规模电路或者自己写的时间电路,可移植性差,可修改为:
reg reset2;
initial
begin
//生成占两个时间宽度的复位信号
reset2=1'b0;
wait(clock=1'b1); //wait语句是电平敏感的时序控制语句,等待clock为电平1时候执行下一条语句
@(negedge clock); //等到为clock下降沿把reset2变为1
reset2<=1'b1;
repeat (2) //重复等待两次clock下降沿结束复位信号
@(negedge clock);
reset2<=1'b0;
end
测试模块中可以同时出现阻塞和非阻塞赋值语句,因为不考虑综合性,后面的非阻塞赋值是为了在时钟边沿的位置不要出现时钟和复位信号的竞争关系。另外要尽量避免可能出现的竞争情况,如果时序电路是上升沿作为触发信号的,可以使用本段代码在下降沿产生复位信号,并在下降沿撤销复位信号;如果是下降沿作为触发信号,最好把代码中的下降沿修改为上升沿,目的是使复位信号和时钟信号错开半个周期,确保不会出现竞争关系。
同样的使用task也可以增强代码的可移植性。
5.4 测试向量
5.5 响应监控
5.6 仿真中对信号的控制
wait、force和release、assign和deassign、event
5.7 代码覆盖
第六章 可综合模型设计
6.1 逻辑综合过程
行为级语法和数据流级语法结合在一起被称为RTL级,该级别的模型是可综合成电路的。
6.2 延迟
实际电路工作是要有延迟时间的,之前编写的verilog代码根本没考虑时间问题,更多的是强调功能方面如何能够得到实现。事实上,前面给出的例子都是功能模块,即使是可综合模型也没有时间的概念,此时进行的仿真称为功能仿真(前仿真)。
功能仿真的目的是: 验证设计的模块功能的正确性。
不加时间概念的原因:
1.因为无法保证功能的正确性,加入时间延迟会增加更多的麻烦;
2. 没有经过后期的综合的布局布线,无法给出合理的延迟时间,凭空估测的话太不切实际。
在功能能够完成的前提下,在总合计布局布线过程中也会生成一些附带的文件,其中有一个文件成为SDF(Standard Delay Format,标准延迟文件),里面记录了所有的时间延迟情况,代码形式如下:
(CELL
(CELLTYPE "cycloneii_clkctrl")
(INSTANCE CLK\~clkctrl)
(DELAY
(ABSOLUTE)
(PROT Inclk[0] (119:119:119) (119:119:119)) //延迟时间
)
)
)
按不同级别的建模
延迟的定义的位置和之前#10一类的语句不同,门级建模:
not nqq(notQ,Q); //没有定义延迟
nand #4 n2(out,in1,in2); //定义了延迟
延迟时间分为三种:
三种延迟都是相对于输入端而言
- 上升延迟:输入端产生驱动信号到输出端出现从0,x,z变化为1的过程。
- 下降延迟:输入端产生驱动信号到输出端出现从1,x,z变化为0的过程。
- 关断延迟:输入端产生驱动信号到输出端出现从0,1,x变化为z的过程。
如果定义了一个延迟时间,就表示该逻辑门输出端的上升延迟,下降延迟和关断延迟都是4ns。还可以多个指定,党多个指定时遵循一定语法:
and # (3,5) a1(out,in1,in2);
bufif0 # (3,4,5) (out,in1,in2);
当定义了两个延迟时,verilog语法规定是定义了上升延迟和下降延迟,关断延迟取两者的最小值。上述代码第一行易知关断延迟为3;当定义三个延迟时,依次为上升延迟、下降延迟和关断延迟。
实际电路工作中往往不会严格按照这个事件进行工作,而是会在这个时间附近波动,例如定义了3ns的延迟,实际电路可能是3.2ns或2.9ns,这是因为实际电路在制造过程中会是每个电炉都不太一样,所以延迟时间也不都不太一样。
为了更精准的反映这一情况,定义了三种延迟:最小延迟、典型延迟和最大延迟。
这三种延迟时间可以认为是这样得来的:制造商生产了一批次同种元器件并进行测试,如测上升延迟,可以测得最小的延迟是多少,如2.3ns;还可以测得最大的延迟,如3.2ns;还可以统计出所有元器件都大概围绕在哪个值附近或者哪个上升值出现的次数占的最多,就是典型值,如3ns。
最大、最小和典型值的语法采用冒号隔开,上升、下降和关断延迟都可以分别定义最小、最大和典型延迟:
notif0 #(1:2:3) a1(out,in1,in2); //(最小:典型:最大)
notif1 #(1:2:3,4:5:6) a2(out,in1,in2);
notif2 #(1:2:3,4:5:6,7:8:9) a3(out,in1,in2);
第二行定义了两个时间,也就是每个都定义了最小、最大和典型延迟,所以a2最小的情况下上升时间1,下降时间4,关断延迟1,典型情况下上升时间2,下降时间5,关断时间2,最大情况下上升时间3,下降时间6,管短时间3。
数据流建模同样可以使用延迟时间,定义方式如同门级。
门级建模和数据流建模使用的延迟被称为惯性延迟,还有另一种延迟成为传输延迟。
惯性延迟主要是模拟的事元件输入端和输出端之间的变化情况;传输延迟主要是模拟连线上左侧输入和右侧输出之间的变化情况。
传输延迟采用行为级定义:
reg b;
always @(b)
a<=# 10 b;
按整体角度
延迟分为三种:分布延迟、集总延迟和路径延迟
-
分布式延迟就是对每个元件都给出详细的定义,整个电路的延迟取决于所有元器件的总和:
module M1(OUT,A,B,C,D); output OUT; input A,B,C,D; wire and1,or1; and #4 u1(and1,A,B); or #3 u2(or1,C,D); and #6 u3(OUT,and1,and2); endmodule
或把其中每一行语句都替换成数据流建模的assign语句也可以,如下:
module M1(OUT,A,B,C,D); output OUT; input A,B,C,D; wire and1,or1; assign #4 and1=A&B; assign #3 or1=C&D); assign #6 OUT=and1&and2; endmodule
-
集总延迟是整个module而言的,它把整个模块的延迟都集中到了最后的输出端,而不是分布延迟一样把延迟分散到每个使用到的元件:
module M2(OUT,A,B,C,D); output OUT; input A,B,C,D; wire and1,or1; and u1(and1,A,B); or u2(or1,C,D); and #10 u3(OUT,and1,and2); endmodule
集总延迟只需在最后模块输出位置加一个最终延迟即可,但是是一个估计值,不如分布延迟精确。
-
路径延迟模型是三种中最详细的,路劲延迟可以指定每一个输入端到输出端的延迟:
mudule M3(OUT,A,B,C,D);
output OUT;
input A,B,C,D;
wire and1,or1;
and u1(and1,A,B);
or u2(or1,C,D);
and u3(OUT,and1,and2);
specify
(A=>OUT)=10;
(B=>OUT)=10;
(C=>OUT)=9;
(D=>OUT)=9;
endspecify
endmodule
specify是独立在建模语句之外的,与门级调用、assign、initial和always结构属于同一层,语法格式:
specify
(指定输入端=>指定输出端)=延迟时间;
endspecify
如果出现多位的情况也可以这样定义,但要注意一些问题:
reg [3:] A,OUT;
specify
(A=>OUT)=10;
endspecify
等价于:
reg [3:0] A,OUT;
specify
(A[0]=>OUT[0])=10;
(A[1]=>OUT[1])=10;
(A[2]=>OUT[2])=10;
(A[3]=>OUT[3])=10;
endspecify
所以必须确保左右两边为宽相同。
specify还支持一种延迟方式全连接,如果有多位或多个信号同时使用全连接才有效果。格式如下:
(指定输入端)*>(指定输出端)=延迟时间;
如:
specify
(A,B*>OUT)=10;
endspecify
所以,前述代码可以改写:
reg [3:0] A,OUT;
specify
(A*>OUT)=10;
endspecify
当输入信号处于不同电平时,引发的输出端信号变化不同,此时可以使用if进行条件延迟赋值,但不允许使用else。
specify
if(A) (A=>OUT)=10;
if(!A) (A=>OUT)=9;
endspecify
specify也可以定义上升、下降、关断延迟或者定义最小、典型和最大延迟。specify可以指定的延迟时间的个数是1个、2个、3个、6个和12个,其他个数都是非法的。
//定义1个时间,所有延迟都是相同的
(A=>OUT)=10;
//定义2个时间,分别对应上升、下降延迟
(A=>OUT)=(10,20);
//定义3个时间,分别是上升、下降和关断延迟
(A=>OUT)=(10,20,30);
//定义6个时间,按顺序定义0>1,1>0,0>z,z>1,1>z,z>0
(A=>OUT)=(10,20,30,10,13,12);
//定义12个时间,按顺序定义了0>1,1>0,0>z,z>1,1>z,z>0,0>x,x>1,1>x,x>0,x>z,z>x
(A=>OUT)=(10,20,30,10,13,12,8,9,8,12,12,9);
在specify中还能设置参数,该参数仅能在块中使用,使用关键字specparam来定义参数。该参数仅在specify块中使用,而且一般只用来定义延迟时间。
specify
specify falltime=8,risetime=10;
(A=>out)=(risetime,falltime);
endspecify
6.3 可综合语法
第七章 用户自定义原语(UDP)
用户自定义原语(User-Defined Primitive,UDP),在UDP中不能调用(实例引用)其他模块或者其他原语,其调用方式和门级原语调用方式相同。
UDP的类型:
- 组合逻辑的UDP:输出仅取决于输入信号的逻辑,如四选一多路选择器。
- 时序逻辑的UDP:下一个输出值不但取决于当前的组合逻辑,还取决于当前的内部状态,如锁存器和触发器。
7.1 UDP定义的组成
定义关键字:primitive开始,原语名称、输出输入端口,initial语句(用于初始化时许逻辑UDP的输出端口)。UDP状态表是UDP最终要的部分,以关键字table开始,endtable结束,状态表定义了输入状态和当前状态得到的输出值,可以是一个查找表,也可类似于逻辑真值表。原语最终endprimitive结束。
//定义一个原语
primitive <udp_name>(<输出端口>,<输入端口1>,<输入端口2>,...<输入端口n>) //第一个必须为输出
oupput <输出端口名>;
input <输入端口名>;
reg <输出端口名>; //可选,只有表示时许逻辑的UDP才需要
initial <端口名> = <值>;
//UDP状态表
table
<状态表>
endtable
endprimitive
7.2 UDP的定义规则
- UDP只能有一个1位输出端口,输出列表的第一个必须是输出端口,不允许多个或多位输出。
output a,b; //不合法:输出只能有一个 output [2:0] a; //不合法:输出只能有一位
2.UDP只能采用标量(1位)输入端口 ,但是允许多个输入端口
input a,b; //合法:输入可以有多个 input [2:0] a; //不合法:输入只能有一位
- 端口声明部分:输入端口声明input;输出端口声明output;不支持输入输出端口声明inout。因为表示时许逻辑的UDP需要保持状态,所以输出端口必须声明位reg类型。
- 时序逻辑的UDP中,可以使用initial语句对reg类型变量(输出)进行赋初值。状态表项可以包含值0,1或x。UDP不能处理z值,输入UDP的z值被当作x值处理。
- UDP与模块同级,因此UDP不能在模块内部定义,但可以在模块内部调用,调用方法和调用原语相同。
7.3 组合逻辑的UDP
7.3.1 组合逻辑的UDP定义
primitive udp_and(out, a, b);
output out;
inutput a,b;
table
//a b : out;
0 0 : 0;
0 1 : 0;
1 0 : 0;
1 1 : 1;
endtable
endprimitive
7.3.2 状态表项
语法形式:
<input1> <input2>......<inputn> : <output>
注意:
状态表每一行的输入端口的顺序一定要和端口列表相同
输入输出<:>隔开,<;>结束
能够出现的输入项出现的组合必须全部列出。否则,如果在状态表各行中找不到与这组输入对应项,相对应的输出就是x。
注意输入的值也最好为确定值,如果输入都非确定值即(x),那么输出也必然是x,那么就会出现x值传播下去。
无关项可以用“?”来表示
7.3.3实例化引用(举例)
由于UDP限制过多且当输入增加时,状态表将成几何倍数增加,因此定义的UDO原语仅仅使用在基本的逻辑功能。
这里我们UDP一个:四选一多路选择器进行举例,这里加入了使能端EN
primitive mux4_to_1( output i0, i1, i2, i3, s1 s0, EN,
input i0, i1, i2, i3, s1 s0, EN );
table
// i0 i1 i2 i3 s1 s0 EN : output;
? ? ? ? ? ? 1 : 0;
1 ? ? ? 0 0 0 : 1;
0 ? ? ? 0 0 0 : 0;
? 1 ? ? 0 1 0 : 1;
? 0 ? ? 0 1 0 : 0;
? ? 1 ? 1 0 0 : 1;
? ? 0 ? 1 0 0 : 0;
? ? ? 1 1 1 0 : 1;
? ? ? 0 1 1 0 : 0;
? ? ? ? x ? 0 : x;
? ? ? ? ? x 0 : x;
endtable
endprimitive
testbench 如下:
module stimulus();
reg i0, i1, i2, i3, s1, s0, EN;
wire output;
initial begin
i0 = 1; i1 = 0; i2 = 1; i3 = 0;
#1 $display("i0 = %b, i1 = %b, i2 = %b, i3 = %b", i0, i1, i2, i3);
#5 EN = 0; $display("output = %b, output);
#5 s1 = 0 s0 = 0; $display("output = %b, output);
#5 s1 = 0 s0 = 1; $display("output = %b, output);
end
mux4_to_1 mux_inst(output, i0, i1, i2, i3, s1, s0, EN);
endmodule
7.4 时序逻辑的UDP
时序逻辑与组合逻辑UDP的区别:
- 表示时序逻辑的UDP的输出必须声明为reg类型,并且可以使用initial进行初始化。
- 表示时序逻辑的UDP,状态表:<输入1><输入2>…<输入N> : <当前状态> : <下一状态>
- 表示时序逻辑的UDP,状态表的输入项可以是电平或者跳变沿的形式。
- 当前状态是寄存器的当前值,下一状态是计算得到值会被存到寄存器中成为新值。
7.4.1 电平敏感的时序逻辑UDP
primitive latch(output reg q = 0
input d, clock, clear);
initial q = 0;
table
// d clock clear : q : q+;
? ? 1 : ? : 0;
//clock = 1时将d值锁存到q中
1 1 0 : ? :1;
0 1 0 : ? : 0;
? 0 0 : ? : -;
endtable
endprimitive
7.4.2 边沿敏感的时序逻辑UDP
//带清零的时钟下降沿触发的D触发器
primitive edge_dff(output reg q=0
input d, clock, clear);
table
// d clock clear : q : q+;
? ? 1 : ? : 0 ;
? ? (10) : ? : - ; //(10) 由1向0跳变
1 (10) 0 : ? : 1 ;
0 (10) 0 : ? : 0 ;
? (1x) 0 : ? : - ; //(1x)由1向不确定状态跳变
? (0?) 0 : ? : - ;
? (x1) 0 : ? : - ;
(??) ? 0 : ? : - : //(??)信号值再0,1,x三者之间任意跳变
endtable
endprimitive
7.4.3 UDP表中的缩写符号
7.4.4 UDP设计指南
设计功能模块时,是使用module还是使用UDP来实现模块功能,下面给出选择的指导原则:
- UDP只能进行功能建模,不能对电路时序和制造工艺(CMOS/TTL/ECL)进行建模。UDP是对模块功能的模型,而module是对于完整的模块模型。
- UDP只能有一个一位输出,且UDP的输入端口数由仿真器决定。
- UDP的描述方式是使用状态表,所以不适合输入参数太多的描述。
- 尽可能完整的列出UDP中的状态表
- 尽可能使用缩写符来表示状态表输入项
- 当电平敏感的与边沿敏感的同时作用是,下一状态先由电平敏感的决定,即电平敏感的优先级高于边沿敏感。