Verilog Language: More Verilog Features 更多 Verilog 特性


Conditional ternary operator

在 Verilog 中,三元条件运算符? :)与 C 语言中的类似,可以用于在一行中根据条件选择两个值之一。它非常适合在组合逻辑中简洁地实现多路复用器(MUX)或条件逻辑,特别是在简单的比较和选择逻辑中。

三元条件运算符的语法

(condition ? if_true : if_false)
  • 如果 condition 条件为真,结果为 if_true
  • 如果 condition 条件为假,结果为 if_false

应用场景

  • 选择两个信号的值:可以用来在两个信号之间进行选择。

    assign out = sel ? b : a;  // 如果 sel 为 1,选择 b,否则选择 a。
    
  • 多级条件嵌套:可以通过嵌套多个三元运算符来构建多路复用器。

    assign min = (a < b) ? a : b;
    

本任务目标

给定四个 8 位无符号整数 abcd,找出它们的最小值。我们可以使用三元条件运算符来构建两两比较的逻辑,并将这些比较结果组合在一起实现四路比较。

设计思路

  1. 先比较 ab,找出较小的值。
  2. 再比较 cd,找出较小的值。
  3. 最后,将上一步的两个结果进行比较,得出最终的最小值。

通过逐步比较,我们可以使用三元条件运算符来实现这些逻辑。

Verilog 实现

module top_module (
    input [7:0] a, b, c, d,   // 4个8位无符号输入
    output [7:0] min          // 输出最小值
);

    // 使用三元条件运算符逐步比较
    wire [7:0] min_ab = (a < b) ? a : b;  // 比较 a 和 b
    wire [7:0] min_cd = (c < d) ? c : d;  // 比较 c 和 d
    assign min = (min_ab < min_cd) ? min_ab : min_cd;  // 比较两者的最小值

endmodule

image-20241022214925789

代码详解

  1. 输入和输出定义
    • a, b, c, d 是 4 个 8 位宽的无符号输入。
    • min 是输出,表示这 4 个数中的最小值。
  2. 比较 ab
    • 使用三元条件运算符 (a < b) ? a : b 比较 ab,将较小的值赋给中间变量 min_ab
  3. 比较 cd
    • 同样使用三元条件运算符 (c < d) ? c : d 比较 cd,将较小的值赋给中间变量 min_cd
  4. 比较最终的最小值
    • 通过比较 min_abmin_cd,得出最终的最小值并赋给输出 min

为什么使用三元条件运算符?

三元条件运算符非常适合在一行中实现条件逻辑,特别是在组合逻辑中通过这种简洁的方式实现多路复用器或比较逻辑。它避免了使用 if-elsealways 块,让代码更简洁。

关键点总结

  1. 三元条件运算符的高效使用
    • 可以用来简洁地实现比较和选择逻辑,避免过多的 if-else 结构,使代码更加简洁和易读。
  2. 组合逻辑的层级设计
    • 通过逐步比较两两输入值,然后将结果进行最终比较,可以实现更复杂的逻辑,而无需大量代码。

通过使用三元条件运算符,我们实现了一个简洁的 4 路最小值比较器。代码结构清晰,逻辑层次分明,通过逐步比较找到四个输入中最小的值。三元条件运算符的使用让代码更为紧凑高效,适用于这种简单的组合逻辑设计。


Reduction operators

归约操作符(Reduction Operators)在 Verilog 中用于对向量的所有位进行按位操作,并产生一个单一的输出。这种操作符特别适合用于对所有位执行逻辑运算,如 AND、OR、XOR 等。它们的作用是将一组位(向量)“归约”成一个结果位。

常见的归约操作符

  • &:按位 AND 归约,例如 &a[3:0] 相当于 a[3] & a[2] & a[1] & a[0]
  • |:按位 OR 归约,例如 |b[3:0] 相当于 b[3] | b[2] | b[1] | b[0]
  • ^:按位 XOR 归约,例如 ^c[2:0] 相当于 c[2] ^ c[1] ^ c[0]

奇偶校验(Parity Check)

