Verilog Language: Modules: Hierarchy 模块:层次结构


Module

在Verilog中,模块化设计是构建复杂电路的核心思想之一。通过将较大的电路分解成多个模块,然后将这些模块组合在一起,可以形成更复杂的系统。这种模块化设计的最大优势在于,子模块可以被复用,不同模块之间可以通过清晰的接口进行通信。在这里,我们将探讨如何通过模块实例化来实现子模块与顶层模块的连接,以及在不同场景下如何使用两种不同的端口连接方式:按位置连接按名称连接

模块实例化的概念

在Verilog中,模块实例化是指在一个顶层模块内使用已经定义的其他模块,形成模块之间的层次结构。模块通过输入和输出端口与外界通信,而子模块通过被实例化,可以与顶层模块的端口相互连接。通过这种方式,设计者可以灵活地将较小的逻辑单元组合成较大的电路。

模块实例化的基本形式如下:

module mod_a ( input in1, input in2, output out );
    // 模块体(module body),此处省略具体实现
endmodule

上述代码定义了一个简单的模块 mod_a,该模块有两个输入端口 in1in2,以及一个输出端口 out。虽然我们不知道 mod_a 的具体实现,但在顶层模块中,我们可以通过实例化 mod_a 来使用它。

img

模块实例化的两种方式

在Verilog中,有两种常用的方式可以将信号连接到模块的端口上,分别是按位置连接按名称连接。这两种方式各有优缺点,设计者可以根据具体情况选择适合的方式。

1. 按位置连接

按位置连接是一种简单的、类C语言风格的连接方式。它通过按照模块定义时端口的顺序,将外部信号依次连接到模块的端口。具体语法如下:

mod_a instance1 ( wa, wb, wc );

在这个例子中,我们实例化了一个 mod_a 模块,并命名为 instance1。然后将信号 wawbwc 依次连接到 mod_ain1in2out 端口。这种方式的主要特点是:

  • 简洁直观:通过端口顺序将信号一一对应地连接,代码显得简洁,特别是在端口较少时,这种方式非常直观。
  • 端口顺序依赖:由于按位置连接完全依赖于模块定义时的端口顺序,因此如果模块的端口列表发生变化(例如端口顺序调整或增加新端口),所有实例化的地方都需要进行修改。对于较大系统,这可能导致维护成本增加。

以实际例子说明:

module top_module ( input a, input b, output out );
    // 按位置连接子模块 mod_a 的实例
    mod_a instance1 (a, b, out);  // a 对应 in1, b 对应 in2, out 对应 out
endmodule

在这个例子中,a 被连接到 mod_ain1b 被连接到 in2out 被连接到 out。此时,信号连接是通过它们在端口列表中的位置进行匹配的。

2. 按名称连接

按名称连接是一种更加明确的信号连接方式。它不依赖于端口在模块声明中的顺序,而是通过端口的名字来进行连接。这种方式的语法如下:

mod_a instance2 ( .out(wc), .in1(wa), .in2(wb) );

在这个例子中,虽然 wawbwc 仍然连接到 mod_a 的端口,但我们显式地指定了每个信号连接到的端口名称。这种方式的优点是:

  • 端口顺序不重要:由于信号是通过端口名称来连接的,即使模块的端口顺序发生了变化,信号仍然会被正确连接,不需要修改实例化代码。
  • 可读性增强:按名称连接使代码更加清晰,特别是在模块的端口较多时,这种方式可以避免顺序上的混淆。
  • 稍显冗长:相比按位置连接,按名称连接的代码显得更加冗长,尤其是当端口和信号数量较多时,代码可能显得重复性较强。

再举一个按名称连接的例子:

module top_module ( input a, input b, output out );
    // 按名称连接子模块 mod_a 的实例
    mod_a instance2 (.in1(a), .in2(b), .out(out));  // 通过端口名进行连接
endmodule

在这个例子中,虽然 a 仍然连接到 in1b 连接到 in2out 连接到 out,但我们通过端口名称来明确指定每个信号的位置。这种方式不受模块端口顺序的影响,保持了代码的灵活性和可维护性。

实例化模块的优势

模块实例化的主要优势体现在模块化设计和可复用性上:

  1. 模块化设计:通过将不同的功能单元划分为独立的模块,设计者可以专注于每个模块的具体实现,而不必考虑系统的全局复杂性。这种模块化方法使得大型设计变得更加可管理。

  2. 复用性:模块一旦设计完成,可以在其他设计中复用,减少重复劳动。例如,一个模块可以在多个不同的顶层模块中被实例化,而不需要重复编写相同的逻辑。

  3. 层次结构:通过模块实例化,设计者可以轻松构建具有层次结构的电路系统。顶层模块可以包含多个子模块,而每个子模块又可以实例化其他模块。这种层次化的设计有助于简化复杂系统的构建和理解。

在Verilog中,模块实例化是一项基本的技能,允许设计者通过连接不同的子模块来构建复杂的电路系统。无论是通过按位置连接还是按名称连接,理解如何正确实例化和连接模块对于有效的硬件设计至关重要。

  • 按位置连接 适用于端口数量较少、结构简单的模块,具有简洁的代码风格,但对模块的端口顺序依赖较强。
  • 按名称连接 提供了更强的灵活性和可读性,适用于端口较多或模块复杂的场景,尽管代码相对冗长,但更具维护优势。

Connecting ports by position

image-20241022213054018

在Verilog设计中,模块实例化是一种常见的设计模式,用于在顶层模块中引入子模块。这种设计方法能够有效地将较为复杂的电路分解为多个可复用的模块。在这个问题中,模块 mod_a 有两个输出端口和四个输入端口,需要将它们按位置连接到顶层模块的端口。

问题分析

模块 mod_a 已经定义,并且有6个端口,按照顺序,前两个是输出端口,后四个是输入端口。顶层模块有以下端口:

  • 输出端口 out1out2
  • 输入端口 a, b, c, d

