Verilog学习(3)initial,always,task,function,常见系统任务
Verilog学习(3)initial,always,task,function,常见系统任务
结构说明语句
Verilog中任何过程模块都属于以下四种结构的说明语句:initial说明语句,always 说明语句,task说明语句,function说明语句。
一个程序模块可以有多个initial和always 过程块。每个initial,always说明语句再仿真开始同时执行。initial 执行一次,always语句不断重复活动直到仿真结束。但always是否运行看触发条件是否满足,满足运行一次,再次满足执行一次,直到仿真结束。
task 和 function 语句可以再程序模块中的一处或多处调用
initial语句
// 对存储器变量赋初始值
initial
begin
areg = 0; // 初始化寄存器
for(index = 0;index <size;index=index+1)
memory[index] = 0; // 初始化一个memory
end
仿真开始时对各变量初始化,这个过程不需要仿真时间 0ns内初始化工作就完成了。一个模块可以有多个initial语句,都是并行的,常用于测试文件和虚拟模块的编写,用来产生仿真测试信号和设置信号记录等仿真环境。
always语句
格式为: always <时许控制> <语句>。always语句由于不断活动的特性,一定要和一定的时许结合,没有时许控制这个always语句就会使仿真器产生死锁。
always areg = ~areg // 0延迟的无限循环跳变过程,发生仿真死锁
always #half_period areg = ~areg; // 生成一个周期为period(=2*half_period) 的无限延续的信号波形,
//常用于描述时钟信号,并作为激励信号测试设计的电路。
reg[7:0] conter;
reg tick;
always @(posedge areg)
begin
tick = ~tick;
counter = counter + 1;
end
// 每当areg信号的上升沿出现将tick信号反向,counter加一
always 的时间控制可以是沿触发也可以是电平触发,可以检测单个信号也可以多个信号,中间用or连接。多个电平触发的always块,一个发生变换就会执行一次。沿触发的always块常用于描述时序行为。电平触发的常用于描述组合逻辑的行为。一个模块可以有多个always块,并行执行。
//有异步复位的电平敏感锁存器
always @ ( reset or clock or d ) // or 也可以用 , 代替 alwyas @(reset, clock, d)
//等待复位信号reset 或 时钟信号clock 或 输入信号d 的改变
begin
if ( reset ) //若 reset 信号为高,把q置零
q = 1 'b0 ;
else if ( clock ) //若clock 信号为高,锁存输入信号d
q = d ;
end
//用reset异步下降沿复位,clk正跳变沿触发的D寄存器
always @ ( posedge clk , negedge reset ) //注意:使用逗号来代替关键字or
if (! reset )
q <= 0 ;
else
q <= d ;
Verilog提供了另外两个特殊符号 @ * , @ (*),表示对 后面语句块中所有输入变量的变化是敏感的。解决输入变量很多的情况。
//用or 操作符的组合逻辑块
//编写敏感列表很繁琐并且容易漏掉一个输入
always @ ( a or b or c or d or e or f or g or h or p or m )
begin
out1 = a ? b + c : d + e ;
out2 = f ? g + h : p + m ;
end
//不用上述方法,用符号 @(*) 来代替,可以把所有输入变量都自动包括进敏感列表。
always @ ( * )
begin
out1 = a ? b + c : d + e ;
out2 = f ? g + h : p + m ;
end
之前的需要等待信号值变化或事件触发,同时也可以用另一种形式表示电平敏感时许控制,即后面的语句和语句块需要等待某个条件为真才执行。使用wait表示等待电平敏感条件为真。
always
wait(count_enable) #20 count = count +1;
仿真器连续监视conunt_enable 的值,为0不执行后面的,仿真停顿,值为1,则再20个时间单位后执行,如果count_enable始终为1,count没过20个时间单位加一。
task 和function说明语句
这俩分别用来定义任务和函数,可以把一个很大的程序模块分解成许多较小的任务和函数便于理解和调试。不同点:
- 函数只能与主模块共用一个仿真时间单位,任务可以自定义自己的仿真时间单位
- 函数不能启动任务,任务可以启动其他任务和函数
- 函数至少有一个输入变量,任务可以没有或有多个任何类型的变量
- 函数返回一个值,任务不返回值
函数就是返回一个值响应输入,任务支持多目的,可以计算多个结果值,这些结果值只能通过被调用的任务的输出或者总线端口送出。
func(a,b) // 任务,通过输出端口的变量输出
b = func(a) // 函数,带返回值
task说明语句
定义了传递给任务的变量值和接受变量,可以用一条语句启动任务,任务完成控制传回启动过程。如果任务内部有定时控制,启动时间可以与控制返回时间不同,任务可以启动其他任务,数量没有限制。不管多少任务启动,只有所有启动任务完成,控制才能返回。
task 任务名;
端口及数据类型声明语句
语句
endtask
// 描述红绿黄交通灯行为的Verilog模块,其中使用了任务。该模块只是一个行为模块不能综合成电路网表。
module traffic_lights;
// 一个模块如果和外部环境没有交互,则可以不用声明端口列表。(端口是模块与外界交互的接口)
reg clock, red, amber, green;
parameter on=1, off=0, red_tics=350,
amber_tics=30,green_tics=200;
//交通灯初始化
initial red=off;
initial amber=off;
initial green=off;
//交通灯控制时序,always 语句是重复执行的
always
begin
red=on; //开红灯
light(red,red_tics); //调用等待任务
green=on; //开绿灯
light(green,green_tics); //等待
amber=on; //开黄灯
light(amber,amber_tics); //等待
end
//定义交通灯开启时间的任务
task light;
output color;
input[31:0] tics;
begin
repeat(tics)
@(posedge clock);//等待tics个时钟的上升沿
color=off;//关灯
end
endtask
//产生时钟脉冲的always块
always
begin
#100 clock=0;
#100 clock=1;
end
endmodule
function说明语句
格式:
function 返回值类型或范围(可选,默认返回一位寄存器类型数据) 函数名
端口说明语句
变量类型说明语句
begin
语句
end
endfunction
function [7:0] getbyte;
input [15:0] address;
begin
<说明语句> //从地址字中提取低字节的程序
getbyte = result_expression; //把结果赋予函数的返回字节
end
endfunction
函数的定义蕴含声明与函数同名,函数内部的寄存器,也就是会生成定义中<返回值类型或范围> 一致的寄存器。函数的定义把函数返回值所赋值寄存器的名称初始化为与函数同名的内部变量
word = control ? {getbyte(a),getbyte(b)} : 0
// 函数调用,这里调用两次函数getbyte,将函数产生的值进行位拼接生成一个字。这里getbyte被赋予的值就是函数的返回值
函数定义不能包含任何时间控制语句,不能启动任务,定义时至少一个输入变量,定义中必须有一条赋值语句给函数中的一个内部变量赋以函数的结果值,这个内部变量名字与函数名相同
module tryfact; // 阶乘
//函数的定义-------------------------------
function[31:0] factorial;
input[3:0]operand;
reg[3:0]index;
begin
factorial = 1; // 0的阶乘为1, 1的阶乘也为1
for(index=2; index<=operand; index=index+1)
factorial = index * factorial; // 返回值
end
endfunction
//函数的测试-------------------------------------
reg[31:0]result;
reg[3:0]n;
initial
begin
result=1;
for(n=2;n<=9;n=n+1)
begin
$display("Partial result n= %d result= %d", n, result);
result = n * factorial(n)/((n*2)+1);
end
$display("Finalresult=%d",result);
end
endmodule // 模块结束
例子:奇偶校验,位移寄存器,数码管
// 偶校验位计算,返回校验值,注意HDL是并行的
/*
奇偶校验(Parity Check)是一种校验代码传输正确性的方法。
根据被传输的一组二进制代码的数位中“1”的个数是奇数或偶数来进行校验。
采用奇数的称为奇校验,反之,称为偶校验。采用何种校验是事先规定好的。
通常专门设置一个奇偶校验位,用它使这组代码中“1”的个数为奇数或偶数。
若用奇校验,则当接收端收到这组代码时,校验“1”的个数是否为奇数,从而确定传输代码的正确性。
*/
// 定义一个模块,其中包含能计算偶校验位的函数(calc_parity)
module parity;
reg [31:0] addr;
reg parity;
initial
begin
addr = 32'h3456_789a;
#10 addr = 32'hc4c6_78ff;
#10 addr = 32'hff56_ff9a;
#10 addr = 32'h3faa_aaaa;
end
//每当地址值发生变化,计算新的偶校验位
always @(addr)
begin
parity = calc_parity(addr); //第一次启动校验位计算函数 calc_parity,赋值给寄存器变量
$display("Parity calculated = %b", calc_parity(addr) );
// 第二次启动校验位计算函数 calc_parity,返回值直接使用系统任务display显示
end
//定义偶校验计算函数
function calc_parity;
input [31:0] address;
begin
//适当地设置输出值,使用隐含的内部寄存器calc_parity
// ^a操作就是将a中的每一位按位逐一进行异或,例如a=4'b1010,则b=1^0^1^0=1,
// 由此可以判断a中为1的位数是奇数还是偶数,是一个便捷的操作。
calc_parity = ^address; //返回所有地址位的异或值
end
endfunction
endmodule
function calc_parity (input [31:0] address); // c风格的声明函数变量
begin
//适当地设置输出值,使用隐含的内部寄存器calc_parity
calc_parity = ^address; //返回所有地址位的异或值
end
endfunction
// 定义一个包含移位函数的模块 ,移位寄存器
module shifter;
// 左/右 移位寄存器
`define LEFT_SHIFT 1'b0
`define RIGHT_SHIFT 1'b1
reg [31:0] addr, left_addr, right_addr;
reg control;
//每当新地址出现时就计算右移位和左移位的值
always @(addr)
begin
//调用下面定义的具有左右移位功能的函数
left_addr = shift(addr, `LEFT_SHIFT);
right_addr = shift(addr, `RIGHT_SHIFT);
end
//定义移位函数,其输出是一个32位的值
function [31:0] shift;
input [31:0] address;
input control;
begin
//根据控制信号适当地设置输出值
shift = (control == `LEFT_SHIFT) ? (address << 1) : (address >> 1);
end
endfunction
endmodule
/*用信号 abcdefg 来控制光亮控制端,用信号 csn 来控制片选,
4 位 10 进制的数字个十百千位分别用 4 个 4bit 信号
single_digit, ten_digit, hundred_digit, kilo_digit 来表示*/
module digital_tube // 4 位 10 进制的数码管译码器
(
input clk ,
input rstn ,
input en ,
input [3:0] single_digit ,
input [3:0] ten_digit ,
input [3:0] hundred_digit ,
input [3:0] kilo_digit ,
output reg [3:0] csn , //chip select, low-available 低电平有效
output reg [6:0] abcdefg //light control
);
reg [1:0] scan_r ; //scan_ctrl
always @ (posedge clk or negedge rstn) begin
if(!rstn)begin // 复位
csn <= 4'b1111;
abcdefg <= 'd0;
scan_r <= 3'd0;
end
else if (en) begin // 使能
case(scan_r)
2'd0:begin
scan_r <= 3'd1; // 逐位赋值
csn <= 4'b0111; //select single digit
abcdefg <= dt_translate(single_digit);
end
2'd1:begin
scan_r <= 3'd2;
csn <= 4'b1011; //select ten digit
abcdefg <= dt_translate(ten_digit);
end
2'd2:begin
scan_r <= 3'd3;
csn <= 4'b1101; //select hundred digit
abcdefg <= dt_translate(hundred_digit);
end
2'd3:begin
scan_r <= 3'd0;
csn <= 4'b1110; //select kilo digit
abcdefg <= dt_translate(kilo_digit);
end
endcase
end
end
/*------------ translate function -------*/
function [6:0] dt_translate;
input [3:0] data;
begin
case(data)
4'd0: dt_translate = 7'b1111110; //number 0 -> 0x7e
4'd1: dt_translate = 7'b0110000; //number 1 -> 0x30
4'd2: dt_translate = 7'b1101101; //number 2 -> 0x6d
4'd3: dt_translate = 7'b1111001; //number 3 -> 0x79
4'd4: dt_translate = 7'b0110011; //number 4 -> 0x33
4'd5: dt_translate = 7'b1011011; //number 5 -> 0x5b
4'd6: dt_translate = 7'b1011111; //number 6 -> 0x5f
4'd7: dt_translate = 7'b1110000; //number 7 -> 0x70
4'd8: dt_translate = 7'b1111111; //number 8 -> 0x7f
4'd9: dt_translate = 7'b1111011; //number 9 -> 0x7b
endcase
end
endfunction
endmodule
自动递归函数
Verilog的函数不能递归调用。若在函数声明时使用了关键字 automatic,那么该函数将称为自动的或可递归的,即仿真器为每一次函数调用动态的分配新的地址空间,每个函数调用对各自的地址空间进行操作,避免了对同一个地址空间操作造成结果不确定。因此自动函数中声明的局部变量不能通过层次名进行访问,而自动函数本身可以通过层次名进行调用,。
//用函数的递归调用定义阶乘计算
module top ;
...
//定义自动(递归)函数
function automatic integer factorial ; // 返回类型 整形
input [ 31 : 0 ] oper ;
integer i ;
begin
if ( operand >= 2 )
factorial = factorial ( oper - 1 ) * oper ; //递归调用
else
factorial = 1 ;
end
endfunction
//调用该函数
integer result ;
initial
begin
result = factorial ( 4 ) ; // 调用4的阶乘
$display ( " Factorial of 4 is % 0d ", result ) ; // 显示24
end
......
......
endmodule
常量函数与带符号函数
一个带有某些限制的常规函数,常数函数是指在仿真开始之前,在编译期间就计算出结果为常数的函数。常数函数不允许访问全局变量或者调用系统函数,但是可以调用另一个常数函数。
这种函数能够用来引用复杂的值,因此可用来代替常量。
带符号函数返回值可以作为带符号数进行计算
parameter MEM_DEPTH = 256 ;
reg [logb2(MEM_DEPTH)-1: 0] addr ; //可得addr的宽度为8bit
function integer logb2;
input integer depth ;
//256为9bit,我们最终数据应该是8,所以需depth=2时提前停止循环
for(logb2=0; depth>1; logb2=logb2+1) begin
depth = depth >> 1 ;
end
endfunction
module top ;
... ...
//
//
function signed [ 63 : 0 ] compute _signed ( input [ 63 : 0 ] vector ) ;
... ...
... ...
endfunction
//
if ( compute_signed (vector) < - 3 )
begin
... ...
end
... ...
endmodule
系统任务
$display $write 任务
这俩个函数和系统任务的作用就是输出信息,将输出列表以固定格式输出。前者自动的在输出后进行换行,$write 不同,在一行里输出多个信息就用write。可以再%write 中加入\n 换行。
module disp;
initial
begin
$display("\\\t%%\n\"\123");
end
endmodule
输出结果为
\%
"S
//从上面的这个例子中可以看到一些特殊字符的输出形式(八进制数123就是字符S)。
module disp;
reg[31:0] rval;
pulldown(pd);
initial
begin
rval=101;
$display("rval=%h hex %d decimal", rval, rval);
$display("rval=%o otal %b binary", rval, rval);
$display("rval has %c ascii character value",rval);
$display("pd strength value is %v",pd);
$display("current scope is %m");
$display("%s is ascii value for 101",101);
$display("simulation time is %t",$time);
end
endmodule
其输出结果为:
rval=00000065 hex 101 decimal
rval=00000000145 octal 00000000000000000000000001100101 binary
rval has e ascii character value
pd strength value is StX
current scope is disp
e is ascii value for 101
simulation time is 0
$display 中,输出列表中数据显示宽度是自动按照输出格式进行调整的。显示数据时,经过格式转换后,总是用表达式的最大可能值所占的位数显示表达式的值。例如一个位宽12的表达式,按照十六进制输出输出结果占三个字符的位置,按照十进制输出,输出结果占4个字符的位置。这时应为这个表达式最大可能值为FFF,4095(十进制).
$display("d=%0h a=%0h",data,addr) // 再%和进制符号间插入一个0自动调整显示输出数据宽度的方式,这样总是用最少的位数显示。
module printval;
reg[11:0]r1;
initial
begin
r1=10;
$display("Printing with maximum size=%d=%h",r1,r1);
$display("Printing with minimum size=%0d=%0h",r1,r1);
end
enmodule
输出结果为:
Printing with maximum size=10=00a:
printing with minimum size=10=a;
输出列表中的表达式包含不确定值或高阻值,输出遵循:
- 输出格式十进制下,所有位是不定值输出x,都是高阻值输出z,部分不定值、高阻值输出X,Z。
- 十六进制八进制,4位二进制一组代替一位十六进制,三位二进制代替八进制。值均为不定值/高阻值,输出小写x,z ,部分输出大写。
$display("%d",1'bx); 输出 x
$display("$h",14'bx0_1010); 输出 xxXa,14位分4组
$display("%h%o",12'b001x_xx10_1x01,12'b001_xxx_101_x01); 输出 XXX1x5X
文件输出
Verilog的结果通常输出到标准输出和文件verilog.log文件中,也可以重定位到选择的文件
- 打开文件 句柄= $fopen(“name”); 返回一个叫多通道描述符的32位值。只有一位设置为1.标准输出最低位为1,也被称为通道0,一直开放。以后$foren 每一次调用就会打开一个新通道,返回一个设置了第1位,2位到32位描述符的第30位。31位保留。
多通道描述符的优点在于可以有选择地同时写多个文件。
//多通道描述符
integer handle1, handle2, handle3; //整型数为 32 位
//标准输出是打开的; descriptor = 32'h0000_0001 ( 第0位置1)
initial
begin
handle1 = $fopen("file1.out"); //handle1 = 32'h0000_0002 (bit 1 set 1)
handle2 = $fopen("file2.out"); //handle2 = 32'h0000_0004 (bit 2 set 1)
handle3 = $fopen("file3.out"); //handle3 = 32'h0000_0008 (bit 3 set 1)
end
- 写文件,$fdisplay, $fmonitor,$fwrite,$fastrobe 都用于写。$fdisplay( 文件描述符, 变量/信号名/字符串)
文件描述符可以是一个多通道描述符或者多个文件句柄的按位组合。会把输出写到文件描述符中值为1的位相关联的所有文件中。
//写到文件中去
integer desc1, desc2, desc3 ; // 三个文件的描述符
initial
begin
desc1 = handle1 | 1; //按位或; desc1 = 32'h0000_0003
$fdisplay(desc1, "Display 1"); //写到文件file1.out和标准输出stdout
desc2 = handle2 | handle1; //desc2 = 32 'h0000_0006
$fdisplay(desc2, "Display 2"); //写到文件file1.out和file2.out
desc3 = handle3 ; //desc3 = 32'h0000_0008
$fdisplay(desc3, "Display 3"); //只写到文件file3.out
end
- 关闭文件 f c l o s e ‘ fclose ` fclose‘fclose(handle1);` 文件关闭,多通道描述符中相应位设置为0.
显示层次
通过任何显示任务,比如%display, $write 等任务中的 %m 选项可以显示任何级别的层次,这个有用。比如一个模块中的毒功而实例执行同一段代码,%m选项会区分那个模块示例在输出。%m选项不需要参数。
//显示层次信息
module M;
initial
$display("Displaying in %m");
endmodule
//调用模块M
module top;
M m1 ( );
M m2 ( );
M m3 ( );
endmodule
仿真输出如下所示:
Displaying in top.m1
Displaying in top.m2
Displaying in top.m3
可以显示全层次路径名,包括模块示例,任务,函数,和命名块。
选通显示
$strobe ,与$display类似,$display与其他语句是在同一时间执行,这些执行顺序是不确定的,但$strobe总是在同时刻其他赋值语句完成后才执行。提供了一种同步机制,可以确保同一时钟沿赋值的其他语句执行完成才显示数据。
//选通显示
always @ (posedge clock)
begin
a = b ;
c = d ;
end
always @ (posedge clock)
$strobe (“Displaying a = %b, c = % b” , a , c ); //显示正跳变沿时刻的值
//时钟上升沿的值在语句a = b和c = d执行完之后才显示。
//如果使用$display,$display可能在语句a = b和c = d之前执行,结果显示不同的值。
值变转储文件
值变转储文件VCD 是一个ASCII文件,包含仿真时间,范围,与信号的定义以及仿真运行过程中信号值的变化等信息。设计中的所有信号或选定的信号集合在仿真过程中都可以写入VCD文件。后处理工具可以把VCD作为输入把层次信息,信号值,信号波形显示出来。也可以使用后处理工具进行调试,分析,验证仿真输出结果。
$dumpvars 选择要转储的模块示例或模块实例信号, $dumpfile选择VCD文件名称,$dumpon,$dumpoff 选择转储过程的起点,终点,$dumpall 选择生成监测点。前俩通常在仿真开始时指定,后面三个在仿真过程中控制转储过程。
//指定VCD文件名。若不指定VCD文件,则由仿真器指定一缺省文件名
initial
$dumpfile (“ myfile.dmp”) ; //仿真信息转储到myfile.dmp文件
// 转储模块中的信号
initial
$dumpvars ; //没有指定变量范围,把设计中全部信号都转储
initial
$dumpvars ( 1, top ) ; //转储模块实例 top中的信号
//数1 表示层次的等级, 只转储top下第一层信号
//即转储top模块中的变量,而不转储在top中调用
//模块中的变量
initial
$dumpvars (2, top.m1) ; //转储top.m1模块下两层的信号
initial
$dumpvars (0, top.m1) ; ///数0 表示转储top.m1模块下面各个层的所有信号
//启动和停止转储过程
initial
begin
$dumpon ; //启动转储过程
#100000 $dumpoff ; //过了100000个仿真时间单位后,停止转储过程
end
//生成一个检查点,转储所有VCD变量的现行值。
initial
$dumpall ;
具有图形显示功能的后处理工具是仿真调试过程中重要组成部分,另外选择真正需要检测的信号进行转储,减小VCD文件大小。
其他
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)