一周掌握FPGA Verilog HDL语法 day 4

一周掌握FPGA Verilog HDL语法 day 4

今天给大侠带来的是一周掌握FPGA Verilog HDL 语法,今天开启第四天。

上一篇提到了阻塞与非阻塞、条件语句、块语句等,此篇我们继续来看case语句以及后续其他内容,结合实例理解理论语法,会让你理解运用的更加透彻。下面咱们废话就不多说了,一起来看看吧。

 

case语句

case语句是一种多分支选择语句,if语句只有两个分支可供选择,而实际问题中常常需要用到多分支选择,Verilog语言提供的case语句直接处理多分支选择。

case语句通常用于微处理器的指令译码,它的一般形式如下:

  1. case(表达式) <case分支项> endcase

  2. casez(表达式) <case分支项> endcase

  3. casex(表达式) <case分支项> endcase case

分支项的一般格式如下:

分支表达式: 语句

缺省项(default项): 语句

说明:

a) case括弧内的表达式称为控制表达式,case分支项中的表达式称为分支表达式。控制表达式通常表示为控制信号的某些位,分支表达式则用这些控制信号的具体状态值来表示,因此分支表达式又可以称为常量表达式。

b) 当控制表达式的值与分支表达式的值相等时,就执行分支表达式后面的语句。如果所有的分支表达式的值都没有与控制表达式的值相匹配的,就执行default后面的语句。

c) default项可有可无,一个case语句里只准有一个default项。下面是一个简单的使用case语句的例子。该例子中对寄存器rega译码以确定result的值。

 
  reg [15:0] rega; 
  reg [9:0] result;
  
  case(rega) 
      16 'd0: result = 10 'b0111111111; 
      16 'd1: result = 10 'b1011111111; 
      16 'd2: result = 10 'b1101111111; 
      16 'd3: result = 10 'b1110111111; 
      16 'd4: result = 10 'b1111011111; 
      16 'd5: result = 10 'b1111101111; 
      16 'd6: result = 10 'b1111110111; 
      16 'd7: result = 10 'b1111111011; 
      16 'd8: result = 10 'b1111111101; 
      16 'd9: result = 10 'b1111111110; 
      default:result = 'bx; 
  endcase

 

d) 每一个case分项的分支表达式的值必须互不相同,否则就会出现矛盾现象(对表达式的同一个值,有多种执行方案)。

e) 执行完case分项后的语句,则跳出该case语句结构,终止case语句的执行。

f) 在用case语句表达式进行比较的过程中,只有当信号的对应位的值能明确进行比较时,比较才能成功。因此要注意详细说明case分项的分支表达式的值。

g) case语句的所有表达式的值的位宽必须相等,只有这样控制表达式和分支表达式才能进行对应位的比较。一个经常犯的错误是用'bx, 'bz 来替代 n'bx, n'bz,这样写是不对的,因为信号x, z的缺省宽度是机器的字节宽度,通常是32位(此处 n 是case控制表达式的位宽)。

下面将给出 case, casez, casex 的真值表:

case语句与if_else_if语句的区别主要有两点:

1)与case语句中的控制表达式和多分支表达式这种比较结构相比,if_else_if结构中的条件表达式更为直观一些。

2)对于那些分支表达式中存在不定值x和高阻值z位时,case语句提供了处理这种情况的手段。下面的两个例子介绍了处理x,z值位的case语句。

例1:

 
  case ( select[1:2] ) 
      2 'b00: result = 0; 
      2 'b01: result = flaga; 
      2 'b0x, 
      2 'b0z: result = flaga? 'bx : 0; 
      2 'b10: result = flagb; 
      2 'bx0, 
      2 'bz0: result = flagb? 'bx : 0; 
      default: result = 'bx; 
  endcase

 

例2:

 

case(sig) 
    1 'bz: $display("signal is floating"); 
    1 'bx: $display("signal is unknown"); 
    default: $display("signal is %b", sig); 
endcase

 