目标是将这些端口按位置顺序连接到 mod_a 的端口上,具体连接要求如下:

  • out1 连接到 mod_a 的第一个端口。
  • out2 连接到 mod_a 的第二个端口。
  • a 连接到 mod_a 的第三个端口。
  • b 连接到 mod_a 的第四个端口。
  • c 连接到 mod_a 的第五个端口。
  • d 连接到 mod_a 的第六个端口。

按位置连接的实现

在Verilog中,按位置连接是最简单的信号连接方式。信号会按照定义时的端口顺序,依次连接到模块的输入和输出端口。下面是实现代码:

module top_module ( 
    input a, 
    input b, 
    input c,
    input d,
    output out1,
    output out2
);

    // 实例化 mod_a 并按位置连接顶层模块的端口
    mod_a instance1 (out1, out2, a, b, c, d);

endmodule

代码详解

  1. 顶层模块声明: 顶层模块 top_module 包含了四个输入端口 a, b, c, d 和两个输出端口 out1, out2

  2. 模块实例化: 模块 mod_a 被实例化为 instance1,并且按顺序连接了对应的信号。具体来说:

    • out1 连接到 mod_a 的第一个输出端口。
    • out2 连接到 mod_a 的第二个输出端口。
    • abcd 分别连接到 mod_a 的四个输入端口。

按位置连接的特点

按位置连接方式通过将信号依次连接到模块端口上,代码简洁、直观。在模块端口顺序不复杂的情况下,这种方式非常有效,且易于理解。但它也有一定的局限性:

  • 端口顺序依赖性:按位置连接非常依赖模块的端口顺序,如果 mod_a 的端口顺序发生变化(如增加新的端口或调整端口顺序),所有实例化的地方都需要重新修改代码,进行端口匹配。
  • 可维护性问题:当模块的端口较多时,按位置连接可能会增加出错的风险,因为无法直观地看到每个信号连接的端口名称,容易出现连接顺序错误。

实例化模块的优势

通过模块实例化,可以实现更高效的模块化设计:

  • 模块化设计:将复杂电路分解为多个功能模块,使得每个模块可以独立设计、测试和调试,简化了设计的复杂度。
  • 模块复用:已设计好的模块可以在不同的系统中复用,避免重复开发。通过实例化同一个模块,能够在不同场景下灵活应用相同的逻辑。
  • 层次化结构:实例化模块可以形成电路设计的层次结构,顶层模块可以通过子模块的组合来实现更复杂的功能,使得整个系统更加易于理解和管理。

Connecting ports by name

image-20241022213107449

在Verilog设计中,模块实例化是电路层次化设计的基础。通过实例化一个模块,可以将顶层模块中的信号连接到子模块的输入和输出端口。这次任务要求使用按名称连接的方式将顶层模块 top_module 的信号连接到子模块 mod_a。这种连接方式特别适用于模块端口顺序不一致的情况,可以提高代码的灵活性和可维护性。

按名称连接的概念

按名称连接意味着在实例化模块时,信号通过端口名称进行连接,而不是通过位置。这种连接方式虽然在代码上更为冗长,但具有更强的灵活性,尤其是当模块端口顺序发生变化时,仍然能够保持连接正确。

在Verilog中,按名称连接的语法格式如下:

module_name instance_name ( .port_name1(signal1), .port_name2(signal2), ... );

这里,port_name 是子模块 mod_a 中定义的端口名,而 signal 是顶层模块中的信号。连接时,无论这些端口在 mod_a 中的顺序如何,都可以通过名称来匹配相应的信号。

问题分析

已知子模块 mod_a 有6个端口,其中2个是输出端口,4个是输入端口,具体的对应关系如下:

子模块 mod_a 的端口 顶层模块 top_module 的端口
输出 out1 输出 out1
输出 out2 输出 out2
输入 in1 输入 a
输入 in2 输入 b
输入 in3 输入 c
输入 in4 输入 d

任务要求将这些端口按名称连接到顶层模块中的对应信号。

实现按名称连接

在Verilog中,使用按名称连接的代码如下:

module top_module ( 
    input a, 
    input b, 
    input c,
    input d,
    output out1,
    output out2
);

    // 实例化 mod_a 并按名称连接
    mod_a instance1 ( .out1(out1), .out2(out2), .in1(a), .in2(b), .in3(c), .in4(d) );

endmodule

代码详解

  1. 顶层模块声明
    • 顶层模块 top_module 包含了四个输入端口 a, b, c, d,以及两个输出端口 out1, out2
  2. 模块实例化
    • 模块 mod_a 被实例化为 instance1,并通过按名称连接的方式将信号与 mod_a 的端口一一对应连接。具体的连接如下:
      • out1 连接到 mod_aout1
      • out2 连接到 mod_aout2
      • a 连接到 mod_ain1
      • b 连接到 mod_ain2
      • c 连接到 mod_ain3
      • d 连接到 mod_ain4

按名称连接的优点

  1. 端口顺序无关:按名称连接的最大优点是它不依赖模块端口的顺序,因此即使子模块 mod_a 的端口顺序发生了变化,只要端口名称不变,信号的连接仍然是正确的。这对于大型设计,或者未来需要修改子模块端口的情况,极大提高了代码的维护性。

  2. 可读性增强:按名称连接通过明确地指定信号与端口的对应关系,代码变得更加清晰和直观,特别是在端口数量较多或端口顺序复杂时,这种方式避免了出错的可能性。

  3. 更易于扩展:如果在未来的设计中需要对 mod_a 模块进行扩展或修改,按名称连接可以轻松应对这种变化,而无需对实例化的代码进行大范围的修改。

按位置连接的对比

与按名称连接不同,按位置连接是通过信号在实例化时的位置顺序来进行连接的。虽然按位置连接的代码更加简洁,但它的一个明显缺点是依赖模块端口的顺序。因此,如果 mod_a 的端口顺序发生变化,所有实例化的代码都需要重新修改。这使得按位置连接在维护和扩展方面的灵活性较差,尤其是在端口数量较多的情况下,容易产生错误。

按位置连接的语法如下:

mod_a instance1 (out1, out2, a, b, c, d);