在数据传输中,奇偶校验(parity checking)是一种常见的错误检测方法。它通过检查数据中 1 的数量来判断数据是否可能存在错误。奇偶校验分为两种:

  • 偶校验(even parity):校验位被设置为使得整个数据字节中的 1 的数量为偶数。
  • 奇校验(odd parity):校验位被设置为使得整个数据字节中的 1 的数量为奇数。

任务要求:偶校验

我们需要计算 8 位输入数据的偶校验位,即输出一个 parity 位,该位为输入 8 位数据的所有位的异或结果。异或运算 (XOR) 可以检测数据中的 1 的数量是奇数还是偶数,如果结果是 1,表示 1 的数量是奇数,输出校验位 1 使其变为偶数。

Verilog 实现

module top_module (
    input [7:0] in,      // 8位输入信号
    output parity        // 校验位输出
);

    // 使用 XOR 归约操作符计算偶校验
    assign parity = ^in;  // 计算 in[7:0] 的 XOR 归约,输出偶校验位

endmodule

image-20241022215016589

代码详解

  1. 输入和输出信号
    • in[7:0]:8 位输入数据,用于计算校验位。
    • parity:输出的偶校验位,它是输入数据的 XOR 归约结果。
  2. ^in 操作
    • ^in 是 Verilog 中的归约 XOR 操作符,它对 in[7:0] 的所有位执行 XOR 运算。归约 XOR 操作符的结果是一个 1 位信号,如果输入数据中 1 的个数为奇数,则结果为 1,否则为 0
    • 因为我们使用的是偶校验,所以直接使用 ^in 就可以生成正确的校验位。

关键点总结

  1. 归约 XOR:归约 XOR 操作符将一个多位信号归约为单一的输出位,特别适合用于奇偶校验的计算。
  2. 偶校验:在偶校验中,校验位的作用是确保数据中 1 的总数为偶数。通过 XOR 运算,可以方便地计算数据中 1 的数量是否为奇数,并生成适当的校验位。

通过使用 Verilog 中的归约 XOR 操作符,我们可以简洁地实现一个偶校验位计算器。该设计通过一行代码直接计算输入数据的 XOR 归约,输出一个校验位,确保数据中的 1 的数量为偶数。这个实现不仅简单易懂,还能高效地应用于实际的硬件设计中,如数据传输的错误检测。


Reduction: Even wider gates

先行的知识讲解

在 Verilog 中,归约操作符非常适合对向量进行逻辑运算,并将所有位的结果归约为一个单一的输出位。对于一个有大量输入的组合逻辑电路,使用归约操作符可以大大简化设计。

