Verilog HDL数字系统设计及实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.2 模块和端口

由例1.1可以看出,“模块”是Verilog HDL设计中的一个基本组成单元,一个设计是由一个或多个模块组成的。一个模块的代码主要由下面几个部分构成:模块名定义、端口描述和内部功能逻辑描述。一个模块通常就是一个电路单元器件,如图1.2所示。

图1.2 模块和端口

图1.2中的代码定义了一个名为exap的电路器件。代码中用关键字module定义了模块的名字,然后用括号列出了该模块的端口。在模块名定义的后面,分别用input和output关键字指定端口的方向。端口定义完成后,给出描述该模块功能的代码,最后用关键字endmodule来结束该模块的描述。上述代码描述的电路,实际上对应于实际硬件中的一个功能模块,该模块有2个输入端口A和B,以及1个输出端口C。通过对该模块的端口进行连线,可以将这个模块与其他模块连接在一起,形成功能更复杂的电路。

1.2.1 模块定义

定义模块要使用关键字module和endmodule,其语法格式为

            module模块名 (端口声明列表);
            端口定义
            ...
            endmodule

模块名在一个设计中必须是唯一的,用以区别其他模块。该模块的描述必须全部写在这两个关键字之间,不允许模块中嵌套定义其他模块。

模块中的逻辑功能描述主要由5部分组成:变量声明、数据流描述语句、门级实例化描述语句、行为描述语句及任务与函数。上述各个部分的描述可以以任意顺序出现,但要注意的是,虽然变量的声明可以出现在任何位置,但必须在该变量使用之前进行声明。关于描述模块功能的语句的介绍,将在本书后面的章节中陆续给出。

1.2.2 端口定义

在Verilog HDL中定义端口有两种风格:普通风格和ANSI C风格。

用普通风格定义模块的端口,首先在模块名后把所有的输入/输出端口列举出来(如果一个模块和外部没有任何连接关系,则可以没有端口列表,直接打空括号即可),如

            module    模块名 (端口名1,端口名2,...);

接下来需要对输入/输出端口进行定义,如

            input    [位宽-1:0]  端口名1,端口名2;
            output    [位宽-1:0]  端口名3;
            inout    [位宽-1:0]  端口名4;

定义端口时,使用关键字input、output和inout来分别指定该端口的方向为输入、输出或双向。之后是用中括号指定位宽的可选语句,再后是端口名。进行端口定义的端口必须首先在端口声明列表中出现,否则将被视为语法错误。端口定义的一行可以定义多个输入/输出方向和位宽均相同的端口,多个端口的端口名用逗号隔开。

用普通风格定义端口时需要对端口分别进行声明和定义,使用起来较为麻烦,因此Verilog HDL语言还提供了另一种称为ANSI C风格的端口定义方式。ANSI C风格的端口定义允许将端口的声明和定义合写在一起,并且都出现在模块名之后的括号中,其格式如下:

            module  模块名
                (   input    [位宽-1:0]  端口名1,端口名2;
                    output    [位宽-1:0]  端口名3;
                    inout    [位宽-1:0]  端口名4;
                );

可以看出,利用ANSI C风格,可以一次性地完成模块名和端口的定义,使得代码更为紧凑,减少了出错的概率,因此推荐使用这种风格进行端口定义。本书中给出的所有例子都采用ANSI C风格来定义端口。

【例1.2】利用ANSI C风格来定义例1.1中加法器模块的端口。

            // example_1_2: full adder
            // 利用ANSI C风格进行全加器模块的端口定义
            module fadder_4
                (   input  [3:0] i_A, i_B,      // 输入端口i_A, i_B
                    input  i_Cin,               // 输入端口i_Cin
                    output [3:0] o_S,           // 输出端口o_S
                    output  o_Cout               // 输出端口o_Cout
                );
            // 全加器功能描述代码, 与例1.1相同
            // ...
            endmodule
            // 定义一个1位全加器
            module fadder_1
                (   input  i_A, i_B,             //输入端口i_A, i_B
                    input  i_Cin,                 //输入端口i_Cin
                    output o_S, o_Cout           //输出端口o_S, o_Cout
                );
            // 一位加法器功能描述代码, 与例1.1相同
            // ...
            endmodule