Verilog HDL针对电路的特性提供了case语句的其它两种形式用来处理case语句比较过程中的不必考虑的情况( don't care condition )。其中casez语句用来处理不考虑高阻值z的比较过程,casex语句则将高阻值z和不定值都视为不必关心的情况。所谓不必关心的情况,即在表达式进行比较时,不将该位的状态考虑在内。这样在case语句表达式进行比较时,就可以灵活地设置以对信号的某些位进行比较。见下面的两个例子:

例3:

 
  reg[7:0] ir; 
  
  casez(ir) 
       8 'b1???????: instruction1(ir); 
       8 'b01??????: instruction2(ir); 
       8 'b00010???: instruction3(ir); 
       8 'b000001??: instruction4(ir); 
  endcase

 

例4:

 

  reg[7:0] r, mask; 
  
  mask = 8'bx0x0x0x0; 
  
  casex(r^mask) 
       8 'b001100xx: stat1; 
       8 'b1100xx00: stat2; 
       8 'b00xx0011: stat3; 
       8 'bxx001100: stat4; 
  endcase

 

由于使用条件语句不当在设计中生成了原本没想到有的锁存器

Verilog HDL设计中容易犯的一个通病是由于不正确使用语言,生成了并不想要的锁存器。下面我们给出了一个在“always"块中不正确使用if语句,造成这种错误的例子。

检查一下左边的"always"块,if语句保证了只有当al=1时,q才取d的值。这段程序没有写出 al = 0 时的结果, 那么当al=0时会怎么样呢?

在"always"块内,如果在给定的条件下变量没有赋值,这个变量将保持原值,也就是说会生成一个锁存器。

如果设计人员希望当 al = 0 时q的值为0,else项就必不可少了,请注意看右边的"always"块,整个Verilog程序模块综合出来后,"always"块对应的部分不会生成锁存器。

Verilog HDL程序另一种偶然生成锁存器是在使用case语句时缺少default项的情况下发生的。

case语句的功能是:在某个信号(本例中的sel)取不同的值时,给另一个信号(本例中的q)赋不同的值。注意看下图左边的例子,如果sel=0,q取a值,而sel=11,q取b的值。这个例子中不清楚的是:如果sel取00和11以外的值时q将被赋予什么值?在下面左边的这个例子中,程序是用Verilog HDL写的,即默认为q保持原值,这就会自动生成锁存器。

右边的例子很明确,程序中的case语句有default项,指明了如果sel不取00或11时,编译器或仿真器应赋给q的值。程序所示情况下,q赋为0,因此不需要锁存器。

以上就是怎样来避免偶然生成锁存器的错误。如果用到if语句,最好写上else项。如果用case语句,最好写上default项。遵循上面两条原则,就可以避免发生这种错误,使设计者更加明确设计目标,同时也增强了Verilog程序的可读性。

 

循环语句

在Verilog HDL中存在着四种类型的循环语句,用来控制执行语句的执行次数。

  1. forever 连续的执行语句。

  2. repeat 连续执行一条语句 n 次。

  3. while 执行一条语句直到某个条件不满足。如果一开始条件即不满足(为假), 则语句一次也不能被执行。

  4. for通过以下三个步骤来决定语句的循环执行。 a) 先给控制循环次数的变量赋初值。 b) 判定控制循环的表达式的值,如为假则跳出循环语句,如为真则执行指定的语句后,转到第三步。 c) 执行一条赋值语句来修正控制循环变量次数的变量的值,然后返回第二步。下面对各种循环语句详细的进行介绍。

 

forever语句

forever语句的格式如下:

forever 语句; 或 forever begin 多条语句 end

forever循环语句常用于产生周期性的波形,用来作为仿真测试信号。它与always语句不同处在于不能独立写在程序中,而必须写在initial块中。

 

repeat语句

repeat语句的格式如下:

repeat(表达式) 语句;或 repeat(表达式) begin 多条语句 end

在repeat语句中,其表达式通常为常量表达式。下面的例子中使用repeat循环语句及加法和移位操作来实现一个乘法器。

 

  parameter size=8,longsize=16; 
  
  reg [size:1] opa, opb; 
  reg [longsize:1] result; 
  
      begin: mult 
          reg [longsize:1] shift_opa, shift_opb;
          shift_opa = opa; 
          shift_opb = opb; 
          result = 0; 
          repeat(size) 
              begin 
                  if(shift_opb[1]) 
                  result = result + shift_opa; 
                  shift_opa = shift_opa <<1; 
                  shift_opb = shift_opb >>1; 
              end 
      end

 

while语句

while语句的格式如下:

while(表达式) 语句 或 while(表达式) begin 多条语句 end

下面举一个while语句的例子,该例子用while循环语句对rega这个八位二进制数中值为1的位进行计数。

 

    begin: count1s 
        reg[7:0] tempreg; 
        count=0; 
        tempreg = rega; 
        while(tempreg) 
            begin 
                if(tempreg[0]) count = count + 1;
                tempreg = tempreg>>1; 
            end 
    end

 

for语句

for语句的一般形式为:

for(表达式1;表达式2;表达式3) 语句

