verilog教程(7)—— verilog的三种建模的代码实现详解-Anlogic-安路社区-FPGA CPLD-ChipDebug

verilog教程(7)—— verilog的三种建模的代码实现详解

结构建模

模块定义结构

一个模块module 的结构如下:

module module_name 
#(parameter_list)
(port_list) ;
              Declarations_and_Statements ;
endmodule
  • 模块定义必须以关键字 module 开始,以关键字 endmodule 结束。
  • 模块名,端口信号,端口声明和可选的参数声明等,出现在设计使用的 Verilog 语句(Declarations_and_Statements)之前。

在结构建模中,描述语句主要是实例化语句,包括对Verilog HDL 内置门如与门( and)异或门( xor)等的例化, 及对其他器件的调用,这里的器件包括FPGA厂家提供的一些宏单元以及设计者已经有的设计。 

在实际应用中,实例化语句主用指后者,对内置门建议不采纳,而用数据流或行为级方式对基本门电路的描述。

端口队列port_list 列出了该模块通过哪些端口与外部模块通信

模块端口

端口是模块与外界交互的接口。对于外部环境来说,模块内部是不可见的,对模块的调用只能通过端口连接进行。模块的端口可以是输入端口、输出端口或双向端口。缺省的端口类型为线网类型(即wire 类型)。输出或输入输出端口能够被重新声明为reg 型。无论是在线网说明还是寄存器说明中,线网或寄存器必须与端口说明中指定的长度相同。

//端口类型声明
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) 端口信号在端口列表中罗列出来以后,就可以在模块实体中进行声明了。

根据端口的方向,端口类型有 3 种: 输入(input),输出(output)和双向端口(inout)。

input、inout 类型不能声明为 reg 数据类型,因为 reg 类型是用于保存数值的,而输入端口只能反映与其相连的外部信号的变化,不能保存这些信号的值。

output 可以声明为 wire 或 reg 数据类型。

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

实例化

一个模块能够在另外一个模块中被引用,这样就建立了描述的层次。模块实例化语句形式如下:

module_name instance_name(port_associations) ;
信号端口可以通过位置或名称关联;但是关联方式不能够混合使用。

咱们先建立一个1bit全加器

module full_adder1  (A, B, Cin, Sum, Count);
    input A;
    input B;
    input Cin;
    output Sum;
    output Count;
    wire S1, T1, T2, T3;
    // -- statements -- //
    xor x1 (S1, A, B);
    xor x2 (Sum, S1, Cin);
    and A1 (T3, A, B );
    and A2 (T2, B, Cin);
    and A3 (T1, A, Cin);
    or O1 (Cout, T1, T2, T3 );
endmodule

然后例化两个1bit全加器构成一个2bit全加器

module full_adder2  (FA, FB, FCin, FSum, FCout ) ;
    parameter SIZE = 2;
    input [SIZE:1] FA;
    input [SIZE:1] FB;
    input FCin;
    output [SIZE:1] FSum;
    output FCout;
    wire FTemp;
    full_adder1  FA1(
    .A (FA[1]),
    .B (FB[1]),
    .Cin (FCin) ,
    .Sum (FSum[1]),
    .Cout (Ftemp)
    );
    full_adder1  FA2(
    .A (FA[2]),
    .B (FB[2]),
    .Cin (FTemp) ,
    .Sum (FSum[2]),
    .Cout (FCount )
    );
endmodule

上面代码中的这段代码就是实例化了一个1bit全加器,并且采用的是命名端口连接。

full_adder1  FA1(
    .A (FA[1]),
    .B (FB[1]),
    .Cin (FCin) ,
    .Sum (FSum[1]),
    .Cout (Ftemp)
    );

命名端口连接

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

顺序端口连接

这种方法将需要例化的模块端口按照模块声明时端口的顺序与外部信号进行匹配连接,位置要严格保持一致。

full_adder1  FA1(
    FA[1],
    FB[1],
    FCin,
    FSum[1],
    Ftemp
    );

建议:在例化的端口映射中请采用名字关联,这样,当被调用的模块管脚改变时不易出错。

端口连接规则

输入端口

模块例化时,从模块外部来讲, input 端口可以连接 wire 或 reg 型变量。这与模块声明是不同的,从模块内部来讲,input 端口必须是 wire 型变量。

输出端口

模块例化时,从模块外部来讲,output 端口必须连接 wire 型变量。这与模块声明是不同的,从模块内部来讲,output 端口可以是 wire 或 reg 型变量。

输入输出端口

模块例化时,从模块外部来讲,inout 端口必须连接 wire 型变量。这与模块声明是相同的。

悬空端口

模块例化时,如果某些信号不需要与外部信号进行连接交互,我们可以将其悬空,即端口例化处保留空白即可,上述例子中有提及。