在定义端口时,各个端口的定义顺序没有任何限制,可先定义输出端口,再定义输入端口。在用普通风格进行端口定义时,端口声明列表和端口定义的排列顺序也可以不同。

1.2.3 模块实例化

在例1.1中提到了模块的实例化。模块定义中是不允许嵌套定义模块的,模块之间的相互调用只能通过实例化来实现。

定义好的模块可以视为一个模板,使用该模板可以创建一个对应的实际对象。当一个模块被调用时,Verilog HDL语言可以根据模板创建一个唯一的模块对象,每个对象都有自己的名字、参数、端口连接关系等。使用定义好的模板创建对象的过程称为实例化(Instantiation),创建的对象称为实例(Instance)。每个实例必须有唯一的名字。图1.3所示为对一位加法器进行多次实例化来构建四位加法器的示意图。

通过多次实例化相同的模块,实际上在电路中设计了4个相同的1位加法器,只是它们在电路中的名字和连接关系各不相同。

对已定义好的模块进行实例化引用的语法格式如下:

            模块名  实例名 (端口连接关系列表);

在实例化时,可以用两种方式书写端口连接关系列表。

第一种方式是命名端口连接方式,其语法格式为

            模块名  实例名 (.端口名(连接线1), .端口名2(连接线2),...);

用命名端口的方式进行连接,每个连接关系用一个点开头,然后是需要进行连接的模块的端口名,端口名后面在括号中指定该端口需要连接到当前层次模块中的哪个信号。例1.1中实例化一位加法器模块的方式使用的就是命名端口连接,例如

图1.3 模块实例化示意图

          fadder_1 u_fadder_1_1
          (   .i_A(i_A[0]),
              .i_B(i_B[0]),
              .i_Cin(i_Cin),
              .o_S(o_S[0]),
              .o_Cout(Cout_1)
          );

其中,i_A,i_B,i_Cin,o_S,o_Cout都是在模块fadder_1中定义过的端口,i_A[0]是连接到端口i_A的信号。因此,用命名端口的方式书写端口列表,其实就是将端口名和需要连接到的信号名成对地写在一起。由于在端口连接列表中明确指定了端口的连接关系,因此各个端口在连接列表中的顺序可以随意交换,而不影响实际的连接结果。例如,上面对模块fadder_1的实例化也可以这样书写:

          fadder_1 u_fadder_1_1
          (   .i_B(i_B[0]),
              .i_Cin(i_Cin),
              .o_S(o_S[0]),
              .o_Cout(Cout_1),
              .i_A(i_A[0])
          );

即将端口i_A的连接关系写在最后面。这样写的效果与之前写法的效果相同,端口的连接关系并未改变。

若需要某个端口不连接,则在连接列表中不列出该端口即可。例如,我们不希望输出端口o_S在实例化时连接到任何信号,则可以这样书写:

          fadder_1 u_fadder_1_1
          (   .i_B(i_B[0]),
              .i_Cin(i_Cin),
              .o_Cout(Cout_1),
              .i_A(i_A[0])
          );

但这种在端口连接列表中忽略某个端口连接关系的写法通常会在仿真工具编译时报警,因此,一种更好的方式是这样书写:

          fadder_1 u_fadder_1_1
          (   .i_B(i_B[0]),
              .i_Cin(i_Cin),
              .o_S(),                              // 端口o_S悬空
              .o_Cout(Cout_1),
              .i_A(i_A[0])
          );

即在端口连接关系列表中写出o_S端口,但是不指定它所连接的信号,而是打一个空括号。在实际使用Verilog HDL进行设计时,应坚持使用这样的方式来指定不进行连接的端口。当他人检查这段代码时,可以得到明确的信息,即设计者是有意不对端口o_S进行连接的,而不是在实例化模块时忘记了写该端口。

第二种进行实例化端口连接的书写方式是顺序端口连接方式,其语法形式为

            模块名  实例名(连接线名1,连接线名2,...);

用顺序端口连接方式来指定连接关系时,不需要给出模块的端口名,只需要按一定的顺序列出需要连接到的信号名即可。Verilog HDL语言将根据端口在模块声明列表中的声明顺序,把信号和模块端口连接起来。排列在连接关系列表第一位的信号,将连接到模块端口声明列表中排列第一位的端口。例如,用顺序连接方式实例化例1.1中的一位加法器模块:

            fadder_1 add_1 (i_A[0], i_B[0], i_Cin, o_S[0], o_Cout);
            // ...
            // 定义一个1位全加器
            module fadder_1
                (   i_A,
                    i_B,
                    i_Cin,
                    o_S,
                    o_Cout
                )
            // ...