它的执行过程如下:

  1. 先求解表达式1;

  2. 求解表达式2,若其值为真(非0),则执行for语句中指定的内嵌语句,然后执行下面的第3步。若为假(0),则结束循环,转到第5步。

  3. 若表达式为真,在执行指定的语句后,求解表达式3。

  4. 转回上面的第2步骤继续执行。

  5. 执行for语句下面的语句。

for语句最简单的应用形式是很易理解的,其形式如下:

 

for(循环变量赋初值;循环结束条件;循环变量增值) 
    执行语句 

 

for循环语句实际上相当于采用while循环语句建立以下的循环结构:

 

begin 
    循环变量赋初值;
    while(循环结束条件) 
        begin 
            执行语句 
            循环变量增值; 
        end 
end

 

这样对于需要8条语句才能完成的一个循环控制,for循环语句只需两条即可。下面分别举两个使用for循环语句的例子。例1用for语句来初始化memory。例2则用for循环语句来实现前面用repeat语句实现的乘法器。

例1:

 

    begin: init_mem 
        reg[7:0] tempi; 
            for(tempi=0;tempi<memsize;tempi=tempi+1) 
                memory[tempi]=0;
    end

 

例2:

 
    parameter size = 8, longsize = 16; 
    
        reg[size:1] opa, opb; 
        reg[longsize:1] result; 
        
        begin:mult 
            integer bindex; 
            result=0; 
            for( bindex=1; bindex<=size; bindex=bindex+1 ) 
                if(opb[bindex]) 
                   result = result + (opa<<(bindex-1)); 
        end

 

在for语句中,循环变量增值表达式可以不必是一般的常规加法或减法表达式。下面是对rega这个八位二进制数中值为1的位进行计数的另一种方法。见下例:

 

    begin: count1s 
        reg[7:0] tempreg; 
        count=0; 
        
        for( tempreg=rega; tempreg; tempreg=tempreg>>1 ) 
            if(tempreg[0]) 
                count=count+1; 
    end

 

结构说明语句

Verilog语言中的任何过程模块都从属于以下四种结构的说明语句。

  1. initial说明语句

  2. always说明语句

  3. task说明语句

  4. function说明语句

initial和always说明语句在仿真的一开始即开始执行。initial语句只执行一次。相反,always语句则是不断地重复执行,直到仿真过程结束。在一个模块中,使用initial和always语句的次数是不受限制的。

task和function语句可以在程序模块中的一处或多处调用。其具体使用方法以后再详细地加以介绍。这里只对initial和always语句加以介绍。

 

initial语句

initial语句的格式如下:

 

initial 
    begin 
        语句1; 
        语句2; 
        ...... 
        语句n; 
    end

 

[例1]:

 

initial 
    begin 
        areg=0; //初始化寄存器areg 
            for(index=0;index<size;index=index+1) 
                memory[index]=0; //初始化一个memory 
    end

 

在这个例子中用initial语句在仿真开始时对各变量进行初始化。

[例2]:

 

  initial 
      begin 
          inputs = 'b000000; //初始时刻为0 
          #10 inputs = 'b011001;
          #10 inputs = 'b011011; 
          #10 inputs = 'b011000; 
          #10 inputs = 'b001000; 
      end

 

从这个例子中,我们可以看到initial语句的另一用途,即用initial语句来生成激励波形作为电路的测试仿真信号。一个模块中可以有多个initial块,它们都是并行运行的。initial块常用于测试文件和虚拟模块的编写,用来产生仿真测试信号和设置信号记录等仿真环境。

 

always语句

always语句在仿真过程中是不断重复执行的。其声明格式如下:

always <时序控制> <语句>

always语句由于其不断重复执行的特性,只有和一定的时序控制结合在一起才有用。如果一个always语句没有时序控制,则这个always语句将会发成一个仿真死锁。见下例:

[例1]:always areg = ~areg;

这个always语句将会生成一个0延迟的无限循环跳变过程,这时会发生仿真死锁。

如果加上时序控制,则这个always语句将变为一条非常有用的描述语句。见下例:

[例2]:always #half_period areg = ~areg;

这个例子生成了一个周期为:period(=2*half_period) 的无限延续的信号波形,常用这种方法来描述时钟信号,作为激励信号来测试所设计的电路。

[例3]:

 

    reg[7:0] counter; 
    reg tick; 
    
    always @(posedge areg) 
        begin 
            tick = ~tick; 
            counter = counter + 1; 
        end

 

