下冰雹 · 3月27日

一周掌握 FPGA Verilog HDL 语法 day 4

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

一周掌握 FPGA Verilog HDL 语法 day 3 被平台综合了,如果想要看详细介绍的话,可以到公众号内部"行侠仗义"栏目下获取。

上一篇提到了阻塞与非阻塞、条件语句、块语句等,此篇我们继续来看 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 的真值表:

image.png

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 语句,造成这种错误的例子。

Image

检查一下左边的"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 保持原值,这就会自动生成锁存器。

Image

右边的例子很明确,程序中的 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

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

  1. 任务的调用及变量的传递

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

任务的调用:

<任务名>(端口 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 中,其输出格式控制是用双引号括起来的字符串,它包括两种信息:

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

Image

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

Image

在$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 

其输出结果为:

Image

输出数据的显示宽度 在$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  继续开始编译预处理,大侠保重,告辞。

END

原文:FPGA技术江湖

相关文章推荐

更多 FPGA 干货请关注FPGA的逻辑技术专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
10654
内容数
613
FPGA Logic 二三事
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息