三种主要的归约操作符

  1. 归约 AND(&:将向量的所有位按位执行 AND 运算。例如,&in[99:0] 会对 in 向量中的所有位进行 AND 运算,相当于 in[99] & in[98] & ... & in[0]
  2. 归约 OR(|:将向量的所有位按位执行 OR 运算。例如,|in[99:0] 会对 in 向量中的所有位进行 OR 运算。
  3. 归约 XOR(^:将向量的所有位按位执行 XOR 运算。例如,^in[99:0] 会对 in 向量中的所有位进行 XOR 运算。

这些归约操作符可以非常方便地应用于大规模输入的组合逻辑电路设计中,比如 100 输入的 AND、OR、XOR 门。

任务要求

构建一个具有 100 个输入的组合逻辑电路,该电路的输出包括:

  1. out_and:100 输入 AND 门的输出。
  2. out_or:100 输入 OR 门的输出。
  3. out_xor:100 输入 XOR 门的输出。

Verilog 实现

module top_module( 
    input [99:0] in,       // 100位输入信号
    output out_and,        // AND 归约结果输出
    output out_or,         // OR 归约结果输出
    output out_xor         // XOR 归约结果输出
);

    // 使用归约操作符进行计算
    assign out_and = &in;  // 100位 AND 归约
    assign out_or  = |in;  // 100位 OR 归约
    assign out_xor = ^in;  // 100位 XOR 归约

endmodule

image-20241022215118985

代码详解

  1. 输入信号
    • in[99:0]:这是 100 位的输入向量,表示 100 个输入信号。
  2. 输出信号
    • out_and:表示对 in 向量的所有位进行 AND 操作的结果。如果 in 中的所有位都为 1,则 out_and1,否则为 0
    • out_or:表示对 in 向量的所有位进行 OR 操作的结果。如果 in 中至少有一个位为 1,则 out_or1,否则为 0
    • out_xor:表示对 in 向量的所有位进行 XOR 操作的结果。如果 in1 的个数为奇数,out_xor1,否则为 0
  3. 使用归约操作符
    • &in:将输入向量 in 的所有位进行 AND 运算。只有当所有位都是 1 时,输出 1,否则输出 0
    • |in:将输入向量 in 的所有位进行 OR 运算。只要有一个位是 1,输出就为 1
    • ^in:将输入向量 in 的所有位进行 XOR 运算。计算所有 1 的位数,如果是奇数则输出 1,如果是偶数则输出 0

使用归约操作符的优点

使用 Verilog 的归约操作符可以大大简化设计:

  • 无需逐位地进行逻辑操作,只需要一个归约操作符就可以对整个向量进行处理。
  • 这种方式既简洁又高效,适合处理大规模输入的逻辑运算。

通过使用 Verilog 中的归约操作符,我们能够非常简洁地实现 100 输入的 AND、OR 和 XOR 组合逻辑电路。该设计通过一行代码实现对 100 位输入信号的归约操作,大大减少了代码量和逻辑复杂度。


Combinational for-loop: Vector reversal 2

本题要求将一个 100 位宽的输入向量 in[99:0] 的位顺序反转,输出反转后的结果 out[99:0]。即,对于输入向量 in[99:0],其最高位 in[99] 需要被赋值给输出向量 out[0],次高位 in[98] 被赋值给 out[1],依此类推,直到最低位 in[0] 被赋值给 out[99]

在 Verilog 中,可以通过多种方式实现这种位反转操作:

  1. 使用 for 循环:通过组合逻辑中的 always @(*) 块,遍历输入向量并反转其位顺序。
  2. 手动赋值:手动将每个输入位分配给相应的输出位,这对于 100 位的向量来说显然不现实,代码会变得冗长。

由于题目涉及 100 位的向量反转,使用 for 循环 是最简洁和高效的方式。

代码实现

module top_module( 
    input [99:0] in,        // 100位输入信号
    output reg [99:0] out   // 100位输出信号,反转后的结果
);

    always @(*) begin
        // 使用 for 循环将输入向量的位反转
        integer i;
        for (i = 0; i < 100; i = i + 1) begin
            out[i] = in[99-i];  // 将 in 的第 i 位赋给 out 的第 (99-i) 位
        end
    end

endmodule

代码详解

  1. 输入信号 in[99:0]
    • 这是一个 100 位宽的输入向量,表示要反转位序的输入数据。
  2. 输出信号 out[99:0]
    • 这是一个 100 位宽的输出向量,表示反转位序后的输出数据。
  3. always @(*)
    • 使用组合逻辑的 always @(*) 块,确保每次 in 的值发生变化时,都会重新计算 out
    • 组合逻辑意味着一旦 in 改变,out 的值也会立即更新。
  4. for 循环
    • 使用 for 循环从 i = 0i = 99,每次将输入向量 in 的第 99-i 位赋值给输出向量 out 的第 i 位。
    • 这意味着 in[99] 会被赋给 out[0]in[98] 会被赋给 out[1],依此类推,直到 in[0] 被赋给 out[99]

关键点总结

  1. 位反转逻辑
    • 通过 for 循环可以有效地对向量进行位反转。每一位的赋值可以通过索引计算出来,从而避免手动处理每一位的赋值。
  2. 组合逻辑的使用
    • always @(*) 块定义了一个组合逻辑电路,每当输入信号 in 改变时,输出 out 会立即更新。该逻辑块在硬件中不会产生任何锁存器或寄存器,完全是组合逻辑。
  3. 高效实现
    • 由于 100 位的反转操作可能涉及很多位的逐位处理,使用 for 循环可以大大减少代码的冗余,简化设计,并提高代码的可读性和可维护性。

通过 for 循环,我们能够有效地对 100 位输入向量进行位反转。这个方法不仅简洁,而且在处理大规模位宽的向量时非常高效。设计中使用组合逻辑 always @(*) 块确保硬件实现中每次输入变化时都能正确计算输出。


Combinational for-loop: 255-bit population count

本题要求将一个 100 位宽的输入向量 in[99:0] 的位顺序反转,输出反转后的结果 out[99:0]。即,对于输入向量 in[99:0],其最高位 in[99] 需要被赋值给输出向量 out[0],次高位 in[98] 被赋值给 out[1],依此类推,直到最低位 in[0] 被赋值给 out[99]

在 Verilog 中,可以通过多种方式实现这种位反转操作:

  1. 使用 for 循环:通过组合逻辑中的 always @(*) 块,遍历输入向量并反转其位顺序。
  2. 手动赋值:手动将每个输入位分配给相应的输出位,这对于 100 位的向量来说显然不现实,代码会变得冗长。

由于题目涉及 100 位的向量反转,使用 for 循环 是最简洁和高效的方式。

代码实现

module top_module( 
    input [99:0] in,        // 100位输入信号
    output reg [99:0] out   // 100位输出信号,反转后的结果
);

    always @(*) begin
        // 使用 for 循环将输入向量的位反转
        integer i;
        for (i = 0; i < 100; i = i + 1) begin
            out[i] = in[99-i];  // 将 in 的第 i 位赋给 out 的第 (99-i) 位
        end
    end

endmodule

代码详解

  1. 输入信号 in[99:0]
    • 这是一个 100 位宽的输入向量,表示要反转位序的输入数据。
  2. 输出信号 out[99:0]
    • 这是一个 100 位宽的输出向量,表示反转位序后的输出数据。
  3. always @(*)
    • 使用组合逻辑的 always @(*) 块,确保每次 in 的值发生变化时,都会重新计算 out
    • 组合逻辑意味着一旦 in 改变,out 的值也会立即更新。
  4. for 循环
    • 使用 for 循环从 i = 0i = 99,每次将输入向量 in 的第 99-i 位赋值给输出向量 out 的第 i 位。
    • 这意味着 in[99] 会被赋给 out[0]in[98] 会被赋给 out[1],依此类推,直到 in[0] 被赋给 out[99]

关键点总结

  1. 位反转逻辑
    • 通过 for 循环可以有效地对向量进行位反转。每一位的赋值可以通过索引计算出来,从而避免手动处理每一位的赋值。
  2. 组合逻辑的使用
    • always @(*) 块定义了一个组合逻辑电路,每当输入信号 in 改变时,输出 out 会立即更新。该逻辑块在硬件中不会产生任何锁存器或寄存器,完全是组合逻辑。
  3. 高效实现
    • 由于 100 位的反转操作可能涉及很多位的逐位处理,使用 for 循环可以大大减少代码的冗余,简化设计,并提高代码的可读性和可维护性。

通过 for 循环,我们能够有效地对 100 位输入向量进行位反转。这个方法不仅简洁,而且在处理大规模位宽的向量时非常高效。设计中使用组合逻辑 always @(*) 块确保硬件实现中每次输入变化时都能正确计算输出。


Generate for-loop: 100-bit binary adder 2

一个Ripple-Carry Adder(逐位进位加法器)由一连串的全加器(Full Adders)组成。每个全加器处理一位的二进制加法,并将进位传递给下一个高位。为了实现 100 位的 Ripple-Carry Adder,我们需要将 100 个全加器级联,每个全加器负责计算一位的和,同时将进位传递给下一个全加器。

全加器的输入和输出

  • 输入:全加器有 3 个输入:
    • a:该位的第一个加数。
    • b:该位的第二个加数。
    • cin:来自前一个全加器的进位(或者是最初的 cin)。
  • 输出
    • sum:该位的和。
    • cout:传递给下一个全加器的进位。

Ripple-Carry Adder 的构成

  1. 初始进位:最低位的加法器使用输入的 cin 作为初始进位。
  2. 进位传递:每个全加器的 cout 会成为下一个全加器的 cin,一直传递到最高位。
  3. 最终进位:最高位的 cout 是整个加法器的最终进位输出。

实现思路

我们可以使用 Verilog 中的 generate 语句来创建 100 个全加器的实例。这些全加器的输入分别是两个 100 位的加数 ab,以及初始进位 cin。每个全加器的 cout 会传递给下一个全加器的 cin

全加器模块定义

首先,我们定义一个全加器模块 full_adder

module full_adder (
    input a, b, cin,       // 单位加数和进位输入
    output sum, cout       // 单位和和进位输出
);
    assign {cout, sum} = a + b + cin;  // 计算 sum 和 cout
endmodule

顶层模块实现(100 位 Ripple-Carry Adder)

module top_module(
    input [99:0] a, b,     // 两个 100 位的加数
    input cin,             // 初始进位
    output [99:0] cout,    // 每一位的进位输出
    output [99:0] sum      // 每一位的和输出
);

    genvar i;  // 定义循环变量
    generate
        for (i = 0; i < 100; i = i + 1) begin: full_adder_array
            if (i == 0) begin
                // 第一个全加器,使用外部输入的 cin
                full_adder fa (
                    .a(a[i]),
                    .b(b[i]),
                    .cin(cin),
                    .sum(sum[i]),
                    .cout(cout[i])
                );
            end else begin
                // 其他全加器,使用前一位的 cout 作为 cin
                full_adder fa (
                    .a(a[i]),
                    .b(b[i]),
                    .cin(cout[i-1]),
                    .sum(sum[i]),
                    .cout(cout[i])
                );
            end
        end
    endgenerate

endmodule

代码详解

  1. full_adder 模块
    • 这是一个简单的 1 位全加器。输入 abcin,输出 sumcout。我们使用 Verilog 中的加法运算符 {cout, sum} = a + b + cin; 来计算每个位的和和进位。
  2. top_module 模块
    • 输入信号 ab 是两个 100 位的二进制加数,cin 是初始进位。
    • 输出信号 cout 是每一位的进位输出,sum 是每一位的加法结果。
  3. generate 语句
    • 使用 genvar 定义一个生成变量 i,通过 generate 循环生成 100 个全加器实例。
    • 第一个全加器:第一个全加器使用输入的 cin 作为初始进位。
    • 后续全加器:从第 2 位到第 100 位的全加器,使用前一个全加器的 cout 作为 cin
  4. 链式进位
    • 每个全加器的 cout 传递给下一个全加器的 cin,从最低位到最高位逐位进行加法,最终在 cout[99] 输出最后的进位。

关键点总结

  1. Ripple-Carry 结构:使用全加器串联形成 Ripple-Carry 加法器,进位逐级传递。
  2. generate 语句的使用:通过 generate 语句可以有效地生成 100 个全加器实例,避免手动实例化每个全加器。
  3. 进位传递逻辑:第一个全加器使用 cin 作为输入,后续全加器依赖前一个全加器的 cout

这个设计实现了一个 100 位的 Ripple-Carry 加法器,通过 generate 语句和全加器模块的实例化,构建了一个能够处理 100 位加法运算的组合电路。


Generate for-loop: 100-digit BCD adder

BCD(Binary-Coded Decimal,二进制编码十进制)是一种将十进制数的每一位使用四位二进制数表示的编码方式。因此,对于一个 BCD 数,每个十进制位是一个 4 位二进制数,范围为 00001001(即 0 到 9)。当我们进行 BCD 加法时,如果加法结果超过 9(即 1001),需要额外处理进位。

在此问题中,您需要创建一个 100 位 BCD 加法器,它可以处理两个 100 位的 BCD 数字,并输出它们的和和进位。

BCD 加法器模块 bcd_fadd

  • 输入
    • ab 是 4 位宽的输入,分别表示两个 BCD 数。
    • cin 是前一位的进位。
  • 输出
    • sum 是 BCD 加法的和,也是 4 位宽。
    • cout 是加法结果中的进位,用于传递给下一位 BCD 加法器。

实现思路

  1. 创建 100 个 BCD 加法器实例
    • 使用 generate 语句来生成 100 个 BCD 加法器,每个加法器处理 4 位 BCD 数字,并且将进位传递到下一个 BCD 加法器。
  2. 输入和输出结构
    • ab 是 400 位的向量,表示两个 100 位 BCD 数字(每个数字用 4 位表示,100 位数字共 400 位)。
    • 输出 sum 也是一个 400 位的向量,每 4 位表示一个 BCD 数字的和。
    • cout 是最终的进位输出。
  3. 进位传递
    • 第一个 BCD 加法器使用 cin 作为初始进位,之后的每个 BCD 加法器从前一个加法器接收 cout 作为进位输入。

Verilog 实现

module top_module( 
    input [399:0] a,      // 400位输入,表示100位BCD数
    input [399:0] b,      // 400位输入,表示100位BCD数
    input cin,            // 初始进位
    output cout,          // 最终进位输出
    output [399:0] sum    // 400位输出,表示100位BCD加法的和
);

    wire [99:0] carry;  // 用于保存每个BCD加法器的进位

    genvar i;
    generate
        for (i = 0; i < 100; i = i + 1) begin: bcd_adder_array
            if (i == 0) begin
                // 第一个 BCD 加法器,使用 cin 作为初始进位
                bcd_fadd bcd_instance (
                    .a(a[3:0]), 
                    .b(b[3:0]), 
                    .cin(cin), 
                    .sum(sum[3:0]), 
                    .cout(carry[0])
                );
            end
            else begin
                // 后续 BCD 加法器,使用前一位的进位 carry[i-1]
                bcd_fadd bcd_instance (
                    .a(a[4*i+3:4*i]), 
                    .b(b[4*i+3:4*i]), 
                    .cin(carry[i-1]), 
                    .sum(sum[4*i+3:4*i]), 
                    .cout(carry[i])
                );
            end
        end
    endgenerate

    // 最后的进位输出
    assign cout = carry[99];

endmodule

代码详解

  1. 输入和输出定义
    • ab:400 位输入信号,表示两个 100 位的 BCD 数字。每 4 位表示一个十进制数,共 100 个 BCD 数字。
    • cin:初始的进位输入。
    • sum:400 位输出信号,表示加法结果。每 4 位表示一个 BCD 数字的和。
    • cout:表示最终的进位输出(从最后一个 BCD 加法器传递出来的进位)。
  2. generate 语句
    • 使用 genvar i 作为循环变量,通过 generate 语句实例化 100 个 BCD 加法器。
    • 第一个 BCD 加法器:第一个加法器的 cin 由输入的 cin 提供。
    • 后续 BCD 加法器:每个加法器的进位输入来自前一个加法器的进位输出 carry[i-1]
  3. carry 信号
    • carry[99:0] 保存了每个 BCD 加法器的进位输出。最终,carry[99] 作为最后的 cout 输出。

关键点总结

  1. BCD 加法器的级联:每个 BCD 加法器处理一位 BCD 数字,并将进位传递给下一个加法器,形成 Ripple-Carry 结构。
  2. 生成 100 个 BCD 加法器实例:通过 generate 语句生成多个实例,简化了手动实例化多个加法器的复杂性。
  3. 进位传递:第一个 BCD 加法器使用 cin 作为初始进位,之后的每个加法器使用前一个加法器的进位输出 carry[i-1]

这个设计通过实例化 100 个 BCD 加法器,实现了两个 100 位 BCD 数字的加法。使用 generate 语句有效地简化了代码结构,同时保证了 Ripple-Carry 进位传递的正确性。

module top_module( 
    input [399:0] a, b,
    input cin,
    output cout,
    output [399:0] sum );
    
    wire [99:0] cout_i;
    genvar i;
    generate
        for (i = 0; i < 400; i = i + 4) begin: bcd_add_array
            if (i == 0) begin
                bcd_fadd bcd_fa (a[3:0], b[3:0], cin, cout_i[0], sum[3:0]);
            end
            else begin
                bcd_fadd bcd_fa (a[i+3:i], b[i+3:i], cout_i[i/4 - 1], cout_i[i/4], sum[i+3:i]);
            end
        end
    endgenerate
    assign cout = cout_i[99];

endmodule