output 端口正常悬空时,我们甚至可以在例化时将其删除。

input 端口正常悬空时,悬空信号的逻辑功能表现为高阻状态(逻辑值为 z)。但是,例化时一般不能将悬空的 input 端口删除,否则编译会报错,例如:

不同端口长度

当端口和局部端口表达式的长度不同时,端口通过无符号数的右对齐或截断方式进行匹配。

带参数例化

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

参数覆盖有 2 种方式:1)使用关键字 defparam,2)带参数值模块例化。

defparam 改写

可以用关键字 defparam 通过模块层次调用的方法,来改写低层次模块的参数值。

这是一个默认为4bit宽4bit深的RAM模块

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;
        end
        else if (EN && !WR) begin
            Q       <= mem[A] & MASK;
        end
    end
 
endmodule

这段代码中parameter        MASK = 3 ; 定义了一个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)    );

defparam     u_ram_4x4.MASK = 7 ;这行代码就对ram_4x4的Mask参数进行了改写,u_ram_4x4是其实例名。

带参数例化

另一种方式就是将新的参数值直接写入模块例化语句:

//instantiation
ram_4x4 
    #(.Mask(7))    
    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)    );

#(.Mask(7)) 这一行就是在例化时对其参数进行改写。

 

结构化建模具体实例

对一个数字系统的设计,我们采用的是自顶向下的设计方式。可把系统划分成几个功能模块,每个功能模块再划分成下一层的子模块。每个模块的设计对应一个module ,一个module 设计成一个verilog HDL 程序文件。因此,对一个系统的顶层模块,我们采用结构化的设计,即顶层模块分别调用了各个功能模块。下面以一个实例(一个频率计数器系统)说明如何用HDL进行系统设计。 在该系统中,我们划分成如下三个部分: 2输入与门模块, LED显示模块, 4位计数器模块。系统的层次描述如下:

20240406103139294-image

顶层模块CNT_BCD,文件名CNT_BCD.v,该模块调用了低层模块 AND2、 CNT_4b和 HEX2LED 。 系统的电路结构图如下:

20240406103031324-image

顶层模块CNT_BCD对应的设计文件 CNT_BCD.v 内容为:

module CNT_BCD (BCD_A,BCD_B,BCD_C,BCD_D,CLK,GATE,RESET) ;
// ------------ Port declarations --------- //
input CLK;
input GATE;
input RESET;
output [3:0] BCD_A;
output [3:0] BCD_B;
output [3:0] BCD_C;
output [3:0] BCD_D;
wire CLK;
wire GATE;
wire RESET;
wire [3:0] BCD_A;
wire [3:0] BCD_B;
wire [3:0] BCD_C;
wire [3:0] BCD_D;
// ----------- Signal declarations -------- //
wire NET104;
wire NET116;
wire NET124;
wire NET132;
wire NET80;
wire NET92;
// -------- Component instantiations -------//
CNT_4b U0(
.CLK(CLK),
.ENABLE(GATE),
.FULL(NET80),
.Q(BCD_A),
.RESET(RESET)
);
CNT_4b U1(
.CLK(CLK),
.ENABLE(NET116),
.FULL(NET92),
.Q(BCD_B),
.RESET(RESET)
);
CNT_4b U2(
.CLK(CLK),
.ENABLE(NET124),
.FULL(NET104),
.Q(BCD_C),
.RESET(RESET)
);
CNT_4b U3(
.CLK(CLK),
.ENABLE(NET132),
.Q(BCD_D),
.RESET(RESET)
);
AND2 U4(
.A0(NET80),
.A1(GATE),
.Y(NET116)
);
AND2 U5(
.A0(NET92),
.A1(NET116),
.Y(NET124)
);
AND2 U6(
.A0(NET104),
.A1(NET124),
.Y(NET132)
);
endmodule

注意:这里的AND2是为了举例说明,在实际设计中,对门级不要重新设计成一个模块,同时
对涉及保留字的(不管大小写)相类似的标识符最好不用。

数据流建模

连续赋值语句

数据流的描述是采用连续赋值语句(assign )语句来实现的。语法如下:
assign net_type = 表达式;
连续赋值语句用于组合逻辑的建模。 等式左边是
wire 类型的变量。等式右边可以是常量、由
运算符如逻辑运算符、算术运算符参与的表达。如下几个实例:

wire [3:0] Z, Preset, Clear; //线网说明
assign Z = Preset & Clear; //连续赋值语句
wire Cout, C i n ;
wire [3:0] Sum, A, B;
. . .
assign {Cout, Sum} = A + B + Cin;
assign Mux = (S = = 3)? D : 'bz;

