Verilog:基础语法(下)

Verilog:基础语法(上)

模块与端口

  • 关键词:模块,端口,双向端口,PAD
  • 结构建模方式有 3 类描述语句: Gate(门级)例化语句,UDP (用户定义原语)例化语句和 module (模块) 例化语句。本次主要讲述使用最多的模块级例化语句。

    模块是 Verilog 中基本单元的定义形式,是与外界交互的接口。

    模块格式定义如下:

    module module_name 
    #(parameter_list)
    (port_list) ;
                  Declarations_and_Statements ;
    endmodule
    

    模块定义必须以关键字 module 开始,以关键字 endmodule 结束。

    模块名,端口信号,端口声明和可选的参数声明等,出现在设计使用的 Verilog 语句(图中 Declarations_and_Statements)之前。

    模块内部有可选的 5 部分组成,分别是变量声明,数据流语句,行为级语句,低层模块例化及任务和函数,如下图表示。这 5 部分出现顺序、出现位置都是任意的。但是,各种变量都应在使用之前声明。变量具体声明的位置不要求,但必须保证在使用之前的位置。

    一个模块如果和外部环境没有交互,则可以不用声明端口列表。例如之前我们仿真时 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 ;
    

    (2) 在 Verilog 中,端口隐式的声明为 wire 型变量,即当端口具有 wire 属性时,不用再次声明端口类型为 wire 型。但是,当端口有 reg 属性时,则 reg 声明不可省略。

    上述例子中的端口声明,则可以简化为:

    //端口类型声明
    input        DIN, OEN ;
    input [1:0]  PULL ;    
    inout        PAD ;    
    output       DOUT ;    
    reg          DOUT ;
    

    (3) 当然,信号 DOUT 的声明完全可以合并成一句:

    output reg      DOUT ;
    

    (4) 还有一种更简洁且常用的方法来声明端口,即在 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 ;
    
  • 关键字:例化,generate,全加器,层次访问
    在一个模块中引用另一个模块,对其端口进行相关连接,叫做模块例化。模块例化建立了描述的层次。信号端口可以通过位置或名称关联,端口连接也必须遵循一些规则。

  • 命名端口连接
    这种方法将需要例化的模块端口与外部信号按照其名字进行连接,端口顺序随意,可以与引用 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
    

    每一个例化模块的名字,每个模块的信号变量等,都使用一个特定的标识符进行定义。在整个层次设计中,每个标识符都具有唯一的位置与名字。

    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 ;
    
  • 关键词: defparam,参数,例化,ram
    当一个模块被另一个模块引用例化时,高层模块可以对低层模块的参数值进行改写。这样就允许在编译时将不同的参数传递给多个相同名字的模块,而不用单独为只有参数不同的多个模块再新建文件。
  • 参数覆盖有 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
    
  • 带参数模块例化
    第二种方法就是例化模块时,将新的参数值写入模块例化语句,以此来改写原有 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
    

    区别与建议

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

  • 关键词:函数,大小端转换,数码管译码
    在 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
    
  • 数码管译码
    每位数码显示端有 8 个光亮控制端(如图中 a-g 所示),可以用来控制显示数字 0-9 。
  • 而数码管有 4 个片选(如图中 1-4),用来控制此时哪一位数码显示端应该选通,即应该发光。倘若在很短的时间内,依次对 4 个数码显示端进行片选发光,同时在不同片选下给予不同的光亮控制(各对应 4 位十进制数字),那么在肉眼不能分辨的情况下,就达到了同时显示 4 位十进制数字的效果。

    下面,我们用信号 abcdefg 来控制光亮控制端,用信号 csn 来控制片选,4 位 10 进制的数字个十百千位分别用 4 个 4bit 信号 single_digit, ten_digit, hundred_digit, kilo_digit 来表示,则一个数码管的显示设计可以描述如下:

    module digital_tube
          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;
            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);
                2'd1:begin
                    scan_r    <= 3'd2;
                    csn       <= 4'b1011;     //select ten digit
                    abcdefg   <= dt_translate(ten_digit);
                2'd2:begin
                    scan_r    <= 3'd3;
                    csn       <= 4'b1101;     //select hundred digit
                    abcdefg   <= dt_translate(hundred_digit);
                2'd3:begin
                    scan_r    <= 3'd0;
                    csn       <= 4'b1110;     //select kilo digit
                    abcdefg   <= dt_translate(kilo_digit);
                endcase
        /*------------ 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
        endfunction
    endmodule
    
  • 关键词:任务

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

    任务在模块中任意位置定义,并在模块内任意位置引用,作用范围也局限于此模块。

    模块内子程序出现下面任意一个条件时,则必须使用任务而不能使用函数。

    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 型,这点需要注意。

    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
    
  • 任务操作全局变量
  • 因为任务可以看做是过程性赋值,所以任务的 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;
    
  • 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)
    
  • 关键词:状态机,售卖机
  • 有限状态机(Finite-State Machine,FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。状态机不仅是一种电路的描述工具,而且也是一种思想方法,在电路设计的系统级和 RTL 级有着广泛的应用。

  • 状态机类型
    Verilog 中状态机主要用于同步时序逻辑的设计,能够在有限个状态之间按一定要求和规律切换时序电路的状态。状态的切换方向不但取决于各个输入值,还取决于当前所在状态。 状态机可分为 2 类:Moore 状态机和 Mealy 状态机。

  • Moore 型状态机

  • Moore 型状态机的输出只与当前状态有关,与当前输入无关。

    输出会在一个完整的时钟周期内保持稳定,即使此时输入信号有变化,输出也不会变化。输入对输出的影响要到下一个时钟周期才能反映出来。这也是 Moore 型状态机的一个重要特点:输入与输出是隔离开来的。

    (0) 首先,根据状态机的个数确定状态机编码。利用编码给状态寄存器赋值,代码可读性更好。
    (1) 状态机第一段,时序逻辑,非阻塞赋值,传递寄存器的状态。
    (2) 状态机第二段,组合逻辑,阻塞赋值,根据当前状态和当前输入,确定下一个状态机的状态。
    (3) 状态机第三代,时序逻辑,非阻塞赋值,因为是 Mealy 型状态机,根据当前状态和当前输入,确定输出信号。

    // vending-machine
    // 2 yuan for a bottle of drink
    // only 2 coins supported: 5 jiao and 1 yuan
    // finish the function of selling and changing
    module  vending_machine_p3  (
        input           clk ,
        input           rstn ,
        input [1:0]     coin ,     //01 for 0.5 jiao, 10 for 1 yuan
        output [1:0]    change ,
        output          sell    //output the drink
        //machine state decode
        parameter            IDLE   = 3'd0 ;
        parameter            GET05  = 3'd1 ;
        parameter            GET10  = 3'd2 ;
        parameter            GET15  = 3'd3 ;
        //machine variable
        reg [2:0]            st_next ;
        reg [2:0]            st_cur ;
        //(1) state transfer
        always @(posedge clk or negedge rstn) begin
            if (!rstn) begin
                st_cur      <= 'b0 ;
            else begin
                st_cur      <= st_next ;
        //(2) state switch, using block assignment for combination-logic
        //all case items need to be displayed completely    
        always @(*) begin
            //st_next = st_cur ;//如果条件选项考虑不全,可以赋初值消除latch
            case(st_cur)
                IDLE:
                    case (coin)
                        2'b01:     st_next = GET05 ;
                        2'b10:     st_next = GET10 ;
                        default:   st_next = IDLE ;
                    endcase
                GET05:
                    case (coin)
                        2'b01:     st_next = GET10 ;
                        2'b10:     st_next = GET15 ;
                        default:   st_next = GET05 ;
                    endcase
                GET10:
                    case (coin)
                        2'b01:     st_next = GET15 ;
                        2'b10:     st_next = IDLE ;
                        default:   st_next = GET10 ;
                    endcase
                GET15:
                    case (coin)
                        2'b01,2'b10:
                                   st_next = IDLE ;
                        default:   st_next = GET15 ;
                    endcase
                default:    st_next = IDLE ;
            endcase
        //(3) output logic, using non-block assignment
        reg  [1:0]   change_r ;
        reg          sell_r ;
        always @(posedge clk or negedge rstn) begin
            if (!rstn) begin
                change_r       <= 2'b0 ;
                sell_r         <= 1'b0 ;
            else if ((st_cur == GET15 && coin ==2'h1)
                   || (st_cur == GET10 && coin ==2'd2)) begin
                change_r       <= 2'b0 ;
                sell_r         <= 1'b1 ;
            else if (st_cur == GET15 && coin == 2'h2) begin
                change_r       <= 2'b1 ;
                sell_r         <= 1'b1 ;
            else begin
                change_r       <= 2'b0 ;
                sell_r         <= 1'b0 ;
        assign       sell    = sell_r ;
        assign       change  = change_r ;
    endmodule
    
  • Verilog 书写规范
    在编程时多注意以下几点,也可以避免大多数的竞争与冒险问题。
  • 1)时序电路建模时,用非阻塞赋值。
    2)组合逻辑建模时,用阻塞赋值。
    3)在同一个 always 块中建立时序和组合逻辑模型时,用非阻塞赋值。
    4)在同一个 always 块中不要既使用阻塞赋值又使用非阻塞赋值。
    5)不要在多个 always 块中为同一个变量赋值。
    6)避免 latch 产生。
    下面,对以上注意事项逐条分析。

    1)时序电路建模时,用非阻塞赋值
    前面讲述非阻塞赋值时就陈述过,时序电路中非阻塞赋值可以消除竞争冒险。

    例如下面代码描述,由于无法确定 a 与 b 阻塞赋值的操作顺序,就有可能带来竞争冒险。

    always @(posedge clk) begin
        a = b ;
        b = a ;
    

    而使用非阻塞赋值时,赋值操作是同时进行的,所以就不会带来竞争冒险,如以下代码描述。

    always @(posedge clk) begin
        a <= b ;
        b <= a ;
    

    2)组合逻辑建模时,用阻塞赋值
    例如,我们想实现 C = A&B, F=C&D 的组合逻辑功能,用非阻塞赋值语句如下。
    两条赋值语句同时赋值,F <= C & D 中使用的是信号 C 的旧值,所以导致此时的逻辑是错误的,F 的逻辑值不等于 A&B&D。

    而且,此时要求信号 C 具有存储功能,但不是时钟驱动,所以 C 可能会被综合成锁存器(latch),导致竞争冒险。

    always @(*) begin
        C <= A & B ;
        F <= C & D ;
    

    对代码进行如下修改,F = C & D 的操作一定是在 C = A & B 之后,此时 F 的逻辑值等于 A&B&D,符合设计。

    always @(*) begin
        C = A & B ;
        F = C & D ;
    

    3)在同一个 always 块中建立时序和组合逻辑模型时,用非阻塞赋值

    虽然时序电路中可能涉及组合逻辑,但是如果赋值操作使用非阻塞赋值,仍然会导致如规范 1 中所涉及的类似问题。

    例如在时钟驱动下完成一个与门的逻辑功能,代码参考如下。

    always @(posedge clk or negedge rst_n)
        if (!rst_n) begin
            q <= 1'b0;
        else begin
            q <= a & b;  //即便有组合逻辑,也不要写成:q = a & b
    

    4)在同一个 always 块中不要既使用阻塞赋值又使用非阻塞赋值
    always 涉及的组合逻辑中,既有阻塞赋值又有非阻塞赋值时,会导致意外的结果,例如下面代码描述。

    此时信号 C 阻塞赋值完毕以后,信号 F 才会被非阻塞赋值,仿真结果可能正确。

    但如果 F 信号有其他的负载,F 的最新值并不能马上传递出去,数据有效时间还是在下一个触发时刻。此时要求 F 具有存储功能,可能会被综合成 latch,导致竞争冒险。

    always @(*) begin
        C = A & B ;
        F <= C & D ;
    

    如下代码描述,仿真角度看,信号 C 被非阻塞赋值,下一个触发时刻才会有效。而 F = C & D 虽然是阻塞赋值,但是信号 C 不是阻塞赋值,所以 F 逻辑中使用的还是 C 的旧值。

    always @(*) begin
        C <= A & B ;
        F = C & D ;
    

    下面分析假如在时序电路里既有阻塞赋值,又有非阻塞赋值会怎样,代码如下。

    假如复位端与时钟同步,那么由于复位导致的信号 q 为 0,是在下一个时钟周期才有效。

    而如果是信号 a 或 b 导致的 q 为 0,则在当期时钟周期内有效。

    如果 q 还有其他负载,就会导致 q 的时序特别混乱,显然不符合设计需求。

    always @(posedge clk or negedge rst_n)
        if (!rst_n) begin  //假设复位与时钟同步
            q <= 1'b0;
        else begin
            q = a & b;  
    

    需要说明的是,很多编译器都支持这么写,上述的分析也都是建立在仿真角度上。实际中如果阻塞赋值和非阻塞赋值混合编写,综合后的电路时序将是错乱的,不利于分析调试。

    5)不要在多个 always 块中为同一个变量赋值
    与 C 语言有所不同,Verilog 中不允许在多个 always 块中为同一个变量赋值。此时信号拥有多驱动端(Multiple Driver),是禁止的。当然,也不允许 assign 语句为同一个变量进行多次连线赋值。 从信号角度来讲,多驱动时,同一个信号变量在很短的时间内进行多次不同的赋值结果,就有可能产生竞争冒险。

    从语法来讲,很多编译器检测到多驱动时,也会报 Error。

    避免 Latch

  • 关键词:触发器,锁存器
  • Latch 的含义
  • 锁存器(Latch),是电平触发的存储单元,数据存储的动作取决于输入时钟(或者使能)信号的电平值。仅当锁存器处于使能状态时,输出才会随着数据输入发生变化。

    当电平信号无效时,输出信号随输入信号变化,就像通过了缓冲器;当电平有效时,输出信号被锁存。激励信号的任何变化,都将直接引起锁存器输出状态的改变,很有可能会因为瞬态特性不稳定而产生振荡现象。

    触发器(flip-flop),是边沿敏感的存储单元,数据存储的动作(状态转换)由某一信号的上升沿或者下降沿进行同步的(限制存储单元状态转换在一个很短的时间内)。

    触发器示意图如下:

    寄存器(register),在 Verilog 中用来暂时存放参与运算的数据和运算结果的变量。一个变量声明为寄存器时,它既可以被综合成触发器,也可能被综合成 Latch,甚至是 wire 型变量。但是大多数情况下我们希望它被综合成触发器,但是有时候由于代码书写问题,它会被综合成不期望的 Latch 结构。

    Latch 的主要危害有:

    1)输入状态可能多次变化,容易产生毛刺,增加了下一级电路的不确定性;
    2)在大部分 FPGA 的资源中,可能需要比触发器更多的资源去实现 Latch 结构;
    3)锁存器的出现使得静态时序分析变得更加复杂。
    Latch 多用于门控时钟(clock gating)的控制,一般设计时,我们应当避免 Latch 的产生。

  • if 结构不完整
    组合逻辑中,不完整的 if - else 结构,会产生 latch。
  • 例如下面的模型,if 语句中缺少 else 结构,系统默认 else 的分支下寄存器 q 的值保持不变,即具有存储数据的功能,所以寄存器 q 会被综合成 latch 结构。

    module module1_latch1(
        input       data,
        input       en ,
        output reg  q) ;
        always @(*) begin
            if (en) q = data ;
    endmodule
    

    避免此类 latch 的方法主要有 2 种,一种是补全 if-else 结构,或者对信号赋初值。

    例如,上面模型中的always语句,可以改为以下两种形式:

        // 补全条件分支结构    
        always @(*) begin
            if (en)  q = data ;
            else     q = 1'b0 ;
        //赋初值
        always @(*) begin
            q = 1'b0 ;
            if (en) q = data ; //如果en有效,改写q的值,否则q会保持为0
    

    但是在时序逻辑中,不完整的 if - else 结构,不会产生 latch,例如下面模型。

    这是因为,q 寄存器具有存储功能,且其值在时钟的边沿下才会改变,这正是触发器的特性。

    module module1_ff(
        input       clk ,
        input       data,
        input       en ,
        output reg  q) ;
        always @(posedge clk) begin
            if (en) q <= data ;
    endmodule
    

    在组合逻辑中,当条件语句中有很多条赋值语句时,每个分支条件下赋值语句的不完整也是会产生 latch。

    其实对每个信号的逻辑拆分来看,这也相当于是 if-else 结构不完整,相关寄存器信号缺少在其他条件下的赋值行为。例如:

    module module1_latch11(
        input       data1,
        input       data2,
        input       en ,
        output reg  q1 ,
        output reg  q2) ;
        always @(*) begin
            if (en)   q1 = data1 ;
            else      q2 = data2 ;
    endmodule
    

    这种情况也可以通过补充完整赋值语句或赋初值来避免 latch。例如:

        always @(*) begin
            //q1 = 0; q2 = 0 ; //或在这里对 q1/q2 赋初值
            if (en)  begin
                q1 = data1 ;
                q2 = 1'b0 ;
            else begin
                q1 = 1'b0 ;
                q2 = data2 ;
    
  • case 结构不完整
    case 语句产生 Latch 的原理几乎和 if 语句一致。在组合逻辑中,当 case 选项列表不全且没有加 default 关键字,或有多个赋值语句不完整时,也会产生 Latch。例如:
  • module module1_latch2(
        input       data1,
        input       data2,
        input [1:0] sel ,
        output reg  q ) ;
        always @(*) begin
            case(sel)
                2'b00:  q = data1 ;
                2'b01:  q = data2 ;
            endcase
    endmodule
    

    当然,消除此种 latch 的方法也是 2 种,将 case 选项列表补充完整,或对信号赋初值。

    当然,补充完整 case 选项列表时,可以罗列所有的选项结果,也可以用 default 关键字来代替其他选项结果。

    例如,上述 always 语句有以下 2 种修改方式。

        always @(*) begin
            case(sel)
                2'b00:    q = data1 ;
                2'b01:    q = data2 ;
                default:  q = 1'b0 ;
            endcase
        always @(*) begin
            case(sel)
                2'b00:  q = data1 ;
                2'b01:  q = data2 ;
                2'b10, 2'b11 :  
                        q = 1'b0 ;
            endcase
    

    原信号赋值或判断
    在组合逻辑中,如果一个信号的赋值源头有其信号本身,或者判断条件中有其信号本身的逻辑,则也会产生 latch。因为此时信号也需要具有存储功能,但是没有时钟驱动。此类问题在 if 语句、case 语句、问号表达式中都可能出现,例如:

        //signal itself as a part of condition
        reg a, b ;
        always @(*) begin
            if (a & b)  a = 1'b1 ;   //a -> latch
            else a = 1'b0 ;
        //signal itself are the assigment source
        reg        c;
        wire [1:0] sel ;
        always @(*) begin
            case(sel)
                2'b00:    c = c ;    //c -> latch
                2'b01:    c = 1'b1 ;
                default:  c = 1'b0 ;
            endcase
        //signal itself as a part of condition in "? expression"
        wire      d, sel2;
        assign    d =  (sel2 && d) ? 1'b0 : 1'b1 ;  //d -> latch
    

    避免此类 Latch 的方法,就只有一种,即在组合逻辑中避免这种写法,信号不要给信号自己赋值,且不要用赋值信号本身参与判断条件逻辑。

    例如,如果不要求立刻输出,可以将信号进行一个时钟周期的延时再进行相关逻辑的组合。上述第一个产生 Latch 的代码可以描述为:

        reg   a, b ;
        reg   a_r ;
        always (@posedge clk)
            a_r  <= a ;
        always @(*) begin
            if (a_r & b)  a = 1'b1 ;   //there is no latch
            else a = 1'b0 ;
    

    敏感信号列表不完整
    如果组合逻辑中 always@() 块内敏感列表没有列全,该触发的时候没有触发,那么相关寄存器还是会保存之前的输出结果,因而会生成锁存器。

    这种情况,把敏感信号补全或者直接用 always@(*) 即可消除 latch。

    总之,为避免 latch 的产生,在组合逻辑中,需要注意以下几点:

    1)if-else 或 case 语句,结构一定要完整
    2)不要将赋值信号放在赋值源头,或条件判断中
    3)敏感信号列表建议多用 always@(*)

  • 关键词:testbench,仿真,文件读写
    Verilog 代码设计完成后,还需要进行重要的步骤,即逻辑功能仿真。仿真激励文件称之为 testbench,放在各设计模块的顶层,以便对模块进行系统性的例化调用进行仿真。
  • 毫不夸张的说,对于稍微复杂的 Verilog 设计,如果不进行仿真,即便是经验丰富的老手,99.9999% 以上的设计都不会正常的工作。不能说仿真比设计更加的重要,但是一般来说,仿真花费的时间会比设计花费的时间要多。有时候,考虑到各种应用场景,testbench 的编写也会比 Verilog 设计更加的复杂。所以,数字电路行业会具体划分设计工程师和验证工程师。

    下面,对 testbench 做一个简单的学习。

    testbench 结构划分
    testbench 一般结构如下:

    根据设计的复杂度,需要引入时钟和复位部分。当然更为复杂的设计,激励部分也会更加复杂。根据自己的验证需求,选择是否需要自校验和停止仿真部分。

    当然,复位和时钟产生部分,也可以看做激励,所以它们都可以在一个语句块中实现。也可以拿自校验的结果,作为结束仿真的条件。

  • testbench 具体分析
    1)信号声明
  • testbench 模块声明时,一般不需要声明端口。因为激励信号一般都在 testbench 模块内部,没有外部信号。

    声明的变量应该能全部对应被测试模块的端口。当然,变量不一定要与被测试模块端口名字一样。但是被测试模块输入端对应的变量应该声明为 reg 型,如 clk,rstn 等,输出端对应的变量应该声明为 wire 型,如 dout,dout_en。

    2)时钟生成

    生成时钟的方式有很多种,例如以下两种生成方式也可以借鉴。

    initial clk = 0 ;
    always #(CYCLE_200MHz/2) clk = ~clk;
    initial begin
        clk = 0 ;
        forever begin
            #(CYCLE_200MHz/2) clk = ~clk;
    

    需要注意的是,利用取反方法产生时钟时,一定要给 clk 寄存器赋初值。

    利用参数的方法去指定时间延迟时,如果延时参数为浮点数,该参数不要声明为 parameter 类型。例如实例中变量 CYCLE_200MHz 的值为 2.5。如果其变量类型为 parameter,最后生成的时钟周期很可能就是 4ns。当然,timescale 的精度也需要提高,单位和精度不能一样,否则小数部分的时间延迟赋值也将不起作用。

    3)复位生成

    复位逻辑比较简单,一般赋初值为 0,再经过一段小延迟后,复位为 1 即可。

    这里大多数的仿真都是用的低有效复位。

    4)激励部分

    激励部分该产生怎样的输入信号,是根据被测模块的需要来设计的。
    5)模块例化

    这里利用 testbench 开始声明的信号变量,对被测试模块进行例化连接。

    6)自校验

    如果设计比较简单,完全可以通过输入、输出信号的波形来确定设计是否正确,此部分完全可以删除。如果数据很多,有时候拿肉眼观察并不能对设计的正确性进行一个有效判定。此时加入一个自校验模块,会是大大增加仿真的效率。

    实例中,我们会在数据输出使能 dout_en 有效时,对输出数据 dout 与参考数据 read_temp(激励部分产生)做一个对比,并将对比结果置于信号 err_cnt 中。最后我们就可以通过观察 err_cnt 信号是否为 0 来直观的对设计进行判断。

    当然如实例中所示,我们也可以将数据写入到对应文件中,利用其他方式做对比。

    7)结束仿真

    如果我们不加入结束仿真部分,仿真就会无限制的运行下去,波形太长有时候并不方便分析。Verilog 中提供了系统任务 $finish 来停止仿真。

  • 文件读写选项
    用于打开文件的系统任务 $fopen 格式如下:
  • fd = $fopen("<name_of_file>", "mode")
    

    和 C 语言类似,打开方式的选项 "mode" 意义如下:

    也许有人会问,直接用乘号 * 来完成 2 个数的相乘不是更快更简单吗?

    如果你有这个疑问,说明你对硬件描述语言的认知还有所不足。就像之前所说,Verilog 描述的是硬件电路,直接用乘号完成相乘过程,编译器在编译的时候也会把这个乘法表达式映射成默认的乘法器,但其构造不得而知。

    例如,在 FPGA 设计中,可以直接调用 IP 核来生成一个高性能的乘法器。在位宽较小的时候,一个周期内就可以输出结果,位宽较大时也可以流水输出。在能满足要求的前提下,可以谨慎的用 * 或直接调用 IP 来完成乘法运算。

    但乘法器 IP 也有很多的缺陷,例如位宽的限制,未知的时序等。尤其使用乘号,会为数字设计的不确定性埋下很大的隐瞒。

    很多时候,常数的乘法都会用移位相加的形式实现,例如:

    A = A<<1 ;       //完成A * 2
    A = (A<<1) + A ;   //对应A * 3
    A = (A<<3) + (A<<2) + (A<<1) + A ; //对应A * 15
    

    用一个移位寄存器和一个加法器就能完成乘以 3 的操作。但是乘以 15 时就需要 3 个移位寄存器和 3 个加法器(当然乘以 15 可以用移位相减的方式)。

    有时候数字电路在一个周期内并不能够完成多个变量同时相加的操作。所以数字设计中,最保险的加法操作是同一时刻只对 2 个数据进行加法运算,最差设计是同一时刻对 4 个及以上的数据进行加法运算。

    如果设计中有同时对 4 个数据进行加法运算的操作设计,那么此部分设计就会有危险,可能导致时序不满足。

    此时,设计参数可配、时序可控的流水线式乘法器就显得有必要了。

    和十进制乘法类似,计算 13 与 5 的相乘过程如下所示:

    由此可知,被乘数按照乘数对应 bit 位进行移位累计,便可完成相乘的过程。

    假设每个周期只能完成一次累加,那么一次乘法计算时间最少的时钟数恰好是乘数的位宽。所以建议,将位宽窄的数当做乘数,此时计算周期短。

    乘法器设计

    考虑每次乘法运算只能输出一个结果(非流水线设计),设计代码如下。

    module    mult_low
        #(parameter N=4,
          parameter M=4)
          input                     clk,
          input                     rstn,
          input                     data_rdy ,  //数据输入使能
          input [N-1:0]             mult1,      //被乘数
          input [M-1:0]             mult2,      //乘数
          output                    res_rdy ,   //数据输出使能
          output [N+M-1:0]          res         //乘法结果
        //calculate counter
        reg [31:0]           cnt ;
        //乘法周期计数器
        wire [31:0]          cnt_temp = (cnt == M)? 'b0 : cnt + 1'b1 ;
        always @(posedge clk or negedge rstn) begin
            if (!rstn) begin
                cnt    <= 'b0 ;
            else if (data_rdy) begin    //数据使能时开始计数
                cnt    <= cnt_temp ;
            else if (cnt != 0 ) begin  //防止输入使能端持续时间过短
                cnt    <= cnt_temp ;
            else begin
                cnt    <= 'b0 ;
        //multiply
        reg [M-1:0]          mult2_shift ;
        reg [M+N-1:0]        mult1_shift ;
        reg [M+N-1:0]        mult1_acc ;
        always @(posedge clk or negedge rstn) begin
            if (!rstn) begin
                mult2_shift    <= 'b0 ;
                mult2_shift    <= 'b0 ;
                mult1_acc      <= 'b0 ;
            else if (data_rdy && cnt=='b0) begin  //初始化
                mult1_shift    <= {{(N){1'b0}}, mult1} << 1 ;  
                mult2_shift    <= mult2 >> 1 ;  
                mult1_acc      <= mult2[0] ? {{(N){1'b0}}, mult1} : 'b0 ;
            else if (cnt != M) begin
                mult1_shift    <= mult1_shift << 1 ;  //被乘数乘2
                mult2_shift    <= mult2_shift >> 1 ;  //乘数右移,方便判断
                //判断乘数对应为是否为1,为1则累加
                mult1_acc      <= mult2_shift[0] ? mult1_acc + mult1_shift : mult1_acc ;
            else begin
                mult2_shift    <= 'b0 ;
                mult2_shift    <= 'b0 ;
                mult1_acc      <= 'b0 ;
        //results
        reg [M+N-1:0]        res_r ;
        reg                  res_rdy_r ;
        always @(posedge clk or negedge rstn) begin
            if (!rstn) begin
                res_r          <= 'b0 ;
                res_rdy_r      <= 'b0 ;
            else if (cnt == M) begin
                res_r          <= mult1_acc ;  //乘法周期结束时输出结果
                res_rdy_r      <= 1'b1 ;
            else begin
                res_r          <= 'b0 ;
                res_rdy_r      <= 'b0 ;
        assign res_rdy       = res_rdy_r;
        assign res           = res_r;
    endmodule
    
  • 流水线乘法器设计
    下面对乘法执行过程的中间状态进行保存,以便流水工作,设计代码如下。
  • 单次累加计算过程的代码文件如下(mult_cell.v ):

    module    mult_cell
        #(parameter N=4,
          parameter M=4)
          input                     clk,
          input                     rstn,
          input                     en,
          input [M+N-1:0]           mult1,      //被乘数
          input [M-1:0]             mult2,      //乘数
          input [M+N-1:0]           mult1_acci, //上次累加结果
          output reg [M+N-1:0]      mult1_o,     //被乘数移位后保存值
          output reg [M-1:0]        mult2_shift, //乘数移位后保存值
          output reg [N+M-1:0]      mult1_acco,  //当前累加结果
          output reg                rdy );
        always @(posedge clk or negedge rstn) begin
            if (!rstn) begin
                rdy            <= 'b0 ;
                mult1_o        <= 'b0 ;
                mult1_acco     <= 'b0 ;
                mult2_shift    <= 'b0 ;
            else if (en) begin
                rdy            <= 1'b1 ;
                mult2_shift    <= mult2 >> 1 ;
                mult1_o        <= mult1 << 1 ;
                if (mult2[0]) begin
                    //乘数对应位为1则累加
                    mult1_acco  <= mult1_acci + mult1 ;  
                else begin
                    mult1_acco  <= mult1_acci ; //乘数对应位为1则保持
            else begin
                rdy            <= 'b0 ;
                mult1_o        <= 'b0 ;
                mult1_acco     <= 'b0 ;
                mult2_shift    <= 'b0 ;
    endmodule
    

    多次模块例化完成多次累加,代码文件如下(mult_man.v ):

    module    mult_man
        #(parameter N=4,
          parameter M=4)
          input                     clk,
          input                     rstn,
          input                     data_rdy ,
          input [N-1:0]             mult1,
          input [M-1:0]             mult2,
          output                    res_rdy ,
          output [N+M-1:0]          res );
        wire [N+M-1:0]       mult1_t [M-1:0] ;
        wire [M-1:0]         mult2_t [M-1:0] ;
        wire [N+M-1:0]       mult1_acc_t [M-1:0] ;
        wire [M-1:0]         rdy_t ;
        //第一次例化相当于初始化,不能用 generate 语句
        mult_cell      #(.N(N), .M(M))
        u_mult_step0
          .clk              (clk),
          .rstn             (rstn),
          .en               (data_rdy),
          .mult1            ({{(M){1'b0}}, mult1}),
          .mult2            (mult2),
          .mult1_acci       ({(N+M){1'b0}}),
          //output
          .mult1_acco       (mult1_acc_t[0]),
          .mult2_shift      (mult2_t[0]),
          .mult1_o          (mult1_t[0]),
          .rdy              (rdy_t[0]) );
        //多次模块例化,用 generate 语句
        genvar               i ;
        generate
            for(i=1; i<=M-1; i=i+1) begin: mult_stepx
                mult_cell      #(.N(N), .M(M))
                u_mult_step
                  .clk              (clk),
                  .rstn             (rstn),
                  .en               (rdy_t[i-1]),
                  .mult1            (mult1_t[i-1]),
                  .mult2            (mult2_t[i-1]),
                  //上一次累加结果作为下一次累加输入
                  .mult1_acci       (mult1_acc_t[i-1]),
                  //output
                  .mult1_acco       (mult1_acc_t[i]),                                      
                  .mult1_o          (mult1_t[i]),  //被乘数移位状态传递
                  .mult2_shift      (mult2_t[i]),  //乘数移位状态传递
                  .rdy              (rdy_t[i]) );
        endgenerate
        assign res_rdy       = rdy_t[M-1];
        assign res           = mult1_acc_t[M-1];