在上面的代码中,信号是通过它们在 mod_a 中的位置依次进行连接的,因此顺序非常重要。

实例化模块的优势

通过模块实例化,设计者可以使用已经定义的模块来构建复杂的系统,从而实现模块化设计的优势:

  • 模块化设计:将复杂的电路设计分割为多个独立的模块,每个模块只需关注其自身的功能。模块实例化允许设计者将这些模块组合起来,形成更复杂的电路。
  • 复用性:设计好的模块可以在不同的设计中复用,避免重复编写相同的逻辑。这不仅节省了开发时间,还提高了代码的可维护性和一致性。
  • 层次结构:通过模块的实例化,可以形成电路设计的层次结构,便于管理和调试复杂的电路系统。

Three modules

image-20241022213145067

在 Verilog 设计中,移位寄存器(shift register)是一种常见的电路,用于将数据按时钟信号依次通过多个存储单元进行传递。在这个问题中,目标是使用三个 D 触发器模块(my_dff)来构建一个长度为3的移位寄存器。通过这种方式,可以逐步将输入信号 d 传递给下一个触发器,最终输出到端口 q

问题分析

模块 my_dff 是一个具有两个输入端 clkd,以及一个输出端 qD 触发器。通过实例化三个 my_dff 模块,并将它们串联起来,便可以实现一个长度为3的移位寄存器。

移位寄存器的基本工作原理如下:

  1. 输入信号 d 通过第一个 D 触发器。
  2. 第一个触发器的输出 q1 作为第二个触发器的输入。
  3. 第二个触发器的输出 q2 作为第三个触发器的输入。
  4. 第三个触发器的输出 q 作为最终输出。

所有触发器共享相同的时钟信号 clk,使得在每个时钟上升沿时,数据可以逐步传递到下一个触发器。

代码实现思路

  1. 模块实例化:需要实例化三个 my_dff 模块,并且每个实例必须有唯一的名称。
  2. 内部连接:每个触发器的输出需要通过一个内部信号(wire)连接到下一个触发器的输入,因此需要声明两个内部信号来传递数据。
  3. 时钟连接:所有触发器的 clk 端口连接到相同的时钟信号 clk

Verilog 实现

module top_module ( 
    input clk,      // 时钟信号
    input d,        // 数据输入
    output q        // 数据输出
);

    // 声明两个内部信号,用于连接触发器之间的输出和输入
    wire q1, q2;

    // 实例化三个 my_dff 模块,构建长度为3的移位寄存器
    my_dff dff1 ( .clk(clk), .d(d),  .q(q1) );   // 第一个触发器
    my_dff dff2 ( .clk(clk), .d(q1), .q(q2) );   // 第二个触发器
    my_dff dff3 ( .clk(clk), .d(q2), .q(q) );    // 第三个触发器

endmodule

代码详解

  1. 输入输出端口声明
    • clk:时钟信号,驱动所有的 D 触发器。
    • d:移位寄存器的输入数据。
    • q:移位寄存器的输出数据,是最后一个 D 触发器的输出。
  2. 内部信号声明
    • q1q2 是两个内部信号,用于连接触发器之间的数据传递。
    • q1:第一个 D 触发器的输出,连接到第二个 D 触发器的输入。
    • q2:第二个 D 触发器的输出,连接到第三个 D 触发器的输入。
  3. 模块实例化
    • 第一个触发器:实例化 my_dff 并命名为 dff1。其时钟信号 clk 直接连接到顶层模块的 clk,输入 d 连接到顶层的输入 d,输出 q1 是第一个 D 触发器的输出,用于传递到下一个触发器。
    • 第二个触发器:实例化 my_dff 并命名为 dff2。其时钟信号仍然连接到 clk,输入信号 d 连接到第一个触发器的输出 q1,输出信号 q2 传递到下一个触发器。
    • 第三个触发器:实例化 my_dff 并命名为 dff3。时钟信号 clk 与前面两个触发器相同,输入 d 连接到第二个触发器的输出 q2,最终输出 q 连接到顶层模块的输出。

时序与移位寄存器的工作原理

在时钟信号的每个上升沿,数据 d 会依次通过每个触发器。具体来说:

  • 在第一个时钟周期,输入 d 被存储在第一个 D 触发器中,并输出 q1
  • 在第二个时钟周期,q1 被存储在第二个 D 触发器中,并输出 q2
  • 在第三个时钟周期,q2 被存储在第三个 D 触发器中,并最终输出 q

因此,这个3级移位寄存器每个时钟周期会将输入信号逐步移动,并在3个时钟周期后输出最后的结果。

模块实例化的优势

通过模块实例化,可以轻松地将多个子模块组合在一起构建更复杂的电路系统。这里通过实例化 my_dff 模块,构建了一个3级移位寄存器,模块化的设计有助于代码的复用性、可读性和可维护性:

  1. 模块复用性:每个 my_dff 模块可以在不同的设计中复用,通过实例化多个触发器,设计者可以轻松地构建不同长度的移位寄存器。
  2. 结构化设计:将电路划分为独立的模块,每个模块只负责一部分逻辑,最终通过连接实现复杂的电路设计。
  3. 易于调试:由于每个触发器都是一个独立的模块,任何问题都可以隔离在特定的实例中进行调试和测试。

Modules and vectors

image-20241022213201977

在这个问题中,目标是使用提供的 my_dff8 模块,构建一个8位宽、3级的移位寄存器,并通过一个4选1的多路复用器(MUX)选择输出。在 Verilog 设计中,模块实例化和向量端口的使用是基本技能。本题的核心是通过模块实例化、信号的正确连接以及多路复用器来实现一个多级移位寄存器,并根据选择信号 sel[1:0] 动态选择不同的输出。