这个例子中,每当areg信号的上升沿出现时把tick信号反相,并且把counter增加1。这种时间控制是always语句最常用的。always 的时间控制可以是沿触发也可以是电平触发的,可以单个信号也可以多个信号,中间需要用关键字 or 连接,如:

 
always @(posedge clock or posedge reset) //由两个沿触发的always块 
 begin 
 ……
 end

 

 

always @( a or b or c ) //由多个电平触发的always块 
 begin 
 ……
 end

 

沿触发的always块常常描述时序逻辑,如果符合可综合风格要求可用综合工具自动转换为表示时序逻辑的寄存器组和门级逻辑,而电平触发的always块常常用来描述组合逻辑和带锁存器的组合逻辑,如果符合可综合风格要求可转换为表示组合逻辑的门级逻辑或带锁存器的组合逻辑。一个模块中可以有多个always块,它们都是并行运行的。

 

task和function说明语句

task和function说明语句分别用来定义任务和函数。利用任务和函数可以把一个很大的程序模块分解成许多较小的任务和函数便于理解和调试。输入、输出和总线信号的值可以传入、传出任务和函数。任务和函数往往还是大的程序模块中在不同地点多次用到的相同的程序段。学会使用task和function语句可以简化程序的结构,使程序明白易懂,是编写较大型模块的基本功。

 

一. task和function说明语句的不同点

任务和函数有些不同,主要的不同有以下四点:

  1. 函数只能与主模块共用同一个仿真时间单位,而任务可以定义自己的仿真时间单位。

  2. 函数不能启动任务,而任务能启动其它任务和函数。

  3. 函数至少要有一个输入变量,而任务可以没有或有多个任何类型的变量。

  4. 函数返回一个值,而任务则不返回值。

函数的目的是通过返回一个值来响应输入信号的值。

任务却能支持多种目的,能计算多个结果值,这些结果值只能通过被调用的任务的输出或总线端口送出。Verilog HDL模块使用函数时是把它当作表达式中的操作符,这个操作的结果值就是这个函数的返回值。下面让我们用例子来说明:

例如,定义一任务或函数对一个16位的字进行操作让高字节与低字节互换,把它变为另一个字(假定这个任务或函数名为: switch_bytes)。 任务返回的新字是通过输出端口的变量,因此16位字字节互换任务的调用源码是这样的:

switch_bytes(old_word,new_word);

任务switch_bytes把输入old_word的字的高、低字节互换放入new_word端口输出。 而函数返回的新字是通过函数本身的返回值,因此16位字字节互换函数的调用源码是这样的:

new_word = switch_bytes(old_word);

下面分两节分别介绍任务和函数语句的要点。

 

二. task说明语句

如果传给任务的变量值和任务完成后接收结果的变量已定义,就可以用一条语句启动任务。任务完成以后控制就传回启动过程。如任务内部有定时控制,则启动的时间可以与控制返回的时间不同。任务可以启动其它的任务,其它任务又可以启动别的任务,可以启动的任务数是没有限制的。不管有多少任务启动,只有当所有的启动任务完成以后,控制才能返回。

1)任务的定义 定义任务的语法如下:任务:

 
task <任务名>; 
<端口及数据类型声明语句> 
<语句1> 
<语句2> 
..... 
<语句n> 
endtask

 

这些声明语句的语法与模块定义中的对应声明语句的语法是一致的。

2)任务的调用及变量的传递

启动任务并传递输入输出变量的声明语句的语法如下:

任务的调用:

<任务名>(端口1,端口2,...,端口n);

下面的例子说明怎样定义任务和调用任务:任务定义:

 

    task my_task; 
        input a, b; 
        inout c; 
        output d, e; 
        …
        <语句> //执行任务工作相应的语句 
        …
        c = foo1; //赋初始值 
        d = foo2; //对任务的输出变量赋值t 
        e = foo3; 
    endtask

 

任务调用:my_task(v,w,x,y,z);

任务调用变量(v,w,x,y,z)和任务定义的I/O变量(a,b,c,d,e)之间是一一对应的。当任务启动时,由v,w,和x.传入的变量赋给了a,b,和c,而当任务完成后的输出又通过c,d和e赋给了x,y和z。下面是一个具体的例子用来说明怎样在模块的设计中使用任务,使程序容易读懂:

 

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 
        begin 
            red=on; //开红灯 
            light(red,red_tics); //调用等待任务 
            green=on; //开绿灯 
            light(green,green_tics); //等待 
            amber=on; //开黄灯 
            light(amber,amber_tics); //等待
        end 
    
    //定义交通灯开启时间的任务 
    task light(color,tics); 
        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说明语句

函数的目的是返回一个用于表达式的值。

