如何编写一个基本的 Verilog Module(模块)

1、概述

这篇文章主要介绍了 Verilog 在 FPGA 设计中的概念和使用方法。首先讨论使用模块(module)关键字构造 Verilog 设计的方式,以及这与所描述的硬件的关系。这包括对参数、端口(port)和例化(instantiaton)的讨论及一个完整示例。

虽然不需要为了使用它而讨论Verilog的整个发展历史,但是却必须考虑另一个重点——所使用的Verilog语言版本。Verilog 的主要标准发布于1995年(Verilog-1995)和2001年(Verilog-2001)。除此之外,2005年(Verilog-2005)还发布了另一个小更新。

原则上,我们应该为创建的任何新 FPGA 设计使用 Verilog-2001 或 Verilog-2005 标准。然而,我们可能仍然会遇到基于 Verilog-1995 标准的遗留设计。因此,我们将在这些文章中看到 Verilog-1995 标准和 Verilog-2001 年标准之间的重要区别。

2、构建 Verilog 代码

当我们设计 FPGA 时,需要记住一个基本的原则——我们是在设计硬件,而不是在编写软件程序。因此,我们必须描述许多不同电子组件(electronic component)的行为,然后将它们连接在一起。这反映在 Verilog 文件的结构方式中。

对于每个电子组件,我们需要知道组件的外部接口。此信息使我们能够将其连接到我们系统中的其他组件。我们还需要知道组件的行为方式,以便我们可以在我们的系统中使用它。

在 Verilog 中,我们使用称为模块(module)的构造来定义此信息。Verilog中的module相当于VHDL中的 entity architecture pair。

下面的代码片段显示了在 Verilog 中声明模块的一般语法。

module (        //module开始
    //定义参数
    parameter <parameter_name> = <default_value>
)
<module_name> (
    //IO定义
    <direction> <data_type> <size> <port_name>
);

    //完整的RTL代码

endmodule        //module结束

在此构造中,<module_name> 是正在设计的模块的名称。虽然我们可以在一个文件中声明多个模块,但最好让一个文件对应一个模块(好的开发习惯)。保持文件名和模块名相同也是一种很好的做法。这会让管理包含许多层级的大型设计变得更加简单。

在 Verilog 中,我们使用 // 字符来表示我们正在编写注释。我们使用注释来包含有关我们代码的重要信息,这些信息可能会帮助到其他人理解代码。Verilog 编译器会忽略我们在注释中写的任何内容。

在上面的 Verilog 代码片段中,我们可以看到这一点,因为这些注释仅用于描述代码的功能。

2.1、Verilog-1995 模块(Modules)

Verilog 模块声明语法作为 verilog-2001 标准的一部分进行了更新。这意味着用于在 Verilog 中声明模块的方法会略有不同。

使用 Verilog-1995 代码时,我们仅在初始模块声明中定义端口名称。然后我们在模块主体中定义每个端口的方向、数据类型和大小。除此之外,我们还在模块中定义了模块参数。

下面的代码片段显示了如何使用 Verilog-1995语法来声明模块。

module <module_name> (
    // All IO names are defined here
    <port_name>
);
    //在这里声明参数,可选项
    parameter <parameter_name> = <default_value>;
    
    //IO定义
    <direction> <data_type> <size> <port_name>;
    
    //完整的RTL代码
    
endmodule