问题分析

  1. 模块 my_dff8
    • 这是一个包含8个D触发器的模块,它有两个8位输入信号和一个8位输出信号。输入 clk 是时钟,输入 d 是8位数据,输出 q 是8位寄存器的值。
  2. 移位寄存器
    • 需要实例化三个 my_dff8 模块,并将它们串联起来,形成一个8位宽度、3级深度的移位寄存器。
    • 第一个 my_dff8 的输入 d 接收外部输入信号,输出 q1 作为第二个模块的输入;第二个模块的输出 q2 作为第三个模块的输入,第三个模块的输出 q3 作为最终输出。
  3. 4选1多路复用器(MUX)
    • 多路复用器将根据 sel[1:0] 的值选择不同级的移位寄存器输出:
      • sel == 2'b00:选择原始输入 d
      • sel == 2'b01:选择第一级 my_dff8 的输出 q1
      • sel == 2'b10:选择第二级 my_dff8 的输出 q2
      • sel == 2'b11:选择第三级 my_dff8 的输出 q3

设计步骤

  1. 实例化三个 my_dff8 模块:需要通过实例化三个 my_dff8 模块并将它们串联起来。输出 q 的信号会传递给下一级的输入。

  2. 定义内部信号:为了连接各级移位寄存器的输出,需要定义两个内部信号 q1q2,它们都是8位宽度的向量。

  3. 多路复用器实现:使用 always 块和 case 语句来实现4选1多路复用器,选择不同的移位寄存器级输出。

Verilog 代码实现

c

代码详解

  1. 内部信号声明
    • q1q2 是两个8位宽的 wire 信号,分别用于存储第一级和第二级 my_dff8 模块的输出。q3 是第三级的输出。
  2. 模块实例化
    • 第一个 my_dff8 模块:第一个模块的输入是 d,输出是 q1q1 作为第二个模块的输入。
    • 第二个 my_dff8 模块:第二个模块的输入是 q1,输出是 q2q2 作为第三个模块的输入。
    • 第三个 my_dff8 模块:第三个模块的输入是 q2,输出是 q3。这是最终的输出信号。
  3. 多路复用器
    • 使用 always @(*) 块实现4选1多路复用器,根据 sel 信号选择输出:
      • sel == 2'b00:输出为原始输入 d
      • sel == 2'b01:输出为第一级移位寄存器的输出 q1
      • sel == 2'b10:输出为第二级移位寄存器的输出 q2
      • sel == 2'b11:输出为第三级移位寄存器的输出 q3

设计要点

  1. 模块实例化:通过实例化 my_dff8 模块来构建8位宽的移位寄存器。每个模块都处理8位数据,彼此串联形成3级的移位结构。

  2. 时钟信号同步:每个 my_dff8 模块都使用相同的时钟信号 clk,确保移位寄存器能够在时钟的上升沿同步工作。

  3. 多路复用器:通过 always @(*) 组合逻辑来实现多路复用器,根据 sel 的值动态选择移位寄存器中不同级的数据输出。

  4. 信号宽度一致:所有的信号(输入 d、输出 q 以及内部信号 q1q2q3)都是8位宽度,确保在不同模块之间传递数据时不需要进行截断或零扩展。

image-20241022213733300

继续改进

1. my_dff8 模块代码

my_dff8 是一个 8 位的 D 触发器模块。它接受一个 8 位输入 d 和一个时钟信号 clk,然后在时钟的上升沿将输入 d 存储到输出 q 中。每个 my_dff8 模块可以看作 8 个独立的 D 触发器,并且它们同时操作。

以下是 my_dff8 的 Verilog 实现:

module my_dff8 (
    input clk,          // 时钟信号
    input [7:0] d,      // 8位输入数据
    output reg [7:0] q  // 8位输出数据
);

    // 在时钟上升沿更新输出 q
    always @(posedge clk) begin
        q <= d;
    end

endmodule

2. 将多路复用器模块化 (mux41)

为了模块化多路复用器 mux41,将其作为一个独立的模块。该模块接受 4 个 8 位输入和一个 2 位选择信号 sel,根据选择信号输出相应的 8 位数据。

mux41 模块化代码

module mux41 (
    input [7:0] d0,     // 第一个输入
    input [7:0] d1,     // 第二个输入
    input [7:0] d2,     // 第三个输入
    input [7:0] d3,     // 第四个输入
    input [1:0] sel,    // 2位选择信号
    output reg [7:0] y  // 输出信号
);

    // 根据 sel 的值选择不同的输入赋值给 y
    always @(*) begin
        case (sel)
            2'b00: y = d0;  // 选择 d0
            2'b01: y = d1;  // 选择 d1
            2'b10: y = d2;  // 选择 d2
            2'b11: y = d3;  // 选择 d3
            default: y = 8'b00000000; // 默认值
        endcase
    end

endmodule

完整设计:移位寄存器和多路复用器模块化集成

my_dff8mux41 模块结合起来,构建一个完整的 3 级 8 位移位寄存器,并通过 mux41 实现 4 选 1 的多路选择。

top_module 完整代码

module top_module (
    input clk,              // 时钟信号
    input [7:0] d,          // 8位输入数据
    input [1:0] sel,        // 选择信号
    output [7:0] q          // 8位输出数据
);

    // 定义内部信号,用于连接移位寄存器的各级输出
    wire [7:0] q1, q2, q3;

    // 实例化三个 my_dff8 模块,构建8位宽、3级移位寄存器
    my_dff8 dff1 ( .clk(clk), .d(d),  .q(q1) );   // 第一个 my_dff8 模块
    my_dff8 dff2 ( .clk(clk), .d(q1), .q(q2) );   // 第二个 my_dff8 模块
    my_dff8 dff3 ( .clk(clk), .d(q2), .q(q3) );   // 第三个 my_dff8 模块

    // 实例化多路复用器 mux41,根据 sel 选择移位寄存器的不同级输出
    mux41 mux (
        .d0(d),     // 原始输入
        .d1(q1),    // 第一级移位寄存器输出
        .d2(q2),    // 第二级移位寄存器输出
        .d3(q3),    // 第三级移位寄存器输出
        .sel(sel),  // 选择信号
        .y(q)       // 最终输出
    );

endmodule

应用场景