1、定义函数的语法:

 
function <返回值的类型或范围> (函数名); 
    <端口说明语句> 
    <变量类型说明语句> 
        begin 
            <语句> 
            ........ 
        end 
endfunction

 

请注意<返回值的类型或范围>这一项是可选项,如缺省则返回值为一位寄存器类型数据。下面用例子说明:

 
function [7:0] getbyte; 
    input [15:0] address; 
    
    begin 
        <说明语句> //从地址字中提取低字节的程序 
        getbyte = result_expression; //把结果赋予函数的返回字节 
    end 
endfunction

 

2、从函数返回的值

函数的定义蕴含声明了与函数同名的、函数内部的寄存器。如在函数的声明语句中<返回值的类型或范围>为缺省,则这个寄存器是一位的,否则是与函数定义中<返回值的类型或范围>一致的寄存器。函数的定义把函数返回值所赋值寄存器的名称初始化为与函数同名的内部变量。下面的例子说明了这个概念:getbyte被赋予的值就是函数的返回值。y 函数的调用

3、函数的调用

是通过将函数作为表达式中的操作数来实现的。

其调用格式如下:<函数名> (<表达式><,<表达式>>*) 其中函数名作为确认符。下面的例子中通过对两次调用函数getbyte的结果值进行位拼接运算来生成一个字。word = control? {getbyte(msbyte),getbyte(lsbyte)} : 0;

4、函数的使用规则 与任务相比较函数的使用有较多的约束,下面给出的是函数的使用规则:

  1. 函数的定义不能包含有任何的时间控制语句,即任何用#、@、或wait来标识的语句。

  2. 函数不能启动任务。

  3. 定义函数时至少要有一个输入参量。

  4. 在函数的定义中必须有一条赋值语句给函数中的一个内部变量赋以函数的结果值,该内部变量具有和函数名相同的名字。

5、举例说明 下面的例子中定义了一个可进行阶乘运算的名为factorial的函数,该函数返回一个32位的寄存器类型的值,该函数可后向调用自身,并且打印出部分结果值。

 

module tryfact; 
     //函数的定义
    function[31:0]factorial; 
    
    input[3:0]operand; 
    
    reg[3:0]index; 
    
    begin 
        factorial = operand? 1 : 0; 
        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//模块结束

 

前面我们已经介绍了足够的语句类型可以编写一些完整的模块。后续更新,将举许多实际的例子进行介绍。这些例子都给出了完整的模块描述,因此可以对它们进行仿真测试和结果检验。通过学习和练习我们就能逐步掌握利用Verilog HDL设计数字系统的方法和技术。

 

系统函数和任务

Verilog HDL语言中共有以下一些系统函数和任务:bitstoreal, rtoi, display, setup, finish, skew, hold, setuphold, itor, strobe, period, time, printtimescale, timefoemat, realtime, width, real tobits, write, $recovery。

在Verilog HDL语言中每个系统函数和任务前面都用一个标识符$来加以确认。这些系统函数和任务提供了非常强大的功能。有兴趣的同学可以自行查阅资料。

下面对一些常用的系统函数和任务逐一加以介绍。

 

display和write任务

格式:

$display(p1,p2,....pn);

$write(p1,p2,....pn);

这两个函数和系统任务的作用是用来输出信息,即将参数p2到pn按参数p1给定的格式输出。参数p1通常称为“格式控制”,参数p2至pn通常称为“输出表列”。这两个任务的作用基本相同。

display自动地在输出后进行换行,write则不是这样。如果想在一行里输出多个信息,可以使用write。在display和$write中,其输出格式控制是用双引号括起来的字符串,它包括两种信息:

  • 格式说明,由"%"和格式字符组成。它的作用是将输出的数据转换成指定的格式输出。格式说明总是由“%”字符开始的。对于不同类型的数据用不同的格式输出。表一中给出了常用的几种输出格式。如下表一:

  • 普通字符,即需要原样输出的字符。其中一些特殊的字符可以通过表二中的转换序列来输出。下面表中的字符形式用于格式字符串参数中,用来显示特殊的字符。如下表二:

在display和write的参数列表中,其“输出表列”是需要输出的一些数据,可以是表达式。下面举几个例子说明一下。

[例1]:

 
module disp; 
    initial 
        begin 
            $display("\\\t%%\n\"\123"); 
        end 
endmodule

 

输出结果为:

%

"S

从上面的这个例子中可以看到一些特殊字符的输出形式(八进制数123就是字符S)。

[例2]:

 
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

 

其输出结果为:

输出数据的显示宽度 在$display中,输出列表中数据的显示宽度是自动按照输出格式进行调整的。这样在显示输出数据时,在经过格式转换以后,总是用表达式的最大可能值所占的位数来显示表达式的当前值。在用十进制数格式输出时,输出结果前面的0值用空格来代替。

对于其它进制,输出结果前面的0仍然显示出来。例如对于一个值的位宽为12位的表达式,如按照十六进制数输出,则输出结果占3个字符的位置,如按照十进制数输出,则输出结果占4个字符的位置。这是因为这个表达式的最大可能值为FFF(十六进制)、4095(十进制)。可以通过在%和表示进制的字符中间插入一个0自动调整显示输出数据宽度的方式。见下例:

$display("d=%0h a=%0h",data,addr);

这样在显示输出数据时,在经过格式转换以后,总是用最少的位数来显示表达式的当前值。下面举例说明:

[例3]:

 

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;

如果输出列表中表达式的值包含有不确定的值或高阻值,其结果输出遵循以下规则:

(1).在输出格式为十进制的情况下:

  • 如果表达式值的所有位均为不定值,则输出结果为小写的x。

  • 如果表达式值的所有位均为高阻值,则输出结果为小写的z。

  • 如果表达式值的部分位为不定值,则输出结果为大写的X。

  • 如果表达式值的部分位为高阻值,则输出结果为大写的Z。

(2).在输出格式为十六进制和八进制的情况下:

每4位二进制数为一组代表一位十六进制数,每3位二进制数为一组代表一位八进制数。

如果表达式值相对应的某进制数的所有位均为不定值,则该位进制数的输出的结果为小写的x。

如果表达式值相对应的某进制数的所有位均为高阻值,则该位进制数的输出结果为小写的z。

如果表达式值相对应的某进制数的部分位为不定值,则该位进制数输出结果为大写的X。

如果表达式值相对应的某进制数的部分位为高阻值,则该位进制数输出结果为大写的Z。

对于二进制输出格式,表达式值的每一位的输出结果为0、1、x、z。下面举例说明: 语句输出结果:

$display("%d", 1'bx); 输出结果为:x

$display("%h", 14'bx0_1010); 输出结果为:xxXa

$display("%h %o",12'b001x_xx10_1x01,12'b001_xxx_101_x01); 输出结果为:XXX 1x5X

注意:因为write在输出时不换行,要注意它的使用。可以在write中加入换行符\n,以确保明确的输出显示格式。

 

系统任务$monitor

格式:

$monitor(p1,p2,.....,pn);

$monitor;

$monitoron;

$monitoroff;

任务monitor提供了监控和输出参数列表中的表达式或变量值的功能。其参数列表中输出控制格式字符串和输出表列的规则和display中的一样。

当启动一个带有一个或多个参数的monitor任务时,仿真器则建立一个处理机制,使得每当参数列表中变量或表达式的值发生变化时,整个参数列表中变量或表达式的值都将输出显示。如果同一时刻,两个或多个参数的值发生变化,则在该时刻只输出显示一次。但在monitor中,参数可以是$time系统函数。这样参数列表中变量或表达式的值同时发生变化的时刻可以通过标明同一时刻的多行输出来显示。如:

monitor(time,,"rxd=%b txd=%b",rxd,txd);

在display中也可以这样使用。注意在上面的语句中,“,,monitoron和monitoroff任务的作用是通过打开和关闭监控标志来控制监控任务monitor的启动和停止,这样使得程序员可以很容易的控制monitor何时发生。其中monitoroff任务用于关闭监控标志,停止监控任务monitor,monitoron则用于打开监控标志,启动监控任务monitor。通常在通过调用monitoron启动monitor时,不管monitor参数列表中的值是否发生变化,总是立刻输出显示当前时刻参数列表中的值,这用于在监控的初始时刻设定初始比较值。在缺省情况下,控制标志在仿真的起始时刻就已经打开了。

在多模块调试的情况下,许多模块中都调用了monitor,因为任何时刻只能有一个monitor起作用,因此需配合monitoron与monitoroff使用,把需要监视的模块用monitoron打开,在监视完毕后及时用monitoroff关闭,以便把monitor 让给其他模块使用。monitor与display的不同处还在于monitor往往在initial块中调用,只要不调用monitoroff,monitor便不间断地对所设定的信号进行监视。

 

时间度量系统函数$time

在Verilog HDL中有两种类型的时间系统函数:time和realtime。用这两个时间系统函数可以得到当前的仿真时刻。

 

1、系统函数$time

$time可以返回一个64比特的整数来表示的当前仿真时刻值。该时刻是以模块的仿真时间尺度为基准的。下面举例说明。

[例1]:

 

`timescale 10ns/1ns 

module test; 
    reg set; 
    
    parameter p=1.6; 
    
    initial 
        begin 
        $monitor($time,,"set=",set); 
        #p set=0; 
        #p set=1; 
        end 
        
endmodule

 

输出结果为:

0 set=x

2 set=0

3 set=1

在这个例子中,模块test想在时刻为16ns时设置寄存器set为0,在时刻为32ns时设置寄存器set为1。但是由$time记录的set变化时刻却和预想的不一样。这是由下面两个原因引起的:

1)time显示时刻受时间尺度比例的影响。在上面的例子中,时间尺度是10ns,因为time输出的时刻总是时间尺度的倍数,这样将16ns和32ns输出为1.6和3.2。

2)因为$time总是输出整数,所以在将经过尺度比例变换的数字输出时,要先进行取整。在上面的例子中,1.6和3.2经取整后为2和3输出。注意:时间的精确度并不影响数字的取整。

 

2、$realtime系统函数

realtime和time的作用是一样的,只是$realtime返回的时间数字是一个实型数,该数字也是以时间尺度为基准的。下面举例说明:

[例2]:

 

`timescale10ns/1ns 

module test; 
    reg set; 
    
    parameter p=1.55; 
    
    initial 
        begin 
            $monitor($realtime,,"set=",set); 
            #p set=0; 
            #p set=1; 
        end 
endmodule

 

输出结果为:

0 set=x

1.6 set=0

3.2 set=1

从上面的例子可以看出,realtime将仿真时刻经过尺度变换以后即输出,不需进行取整操作。所以realtime返回的时刻是实型数。

 

系统任务$finish

格式:

$finish;

$finish(n);

系统任务finish的作用是退出仿真器,返回主操作系统,也就是结束仿真过程。任务finish可以带参数,根据参数的值输出不同的特征信息。

如果不带参数,默认$finish的参数值为1。下面给出了对于不同的参数值,系统输出的特征信息:

0 不输出任何信息

1 输出当前仿真时刻和位置

2 输出当前仿真时刻,位置和在仿真过程中 所用memory及CPU时间的统计

 

系统任务$stop

格式:

$stop;

$stop(n);

$stop任务的作用是把EDA工具(例如仿真器)置成暂停模式,在仿真环境下给出一个交互式的命令提示符,将控制权交给用户。这个任务可以带有参数表达式。根据参数值(0,1或2)的不同,输出不同的信息。参数值越大,输出的信息越多。

 

系统任务readmemb和readmemh

在Verilog HDL程序中有两个系统任务readmemb和readmemh用来从文件中读取数据到存贮器中。这两个系统任务可以在仿真的任何时刻被执行使用,其使用格式共有以下六种:

  1. $readmemb("<数据文件名>",<存贮器名>);

  2. $readmemb("<数据文件名>",<存贮器名>,<起始地址>);

  3. $readmemb("<数据文件名>",<存贮器名>,<起始地址>,<结束地址>);

  4. $readmemh("<数据文件名>",<存贮器名>);

  5. $readmemh("<数据文件名>",<存贮器名>,<起始地址>);

  6. $readmemh("<数据文件名>",<存贮器名>,<起始地址>,<结束地址>);

在这两个系统任务中,被读取的数据文件的内容只能包含:空白位置(空格,换行,制表格(tab)和form-feeds),注释行(//形式的和/.../形式的都允许),二进制或十六进制的数字。数字中不能包含位宽说明和格式说明,对于readmemb系统任务,每个数字必须是二进制数字,对于readmemh系统任务,每个数字必须是十六进制数字。数字中不定值x或X,高阻值z或Z,和下划线(_)的使用方法及代表的意义与一般Verilog HDL程序中的用法及意义是一样的。另外数字必须用空白位置或注释行来分隔开。

在下面的讨论中,地址一词指对存贮器(memory)建模的数组的寻址指针。当数据文件被读取时,每一个被读取的数字都被存放到地址连续的存贮器单元中去。存贮器单元的存放地址范围由系统任务声明语句中的起始地址和结束地址来说明,每个数据的存放地址在数据文件中进行说明。当地址出现在数据文件中,其格式为字符“@”后跟上十六进制数。如: @hh...h

对于这个十六进制的地址数中,允许大写和小写的数字。在字符“@”和数字之间不允许存在空白位置。可以在数据文件里出现多个地址。当系统任务遇到一个地址说明时,系统任务将该地址后的数据存放到存贮器中相应的地址单元中去。

对于上面六种系统任务格式,需补充说明以下五点:

  1. 如果系统任务声明语句中和数据文件里都没有进行地址说明,则缺省的存放起始地址为该存贮器定义语句中的起始地址。数据文件里的数据被连续存放到该存贮器中,直到该存贮器单元存满为止或数据文件里的数据存完。

  2. 如果系统任务中说明了存放的起始地址,没有说明存放的结束地址,则数据从起始地址开始存放,存放到该存贮器定义语句中的结束地址为止。

  3. 如果在系统任务声明语句中,起始地址和结束地址都进行了说明,则数据文件里的数据按该起始地址开始存放到存贮器单元中,直到该结束地址,而不考虑该存贮器的定义语句中的起始地址和结束地址。

  4. 如果地址信息在系统任务和数据文件里都进行了说明,那么数据文件里的地址必须在系统任务中地址参数声明的范围之内。否则将提示错误信息,并且装载数据到存贮器中的操作被中断。

  5. 如果数据文件里的数据个数和系统任务中起始地址及结束地址暗示的数据个数不同的话,也要提示错误信息。

下面举例说明:

先定义一个有256个地址的字节存贮器

mem: reg[7:0] mem[1:256];

下面给出的系统任务以各自不同的方式装载数据到存贮器mem中。

 

initial $readmemh("mem.data",mem); 
initial $readmemh("mem.data",mem,16); 
initial $readmemh("mem.data",mem,128,1);

 

第一条语句在仿真时刻为0时,将装载数据到以地址是1的存贮器单元为起始存放单元的存贮器中去。第二条语句将装载数据到以单元地址是16的存贮器单元为起始存放单元的存贮器中去,一直到地址是256的单元为止。第三条语句将从地址是128的单元开始装载数据,一直到地址为1的单元。在第三种情况中,当装载完毕,系统要检查在数据文件里是否有128个数据,如果没有,系统将提示错误信息。

 

系统任务 $random

这个系统函数提供了一个产生随机数的手段。当函数被调用时返回一个32bit的随机数。它是一个带符号的整形数。

random一般的用法是:ramdom % b ,其中 b>0.它给出了一个范围在(-b+1):(b-1)中的随机数。

下面给出一个产生随机数的例子:

reg[23:0] rand;

rand = $random % 60;

上面的例子给出了一个范围在-59到59之间的随机数,下面的例子通过位并接操作产生一个值在0到59之间的数。

reg[23:0] rand;

rand = {$random} % 60;

利用这个系统函数可以产生随机脉冲序列或宽度随机的脉冲序列,以用于电路的测试。下面例子中的Verilog HDL模块可以产生宽度随机的随机脉冲序列的测试信号源,在电路模块的设计仿真时非常有用。同学们可以根据测试的需要,模仿下例,灵活使用$random系统函数编制出与实际情况类似的随机脉冲序列。

[例]:

 

 `timescale 1ns/1ns 
 
module random_pulse( dout ); 
    output [9:0] dout; 
    
    reg dout; 
    
    integer delay1,delay2,k; 
    
    initial 
        begin 
        #10 dout=0; 
        for (k=0; k< 100; k=k+1) 
            begin 
                delay1 = 20 * ( {$random} % 6); 
                // delay1 在0到100ns间变化
                delay2 = 20 * ( 1 + {$random} % 3); 
                // delay2 在20到60ns间变化
                #delay1 dout = 1 << ({$random} %10); 
                //dout的0--9位中随机出现1,并出现的时间在0-100ns间变化
                #delay2 dout = 0; 
                //脉冲的宽度在在20到60ns间变化
            end 
        end 
        
endmodule

 

Day 4 就到这里,Day 5 继续开始编译预处理。

 

【QQ交流群】

群号:173560979,进群暗语:FPGA技术江湖粉丝。

多年的FPGA企业开发经验,各种通俗易懂的学习资料以及学习方法,浓厚的交流学习氛围,QQ群目前已有1000多名志同道合的小伙伴,无广告纯净模式,给技术交流一片净土,从初学小白到行业精英业界大佬等,从军工领域到民用企业等,从通信、图像处理到人工智能等各个方向应有尽有。

 

【微信交流群】

现微信交流群已建立08群,人数已达数千人,欢迎关注“FPGA技术江湖”微信公众号,可获取进群方式。

后续会持续更新,带来Vivado、 ISE、Quartus II 、candence等安装相关设计教程,学习资源、项目资源、好文推荐等,希望大侠持续关注。

江湖偌大,继续闯荡,愿大侠一切安好,有缘再见!

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
06-10 87
02-09 411

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值