2.2、Verilog 模块中的参数(Parameters

参数(parameter)是常量(constant)的局部形式,我们可以使用它来配置 Verilog 中的模块。当我们在设计的另一部分实例化我们的模块时,可以为参数分配一个值来配置模块的行为。

由于参数的作用域有限,所以我们可以多次调用同一个模块,每次都为参数赋不同的值。因此,参数允许我们随时修改模块的行为。

参数是 Verilog 模块声明的可选部分,在大多数情况下我们并不需要使用它们。然而,参数允许我们编写更通用的模块接口,这些接口更容易在其他 Verilog 设计中被复用。

在模块中声明了一个参数之后,我们就可以像使用普通变量一样使用它。但是,必须记住–参数是一个常量值,所以只能读取它。因此,我们只能在参数声明时为其赋值。

2.3、功能代码

我们使用模块 IO 声明下方的空间来定义模块的功能。我们最常为此使用RTL(Register Transfer Level),但也可以编写结构代码或描述性原语。

我们使用关键字 module 开始一个模块的设计,从关键字 module 到关键字 endmodule 内的所有内容构成了一个完整的模块。

当我们完成描述模块行为的代码时,使用 endmodule 关键字来作为结束。在此关键字之后编写的任何代码都不会包含该模块中。

2.4、Verilog 模块端口(Ports)

我们在模块声明中使用端口来定义 Verilog 模块的输入和输出。可以近似地认为这些端口相当于传统电子元件中的管脚。

下面的代码片段显示了我们用来声明端口的一般语法。

<direction> <data_type> <size> <port_name>

<direction> :我们可以在 Verilog 模块中将端口定义为in(输入)、out(输出)或 inout(双向端口)。上述构造中的可用于执行此操作。

<port_name> :字段用于为端口指定一个唯一的名称。

<data_type> :声明端口的数据类型,最常见的类型是reg 和 wire 。

<size> :用来表述端口多位向量的位数。当我们定义向量类型输入的大小时,我们必须指出向量中的最高有效位(most significant bit,MSB)和最低有效位(least significant bit,LSB)。因此,我们在声明端口大小时使用 [MSB:LSB]。

下面的示例显示了名为 example_in 的 8 位输入的声明。

input wire [7:0] example_in;

在此示例中,[7:0] 表示第 7 位是最高有效位。这称为小字节序或低字节序(little endian),是 FPGA 设计中最常用的表示方法。

如果我们将声明改为 [0:7],也可以将 MSB 定义为位置 0。这种称为大字节序或高字节序(big endian)的表示方法在设计 FPGA 时不像小字节序那样常用。

2.5、Verilog 中的 Reg 和 Wire 类型

Verilog中的数据类型有很多,但是最常用的只有两种–reg 和 wire。

wire 类型被用来来声明信号,这些信号在我们的 Verilog 代码中是简单的点对点连接(电线)。因此,电线不能驱动数据,也不能存储数值。顾名思义,它们大致相当于传统电路中的一根导线。

reg 类型被用来声明一个信号,该信号在我们的 Verilog 代码中主动驱动数据。顾名思义,它们大致相当于传统数字电路中的触发器(flip flop)。

由于wire类型是基本的点对点连接,所以我们可以在声明 Verilog 模块时将wire用作in或者out类型。相反,reg 类型只能用于 Verilog 模块中的输出。

wire类型主要被用来构建组合逻辑电路,当我们使用 assign 关键字在 Verilog 中对组合逻辑建模时,我们只能将其与wire类型一起使用。

reg 类型主要被用来构建时序逻辑电路,我们必须使用always块(always block)来为时序逻辑电路建模。我们只能在 always 块中使用 reg 类型。

在声明模块端口时,其默认数据类型是wire。因此,当我们使用wire端口时,可以省略掉wire这个关键字,但是reg关键字则不能被省略。

3、Verilog 模块例化(Instantiation

我们可以调用已经在设计的另一部分编写的 Verilog 模块。在 Verilog 中调用模块的过程称为实例化(实例化,instantiation)。

每次例化一个模块时,都会创建一个唯一的对象(Object),它有自己的名称、参数和端口。

在 Verilog 设计中,我们将每个例化模块称为模块的实例(instance)。我们使用例化来创建许多不同的实例,并用它们来构建更复杂的设计。

我们可以认为Verilog中的模块例化相当于在传统电子电路中放置了一个电子元件。一旦我们创建了设计中需要的所有实例,就必须将它们互连以创建一个完整的系统。这与在传统电子系统中将组件接线在一起完全相同。

Verilog 为我们提供了两种可用于模块实例化的方法——命名例化(named instatiation)和位置例化(positional instatiation)。

Verilog-2001 标准编支持以上两种例化方式,而Verilog-1995标准只支持位置例化方法。

3.1、位置例化方法

在 Verilog 中使用位置例化方法时,需要使用有序列表来连接模块端口。被例化的模块实例的端口顺序必须与我们模块中声明端口的顺序一样。

例如,如果我们依次声明时钟clock,复位rst,那么我们在例化时就必须先将时钟信号连接到模块端口。

下面的 Verilog 代码片段显示了位置模块例化方法的一般语法。

<module_name> # (
    //例化的参数列表
    <parameter_value>
)
<instance_name> (
    //例化的端口,需要按照被例化模块的顺序排列出来
    <signal_name>, //第一个端口
    <signal_name>  //第二个端口
);

<module_name> :必须与在声明模块时给它的名称相匹配。

<instance_name>:设计中的例化模块的唯一名称。

这种方法可能难以维护,因为我们的端口顺序可能会随着我们设计的发展而改变。

下面看一个简单的例子来展示如何使用位置例化方法。对于这个例子,我们将创建一个如下所示的简单电路的实例。

如何编写一个基本的 Verilog Module(模块)

当我们使用位置例化方法时,模块声明中端口的顺序很重要。下面的代码片段显示了如何为这个电路声明一个模块。

and_or (
  input a,
  input b,
  input c,
  output logic_out
);

下面的 Verilog 代码片段显示了如何使用位置例化方法来创建此模块的实例。

and_or example_and_or (
 in_a,
 in_b,
 in_c,
 and_or_out
);

3.2、命名例化方法

在 Verilog 中使用命名例化方法时,我们需要明确定义将信号连接到的端口的名称。与位置例化方法不同的是–声明端口的顺序并不重要。

这种方法通常优于位置例化方法,因为它生成的代码更易于阅读和理解。它也更容易维护,因为我们可以修改端口而不必担心我们声明它们的顺序。

下面的 Verilog 代码片段显示了命名模块实例化的一般语法。

<module_name> # (
    //被例化的参数与例化的参数
    .<parameter_name> (<parameter_value>)
)
<instance_name> (
    //被例化的端口与例化的端口
    .<port_name> (<signal_name>),
    .<port_name> (signal_name>)
);