这个移位寄存器模块确实有具体的应用场景,并不是几个 q 值完全相同。该模块实现了一个带有可选输出的移位寄存器,这在很多数字电路和信号处理的应用中非常有用。移位寄存器的主要功能是将输入数据按时钟信号依次传递到后面的级别,每一级都有不同的输出状态,而不同的输出在时钟周期上存在时间差。

移位寄存器的实际用途

  1. 延迟线:该设计可以用作可变延迟线。根据选择信号 sel[1:0],可以选择数据的不同延迟版本:
    • sel = 2'b00 时,输出直接是输入数据,没有延迟。
    • sel = 2'b01 时,输出是输入数据延迟了一个时钟周期的值。
    • sel = 2'b10 时,输出是延迟了两个时钟周期的值。
    • sel = 2'b11 时,输出是延迟了三个时钟周期的值。

    这种功能在信号处理、图像处理和通信系统中非常有用。例如,可以用来同步多个信号,或者用来生成延迟版本的信号用于数据对齐。

  2. 数据缓存与流水线:在数据传输中,移位寄存器可以充当数据缓存,让数据在不同的时间点有不同的存储位置,这可以帮助缓冲不同的数据流,确保系统能够处理不同步的数据输入。此外,这种设计可以用于流水线处理,其中每个时钟周期数据都会移动到下一级,可以提高系统的处理吞吐量。

  3. 时间序列处理:在某些应用中,需要在连续时钟周期中对数据执行某种时间相关的操作。例如,在数字滤波器或其他数字信号处理系统中,历史数据是重要的。移位寄存器可以帮助保留之前几个时钟周期的数据,从而实现滤波或其他需要历史数据的算法。

为什么 q 值不同?

虽然 q1, q2, q3 代表的是同一输入信号 d 在不同的时钟周期下的状态,但这些 q 并不相同,因为它们对应的是输入信号 d 在不同时间点上的值。

  1. q1:存储的是输入信号 d 在上一个时钟周期的值。
  2. q2:存储的是 q1 在上一个时钟周期的值,即 d 在上两个时钟周期的值。
  3. q3:存储的是 q2 在上一个时钟周期的值,即 d 在上三个时钟周期的值。

因此,移位寄存器在每个时钟周期内传递数据,导致不同的 q 输出在时间上是错开的。也就是说:

  • q1d 的一个时钟周期前的状态,
  • q2d 的两个时钟周期前的状态,
  • q3d 的三个时钟周期前的状态。

通过选择 sel,可以选择输出 d 在不同时间的状态。

选择信号的作用

sel 信号决定从移位寄存器的哪个阶段输出数据:

  • sel = 2'b00 时,直接输出当前输入 d
  • sel = 2'b01 时,输出的是 q1,也就是输入 d 延迟一个时钟周期后的状态。
  • sel = 2'b10 时,输出 q2,表示 d 延迟了两个时钟周期。
  • sel = 2'b11 时,输出 q3,表示 d 延迟了三个时钟周期。

这就使得这个模块不仅仅是一个移位寄存器,而是一个可以灵活选择延迟输出的模块。

实际应用场景总结

  1. 信号同步:例如在通信系统中,多个数据流的延迟可能不同,通过这样的移位寄存器,可以将不同数据流对齐。

  2. 流水线处理:通过多个寄存器级,可以实现流水线操作,使得每个时钟周期都有不同的数据进入下一级处理,从而增加系统的处理效率。

  3. 延迟生成:在某些信号处理系统中,延迟是一个重要的处理步骤,通过这个模块,可以轻松地生成不同长度的延迟信号。

综上所述,这个模块能够根据选择信号生成具有不同延迟的信号输出,在各种数字电路、通信系统和信号处理系统中非常有用。


Adder 1

image-20241022213218477

任务说明

在这个任务中,目标是使用两个16位的加法器模块 add16 来构建一个32位的加法器。每个 add16 模块可以执行16位加法,而32位加法则通过组合两个 add16 模块实现。

模块分析

给定的 add16 模块具有以下端口:

  • 输入端口
    • a[15:0]:16位输入a。
    • b[15:0]:16位输入b。
    • cin:进位输入。
  • 输出端口
    • sum[15:0]:16位和。
    • cout:进位输出。

32位加法器的构建

32位加法可以通过两次16位加法实现:

  1. 低16位加法:使用第一个 add16 模块计算输入 a[15:0]b[15:0] 的和,并输出低16位的结果。
    • 进位输入 cin 设置为 0,因为这是最低有效位部分的计算。
    • 输出 sum[15:0]cout(进位输出)。
  2. 高16位加法:使用第二个 add16 模块计算输入 a[31:16]b[31:16] 的和,并且将第一个加法器的进位输出 cout 作为第二个加法器的进位输入 cin
    • 输出 sum[31:16]

Verilog 实现