Verilog HDL语句将按照模块fadder_1定义中端口的声明顺序,把端口连接列表中的各个信号与模块连接起来。因此,对于上述代码,i_A[0]信号将连接到i_A端口,i_B[0]信号将连接到i_B端口,依次类推。若利用ANSI C风格定义模块的端口,情况也类似,如

            fadder_1 add_1 (i_A[0], i_B[0], i_Cin, o_S[0], o_Cout);
            // ...
            // 定义一个1位全加器
            module fadder_1
                (   input  i_A, i_B,                  //输入端口i_A, i_B
                    input  i_Cin,                     //输入端口i_Cin
              output o_S, o_Cout               //输出端口o_S, o_Cout
          );
      // ...

使用顺序端口连接方式进行实例化时,不能随意改变端口连接列表中信号的排列顺序,否则会导致错误的连接关系,比如,若写成

            // 错误的一位加法器连接关系
            fadder_1 add_1 (i_A[0], i_Cin, i_B[0], o_S[0], o_Cout);
            // ...
            // 定义一个1位全加器
            module fadder_1
                (   i_A,
                    i_B,
                    i_Cin,
                    o_S,
                    o_Cout
                )
            // ...

则这时加法器的连接关系就被改变了。i_A[0]依然与i_A端口相连,但是由于调换了i_Cin和i_B[0]信号在端口连接列表中的顺序,这时i_B端口和i_Cin信号连接在一起,而i_Cin端口则和i_B[0]信号连接在一起,造成了连接错误。因此,使用顺序端口连接方式进行实例化时,需要十分小心地安排信号在端口连接列表中的顺序,以免造成连接错误。在使用Verilog HDL时,应坚持使用命名端口的方式进行实例化和端口连接,以减少出现设计错误的概率。

提示:顺序端口连接参考的是端口声明顺序而非定义顺序

用顺序端口连接方式进行实例化时,端口的连接关系参考的是模块定义中端口的声明顺序,而非定义顺序,例如

            fadder_1 add_1 (i_A[0], i_B[0], i_Cin, o_S[0], o_Cout);
            // ...
            // 定义一个1位全加器
            module fadder_1
                (   // 端口声明
                    i_A,
                    i_B,
                    i_Cin,
                    o_S,
                    o_Cout
            );
            // 端口定义
                output o_S, o_Cout;
                input  i_A, i_B;
                input  i_Cin;
            // ...

虽然输出端口o_S在端口定义时排在最前面,但是信号i_A[0]还是连接在i_A端口,因为i_A端口出现在端口声明列表的第一位。

在利用顺序端口连接方式进行实例化时,若希望某个端口不做连接,则可在端口连接列表中留出其位置,但不指定任何要连接的信号,如

            fadder_1 add_1 (i_A[0], i_B[0], i_Cin, , o_Cout);

上述实例化代码同样使得输出端口o_S不连接到任何信号。需要特别注意的是,要在顺序端口连接方式中使某个端口悬空,不能像命名端口连接方式那样直接在连接列表中忽略该端口,而要在连接列表中预留该端口的位置,但不指定任何连接信号。例如,下面的实例化代码产生错误的连接关系:

            // 错误的一位加法器连接关系
            fadder_1 add_1 (i_A[0], i_B[0], i_Cin, o_Cout);

该代码与上面正确的实例化代码相比,在o_Cout信号前少一个逗号。这时按照端口连接列表和模块端口声明的排列顺序,o_S端口将连接到o_Cout信号,而模块的最后一个端口将悬空。

注意:信号连接类型

模块端口和与之连接的信号的数据类型必须遵循如下规定:

1.输入端口在模块内部必须为wire型数据,在模块外部可以连接wire或reg型数据。

2.输出端口在模块内部可以为wire或reg型数据,在模块外部必须连接到wire型数据。

3.连接的两个端口位宽可以不同,但其仿真结果可能因Verilog HDL仿真器而异,通常会有警告。