<module_name>、<parameter_name> 和 <port_name> 必须与在定义模块时使用的名称相匹配。

<instance_name> 对位置例化和命名例化具有相同的功能。

使用上一节用到的电路作为简单案例来展示如何使用命名例化方法。

如何编写一个基本的 Verilog Module(模块)

下面的 Verilog 代码片段显示了我们如何使用命名例化创建此模块的实例。

and_or example_and_or (
    .a (in_a),
    .b (in_b),
    .c (in_c),
    .logic_out (and_or_out)
);

4、完整的Verilog module示例

为了完全理解我们在这篇文章中讨论的所有概念,让我们看一个基本示例。

在这个例子中,我们将创建一个使用参数的同步计数器电路,然后实例化它的两个实例。

这些实例之一将在输出中具有 12 位,而另一个将只有 8 位。

我们将在这里排除这些模块的 RTL,因为我们还没有学会如何编写它。相反,我们将简单地定义模块的 IO 以及它们之间的互连。

计数器模块将有两个输入——时钟和复位——和一个输出——计数器值。

除此之外,我们还需要一个参数来定义输出中的位数。

下面的代码片段显示了使用 verilog 2001 和 verilog 1995 兼容代码的计数器模块的声明。

// Verilog-2001
module counter #(
    parameter WIDTH = 8
)
(
    input clock,
    input reset,
    output reg [WIDTH-1:0] count
);



// Verilog-1995
module counter (
    clock,
    reset,
    count
);

paramater WIDTH = 8;

input clock;
input reset;
output reg [WIDTH-1:0] count;

endmodule

我们现在需要一个模块–用它来例化这个计数器的两个实例。该模块将有两个输入–时钟和复位以及两个来自例化计数器的输出。

在计数器模块中,我们定义了默认的计数器输出为 8 位。这意味着我们可以在不改变参数值的情况下实例化 8 位计数器。但是,当例化 12 位计数器时,则必须将 WIDTH 参数的值设置为 12。

下面的代码片段显示了使用命名例化方法连接到端口时此模块的代码。

module top_level (
    input clock,
    input reset,
    output reg [7:0] count_8,
    output reg [11:0] count_12
);

//例化一个8bit的计数器
counter 8bit_count (
    .clock (clock),
    .reset (reset),
    .count (count_8)
);

//例化一个12bit的计数器
counter #(.WIDTH (12)) 12_bit_count (
    .clock (clock),
    .reset (reset),
    .count (count_12)
);

endmodule
  • 📣您有任何问题,都可以在评论区和我交流📃

  • 📣您的支持是我持续创作的最大动力!如果本文对您有帮助,还请多多点赞👍、评论💬和收藏

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
心中带点小风骚的头像心中带点小风骚普通用户
上一篇 2023年3月18日 上午11:04
下一篇 2023年3月18日 下午12:43

相关推荐