注意如下几个方面:
1、连续赋值语句的执行是:只要右边表达式任一个变量有变化,表达式立即被计算,计算的
结果立即赋给左边信号。
2、连续赋值语句之间是并行语句,因此与位置顺序无关。

Verilog 还提供了另一种对 wire 型赋值的简单方法,即在 wire 型变量声明的时候同时对其赋值。wire 型变量只能被赋值一次,因此该种连续赋值方式也只能有一次。例如下面赋值方式和上面的赋值例子的赋值方式,效果都是一致的。

wire      A, B ;
wire      Cout = A & B ;

阻塞赋值语句

=”用于阻塞的赋值,凡是在组合逻辑(如在assign 语句中)赋值的请用阻塞赋值。

以上面的 频率计数器为例,其中的AND2模块我们用数据流来建模。
AND2模块对应文件AND2.v 的内容如下:

module AND2 (A0, A1, Y);
input A0;
input A1;
output Y;
wire A0;
wire A1;
wire Y;
// add your code here
assign Y = A0 & A1;
endmodule

行为建模

行为建模方式是通过对设计的行为的描述来实现对设计建模,一般是指用过程赋值语句(initial 语句和always 语句 )来设计的称为行为建模。

一个模块中可以包含多个 initial 和 always 语句,但 2 种语句不能嵌套使用。

这些语句在模块间并行执行,与其在模块的前后顺序没有关系。

但是 initial 语句或 always 语句内部可以理解为是顺序执行的(非阻塞赋值除外)。

每个 initial 语句或 always 语句都会产生一个独立的控制流,执行时间都是从 0 时刻开始。

initial语句

initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。 通常只用在对设计进行
仿真的测试文件中,用于对一些信号进行初始化和产生特定的信号波形。
 

如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。

如果 initial 块内只要一条语句,关键字 begin 和 end 可使用也可不使用。

initial 理论上来讲是不可综合的,多用于初始化、信号检测等。有些厂商的工具也可以综合initial,比如用来初始化RAM。

always 语句

与 initial 语句相反,always 语句是重复执行的 ,执行机制是通过对一个称为敏感变量表的事件驱动来实现的,下面会具体讲到。always 语句块从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。

由于循环执行的特点,always 语句多用于仿真时钟的产生,信号行为的检测等。

always @ ( posedge Clk or posedge Rst )
begin
    if Rst
        Q <= ‘ b 0;
    else
        Q <= D;
end

上面括号内的内容称为敏感变量,即整个always 语句当敏感变量有变化时被执行,否则不执行。因此,当Rst 1 时, Q被复位,在时钟上升沿时, D被采样到Q。 有@ 的用来描述一个时序器件。

顺序语句块

顺序语句块语句块块提供将两条或更多条语句组合成语法结构上相当于一条语句的机制。这里主要讲 Verilog HDL 的顺序语句块(begin . . . end) :语句块中的语句按给定次序顺序执行。 顺序语句块中的语句按顺序方式执行。每条语句中的时延值与其前面的语句执行的模拟时间相关。一旦顺序语句块执行结束,跟随顺序语句块过程的下一条语句继续执行。顺序语句块的语法如下:

begin
    [ :block_id{declarations} ]
    procedural_statement ( s )
end

例如:

// 产生波形:
begin
#2 Stream = 1;
#5 Stream = 0;
#3 Stream = 1;
#4 Stream = 0;
#2 Stream = 1;
#5 Stream = 0;
end

代码中的#2 #5 #3 表示在赋值前延时,延时单位由`timescale决定,后面一个例子会看到,如果出现在赋值之后则表示赋值后延时。

假定顺序语句块在第1 0 个时间单位开始执行。两个时间单位后第1 条语句执行,即第12 个时
间单位。此执行完成后,下
1 条语句在第17 个时间单位执行(延迟5 个时间单位)。然后下1 条语句
在第
2 0 个时间单位执行,以此类推。该顺序语句块执行过程中产生的波形如图:

20240406112005369-image

11 顺序语句块中积累延时

下面用 always 产生一个 100MHz 时钟源,并在 1010ns 时停止仿真代码如下。

代码如下:

`timescale 1ns/1ns
 
module test ;
 
    parameter CLK_FREQ   = 100 ; //100MHz
    parameter CLK_CYCLE  = 1e9 / (CLK_FREQ * 1e6) ;   //switch to ns
 
    reg  clk ;
    initial      clk = 1'b0 ;      //clk is initialized to "0"
    always     # (CLK_CYCLE/2) clk = ~clk ;       //generating a real clock by reversing
 
    always begin
        #10;
        if ($time >= 1000) begin
            $finish ;
        end
    end
 
endmodule

仿真结果如下:

可见,时钟周期是我们想要得到的 100MHz。而且仿真在 1010ns 时停止。

20240406112412935-image

请登录后发表评论

    没有回复内容