相关文章推荐
绅士的毛衣  ·  基于JavaScript ...·  1 月前    · 
严肃的面包  ·  拥抱科技、融合创新!看复旦老师们如何玩转线上课堂·  3 月前    · 
内向的冲锋衣  ·  浙江省卫生健康委等关于加强心理健康服务的指导意见·  4 月前    · 
听话的眼镜  ·  区块链泡沫中掘金: 巨头加码、 ...·  4 月前    · 
满身肌肉的火柴  ·  北京市小客车指标管理信息系统·  12 月前    · 
飞翔的创口贴  ·  京籍家庭与非京籍家庭关于异地高考的故事--时 ...·  1 年前    · 
Code  ›  Verilog 学习笔记(5)——模块和例化、函数和任务_verilog模块连接和例化有什么区别吗_FPGA 学习工的博客
函数调用 input 函数声明
https://blog.csdn.net/weixin_38489928/article/details/117651951
近视的鸭蛋
2 年前
  • 5.1 Verilog 模块与端口
    • 模块
    • 端口
    • inout 端口仿真
  • 5.2 Verilog 模块例化
    • 命名端口连接
    • 顺序端口连接
    • 端口连接规则
  • 5.3 Verilog 带参数例化
    • defparam 语句
    • 带参数模块例化
  • 5.4 Verilog 函数
    • 函数
    • 常数函数
    • automatic 函数
  • 5.5 Verilog 任务
    • 任务与函数的区别
    • 任务
    • 任务操作全局变量
    • automatic 任务
    endmodule

    模块定义必须以关键字 module 开始,以关键字 endmodule 结束。
    模块名,端口信号,端口声明和可选的参数声明等,出现在设计使用的 Verilog 语句(图中 Declarations_and_Statements)之前。
    模块内部有可选的 5 部分组成,分别是变量声明,数据流语句,行为级语句,低层模块例化及任务和函数,如下图表示。这 5 部分出现顺序、出现位置都是任意的。但是,各种变量都应在使用之前声明。变量具体声明的位置不要求,但必须保证在使用之前的位置。
    在这里插入图片描述

    端口是模块与外界交互的接口。对于外部环境来说,模块内部是不可见的,对模块的调用只能通过端口连接进行。

    端口列表
    模块的定义中包含一个可选的端口列表,一般将不带类型、不带位宽的信号变量罗列在模块声明里。下面是一个 PAD 模型的端口列表:

    module pad(
        DIN, OEN, PULL,
        DOUT, PAD);
    

    一个模块如果和外部环境没有交互,则可以不用声明端口列表。例如之前我们仿真时 test.sv 文件中的 test 模块都没有声明具体端口。

    module test ;  //直接分号结束
        ......     //数据流或行为级描述
    endmodule
    
    1. 端口信号在端口列表中罗列出来以后,就可以在模块实体中进行声明了。
      根据端口的方向,端口类型有 3 种: 输入(input),输出(output)和双向端口(inout)。
      input、inout 类型不能声明为 reg 数据类型,因为 reg 类型是用于保存数值的,而输入端口只能反映与其相连的外部信号的变化,不能保存这些信号的值。
      output 可以声明为 wire 或 reg 数据类型。
      上述例子中 pad 模块的端口声明,在 module 实体中就可以表示如下:
    //端口类型声明
    input        DIN, OEN ;
    input [1:0]  PULL ;  //(00,01-dispull, 11-pullup, 10-pulldown)
    inout        PAD ;   //pad value
    output       DOUT ;  //pad load when pad configured as input
    //端口数据类型声明
    wire         DIN, OEN ;
    wire  [1:0]  PULL ;
    wire         PAD ;
    reg          DOUT ;
    
    1. 在 Verilog 中,端口隐式的声明为 wire 型变量,即当端口具有 wire 属性时,不用再次声明端口类型为 wire 型。但是,当端口有 reg 属性时,则 reg 声明不可省略。
      上述例子中的端口声明,则可以简化为:
    input        DIN, OEN ;
    input [1:0]  PULL ;     
    inout        PAD ;     
    output       DOUT ;    
    reg          DOUT ;
    
    1. 当然,信号 DOUT 的声明完全可以合并成一句:
    output reg      DOUT ;
    
    1. 还有一种更简洁且常用的方法来声明端口,即在 module 声明时就陈列出端口及其类型。reg 型端口要么在 module 声明时声明,要么在 module 实体中声明,例如以下 2 种写法是等效的。
    module pad(
        input        DIN, OEN ,
        input [1:0]  PULL ,
        inout        PAD ,
        output reg   DOUT
    module pad(
        input        DIN, OEN ,
        input [1:0]  PULL ,
        inout        PAD ,
        output       DOUT
        reg        DOUT ;
    

    inout 端口仿真

    对包含有 inout 端口类型的 pad 模型进行仿真。pad 模型完整代码如下:

    module pad(
        //DIN, pad driver when pad configured as output
        //OEN, pad direction(1-input, o-output)
        input        DIN, OEN ,
        //pull function (00,01-dispull, 10-pullup, 11-pulldown)
        input [1:0]  PULL ,
        inout        PAD ,
        //pad load when pad configured as input
        output reg   DOUT
        //input:(not effect pad external input logic), output: DIN->PAD
        assign       PAD = OEN? 'bz : DIN ;
        //input:(PAD->DOUT)
        always @(*) begin
            if (OEN == 1) begin //input
                DOUT   = PAD ;
            else begin
                DOUT   = 'bz ;
        //use tristate gate in Verilog to realize pull up/down function
        bufif1  puller(PAD, PULL[0], PULL[1]);
    endmodule
    

    testbench代码如下:

    `timescale 1ns/1ns
    module test ;
        reg          DIN, OEN ;
        reg [1:0]    PULL ;
        wire         PAD ;
        wire         DOUT ;
        reg          PAD_REG ;
        assign       PAD = OEN ? PAD_REG : 1'bz ; //
        initial begin
            PAD_REG   = 1'bz ;        //pad with no dirve at first
            OEN       = 1'b1 ;        //input simulation
            #0 ;      PULL      = 2'b10 ;   //pull down
            #20 ;     PULL      = 2'b11 ;   //pull up
            #20 ;     PULL      = 2'b00 ;   //dispull
            #20 ;     PAD_REG   = 1'b0 ;
            #20 ;     PAD_REG   = 1'b1 ;
            #30 ;     OEN       = 1'b0 ;    //output simulation
                      DIN       = 1'bz ;
            #15 ;     DIN       = 1'b0 ;
            #15 ;     DIN       = 1'b1 ;
        pad     u_pad(
            .DIN     (DIN) ,
            .OEN     (OEN) ,
            .PULL    (PULL) ,
            .PAD     (PAD) ,
            .DOUT    (DOUT)
        initial begin
            forever begin
                #100;
                if ($time >= 1000)  $finish ;
    endmodule // test
    

    仿真结果如下:
    在这里插入图片描述

    • 当 PAD 方向为 input 且没有驱动时,pull 功能能通过 PAD 的值而体现。
    • 前 60ns 内,PAD 的驱动端 PAD_REG 为 z, 可认为没有驱动,所以开始时 PULL=2, 下拉,PAD值为 0; 20ns 时,PULL=3,上拉,PAD 值为 1;
    • 40ns 时,PULL=0,没有 pull 功能,PAD 值输入为 z。
    • 60ns~100ns 后,PAD 的驱动端 PAD_REG 开始正常驱动。此时相当于 PAD 直接与 PAD_REG 相连,所以 PAD 值与其驱动值保持一致。

    以上分析,PAD 方向都是 input,所有输出端 DOUT 与 PAD 值保持一致。
    当 PAD 方向为 output 时,即 120ns 时 OEN= 0,PAD 值与输入端 DIN 值保持一致。

    5.2 Verilog 模块例化

    在一个模块中引用另一个模块,对其端口进行相关连接,叫做模块例化。模块例化建立了描述的层次。信号端口可以通过位置或名称关联,端口连接也必须遵循一些规则。

    命名端口连接

    这种方法将需要例化的模块端口与外部信号按照其名字进行连接,端口顺序随意,可以与引用 module 的声明端口顺序不一致,只要保证端口名字与外部信号匹配即可。
    下面是例化一次 1bit 全加器的例子:

    full_adder1  u_adder0(
        .Ai     (a[0]),
        .Bi     (b[0]),
        .Ci     (c==1'b1 ? 1'b0 : 1'b1),
        .So     (so_bit0),
        .Co     (co_temp[0]));
    

    如果某些输出端口并不需要在外部连接,例化时 可以悬空不连接,甚至删除。一般来说,input 端口在例化时不能删除,否则编译报错,output 端口在例化时可以删除。例如:

    //output 端口 Co 悬空
    full_adder1  u_adder0(
        .Ai     (a[0]),
        .Bi     (b[0]),
        .Ci     (c==1
    
    
    
    
        
    'b1 ? 1'b0 : 1'b1),
        .So     (so_bit0),
        .Co     ());
    //output 端口 Co 删除
    full_adder1  u_adder0(
        .Ai     (a[0]),
        .Bi     (b[0]),
        .Ci     (c==1'b1 ? 1'b0 : 1'b1),
        .So     (so_bit0));
    

    顺序端口连接

    这种方法将需要例化的模块端口按照模块声明时端口的顺序与外部信号进行匹配连接,位置要严格保持一致。例如例化一次 1bit 全加器的代码可以改为:

    full_adder1  u_adder1(
        a[1], b[1], co_temp[0], so_bit1, co_temp[1]);
    

    虽然代码从书写上可能会占用相对较少的空间,但代码可读性降低,也不易于调试。有时候在大型的设计中可能会有很多个端口,端口信号的顺序时不时的可能也会有所改动,此时再利用顺序端口连接进行模块例化,显然是不方便的。所以平时,建议采用命名端口方式对模块进行例化。

    端口连接规则

    输入端口
    模块例化时,从模块外部来讲, input 端口可以连接 wire 或 reg 型变量。这与模块声明是不同的,从模块内部来讲,input 端口必须是 wire 型变量。
    输出端口
    模块例化时,从模块外部来讲,output 端口必须连接 wire 型变量。这与模块声明是不同的,从模块内部来讲,output 端口可以是 wire 或 reg 型变量。
    输入输出端口
    模块例化时,从模块外部来讲,inout 端口必须连接 wire 型变量。这与模块声明是相同的。
    悬空端口
    模块例化时,如果某些信号不需要与外部信号进行连接交互,我们可以将其悬空,即端口例化处保留空白即可,上述例子中有提及。
    output 端口正常悬空时,我们甚至可以在例化时将其删除。
    input 端口正常悬空时,悬空信号的逻辑功能表现为高阻状态(逻辑值为 z)。但是,例化时一般不能将悬空的 input 端口删除,否则编译会报错,例如:

    //下述代码编译会报Warning
    full_adder4  u_adder4(
        .a      (a),
        .b      (b),
        .c      (),
        .so     (so),
        .co     (co));
    
    /如果模块full_adder4有input端口c,则下述代码编译是会报Error
    full_adder4  u_adder4(
        .a      (a),
        .b      (b),
        .so     (so),
        .co     (co));
    

    一般来说,建议 input 端口不要做悬空处理,无其他外部连接时赋值其常量,例如:

    full_adder4  u_adder4(
        .a      (a),
        .b      (b),
        .c      (1'b0),
        .so     (so),
        .co     (co));
    

    位宽匹配
    当例化端口与连续信号位宽不匹配时,端口会通过无符号数的右对齐或截断方式进行匹配。
    假如在模块 full_adder4 中,端口 a 和端口 b 的位宽都为 4bit,则下面代码的例化结果会导致:u_adder4.a = {2’bzz, a[1:0]}, u_adder4.b = b[3:0] 。

    full_adder4  u_adder4(
        .a      (a[1:0]),      //input a[3:0]
        .b      (b[5:0]),      //input b[3:0]
        .c      (1'b0),
        .so     (so),
        .co     (co));
    

    端口连续信号类型
    连接端口的信号类型可以是,1)标识符,2)位选择,3)部分选择,4)上述类型的合并,5)用于输入端口的表达式。
    当然,信号名字可以与端口名字一样,但他们的意义是不一样的,分别代表的是 2 个模块内的信号。

    用 generate 进行模块例化
    当例化多个相同的模块时,一个一个的手动例化会比较繁琐。用 generate 语句进行多个模块的重复例化,可大大简化程序的编写过程。
    重复例化 4 个 1bit 全加器组成一个 4bit 全加器的代码如下:

    module full_adder4(
        input [3:0]   a ,   //adder1
        input [3:0]   b ,   //adder2
        input         c ,   //input carry bit
        output [3:0]  so ,  //adding result
        output        co    //output carry bit
        wire [3:0]    co_temp ; 
        //第一个例化模块一般格式有所差异,需要单独例化
        full_adder1  u_adder0(
            .Ai     (a[0]),
            .Bi     (b[0]),
            .Ci     (c==1'b1 ? 1'b1 : 1'b0),
            .So     (so[0]),
            .Co     (co_temp[0]));
        genvar        i ;
        generate
            for(i=1; i<=3; i=i+1) begin: adder_gen
            full_adder1  u_adder(
                .Ai     (a[i]),
                .Bi     (b[i]),
                .Ci     (co_temp[i-1]), //上一个全加器的溢位是下一个的进位
                .So     (so[i]),
                .Co     (co_temp[i]));
        endgenerate
        assign co    = co_temp[3] ;
    endmodule
    

    testbench 如下:

    `timescale 1ns/1ns
    module test ;
        reg  [3:0]   a ;
        reg  [3:0]   b ;
        //reg          c ;
        wire [3:0]   so ;
        wire         co ;
        //简单驱动
        initial begin
            a = 4'd5 ;
            b = 4'd2 ;
            #10 ;
            a = 4'd10 ;
            b = 4'd8 ;
        full_adder4  u_adder4(
                   .a      (a),
                   .b      (b),
                   .c      (1'b0),   //端口可以连接常量
                   .so
    
    
    
    
        
         (so),
                   .co     (co));
        initial begin
            forever begin
                #100;
                if ($time >= 1000)  $finish ;
    endmodule // test
    

    仿真结果如下,可知 4bit 全加器工作正常:
    在这里插入图片描述
    层次访问
    每一个例化模块的名字,每个模块的信号变量等,都使用一个特定的标识符进行定义。在整个层次设计中,每个标识符都具有唯一的位置与名字。
    Verilog 中,通过使用一连串的 . 符号对各个模块的标识符进行层次分隔连接,就可以在任何地方通过指定完整的层次名对整个设计中的标识符进行访问。
    层次访问多见于仿真中。
    例如,有以下层次设计,则叶单元、子模块和顶层模块间的信号就可以相互访问。

    //u_n1模块中访问u_n3模块信号: 
    a = top.u_m2.u_n3.c ;
    //u_n1模块中访问top模块信号
    if (top.p == 'b0) a = 1'b1 ; 
    //top模块中访问u_n4模块信号
    assign p = top.u_m2.u_n4.d ;
    

    5.3 Verilog 带参数例化

    当一个模块被另一个模块引用例化时,高层模块可以对低层模块的参数值进行改写。这样就允许在编译时将不同的参数传递给多个相同名字的模块,而不用单独为只有参数不同的多个模块再新建文件。
    参数覆盖有 2 种方式:1)使用关键字 defparam,2)带参数值模块例化。

    defparam 语句

    可以用关键字 defparam 通过模块层次调用的方法,来改写低层次模块的参数值。
    例如对一个单口地址线和数据线都是 4bit 宽度的 ram 模块的 MASK 参数进行改写:

    //instantiation
    defparam     u_ram_4x4.MASK = 7 ;
    ram_4x4    u_ram_4x4
            .CLK    (clk),
            .A      (a[4-1:0]),
            .D      (d),
            .EN     (en),
            .WR     (wr),    //1 for write and 0 for read
            .Q      (q)    );
    

    ram_4x4 的模型如下:

    module  ram_4x4
         input               CLK ,
         input [4-1:0]       A ,
         input [4-1:0]       D ,
         input               EN ,
         input               WR ,    //1 for write and 0 for read
         output reg [4-1:0]  Q    );
        parameter        MASK = 3 ;
        reg [4-1:0]     mem [0:(1<<4)-1] ;
        always @(posedge CLK) begin
            if (EN && WR) begin
                mem[A]  <= D & MASK;
            else if (EN && !WR) begin
                Q       <= mem[A] & MASK;
    endmodule
    

    对此进行一个简单的仿真,testbench 编写如下:

    `timescale 1ns/1ns
    module test ;
        parameter    AW = 4 ;
        parameter    DW = 4 ;
        reg                  clk ;
        reg [AW:0]           a ;
        reg [DW-1:0]         d ;
        reg                  en ;
        reg                  wr ;
        wire [DW-1:0]        q ;
        //clock generating
        always begin
            #15 ;     clk = 0 ;
            #15 ;     clk = 1 ;
        initial begin
            a         = 10 ;
            d         = 2 ;
            en        = 'b0 ;
            wr        = 'b0 ;
            repeat(10) begin
                @(negedge clk) ;
                en     = 1'b1;
                a      = a + 1 ;
                wr     = 1'b1 ;  //write command
                d      = d + 1 ;
            a         = 10 ;
            repeat(10) begin
                @(negedge clk) ;
                a      = a + 1 ;
                wr     = 1'b0 ;  //read command
        end // initial begin
        //instantiation
        defparam     u_ram_4x4.MASK = 7 ;
        ram_4x4    u_ram_4x4
            .CLK    (clk),
            .A      (a[AW-1:0]),
            .D      (d),
            .EN     (en),
            .WR     (wr),    //1 for write and 0 for read
            .Q      (q)
        //stop simulation
        initial begin
            forever begin
                #100;
                if ($time >= 1000)  $finish ;
    endmodule // test
    

    仿真结果如下:
    在这里插入图片描述

    图中黄色部分,当地址第一次为 c 时写入数据 4, 当第二次地址为 c 时读出数据为 4;可知此时 ram 行为正确,且 MASK 不为 3。 因为 ram 的 Q 端 bit2 没有被屏蔽。

    当第一次地址为 1 时写入数据为 9,第二次地址为 1 时读出的数据却是 1,因为此时 MASK 为 7,ram 的 Q 端信号 bit3 被屏蔽。由此可知,MASK 参数被正确改写。

    带参数模块例化

    第二种方法就是例化模块时,将新的参数值写入模块例化语句,以此来改写原有 module 的参数值。
    例如对一个地址和数据位宽都可变的 ram 模块进行带参数的模块例化:

    ram #(.AW(4), .DW(4))
        u_ram
            .CLK    (clk),
            .A      (a[AW-1:0]),
            .D      (d),
            .EN     (en),
            .WR     (wr),    //1 for write and 0 for read
            .Q      (q)
    

    ram 模型如下:

    module  ram
        #(  parameter       AW = 2 ,
            parameter       DW = 3 )
            input                   CLK ,
            input [AW-1:0]          A ,
            input [DW-1:0]          D ,
            input                   EN ,
    
    
    
    
        
    
            input                   WR ,    //1 for write and 0 for read
            output reg [DW-1:0]     Q
        reg [DW-1:0]         mem [0:(1<<AW)-1] ;
        always @(posedge CLK) begin
            if (EN && WR) begin
                mem[A]  <= D ;
            else if (EN && !WR) begin
                Q       <= mem[A] ;
    endmodule
    

    仿真时,只需在上一例的 testbench 中,将本次例化的模块 u_ram 覆盖掉 u_ram_4x4, 或重新添加之即可。
    仿真结果如下。由图可知,ram 模块的参数 AW 与 DW 均被改写为 4, 且 ram 行为正确。
    在这里插入图片描述
    区别与建议
    (1) 和模块端口实例化一样,带参数例化时,也可以不指定原有参数名字,按顺序进行参数例化,例如 u_ram 的例化可以描述为:

    ram #(4, 4)   u_ram (......) ;
    

    (2) 当然,利用 defparam 也可以改写模块在端口声明时声明的参数,利用带参数例化也可以改写模块实体中声明的参数。例如 u_ram 和 u_ram_4x4 的例化分别可以描述为:

    defparam     u_ram.AW = 4 ;
    defparam     u_ram.DW = 4 ;
    ram   u_ram(......);
    ram_4x4  #(.MASK(7))    u_ram_4x4(......);
    

    (3) 那能不能混合使用这两种模块参数改写的方式呢?当然能!前提是所有参数都是模块在端口声明时声明的参数或参数都是模块实体中声明的参数,例如 u_ram 的声明还可以表示为(模块实体中参数可自行实验验证):

    defparam     u_ram.AW = 4 ;
    ram #(.DW(4)) u_ram (......); 
    

    (4) 那如果一个模块中既有在模块在端口声明时声明的参数,又有在模块实体中声明的参数,那这两种参数还能同时改写么?例如在 ram 模块中加入 MASK 参数,模型如下:

    module  ram
        #(  parameter       AW = 2 ,
            parameter       DW = 3 )
            input                   CLK ,
            input [AW-1:0]          A ,
            input [DW-1:0]          D ,
            input                   EN ,
            input                   WR ,    //1 for write and 0 for read
            output reg [DW-1:0]     Q    );
        parameter            MASK = 3 ;
        reg [DW-1:0]         mem [0:(1<<AW)-1] ;
        always @(posedge CLK) begin
            if (EN && WR) begin
                mem[A]  <= D ;
            else if (EN && !WR) begin
                Q       <= mem[A] ;
    endmodule
    

    此时再用 defparam 改写参数 MASK 值时,编译报 Error:

    //都采用defparam时会报Error
    defparam     u_ram.AW = 4 ;
    defparam     u_ram.DW = 4 ;
    defparam     u_ram.MASK = 7 ;
    ram   u_ram  (......);
    //模块实体中parameter用defparam改写也会报Error
    defparam     u_ram.MASK = 7 ;
    ram #(.AW(4), .DW(4))   u_ram (......);
    

    重点来了!!!如果你用带参数模块例化的方法去改写参数 MASK 的值,编译不会报错,MASK 也将被成功改写!

    ram #(.AW(4), .DW(4), .MASK(7)) u_ram (......);
    

    可能的解释为,在编译器看来,如果有模块在端口声明时的参数,那么实体中的参数将视为 localparam 类型,使用 defparam 将不能改写模块实体中声明的参数。

    也可能和编译器有关系,大家也可以在其他编译器上实验。

    (5)建议,对已有模块进行例化并将其相关参数进行改写时,不要采用 defparam 的方法。除了上述缺点外,defparam 一般也不可综合。

    (6)而且建议,模块在编写时,如果预知将被例化且有需要改写的参数,都将这些参数写入到模块端口声明之前的地方(用关键字井号 # 表示)。这样的代码格式不仅有很好的可读性,而且方便调试。

    5.4 Verilog 函数

    在 Verilog 中,可以利用任务(关键字为 task)或函数(关键字为 function),将重复性的行为级设计进行提取,并在多个地方调用,来避免重复代码的多次编写,使代码更加的简洁、易懂。

    函数只能在模块中定义,位置任意,并在模块的任何地方引用,作用范围也局限于此模块。函数主要有以下几个特点:

    • 1)不含有任何延迟、时序或时序控制逻辑
    • 2)至少有一个输入变量
    • 3)只有一个返回值,且没有输出
    • 4)不含有非阻塞赋值语句
    • 5)函数可以调用其他函数,但是不能调用任务

    Verilog 函数声明格式如下:

    function [range-1:0]     function_id ;
    input_declaration ;
     other_declaration ;
    procedural_statement ;
    endfunction
    

    函数在声明时,会隐式的声明一个宽度为 range、 名字为 function_id 的寄存器变量,函数的返回值通过这个变量进行传递。当该寄存器变量没有指定位宽时,默认位宽为 1。
    函数通过指明函数名与输入变量进行调用。函数结束时,返回值被传递到调用处。

    函数调用格式如下:

    function_id(input1, input2, …);
    

    下面用函数实现一个数据大小端转换的功能。
    当输入为 4’b0011 时,输出可为 4’b1100。例如:

    module endian_rvs
        #(parameter N = 4)
                input             en,     //enable control
                input [N-1:0]     a ,
                output [N-1:0]    b
            reg [N-1:0]          b_temp ;
            always @(*) begin
            if (en) begin
                    b_temp =  data_rvs(a);
                else begin
                    b_temp = 0 ;
            assign b = b_temp ;
        //function entity
            function [N-1:0]     data_rvs ;
                input     [N-1:0] data_in ;
                parameter         MASK = 32'h3 ;
                integer           k ;
                begin
                    for(k=0; k<N; k=k+1) begin
                        data_rvs[N-k-1]  = data_in[k] ;  
        endfunction
    endmodule        
    

    函数里的参数也可以改写,例如:

    defparam data_rvs.MASK =
    
    
    
    
        
     32'd7 ;
    

    但是仿真时发现,此种写法编译可以通过,仿真结果中,函数里的参数 MASK 实际并没有改写成功,仍然为 32’h3。这可能和编译器有关,有兴趣的学者可以用其他 Verilog 编译器进行下实验。
    函数在声明时,也可以在函数名后面加一个括号,将 input 声明包起来。
    例如上述大小端声明函数可以表示为:

    function [N-1:0]     data_rvs(
    input     [N-1:0] data_in 
        ......
    

    常数函数是指在仿真开始之前,在编译期间就计算出结果为常数的函数。常数函数不允许访问全局变量或者调用系统函数,但是可以调用另一个常数函数。
    这种函数能够用来引用复杂的值,因此可用来代替常量。
    例如下面一个常量函数,可以来计算模块中地址总线的宽度:

    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 ;
    endfunction
    

    automatic 函数

    在 Verilog 中,一般函数的局部变量是静态的,即函数的每次调用,函数的局部变量都会使用同一个存储空间。若某个函数在两个不同的地方同时并发的调用,那么两个函数调用行为同时对同一块地址进行操作,会导致不确定的函数结果。
    Verilog 用关键字 automatic 来对函数进行说明,此类函数在调用时是可以自动分配新的内存空间的,也可以理解为是可递归的。因此,automatic 函数中声明的局部变量不能通过层次命名进行访问,但是 automatic 函数本身可以通过层次名进行调用。

    下面用 automatic 函数,实现阶乘计算:

    wire [31:0]          results3 = factorial(4);
    function automatic   integer         factorial ;
        input integer     data ;
        integer           i ;
        begin
            factorial = (data>=2)? data * factorial(data-1) : 1 ;
    endfunction // factorial
    

    下面是加关键字 automatic 和不加关键字 automatic 的仿真结果。
    由图可知,信号 results3 得到了我们想要的结果,即 4 的阶乘。
    而信号 results_noauto 值为 1,不是可预知的正常结果,这里不再做无用分析。
    在这里插入图片描述

    5.5 Verilog 任务

    任务与函数的区别

    和函数一样,任务(task)可以用来描述共同的代码段,并在模块内任意位置被调用,让代码更加的直观易读。函数一般用于组合逻辑的各种转换和计算,而任务更像一个过程,不仅能完成函数的功能,还可以包含时序控制逻辑。下面对任务与函数的区别进行概括:

    比较点函数任务
    输入函数至少有一个输入,端口声明不能包含 inout 型任务可以没有或者有多个输入,且端口声明可以为 inout 型
    输出函数没有输出任务可以没有或者有多个输出
    返回值函数至少有一个返回值任务没有返回值
    仿真时刻函数总在零时刻就开始执行任务可以在非零时刻执行
    时序逻辑函数不能包含任何时序控制逻辑任务不能出现 always 语句,但可以包含其他时序控制,如延时语句
    调用函数只能调用函数,不能调用任务任务可以调用函数和任务
    书写规范函数不能单独作为一条语句出现,只能放在赋值语言的右端任务可以作为一条单独的语句出现语句块中

    任务在模块中任意位置定义,并在模块内任意位置引用,作用范围也局限于此模块。
    模块内子程序出现下面任意一个条件时,则必须使用任务而不能使用函数。

    • 1)子程序中包含时序控制逻辑,例如延迟,事件控制等
    • 2)没有输入变量
    • 3)没有输出或输出端的数量大于 1

    Verilog 任务声明格式如下:

    task       	task_id ;
       		    port_declaration ;
       		    procedural_statement ;
    endtask
    

    任务中使用关键字 input、output 和 inout 对端口进行声明。input 、inout 型端口将变量从任务外部传递到内部,output、inout 型端口将任务执行完毕时的结果传回到外部。
    进行任务的逻辑设计时,可以把 input 声明的端口变量看做 wire 型,把 output 声明的端口变量看做 reg 型。但是不需要用 reg 对 output 端口再次说明。
    对 output 信号赋值时也不要用关键字 assign。为避免时序错乱,建议 output 信号采用阻塞赋值。
    例如,一个带延时的异或功能 task 描述如下:

    task xor_oper_iner;
        input [N-1:0]   numa;
        input [N-1:0]   numb;
        output [N-1:0]  numco ;
        //output reg [N-1:0]  numco ; //无需再注明 reg 类型,虽然注明也可能没错
        #3  numco = numa ^ numb ;
        //assign #3 numco = numa ^ numb ; //不用assign,因为输出默认是reg
    endtask
    

    任务在声明时,也可以在任务名后面加一个括号,将端口声明包起来。
    上述设计可以更改为:

    task xor_oper_iner(
        input [N-1:0]   numa,
        input [N-1:0]   numb,
        output [N-1:0]  numco  ) ;
        #3  numco       = numa ^ numb ;
    endtask
    

    任务可单独作为一条语句出现在 initial 或 always 块中,调用格式如下:

    task_id(input1, input2, …,outpu1, output2, …);
    

    任务调用时,端口必须按顺序对应。
    输入端连接的模块内信号可以是 wire 型,也可以是 reg 型。输出端连接的模块内信号要求一定是 reg 型,这点需要注意。
    对上述异或功能的 task 进行一个调用,完成对异或结果的缓存。

    module xor_oper
        #(parameter         N = 4)
          input             clk ,
          input             rstn ,
          input [N-1:0]     a ,
          input [N-1:0]     b ,
          output [N-1:0]    co  );
        reg [N-1:0]          co_t ;
        always @(*) begin          //任务调用
            xor_oper_iner(a, b, co_t);
        reg [N-1:0]          co_r ;
        always @(posedge clk or negedge rstn) begin
            if (!rstn) begin
                co_r   <= 'b0 ;
            else begin
                co_r   <= co_t ;         //数据缓存
        assign       co = co_r ;
       /*------------ task -------*/
        task xor_oper_iner;
            input [N-1:0]   numa;
            input [N-1:0]   numb;
            output [N-1:0]  numco ;
            #3  numco       = numa ^ numb ;   //阻塞赋值,易于控制时序
        endtask
    endmodule
    

    对上述异或功能设计进行简单的仿真,testbench 描述如下。
    激励部分我们使用简单的 task 进行描述,激励看起来就更加的清晰简洁。
    其实,task 最多的应用场景还是应用于 testbench 中进行仿真。task 在一些编译器中也不支持综合。

    `timescale 1ns/1ns
    module test ;
        reg          clk, rstn ;
        initial begin
            rstn      = 0 ;
            #8 rstn   = 1 ;
            forever begin
                clk = 0 ; # 5;
                clk = 1 ; # 5;
        reg  [3:0]   a, b;
        wire [3:0]   co ;
        initial begin
            a         = 0 ;
            b         = 0 ;
            sig_input(4'b1111, 4'b1001, a, b);
            sig_input(4'b0110, 4'b1001, a, b);
            sig_input(4'b1000, 4'b1001, a, b);
        task sig_input ;
            input [3:0]       a ;
            input [3:0]       b ;
            output [3:0]      ao ;
            output [3:0]      bo ;
            @(posedge clk) ;
            ao = a ;
            bo = b ;
        endtask ; // sig_input
        xor_oper         u_xor_oper
          .clk              (clk  ),
          .rstn             (rstn ),
          .a                (a    ),
          .b                (b    ),
          .co               (co   ));
        initial begin
            forever begin
                #100;
                if ($time >= 1000)  $finish ;
    endmodule // test
    

    仿真结果如下。
    在这里插入图片描述
    由图可知,异或输出逻辑结果正确,相对于输入有 3ns 的延迟。
    且连接信号 a,b,co_t 与任务内部定义的信号 numa,numb,numco 状态也保持一致。

    任务操作全局变量

    因为任务可以看做是过程性赋值,所以任务的 output 端信号返回时间是在任务中所有语句执行完毕之后。
    任务内部变量也只有在任务中可见,如果想具体观察任务中对变量的操作过程,需要将观察的变量声明在模块之内、任务之外,可谓之"全局变量"。
    例如有以下 2 种尝试利用 task 产生时钟的描述方式。

    //way1 to decirbe clk generating, not work
    task clk_rvs_iner ;
            output    clk_no_rvs ;
            # 5 ;     clk_no_rvs = 0 ;
            # 5 ;     clk_no_rvs = 1 ;
    endtask
    reg          clk_test1 ;
    always clk_rvs_iner(clk_test1);
    //way2: use task to operate global varialbes to generating clk
    reg          clk_test2 ;
    task clk_rvs_global ;
            # 5 ;     clk_test2 = 0 ;
            # 5 ;     clk_test2 = 1 ;
    endtask // clk_rvs_iner
    always clk_rvs_global;
    

    仿真结果如下:
    在这里插入图片描述
    第一种描述方式,虽然任务内部变量会有赋值 0 和赋值 1 的过程操作,但中间变化过程并不可见,最后输出的结果只能是任务内所有语句执行完毕后输出端信号的最终值。所以信号 clk_test1 值恒为 1,此种方式产生不了时钟。
    第二种描述方式,虽然没有端口信号,但是直接对"全局变量"进行过程操作,因为该全局变量对模块是可见的,所以任务内信号翻转的过程会在信号 clk_test2 中体现出来。

    automatic 任务

    和函数一样,Verilog 中任务调用时的局部变量都是静态的。可以用关键字 automatic 来对任务进行声明,那么任务调用时各存储空间就可以动态分配,每个调用的任务都各自独立的对自己独有的地址空间进行操作,而不影响多个相同任务调用时的并发执行。
    如果一任务代码段被 2 处及以上调用,一定要用关键字 automatic 声明。
    当没有使用 automatic 声明任务时,任务被 2 次调用,可能出现信号间干扰,例如下面代码描述:

    task test_flag ;
            input [3:0]       cnti ;
            input             en ;
            output [3:0]      cnto ;
            if (en) cnto = cnti ;
    endtask
    reg          en_cnt ;
    reg [3:0]    cnt_temp ;
    initial begin
            en_cnt    = 1 ;
            cnt_temp  = 0 ;
            #25 ;     en_cnt = 0 ;
    always #10 cnt_temp = cnt_temp + 1 ;
    reg [3:0]             cnt1, cnt2 ;
    always @(posedge clk) test_flag(2, en_cnt, cnt1);       //task(1)
    always @(posedge clk) test_flag(cnt_temp, !en_cnt, cnt2);//task(2)
    

    仿真结果如下:
    在这里插入图片描述
    en_cnt 为高时,任务 (1) 中信号 en 有效, cnt1 能输出正确的逻辑值;
    此时任务 (2) 中信号 en 是不使能的,所以 cnt2 的值被任务 (1) 驱动的共用变量 cnt_temp 覆盖。
    en_cnt 为低时,任务 (2) 中信号 en 有效,所以任务 (2) 中的信号 cnt2 能输出正确的逻辑值;而此时信号 cnt1 的值在时钟的驱动下,一次次被任务 (2) 驱动的共用变量 cnt_temp 覆盖。
    可见,任务在两次并发调用中,共用存储空间,导致信号相互间产生了影响。
    其他描述不变,只在上述 task 声明时加入关键字 automatic,如下所以。

    task automatic test_flag ;
    

    此时仿真结果如下:
    在这里插入图片描述
    en_cnt 为高时,任务 (1) 中信号 cnt1 能输出正确的逻辑值,任务 (2) 中信号 cnt2 的值为 X;
    en_cnt 为低时,任务 (2) 中信号 cnt2 能输出正确的逻辑值,任务 (1) 中信号 cnt1 的值为 X;
    可见,任务在两次并发调用中,因为存储空间相互独立,信号间并没有产生影响。

      结构建模方式有 3 类描述语句: Gate(门级)例化语句,UDP (用户定义原语)例化语句和 module (模块) 例化语句。本次主要讲述使用最多的模块级例化语句。文章目录5.1 Verilog 模块与端口模块端口inout 端口仿真5.2 Verilog 模块例化命名端口连接顺序端口连接端口连接规则5.1 Verilog 模块与端口模块模块是 Verilog 中基本单元的定义形式,是与外界交互的接口。模块格式定义如下:module module_name #(parameter_l
    所有的端口声明都隐含地声明为wire,因此在这种情况下端口方向就足够了。然而需要存储值的输出端口应该声明为 reg 数据类型,并且可以在程序块中使用,比如 always 和 initial only。 输入或inout类型的端口不能声明为reg,因为它们是由外部连续驱动的,不应该存储值,而是尽快反映外部信号的变化。连接两个不同向量大小的端口是完全合法的,但以向量大小较小的端口为准,而另一个宽度较大的端口的剩余位将被忽略。
    Verilog module 模块例化与简单层次电路设计 在上节课中我们介绍了在Verilog 中,逻辑电路的入口及行为描述。语法规则非常简单,说穿了就是数字电路的文字描述。也就在数字电路中的单一电路都可以用这种方法进行描述。但是在数字电路设计中,复杂电路往往是由多个子电路或器件实现的,在Verilog中是如何对应的呢? 在Verilog 结构中可以通过例化的方式实现设计的层次结构。本节课程中我们将以1位全加器的设计,例化(instance), 然后在4位全加器中引用为例介绍如何通过这种方式,实现层次电
    一个模块能够在另外一个模块中被引用, 这样就建立了描述的层次。模块实例化语句形式如下: module_name instance_name(port_associations) ; 连接信号到模块端口 有两种常用的方法将信号连接到端口上:位置和名称。 通过位置将信号连到端口上的语法应该比较熟悉,因为这是类c语言的语法。当实例化一个模块时,根据这个模块声明时的端口顺序从...
    最近在做nandflash的擦写,要用VHDL编写激励文件做仿真,之前一直用的Verilog,现在总结如何用VHDL语言编写testbench。 library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.STD_LOGIC_UNSIGNED.ALL; use IEEE.STD_LOGIC_ARITH.ALL; entity (测试平台名)is end...
 
推荐文章
绅士的毛衣  ·  基于JavaScript 声明全局变量的三种方式详解 - 听风。
1 月前
严肃的面包  ·  拥抱科技、融合创新!看复旦老师们如何玩转线上课堂
3 月前
内向的冲锋衣  ·  浙江省卫生健康委等关于加强心理健康服务的指导意见
4 月前
听话的眼镜  ·  区块链泡沫中掘金: 巨头加码、 连续创业者入场背后 - 21财经
4 月前
满身肌肉的火柴  ·  北京市小客车指标管理信息系统
12 月前
飞翔的创口贴  ·  京籍家庭与非京籍家庭关于异地高考的故事--时政--人民网
1 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号