module top_module(
    input [31:0] a,    // 32位输入a
    input [31:0] b,    // 32位输入b
    output [31:0] sum  // 32位输出sum
);

    wire cout_low;     // 第一个加法器的进位输出

    // 实例化第一个 add16 模块,用于计算低16位
    add16 adder_low (
        .a(a[15:0]),
        .b(b[15:0]),
        .cin(1'b0),           // 低16位的加法进位输入为0
        .sum(sum[15:0]),      // 输出的低16位
        .cout(cout_low)       // 输出的进位
    );

    // 实例化第二个 add16 模块,用于计算高16位
    add16 adder_high (
        .a(a[31:16]),
        .b(b[31:16]),
        .cin(cout_low),       // 高16位的加法进位输入是低16位的进位输出
        .sum(sum[31:16]),     // 输出的高16位
        .cout()               // 忽略高16位的进位输出
    );

endmodule

代码详解

  1. 内部信号 cout_low
    • 定义了 wire cout_low 来保存第一个加法器的进位输出,这个进位输出会作为第二个加法器的进位输入。
  2. 实例化第一个 add16 模块
    • 第一个 add16 模块负责计算低16位输入 a[15:0]b[15:0] 的加法。
    • cin 被设置为 1'b0,因为这是最低有效位部分的加法,不需要进位输入。
    • sum[15:0] 保存计算后的低16位结果,cout_low 保存进位输出。
  3. 实例化第二个 add16 模块
    • 第二个 add16 模块负责计算高16位输入 a[31:16]b[31:16] 的加法。
    • 进位输入 cin 连接到第一个 add16 模块的进位输出 cout_low,确保进位可以正确地传播到高16位的加法器。
    • sum[31:16] 保存高16位的加法结果。

通过两个16位加法器模块 add16 的组合,可以构建一个32位的加法器。低16位的加法结果直接由第一个加法器计算,高16位的加法结果由第二个加法器计算,进位通过 cout 在两个加法器之间传递。

image-20241022213926752

16位加法器的实现可以基于Ripple Carry Adder(串行进位加法器)的结构,或者使用更复杂的加法逻辑如CLA(Carry Look-ahead Adder)。在这里,给出一个简单的基于 D 触发器实现的 add16 加法器模块,其功能是进行16位宽度的加法运算。

根据之前的要求,add16 模块有三个输入和两个输出:

  • a[15:0]b[15:0] 是两个16位的输入数据。
  • cin 是进位输入。
  • 输出 sum[15:0] 是16位的加法结果。
  • 输出 cout 是加法后的进位输出。

16位加法器 add16 模块代码

module add16 (
    input [15:0] a,         // 16位输入a
    input [15:0] b,         // 16位输入b
    input cin,              // 进位输入
    output [15:0] sum,      // 16位加法结果
    output cout             // 进位输出
);

    // 使用 Verilog 中的 + 运算符进行加法
    assign {cout, sum} = a + b + cin;

endmodule

代码详解

  1. 输入输出声明
    • ab 是两个16位的输入信号,表示要进行加法运算的两个数。
    • cin 是加法器的进位输入,用于处理从低位传递过来的进位。
    • sum 是16位的输出信号,表示加法运算的结果。
    • cout 是加法器的进位输出,用于传递到更高位的加法器。
  2. 加法运算
    • assign {cout, sum} = a + b + cin; 使用 Verilog 的加法运算符 + 来计算两个16位数 ab 的和,同时加上进位输入 cin
    • {cout, sum} 是一个拼接表达式。它将 coutsum 拼接在一起,以便通过加法器计算时能够产生一个17位的结果,其中最高位是 cout,表示进位输出。

使用此 add16 模块的说明

这个 add16 模块可以直接用于更大宽度的加法运算(如32位、64位等)中。通过将两个16位的 add16 模块串联起来,可以构建一个32位加法器。第一个 add16 模块处理较低的16位加法,第二个 add16 模块处理较高的16位,并且第一个 add16 的进位输出 cout 会传递给第二个 add16 的进位输入 cin


Adder 2

image-20241022213232766

任务说明

在这个任务中,目标是构建一个32位加法器。这个设计分为三个模块:

  1. top_module:顶层模块,负责组合两个16位加法器模块 add16,实现32位的加法运算。
  2. add16:提供的16位加法器模块,使用16个1位的全加器 add1 实现。
  3. add1:需要编写的1位全加器模块,实现对两个输入位和一个进位的加法。

模块分析

  • add1 模块:这是一个标准的1位全加器,输入为 abcin,输出为 sumcout。全加器的逻辑是计算 a + b + cin 的和,并产生进位输出 cout
  • add16 模块:16位加法器模块,使用16个 add1 实现,将16位的 ab 相加,同时传递进位。
  • top_module 模块:这是32位加法器的顶层模块,通过实例化两个 add16 模块,计算32位的加法。

1位全加器 (add1) 实现

全加器的功能是计算 a + b + cin,输出 sum(和)和 cout(进位)。全加器的逻辑公式如下:

  • sum = a ⊕ b ⊕ cin (异或运算)
  • cout = (a ∧ b) ∨ (cin ∧ (a ⊕ b)) (与和或运算)
module add1 (
    input a,        // 输入a
    input b,        // 输入b
    input cin,      // 进位输入
    output sum,     // 加法结果
    output cout     // 进位输出
);

    // 使用全加器的公式实现 sum 和 cout
    assign sum = a ^ b ^ cin;         // 异或计算 sum
    assign cout = (a & b) | (cin & (a ^ b)); // 进位输出

endmodule

16位加法器 (add16) 使用 add1

在16位加法器中,每一位的加法由一个 add1 来完成。进位在相邻的 add1 之间传递。第一个 add1 模块的进位输入为 cin,而后续的每个 add1 模块的进位输入来自前一个 add1 模块的 cout

module add16 (
    input [15:0] a,        // 16位输入a
    input [15:0] b,        // 16位输入b
    input cin,             // 进位输入
    output [15:0] sum,     // 16位加法结果
    output cout            // 进位输出
);

    wire [14:0] carry;     // 内部进位信号
    
    // 实例化16个add1模块,连接各个位的加法
    add1 a0 ( .a(a[0]),  .b(b[0]),  .cin(cin),       .sum(sum[0]),  .cout(carry[0]) );
    add1 a1 ( .a(a[1]),  .b(b[1]),  .cin(carry[0]),  .sum(sum[1]),  .cout(carry[1]) );
    add1 a2 ( .a(a[2]),  .b(b[2]),  .cin(carry[1]),  .sum(sum[2]),  .cout(carry[2]) );
    add1 a3 ( .a(a[3]),  .b(b[3]),  .cin(carry[2]),  .sum(sum[3]),  .cout(carry[3]) );
    add1 a4 ( .a(a[4]),  .b(b[4]),  .cin(carry[3]),  .sum(sum[4]),  .cout(carry[4]) );
    add1 a5 ( .a(a[5]),  .b(b[5]),  .cin(carry[4]),  .sum(sum[5]),  .cout(carry[5]) );
    add1 a6 ( .a(a[6]),  .b(b[6]),  .cin(carry[5]),  .sum(sum[6]),  .cout(carry[6]) );
    add1 a7 ( .a(a[7]),  .b(b[7]),  .cin(carry[6]),  .sum(sum[7]),  .cout(carry[7]) );
    add1 a8 ( .a(a[8]),  .b(b[8]),  .cin(carry[7]),  .sum(sum[8]),  .cout(carry[8]) );
    add1 a9 ( .a(a[9]),  .b(b[9]),  .cin(carry[8]),  .sum(sum[9]),  .cout(carry[9]) );
    add1 a10( .a(a[10]), .b(b[10]), .cin(carry[9]),  .sum(sum[10]), .cout(carry[10]) );
    add1 a11( .a(a[11]), .b(b[11]), .cin(carry[10]), .sum(sum[11]), .cout(carry[11]) );
    add1 a12( .a(a[12]), .b(b[12]), .cin(carry[11]), .sum(sum[12]), .cout(carry[12]) );
    add1 a13( .a(a[13]), .b(b[13]), .cin(carry[12]), .sum(sum[13]), .cout(carry[13]) );
    add1 a14( .a(a[14]), .b(b[14]), .cin(carry[13]), .sum(sum[14]), .cout(carry[14]) );
    add1 a15( .a(a[15]), .b(b[15]), .cin(carry[14]), .sum(sum[15]), .cout(cout) );

endmodule

32位加法器 (top_module) 使用两个 add16

在顶层模块 top_module 中,两个 add16 模块用于分别计算32位加法器的低16位和高16位。

  • 低16位:第一个 add16 模块计算输入 a[15:0]b[15:0] 的加法,进位输入为 0
  • 高16位:第二个 add16 模块计算输入 a[31:16]b[31:16] 的加法,其进位输入来自第一个 add16 模块的进位输出。
module top_module (
    input [31:0] a,        // 32位输入a
    input [31:0] b,        // 32位输入b
    output [31:0] sum      // 32位加法结果
);

    wire cout_low;         // 第一个 add16 的进位输出

    // 实例化第一个 add16 模块,用于计算低16位
    add16 adder_low (
        .a(a[15:0]),
        .b(b[15:0]),
        .cin(1'b0),           // 低16位的加法进位输入为0
        .sum(sum[15:0]),      // 输出的低16位
        .cout(cout_low)       // 输出的进位
    );

    // 实例化第二个 add16 模块,用于计算高16位
    add16 adder_high (
        .a(a[31:16]),
        .b(b[31:16]),
        .cin(cout_low),       // 高16位的加法进位输入是低16位的进位输出
        .sum(sum[31:16]),     // 输出的高16位
        .cout()               // 忽略高16位的进位输出
    );

endmodule
  1. add1:这是一个标准的1位全加器模块,用于执行一位的加法操作。
  2. add16:这是一个16位的加法器,通过16个 add1 模块组合而成,每个位的进位信号传递到下一个 add1 模块。
  3. top_module:顶层模块,使用两个 add16 模块实现了一个32位的加法器,低16位和高16位的加法通过进位级联连接起来。
module top_module (
    input [31:0] a,
    input [31:0] b,
    output [31:0] sum
);
    wire cout_1;
    add16 a16_1 (a[15:0], b[15:0], 1'b0, sum[15:0], cout_1);
    add16 a16_2 (a[31:16], b[31:16], cout_1, sum[31:16]);    

endmodule

module add16 ( input[15:0] a, input[15:0] b, input cin, output[15:0] sum, output cout );
    wire a1o, a2o, a3o, a4o, a5o, a6o, a7o, a8o, a9o, a10o, a11o, a12o, a13o, a14o, a15o;
    add1 a1 (a[0], b[0], cin, sum[0], a1o);
    add1 a2 (a[1], b[1], a1o, sum[1], a2o);
    add1 a3 (a[2], b[2], a2o, sum[2], a3o);
    add1 a4 (a[3], b[3], a3o, sum[3], a4o);
    add1 a5 (a[4], b[4], a4o, sum[4], a5o);
    add1 a6 (a[5], b[5], a5o, sum[5], a6o);
    add1 a7 (a[6], b[6], a6o, sum[6], a7o);
    add1 a8 (a[7], b[7], a7o, sum[7], a8o);
    add1 a9 (a[8], b[8], a8o, sum[8], a9o);
    add1 a10 (a[9], b[9], a9o, sum[9], a10o);
    add1 a11 (a[10], b[10], a10o, sum[10], a11o);
    add1 a12 (a[11], b[11], a11o, sum[11], a12o);
    add1 a13 (a[12], b[12], a12o, sum[12], a13o);
    add1 a14 (a[13], b[13], a13o, sum[13], a14o);
    add1 a15 (a[14], b[14], a14o, sum[14], a15o);
    add1 a16 (a[15], b[15], a15o, sum[15], cout);
endmodule

module add1 ( input a, input b, input cin,   output sum, output cout );
 assign sum = a ^ b ^ cin;
    assign cout = (a & b) | ((a ^ b) & cin);

endmodule

QQ_1729566655960

右键-新标签页打开图片


Carry-select adder

image-20241022213249738

任务说明

这次任务的目标是设计一个进位选择加法器(Carry-Select Adder),这是相对于Ripple Carry Adder(串行进位加法器)的改进。进位选择加法器通过并行计算可能的结果来减少延迟。它的设计思路是:

  • 对高位部分,假设有两种可能的进位:cin = 0cin = 1,并行计算这两种情况下的结果。
  • 最后使用一个2选1的多路复用器来选择正确的结果,这样可以加速进位传播。

设计思路

  1. 低16位加法:使用提供的 add16 模块计算低16位的加法结果 sum[15:0],并生成进位输出 cout,也就是 add16_lout
  2. 高16位加法:为了避免等待 cout,并行计算两个可能的高16位加法结果:
    • add16_h0 假设 cin = 0
    • add16_h1 假设 cin = 1
    • 两个加法结果分别为 add16_hs0add16_hs1
  3. 多路复用器:根据低16位加法器的进位输出 cout(即 add16_lout),通过2选1多路复用器选择正确的高16位加法结果。

模块实现

1. top_module 模块实现

module top_module(
    input [31:0] a,        // 32位输入a
    input [31:0] b,        // 32位输入b
    output [31:0] sum      // 32位加法结果
);

    wire add16_lout;       // 低16位加法器的进位输出
    wire [15:0] add16_hs0, add16_hs1;  // 高16位两种进位情况下的加法结果

    // 低16位加法器,cin = 0
    add16 add16_l (
        .a(a[15:0]),
        .b(b[15:0]),
        .cin(1'b0),
        .sum(sum[15:0]),
        .cout(add16_lout)
    );

    // 高16位加法器,假设cin = 0
    add16 add16_h0 (
        .a(a[31:16]),
        .b(b[31:16]),
        .cin(1'b0),
        .sum(add16_hs0),
        .cout()  // 忽略 cout
    );

    // 高16位加法器,假设cin = 1
    add16 add16_h1 (
        .a(a[31:16]),
        .b(b[31:16]),
        .cin(1'b1),
        .sum(add16_hs1),
        .cout()  // 忽略 cout
    );

    // 通过多路复用器选择正确的高16位加法结果
    mux21_16bits mux_a (
        .a(add16_hs0),        // cin = 0 的加法结果
        .b(add16_hs1),        // cin = 1 的加法结果
        .sel(add16_lout),     // 根据低16位的进位输出来选择
        .mout(sum[31:16])     // 高16位的最终加法结果
    );

endmodule

2. mux21_16bits 模块实现

module mux21_16bits(
    input [15:0] a,     // 当选择信号为0时的输入
    input [15:0] b,     // 当选择信号为1时的输入
    input sel,          // 选择信号
    output [15:0] mout  // 输出
);

    // 根据选择信号 sel 来选择 a 或者 b
    assign mout = sel ? b : a;

endmodule

代码详解

  1. top_module
    • 低16位加法器:使用 add16 计算低16位的加法结果 sum[15:0],并输出进位 cout 作为信号 add16_lout
    • 高16位加法器并行计算
      • add16_h0 假设 cin = 0,并计算高16位的加法结果。
      • add16_h1 假设 cin = 1,并计算高16位的加法结果。
    • 多路复用器:根据低16位加法器的进位输出 add16_lout,通过 mux21_16bits 选择两个并行计算的高16位加法结果之一作为最终输出。
  2. mux21_16bits
    • 这是一个2选1的多路复用器,基于选择信号 sel 来选择 a 或者 b 作为输出。
    • sel = 0 时,选择 a,即高16位加法器 cin = 0 时的结果。
    • sel = 1 时,选择 b,即高16位加法器 cin = 1 时的结果。

Adder-subtractor

image-20241022213300403

任务分析

在本任务中,需要构建一个加法-减法器(Adder-Subtractor),它能够执行两种运算:

  1. 加法a + b
  2. 减法a - b

减法可以通过两的补数表示,这意味着:

  • a - b 等价于 a + (~b + 1),即将 b 取反后加1。
  • 使用 XOR 操作 可以方便地实现条件取反。具体来说,当 sub = 0 时执行加法,而当 sub = 1 时,对 b 进行按位取反并加上1。

模块设计

  1. 输入
    • ab:32位的输入数据。
    • sub:控制加法或减法的信号,当 sub = 1 时执行减法,当 sub = 0 时执行加法。
  2. XOR 操作
    • 使用32位的 XOR 门,根据 sub 信号对 b 进行条件取反。
    • sub = 0 时,b XOR 0 = b,即不改变 b
    • sub = 1 时,b XOR 1 = ~b,即对 b 按位取反,准备执行减法。
  3. 加法器
    • 使用提供的 add16 模块(两次实例化),分别计算低16位和高16位的加法或减法。
    • sub 作为低16位加法器的进位输入 cin,用来实现 +1 操作(对于减法来说,就是 ~b + 1)。

Verilog 实现

module top_module(
    input [31:0] a,       // 32位输入a
    input [31:0] b,       // 32位输入b
    input sub,            // 加法或减法控制信号
    output [31:0] sum     // 32位加法/减法结果
);

    wire [31:0] b_xor;    // XOR后b的值
    wire cout_low;        // 低16位加法器的进位输出

    // 使用32位的XOR门条件地对b进行取反
    assign b_xor = b ^ {32{sub}};  // 当sub = 1时,b按位取反;当sub = 0时,b保持不变

    // 低16位加法器
    add16 add_low (
        .a(a[15:0]),
        .b(b_xor[15:0]),
        .cin(sub),             // sub 作为cin,用于控制加1操作
        .sum(sum[15:0]),
        .cout(cout_low)        // 低16位的进位
    );

    // 高16位加法器
    add16 add_high (
        .a(a[31:16]),
        .b(b_xor[31:16]),
        .cin(cout_low),        // 高16位的cin来自低16位的cout
        .sum(sum[31:16]),
        .cout()                // 忽略最终进位
    );

endmodule

代码详解

  1. b_xor 信号
    • b ^ {32{sub}} 使用了 Verilog 的复制运算符 {32{sub}},将 sub 复制成32位。
    • sub = 0 时,b ^ 0 保持 b 不变,执行加法。
    • sub = 1 时,b ^ 1b 进行按位取反,为减法做准备。
  2. 低16位加法器
    • add16 add_low 负责处理低16位的加法或减法。cin 直接连接到 sub,这样可以控制是否执行加1(实现减法的+1部分)。
    • 低16位加法器的进位输出 cout_low 会传递给高16位加法器的进位输入。
  3. 高16位加法器
    • add16 add_high 负责处理高16位的加法或减法。它的进位输入 cin 来自低16位加法器的进位输出 cout_low
    • 最终的进位输出 cout 被忽略,因为这不是我们关心的内容。

该设计实现了一个32位的加法-减法器,通过 sub 信号来控制是执行加法还是减法。XOR 门用于条件地对输入 b 进行取反,同时加法器模块通过 sub 作为 cin 来控制是否执行 +1 操作,从而实现了减法操作。

image-20241022214239593