目录
- 一、数据流建模
- 二、行为级建模
- 2.1 应用场景
- 2.2 initial过程语句
- 2.3 always过程语句
- 2.3.1 电平敏感信号:
- 2.3.2 边沿敏感信号:
- 2.3.3 initial和always语句使用注意
- 2.4 例题:用always过程语句描述4选1数据选择器
- 2.5 例题: 用always过程语句完成时序电路描述
- 2.5.1 用always语句描述异步清零计数器
- 2.5.2 异步清零计数器-计数:tsetbench
- 2.5.3 用always语句描述同步置数与清零计数器
- 2.5.4 同步置数与清零计数器:tsetbench
- 2.6 语句块
- 2.7 过程赋值语句
- 2.7.1 阻塞赋值
- 2.7.2 非阻塞赋值
- 2.8 过程连续赋值语句
- 2.9 条件分支语句
- 2.9.1 会产生锁存器的case语句和不会产生锁存器的case语句
- 2.9.2 casez与casex语句
- 2.10 循环语句
- 2.10.1 用 forever语句产生时钟信号
- 2.10.2 repeat循环语句
- 2.10.3 while循环语句
- 2.10.4 for循环语句
- 三、结构化建模
- 3.1 模块级建模
- 3.1.1 采用模块结构建模方式用1 bit半加器构成1 bit全加器
- 3.1.2 模块端口对应方式
- 3.1.3 用结构建模方式实现4位移位寄存器
- 3.2 门级建模
- 3.3 开关级建模
- 4.1 Testbench
一、数据流建模
数据流建模:指根据数据在寄存器之间的流动和处理过程对电路进行描述。
在数字电路中,输入信号经过组合逻辑电路传到输出时类似于数据流动,而不会在其中存储。可以通过连续赋值语句这种特性进行建模。最基本的语句是由assign关键词引导的
用逻辑方程描述多路选择器:
module mux21a (
input wire a,
input wire b,
input wire s,
output wire y
);
assign y = ~s & a | s & b;
endmodule
assign连续赋值语句的声明方式
assign是Verilog数据流建模的基本语句
assign #<delay><name>= Assignment expression;
理解assign的使用:
module andwireout(in1,in2,out);
input in1,in2;
output out ;
wire in1,in2,out;
assign out = in1 & in2;
/* assign #5 out = in1 & in2; //in1和in2逻辑与的结果在5个时间周期后才施加在out上 */
endmodule
对线网类型变量的连续赋值是数字电路数据流建模的重要步骤,数字系统不含时序的组合逻辑部分可以使用线网的连续赋值描述。
线网不能够像寄存器那样储存当前数值,它需要驱动源提供信号,这种驱动是连续不断的,因此线网变量的赋值称为连续赋值,这与寄存器变量在过程中的单次赋值不同,而且所用的运算符也有区别。
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,4) c=a&b; // and #(3,4,5) b1 (out,in,control); //上升延迟为3,下降延迟为4,关断延迟为5
// 即c经过3个时间单位延迟 从低变高,经过2个时间单位延迟,从高变低
endmodule
timescale 1ns / 1ps
基本知识:1ns=1000ps
含义为:时延单位为1ns,时延精度为1ps。
简而言之,前面是总时间,后面是每个单元的时间间隔。
普通连续赋值
wire out;
assign out=in1&in2;
隐式连续赋值实现同样功能
wire out = in1&in2;
连续赋值语句的特点
1、assign语句的 左值 必须是一个线网而不能是寄存器
2、assign语句总是处于激活状态,即:只要任意一个操作数发生变化,表达式就会被立即从新计算,并将结果赋给等号左边的线网。
3、操作数可以是线网型、寄存器型变量,也可以是函数调用。
4、多个连续赋值语句之间是并行关系,因此与位置顺序无关。
二、行为级建模
随着电路设计复杂度的不断提升,我们需要从整体结构和算法层次上对硬件电路进行抽象和描述。这种描述是比数据流描述更高层级的描述,称为行为级或算法级建模。
2.1 应用场景
- 行为描述可用于复杂数字逻辑系统的顶层设计中。
- 把一个大的系统合理地分解为若干个较小的子系统,然后再将每个子系统用可综合风格的Verilog HDL模块(门级结构或RTL级、算法级、系统级的模块)加以描述。
- 行为建模也可以用来生成仿真测试信号,对已设计的模块进行检测。
initial和always是verilog中用于行为级建模的两种基本语句,其他所有的行为语句只能出现在这两种过程语句中
每个initial和always语句代表一个独立的(并发)执行过程。每个执行过程从仿真时间0开始,并且这两种语句不能嵌套使用
2.2 initial过程语句
initial
begin
语句1;
语句2;
......
语句n;
end
所有在initial语句内的语句构成了一个initial块,initial块从仿真0时刻开始执行,在整个仿真过程中只执行一次。
如果模块中包含多个initial块,则这些initial块各自独立并发执行。
用initial语句对变量A、B、C进行赋值——经常用于在仿真时施加激励信号
module initial_tb1;
reg A,B,C;
initial
begin
A=0;B=1;C=0;
#100 A=1;B=0;
#100 A=0;C=1;
#100 B=1;
#100 B=0;C=0;
end
endmodule
当一个module中有多个initial
module stimulus;
reg x,y,a,b,m;
initial
m =1’b0;
initial
begin
#5 a = 1’b1;
#25 b =1’b0;
end
initial
begin
#10 x= 1’b0;
#25 y= 1’b1;
end
endmodule
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.3 always过程语句
声明格式:
always @ (<敏感事件列表>) 语句块;
always语句包括的所有行为语句构成了一个always块。该块从仿真0时刻开始顺序执行其中的语句。在仿真过程中不断重复执行的。必须配合时序控制使用,否则会产生仿真死锁。
触发条件可以是:延迟时间、电平敏感信号、边沿敏感信号。
延迟时间:
always #half_period clk = ~clk;
2.3.1 电平敏感信号:
@(a) //当信号a的值发生改变时
@(a or b) //当信号a或信号b的值发生改变时
/敏感信号列表中应列出影响块内取值的所有信号。若有两个或两个以上信号,则它们之间可以用“or”连接,也可以用逗号“,”连接/
@(*) //包括所有的敏感信号
2.3.2 边沿敏感信号:
@(posedge clock)//当clock的上升沿到来时
@(negedge clock)//当clock的下降沿到来时
@(posedge clk or negedge reset)
/当clk的上升到来或reset信号的下降沿到来时/
边沿触发的always块常常描述时序逻辑,如果符合可综合风格要求,可用综合工具自动转换为表示时序逻辑的寄存器组和门级逻辑。
电平触发的always块常常用来描述组合逻辑和锁存器,如果符合可综合风格要求,可转换为表示组合逻辑的门级逻辑或锁存器。
一个模块(module)中可以有多个always块,它们都是并行运行的
2.3.3 initial和always语句使用注意
可以描述时序逻辑电路与组合逻辑电路。无论是对时序逻辑电路还是对组合逻辑电路进行描述,Verilog HDL要求在过程语句(initial和always)中,被赋值信号必须定义为“reg”类型
- 采用过程语句对组合电路进行描述时,需要把全部的输入信号列入敏感信号列表。always @(*)
- 采用过程语句对时序电路进行描述时,需要把时间信号和部分输入信号列入敏感信号列表
2.4 例题:用always过程语句描述4选1数据选择器
分析:
首先定义模块
module mux4_1(out,in0,in1,in2,in3,sel);
endmodule
接下来进行端口类型说明,数据类型说明
module mux4_1(out,in0,in1,in2,in3,sel);
output out;
input in0,in1,in2,in3;
input [1:0] sel;
reg out; // 被赋值信号定义为reg类型
endmodule
always过程语句和case分支语句
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) // 等价于always (*)
case(sel)
2'b00: out = in0;
2'b01: out = in1;
2'b10: out = in2;
2'b11: out = in3;
deafult: out = 2'bx;
endcase
endmodule
若采用数据流建模(assign),则代码如下:
module mux4_1(out,in0,in1,in2,in3,sel);
output wire out;
input wire in0,in1,in2,in3;
input wire [1:0] sel;
assign out = ~sel[1] & ~sel[0] & in0 | ~sel[1] & sel[0] & in1 | sel[1] & ~sel[0] & in2| sel[1] & sel[0] & in3
endmodule
2.5 例题: 用always过程语句完成时序电路描述
用always语句描述同步置数计数器,用always语句描述异步清零计数器
同步,异步是对于时钟而言的。
同步指的是,状态的变化需要等待时钟有效沿来触发,所有动作同时跟随这个时钟变化,而异步时,状态变化不依赖与时钟。
异步清零说的是,你需要对一个计数器在满足某种条件时想要他归零,但又不需要等下一个时钟过来,是一个条件满足就立即执行的过程;
同步置数,比如你想让计数器采用置数法实现任一模计数,你要设置一个条件,计数记到1001时实现置数,但是,当计数器记到1001的时候,不能立马置数,要等下一个时钟有效沿来,你的条件才能生效,才能完成置数!
异步清零,当输出的状态是1001时,不等下一个有效时钟到来,这一瞬间立马计数器被清零,表现在状态图里面就是,1001这个状态是一个过渡状态,他的出现只是短暂一瞬!
2.5.1 用always语句描述异步清零计数器
module counter2(clear, clk, out);
output[7:0] out;
input clk, clear;
reg[7:0] out;
always @(posedge clk or negedge clk)
begin
if (!clear) // clear为0的时候有效,输出0
out <= 0;
else
out <= out + 1;
end
endmodule
2.5.2 异步清零计数器-计数:tsetbench
module counter_tb;
reg clear, clk;
wire[7:0] out;
counter2 U1(clear, clk, out);
always #3 clk = ~ clk;
initial
begin
clear = 0; clk = 0;
#10 clear = 1;
#60 clear = 0;
#60 clear = 1;
end
endmodule
2.5.3 用always语句描述同步置数与清零计数器
module counter1(
input[7:0] data,
input load,
input clk,
input resert,
output reg[7:0] out
);
always @(posedge clk)
begin
if (!resert)
out <= 8'h00;
else if (load)
out <= data;
else
out <= out + 1;
end
endmodule
同步置数:置数行为只能在clk的上升沿完成,与clk的上升沿同步
2.5.4 同步置数与清零计数器:tsetbench
module counter2_tb;
reg [7:0] data;
reg load;
reg clk;
reg reset;
wire [7:0] out;
counter1 uut (
.data(data),
.load(load),
.clk(clk),
.reset(reset),
.out(out)
);
always #3 clk=~clk;
initial begin
data = 8'b00001111;
load = 0;
clk = 0;
reset = 0;
#10
reset=1;
load=1;
#10
load=0;
#40
reset=0;
end
2.6 语句块
begin_end语句,通常用来标识顺序执行的语句,用它来标识的块称为串行语句块。
module wave_tb1;
reg wave;
parameter T=10;
initial
begin
wave=0;
#T wave=1;
#T wave=0;
#T wave=1;
#T wave=0;
#T wave=1;
end
endmodule
当块内有局部变量时必须有块名,否则在编译时将出现语法错误
begin:块名
块内声明语句
语句1;
语句2;
...
语句n;
end
fork_join语句,通常用来标识并行执行的语句,用它来标识的块称为并行语句块。
module wave_tb1;
reg wave;
parameter T=10;
initial
fork
wave=0;
#T wave=1;
#2*T wave=0;
#3*T wave=1;
#4*T wave=0;
#5*T wave=1;
end
endmodule
串行语句块的起始执行时间就是串行语句块中第一条语句开始执行的时间;串行语句块的结束时间就是块中最后一条语句执行结束的时间。
当语句块中只包含一条语句时,可以直接写这条语句,此时块标识符可以缺省
用于测试:采用串行语句块和并行语句块都可以产生相同的测试信号,具体采用哪种语句进行设计主要取决于设计者的习惯。
用于设计:在对于电路的描述性设计中,部分综合工具不支持并行语句块,因此主要采用串行语句块进行设计。
2.7 过程赋值语句
在initial语句或always语句内,实现对寄存器数据类型的变量赋值。
在initial语句或always语句内,实现对线网数据类型的变量赋值。
在initial语句和always语句内的赋值,能对reg型变量赋值;能对存储器型选定的地址单元赋值。
阻塞赋值语句
变量 = 表达式;
非阻塞赋值语句
变量<=表达式;
操作符号不同,进行电路描述时,差别也很大。
2.7.1 阻塞赋值
always @( posedge clk )
begin
b = a;
c = b;
end //阻塞赋值
赋值时先计算等号右边表达式或变量(RHS)的值,此时不允许别的Verilog语句干扰,直到把RHS赋值给LHS(等号左边表达
式或变量),才允许别的赋值语句执行,所以RHS不能设定有延迟。
clk上升沿到来时,马上发生如下变化:b马上取a的值,c马上取b的值
在时钟沿发的always进程里,老先执行b=C,再执行a=d,那么本质上,在一个时钟沿钟发里面,a=c成立,即是说,不要b变量,直接在进程里赋值a=C,结果是一样的。这和c语言中b=c,a=b性质相同
2.7.2 非阻塞赋值
always @( posedge clk )
begin
b <= a;
c <= b;
end //非阻塞赋值
赋值操作时刻开始时计算非阻塞赋值符号的RHS表达式,赋值操作结束时刻更新给LHS。
计算RHS和更新LHS期间,允许其它Verilog语句同时操作,包括其它的非阻塞赋值语句的操作。
操作步骤:
- 赋值开始时刻,计算非阻塞赋值RHS表达式;
- 在赋值结束时刻,更新非阻塞赋值LHS表达式。
赋值是在always块结束时执行的,所以c为原来b的值。
两种不同的赋值方式结果是不同的,非阻塞赋值b<=a;c<=b;两条语句是同时执行的,而阻塞赋值b=a;c=b;两条语句先执行b=a后执行
可综合风格的Verilog模块编程的原则:
-
阻塞语句运用在组合逻辑电路设计里面,非阻塞语句运用在时席逻辑电路设计里面。
-
锁存器电路建模时,用非阻塞赋值
-
用always块建立组合逻辑模型时,用阻塞赋值
-
在同一个always块中建立时序和组合逻辑电路时,用非阻塞赋值
-
assign用于数据流建模的连续赋值,阻塞和非阻塞赋值用于行为建模的过程赋值
2.8 过程连续赋值语句
assign-deassign(赋值-重新赋值)
可用于对寄存器信号进行赋值
force-release(强制-释放)
适用于对寄存器型或者连线型的强制赋值赋值期间,别处对该变量的赋值无效
不可综合,通常用于测试模块中
2.9 条件分支语句
if语句:判断所给的条件是否满足,然后根据判断的结果来确定下一步的操作。
case语句:用于实现多路分支选择控制。
条件语句只能在initial和always语句引导的语句块(begin-end)中使用,模块的其它部分都不能使用
module mux2_1(a,b,sel,out);
input a,b,sel;
output out;
reg out;
always @(a,b,sel)
begin
if(sel) out=a;
else out=b;
end
endmodule
在if和else后面可以包含一个内嵌的操作语句,也可以有多个操作语句,此时用begin和end这两个关键词将几个语句包含起来成为一个复合块语句
if(a>b)
begin
out1<=int1;
out2<=int2;
end
else
begin
out1<=int2;
out2<=int1;
end
2.9.1 会产生锁存器的case语句和不会产生锁存器的case语句
module latch_case(a,b,sel,out);
input a,b;
input [1:0]sel;
output out;
reg out;
always@(a,b,sel)
case(sel)
2'b00:out=a;
2'b11:out=b;
endcase
endmodule
module non_latch_case(a,b,sel,out);
input a,b;
input [1:0]sel;
output out;
reg out;
always@(a,b,sel)
case(sel)
2'b00:out=a;
2'b11:out=b;
default:out=0; // 不会产生锁存
endcase
endmodule
2.9.2 casez与casex语句
在casez语句中,如果比较的双方(控制表达式与值项)有一边的某一位的值是z,那么这一位的比较就不予考虑,即认为这一位的比较结果永远是真,因此只需关注其它位的比较结果。
在casex语句中,则把这种处理方式进一步扩展到对x的处理,即如果比较的双方(控制表达式与值项)有一边的某一位的值是z或x,那么这一位的比较就不予考虑
2.10 循环语句
forever、repeat、while和for语句
高级程序语句,多用于测试仿真程序设计
2.10.1 用 forever语句产生时钟信号
module forever_tb;
reg clock;
initial
begin
clock=0;
forever #50 clock=~clock;
end
endmodule
forever循环语句连续不断地执行后面的语句或语句块,常用来产生周期性的波形,作为仿真激励信号
语法格式: forever 语句(块);
表示永久循环。在永久循环中不包含任何条件表达式,只执行无限循环,直至遇到系统任务$finish。如果需要从forever循环中退出,则可以使用disable语句。
forever不能独立写在程序中。一般用在initial过程语句中,如果在forever语句中没有加入时延控制,forever语句将在0时延后无限循环下去
forever循环语句常用于产生周期性的波形,用来作为仿真测试信号。它与always语句不同之处在于不能独立写在程序中,而必须写在initial块中
2.10.2 repeat循环语句
使用repeat循环语句产生固定周期数的时钟信号
module repeat_tb;
reg clock;
initial
begin
clock=0;
repeat(8) clock=~clock;
end
endmodule
语法格式:
repeat(循环次数表达式)
语句或语句块(循环体);
执行过程为:
- 计算出循环次数表达式的值,并将它作为循环次数保存起来;
- 接着执行后面的语句块(循环体),语句块执行结束后,将重复执行次数减去一次,
- 再接着重新执行下一次的语句块操作,如此重复,直至循环执行次数被减为0
2.10.3 while循环语句
使用while语句产生时钟信号
module while_tb;
reg clock;
initial
begin
clock=0;
while(1)
#50 clock=~clock;
end
endmodule
2.10.4 for循环语句
语法格式:
for(循环变量赋初值;循环结束条件;循环变量增值)
语句块;
执行过程:
- 循环变量赋初值
- 判断“循环结束条件”
- 若其值为真,则执行for循环语句中指定的语句块,然后进行“循环变量增值”操作,直至循环结束条件满足时,结束循环
[例]用Verilog HDL语言设计一个8位移位寄存器
方法1:采用赋值语句实现
module shift_regist1(Q,D,rst,clk);
output [7:0] Q;
input D,rst,clk;
reg [7:0] Q;
always @(posedge clk)
if (!rst)
Q<=8'b000000;
else
Q<={Q[6:0],D};
endmodule
采用for循环语句实现
module shift_regist2(Q,D,rst,clk);
output [7:0] Q;
input D,rst,clk;
reg [7:0] Q;
integer i;
always @(posedge clk)
if (!rst)
Q<=8'b000000;
else
begin
for (i=7;i>0;i=i-1)
Q[i]<=Q[i-1];
Q[0]<=D;
end
endmodule
三、结构化建模
将硬件电路描述成一个分级子模块系统,通过逐层调用这些子模块构成功能复杂的数字逻辑电路和系统。
此时组成硬件电路的各个子模块之间的相互层次关系以及相互连接关系都需要得到说明。需要理解掌握模块端口连接的方式
3.1 模块级建模
调用由用户自己描述产生的module模块来对硬件电路结构进行说明。
模块级建模方式可以把一个模块看做由其它模块像积木一样搭建而成的。模块中被调用模块属于低一层次的模块,如果当前模块不再被其它模块所调用,那么这个模块一定是所谓的顶层模块。在对一个硬件系统的描述中,必定有而且只能有一个顶层模块。
3.1.1 采用模块结构建模方式用1 bit半加器构成1 bit全加器
module halfadder(a,b,s,c); //半加器模块
input a,b;
output c,s;
assign s=a^b;
assign c=a&b;
endmodule
module fulladder(p,q,ci,co,sum);
input p,q,ci;
output co,sum;
endmodule
module fulladder(p,q,ci,co,sum);
input p,q,ci;
output co,sum;
wire w1,w2,w3; // 新加
endmodule
实例化:
module fulladder(p,q,ci,co,sum);
input p,q,ci;
output co,sum;
wire w1,w2,w3;
halfadder U1 (p,q,w1,w2);
halfadder U2 (ci,w1,sum,w3);
or U3 (co,w2,w3);
endmodule
3.1.2 模块端口对应方式
端口位置对应方式
语法格式:
模块名<参数值列表> 实例名(<信号名1>,<信号名2>,…,<信号名n>);
端口名列表中的这些信号将与所调用模块定义的端口依次连接,缺省信号表示悬空
halfadder U1(p,q, ,w2);
端口列表中第三项的信号名是缺省的,这种情况表明该项对应的模块端口(端口列端口位置对应方式表中的第三个端口s)是悬空的。
端口名称对应方式
语法格式:
模块名 <参数值列表> 实例名(.端口名1<信号名1>,.端口名2<信号名2>,…,.端口名n<信号名n>);
在模块引用时用“.”标明所调用模块定义的端口名,调用时端口名的排列顺序可以改变。
module fulladder(p,q,ci,co,sum); //全加器模块
input p,q,ci;
output co,sum;
wire w1,w2,w3;
halfadder U1 (.a(p),.b(q),.s(w1),.c(w2));
halfadder U2 (.a(ci),.b(w1),.s(sum),.c(w3));
or U3 (co,w2,w3);
endmodule
3.1.3 用结构建模方式实现4位移位寄存器
描述D触发器,定义module
module dff(c,clk,clr,q);
input d, clk, clr;
output q;
reg q;
always @(posedge clk or negdege clr)
begin
if (!clr)
q <= 0;
else
q <= d;
end
endmodule
调用D触发器,描述4位移位寄存器——端口名对应方式
module shifter_D(din, clock, clear, out);
input din, clock, clear;
output [3:0] out;
dff U1(.q(out[0]), .d(din), .clk(clock), .clr(clear));
dff U2(.q(out[1]),.d(out[0]),.clk(clock),.clr(clear));
dff U3(.q(out[2]),.d(out[1]),.clk(clock),.clr(clear));
dff U4(.q(out[3]),.d(out[2]),.clk(clock),.clr(clear));
endmodule
注意:对于同一个模块来说,参数一旦被定义就不能够通过其它语句对它重新赋值
若要改变参量的值,可通过模块之间的参数传递来实现
在模块实例引用时用“#” 号后跟参数的语法来重新定义参数。
module para1(C,D);
parameter a=1;
parameter b=1;
...
endmodule
3.2 门级建模
通过调用Verilog HDL内部的基本门级元件来对硬件电路的结构进行说明,这种情况下的模块由基本门级元件的实例组成
电路是用表示门的原语来描述的。
一个逻辑网络是由许多逻辑门和开关所组成,因此用逻辑门的模型来描述逻辑网络是比较直观方法。Verilog HDL提供预定义的一些门类型的关键字(原语),可以用于门级结构建模
多输入门元件调用的语法格式:
元件名<实例名>(<输出端口>,<输入端口1>,<输入端口2>,…,<输入端口n>);
3.3 开关级建模
4.1 Testbench
仿真时,Testbench用来产生测试激励给待验证设计,检查输出是否与预期的一致,从而达到验证设计功能的目的
如何理解测试模块TB(TestBench)与被测试模块DUT(Design Under Tset )之间的关系?
- 测试的目的是确认DUT的输入输出的行为是否与设计期望一致
- TB中定义需要给DUT的输入施加的激励信号(reg型)和连接DUT输出的响应输出(wire型)
- 在TB中实例化DUT,并将TB中定义的reg型激励信号与DUT实例的输入端口连接,wire型响应输出与DUT实例的输出端口连接
Testbench是一个测试平台,信号集成在模块内部,因此没有输入、输出。
module testmux41;
reg[3:0] c;
reg[1:0] s;
wire z; // 输入输出变量定义
mux41a U(.z(z),.s(s),.c(c)); // 模块实例化
initial // 施加激励信号
begin
c = 4'b0110;
s = 2'b00;
#10 s = 2'b01;
#10 s = 2'b10;
#10 s = 2'b11;
#10 c = 4'b1001; s = 2'b00;
#10 s = 2'b01;
#10 s = 2'b10;
#10 s = 2'b11;
end
endmodule