Verilog Language: Procedures 程序

程序包括 always 块、initial 块、task 和 function 块。程序允许使用顺序语句(这些语句不能在程序外使用)来描述电路的行为。


Always blocks (combinational)

image-20241022214515055

在 Verilog 中,设计硬件逻辑有两种常见的方式:连续赋值过程赋值,它们的实现方式分别为 assign 语句always @(*) 组合逻辑块。这两种方式在语法和使用场景上有一些差异,但在很多情况下,它们可以实现相同的功能。

1. 连续赋值 (assign 语句)

连续赋值用于描述组合逻辑,可以用来直接表达电路中的逻辑关系。它的主要特性是当赋值的右侧表达式中任何一个信号发生变化时,左侧的输出会立即更新。因此,assign 语句非常适合描述简单的逻辑电路。

  • 语法assign <wire> = <表达式>;
  • 使用限制assign 语句的左边必须是wire 类型,因为它代表电路中的信号连接,类似物理上的导线。
  • 例子assign sum = a + b; 这条语句表示将 ab 的和赋值给 sum,当 ab 的值发生变化时,sum 也会自动更新。

2. 组合逻辑的 always @(*)

assign 不同,always是一种更为通用的语法结构,可以用来实现组合逻辑,也可以用来实现时序逻辑。对于组合逻辑,通常使用 always @(*),表示当任何输入信号发生变化时重新计算输出。

  • 语法always @(*) <组合逻辑描述>
  • 使用限制always @(*) 块的左边输出必须是reg 类型,虽然 reg 通常用于存储数据,但在这里它仅仅是作为一个变量来存储组合逻辑的结果,并不真正代表时钟触发的寄存器。
  • 例子always @(*) begin sum = a + b; end 表示组合逻辑块,当 ab 变化时重新计算 sum 的值。

3. 区别与应用场景

  • assign 语句:适用于简单、直观的组合逻辑,特别是只涉及单一表达式的逻辑关系。
  • always @(*) 组合逻辑块:适合用于需要多条件判断(如 ifcase 语句)或是更复杂的组合逻辑设计。

例如,如果我们想实现一个简单的 AND 门,既可以使用 assign 语句,也可以使用 always @(*) 块。两者在硬件上表现相同,但在代码风格和可读性上可能会有所不同。

示例设计:实现一个 AND

我们将通过两种方式实现一个 AND 门:一种使用 assign 语句,另一种使用 always @(*) 块。

// synthesis verilog_input_version verilog_2001
module top_module(
    input a, 
    input b,
    output wire out_assign,      // 使用 assign 语句的输出,必须是 wire 类型
    output reg out_alwaysblock   // 使用 always 块的输出,必须是 reg 类型
);

    // 使用 assign 语句实现 AND 门
    assign out_assign = a & b;

    // 使用 always @(*) 块实现 AND 门
    always @(*) begin
        out_alwaysblock = a & b;
    end

endmodule

image-20241022215526049

代码详解

  1. assign 语句
    • assign out_assign = a & b; 表示通过 assign 语句实现组合逻辑的 AND 操作。当输入信号 ab 发生变化时,输出 out_assign 会立即更新,反映 a & b 的值。
    • 左侧的信号类型必须是 wire,因为 assign 语句会连续驱动该信号。
  2. always @(*) 组合逻辑块
    • always @(*) 块描述了同样的 AND 操作。当 ab 变化时,always 块会重新计算输出 out_alwaysblock 的值。
    • 左侧的信号类型必须是 reg,尽管名字是 reg,但它在这里并没有用作时序逻辑中的寄存器,而是作为组合逻辑的输出。

关键点总结

  1. assignalways @(*) 的相似性
    • 两者都用于描述组合逻辑,它们在电路实现中等效,并且会产生相同的硬件。
    • 当输入发生变化时,输出也会立即更新。
  2. 何时使用 assign
    • 适合简单、单一表达式的逻辑设计,代码简洁清晰。
    • 左侧信号必须是 wire
  3. 何时使用 always @(*)
    • 适合更复杂的组合逻辑,尤其是需要 if-elsecase 语句时。
    • 左侧信号必须是 reg

Verilog 提供了不同的方式来描述硬件电路,其中 assign 语句和 always @(*) 块在组合逻辑的设计中非常常用。虽然它们在硬件上的行为是相同的,但它们的语法和使用场景有所不同。通过本次练习,可以熟悉这两种方式的不同写法以及它们的适用场景。在实际设计中,选择哪种方式取决于逻辑的复杂性以及代码的可读性。


Always blocks (clocked)

image-20241022214529632

在 Verilog 中,always是用于描述硬件行为的核心结构。根据其用途,always 块可以分为两种类型:

  1. 组合逻辑 alwaysalways @(*)
  2. 时序逻辑 alwaysalways @(posedge clk)

这两种 always 块在 Verilog 设计中的作用非常不同:

  • 组合逻辑块用于描述纯粹的组合逻辑,没有时序元素。
  • 时序逻辑块用于描述时钟触发的逻辑(如寄存器或触发器),即每当时钟上升沿时,更新输出。

1. 组合逻辑 always 块 (always @(*))

组合逻辑块(always @(*))描述的是纯逻辑关系,当任何输入信号发生变化时,组合逻辑块会立即重新计算输出。这种方式等效于 assign 语句,并且在组合逻辑块内部使用的是阻塞赋值=)。

  • 阻塞赋值:阻塞赋值是一次性的,并且是顺序执行的。它在组合逻辑块中使用,保证操作按照顺序进行,模拟硬件中的组合逻辑。

2. 时序逻辑 always 块 (always @(posedge clk))

时序逻辑块描述时钟触发的逻辑,特别是寄存器。这里的逻辑仅在时钟上升沿时触发更新。这种设计模式用于实现触发器和寄存器,数据只会在时钟的上升沿进行更新。在时序逻辑块中,我们使用非阻塞赋值<=)。

  • 非阻塞赋值:非阻塞赋值允许多个赋值同时发生,这与硬件中的寄存器行为一致。在时序逻辑中,非阻塞赋值确保不会产生竞争,并且可以保证不同逻辑单元的同步更新。

3. 组合逻辑与时序逻辑的区别

组合逻辑的输出在输入变化时立即更新,而时序逻辑的输出只有在时钟沿触发时才会更新,这意味着时序逻辑的输出会相对滞后。时序逻辑通常用于构建状态机流水线数据存储单元,而组合逻辑则主要用于实现快速的逻辑计算。

实现 XOR 门

在这个示例中,我们会用三种方式实现一个 XOR 门:

  1. 使用 assign 语句进行连续赋值。
  2. 使用组合逻辑的 always @(*) 块。
  3. 使用时序逻辑的 always @(posedge clk) 块。

代码实现

// synthesis verilog_input_version verilog_2001
module top_module(
    input clk,
    input a,
    input b,
    output wire out_assign,       // 使用 assign 语句实现 XOR
    output reg out_always_comb,   // 使用组合逻辑的 always 块实现 XOR
    output reg out_always_ff      // 使用时序逻辑的 always 块实现 XOR
);

    // 1. 使用 assign 语句实现 XOR 门
    assign out_assign = a ^ b;

    // 2. 使用组合逻辑 always 块实现 XOR 门
    always @(*) begin
        out_always_comb = a ^ b;  // 使用阻塞赋值
    end

    // 3. 使用时序逻辑 always 块实现 XOR 门
    always @(posedge clk) begin
        out_always_ff <= a ^ b;   // 使用非阻塞赋值,模拟触发器行为
    end

endmodule

image-20241022215605133

代码详解

  1. assign 语句
    • assign out_assign = a ^ b; 使用连续赋值方式实现 XOR 逻辑。这种方式最简洁,直接声明了 out_assignab 的 XOR 结果。
    • 输出 out_assign 会在 ab 变化时立即更新。
  2. 组合逻辑的 always @(*)
    • always @(*) 块实现的 XOR 门与 assign 语句功能上相同,但它使用了过程赋值。
    • 使用的是阻塞赋值 =,当 ab 变化时,组合逻辑块会重新计算 out_always_comb 的值。
    • 这种方式适合更复杂的组合逻辑设计。
  3. 时序逻辑的 always @(posedge clk)
    • always @(posedge clk) 块实现的是时钟触发的 XOR 门,使用非阻塞赋值 <=
    • 每当时钟上升沿发生时,out_always_ff 才会更新为 a ^ b 的结果。这相当于在 XOR 门之后加了一个触发器,导致 out_always_ff 的值延迟一个时钟周期。
    • 这种方式适用于描述寄存器或触发器行为。

总结

  1. 组合逻辑 vs. 时序逻辑
    • 组合逻辑:通过 always @(*)assign 实现,输出与输入信号立即相关联,适合描述无时钟依赖的逻辑电路。
    • 时序逻辑:通过 always @(posedge clk) 实现,输出只有在时钟边沿时才更新,适合描述触发器和寄存器等需要同步的逻辑电路。
  2. 阻塞赋值 vs. 非阻塞赋值
    • 阻塞赋值:用于组合逻辑,在 always @(*) 块中使用。它按顺序执行,所有赋值都会立即生效。
    • 非阻塞赋值:用于时序逻辑,在 always @(posedge clk) 块中使用。它模拟了寄存器的行为,多个赋值可以在时钟边沿同时发生。

通过这三种方式实现 XOR 门,可以帮助理解 Verilog 中组合逻辑和时序逻辑的不同表达方式,以及阻塞赋值与非阻塞赋值的使用场景。在实际设计中,选择哪种方式取决于电路的功能需求:组合逻辑适合快速计算,而时序逻辑用于数据存储和状态同步。


If statement

image-20241022214542915

在 Verilog 设计中,if-else 语句条件运算符是两种常见的方法来实现选择逻辑,例如多路复用器(MUX)。两者都可以实现相同的硬件逻辑,但在不同的场景下使用有所差异。

1. if-else 语句的硬件意义

if-else 语句通常用于描述多路复用器(MUX),根据条件来选择不同的输入。具体的逻辑如下:

  • 如果条件为真,则选择某个输入。
  • 如果条件为假,则选择另一个输入。

在 Verilog 中,当 if-else 语句用在 组合逻辑块always @(*))中时,它通常会被综合为一个 2-to-1 MUX 或者更复杂的多路选择逻辑。这种语法的关键在于:输出必须在所有情况下都被赋值,否则可能会导致不完整赋值,进而产生锁存器(latch),这是组合逻辑设计中应该避免的。

2. 条件运算符(三元运算符)

条件运算符是一种更简洁的语法,效果与 if-else 语句类似。它的形式如下:

assign out = (condition) ? x : y;
  • condition 为真时,选择 x
  • condition 为假时,选择 y

条件运算符一般用于简洁描述简单的选择逻辑,它与 if-else 语句在逻辑上是等效的。

3. 组合逻辑中的错误陷阱

在组合逻辑设计中,输出信号必须在 if-else 块中始终被赋值,否则会生成锁存器。锁存器通常会引入意外的时序问题,导致电路行为与预期不一致。因此,在 always @(*) 中,确保所有条件分支都赋值是一个良好的设计习惯。

任务:实现一个 2-to-1 MUX

这里的任务是构建一个2-to-1 多路复用器,它根据两个选择信号 sel_b1sel_b2 的状态来决定选择输入 a 还是输入 b

  • sel_b1sel_b2 都为真时,选择 b
  • 否则,选择 a

我们将通过两种方式实现:

  1. 使用 assign 条件运算符
  2. 使用 if-else 语句

代码实现

// synthesis verilog_input_version verilog_2001
module top_module(
    input a,
    input b,
    input sel_b1,
    input sel_b2,
    output wire out_assign,  // 使用 assign 语句的输出
    output reg out_always    // 使用 always 块的输出
);

    // 1. 使用 assign 语句实现 2-to-1 MUX
    assign out_assign = (sel_b1 & sel_b2) ? b : a;

    // 2. 使用组合逻辑 always 块实现 2-to-1 MUX
    always @(*) begin
        if (sel_b1 & sel_b2) begin
            out_always = b;
        end
        else begin
            out_always = a;
        end
    end

endmodule

image-20241022215652192

代码详解

  1. 使用 assign 语句
    • assign out_assign = (sel_b1 & sel_b2) ? b : a; 这一行代码使用条件运算符来描述一个 2-to-1 MUX。
    • sel_b1sel_b2 同时为真时,选择 b,否则选择 a
    • 这种方式非常简洁,适合简单的组合逻辑设计。
  2. 使用组合逻辑的 always @(*)
    • always @(*) 块的作用是通过 if-else 语句实现同样的 2-to-1 MUX 逻辑。
    • sel_b1 & sel_b2 为真时,选择 b,否则选择 a
    • if-else 语句可以处理更复杂的逻辑,因此在需要多分支条件时更加直观。

关键点总结

  1. 条件运算符 vs. if-else
    • 条件运算符:适合简洁表达选择逻辑,可以直接通过 assign 语句实现,语法简洁且易读。
    • if-else 语句:更适合复杂的逻辑选择,可以处理多个条件分支。
  2. 组合逻辑中的 always @(*)
    • always @(*) 块中,务必确保输出信号在所有条件下都被赋值,避免产生锁存器(latch)。
    • always @(*) 块的优势在于可以处理复杂的逻辑表达,比如多重条件选择。
  3. 2-to-1 MUX 的实现
    • 使用两种不同的方法实现了一个 2-to-1 多路复用器,效果等效,但在不同场景下可以选择不同的方法以提高代码的简洁性或可读性。

通过这个练习,我们可以看到如何使用两种方式来实现选择逻辑:assign 条件运算符和 if-else 语句。这两者在逻辑上是等效的,但 if-else 语句提供了更大的灵活性,适用于复杂的组合逻辑。另一方面,条件运算符更简洁,适合简单的逻辑设计。


If statement latches

image-20241022214556637

在 Verilog 中,组合逻辑的设计必须保证所有可能的条件分支下,输出信号都被赋值。如果某些条件下输出没有被明确赋值,Verilog 将默认保持该输出的上一状态。为了达到这个效果,Verilog 编译器可能会隐式推断出锁存器(latch),而这是通常我们不希望出现的,因为锁存器可能引发意外的时序问题。

锁存器(latch)的推断主要来源于以下原因:

  • 设计者没有在所有可能的情况下给组合逻辑的输出赋值。
  • 组合逻辑需要记住某些状态,但并没有明确设计为同步逻辑(如寄存器)。

在组合逻辑设计中,我们应该避免使用锁存器,除非这是设计所需的。因此,在 always @(*) 块中,应该保证所有输出在任何情况下都被赋值,以避免 Verilog 编译器推断出锁存器。

锁存器推断的示例

以下是两个可能会推断出锁存器的例子:

  1. 未在 else 分支中赋值

    always @(*) begin
        if (cpu_overheated)
            shut_off_computer = 1;
        // 缺少 else 分支,可能会生成锁存器
    end
    
  2. 部分条件未处理

    always @(*) begin
        if (~arrived)
            keep_driving = ~gas_tank_empty;
        // 缺少 arrived == 1 的情况,可能会生成锁存器
    end
    

在这两种情况下,如果没有明确指定 else 或未处理的条件,Verilog 编译器将保持输出的状态不变,从而推断出锁存器。

解决锁存器推断的办法

为了避免这种情况,设计组合逻辑时,我们应该始终确保:

  • 为每一个条件都指定输出的值,要么通过 else,要么通过在每个 always 块的开头为输出赋一个默认值

例如:

always @(*) begin
    shut_off_computer = 0; // 默认值,避免锁存器
    if (cpu_overheated)
        shut_off_computer = 1;
end

always @(*) begin
    keep_driving = 0; // 默认值,避免锁存器
    if (~arrived)
        keep_driving = ~gas_tank_empty;
end

任务分析

  1. shut_off_computer 的逻辑:当 cpu_overheated1 时,应该关闭计算机,即 shut_off_computer = 1。否则,shut_off_computer 应该保持 0
  2. keep_driving 的逻辑:当没有到达目的地 (~arrived) 时,如果油箱不为空 (~gas_tank_empty),继续行驶。否则,停止驾驶。

代码实现

我们需要通过为每个输出信号设置默认值来防止锁存器推断。修正后的代码如下:

// synthesis verilog_input_version verilog_2001
module top_module (
    input      cpu_overheated,
    output reg shut_off_computer,
    input      arrived,
    input      gas_tank_empty,
    output reg keep_driving
);

    // 修正 1: 为 shut_off_computer 设置默认值,避免锁存器
    always @(*) begin
        shut_off_computer = 0;  // 默认值:未过热时不关闭计算机
        if (cpu_overheated)
            shut_off_computer = 1;  // 如果过热,则关闭计算机
    end

    // 修正 2: 为 keep_driving 设置默认值,避免锁存器
    always @(*) begin
        keep_driving = 0;  // 默认值:默认停止驾驶
        if (~arrived)
            keep_driving = ~gas_tank_empty;  // 如果未到达目的地且油箱不为空,继续行驶
    end

endmodule

image-20241022215754919

代码详解

  1. shut_off_computer 的逻辑
    • always @(*) 块的开头,将 shut_off_computer 赋值为 0,这意味着默认情况下不关闭计算机。
    • 如果 cpu_overheated1,将 shut_off_computer 置为 1,表示关闭计算机。
  2. keep_driving 的逻辑
    • 默认情况下,keep_driving 被赋值为 0,表示停止驾驶。
    • 如果尚未到达目的地 (~arrived) 且油箱不为空 (~gas_tank_empty),则将 keep_driving 设置为 1,表示继续驾驶。

关键点总结

  1. 避免锁存器推断:为了避免锁存器被意外推断,组合逻辑的每个输出信号必须在所有条件下都有明确的赋值。这可以通过 else 语句或为输出赋默认值来实现。
  2. 组合逻辑的行为:在设计组合逻辑时,要确保输出信号是由当前的输入条件完全决定的,而不是依赖于之前的状态。这一点对于避免不必要的锁存器至关重要。
  3. 改进设计:通过在 always @(*) 块的开头设置输出信号的默认值,可以有效地避免锁存器的推断,并确保组合逻辑的行为是预期的。

Verilog 代码中的锁存器推断是由于组合逻辑设计不完整造成的。在组合逻辑中,所有输出信号必须在所有条件下都有赋值。通过设置默认值或确保所有分支条件都被覆盖,可以避免这种问题,并确保生成的电路符合预期的行为。本次练习展示了如何通过简单的修正来避免 Verilog 中的常见错误。


Case statement

在 Verilog 中,case 语句非常类似于其他编程语言中的 switch 语句,但也有一些显著差异:

  • Verilog 中没有 break 语句,因为每个 case 分支只会执行一个语句或一个 begin-end 块。
  • case 语句适用于处理多个条件分支时的组合逻辑,非常适合用来实现多路复用器(MUX)等电路。

case 语句的使用

  • 每个 case 项使用冒号(:)分隔,当输入信号 in 匹配某个 case 项时,会执行该分支的代码。
  • 需要注意的是,如果需要执行多条语句,必须使用 begin ... end 块。
  • 如果所有的可能条件都没有匹配到 case 项,可以使用 default 分支,确保输出有一个确定的值。如果没有 default 分支,并且没有匹配的 case 项,Verilog 可能会推断出锁存器。

设计注意事项

  • 避免锁存器:在组合逻辑中,应该确保所有输出在任何条件下都有明确的赋值。这可以通过在 case 语句中覆盖所有可能的情况,或者提供一个 default 分支来实现。

任务分析

本任务要求构建一个6选1多路复用器(6-to-1 MUX),选择信号 sel 范围为 05,选择相应的 4 位数据输入 data0data5。如果 sel 的值超出 0 到 5 的范围,则输出 out 应为 0

Verilog 代码实现

// synthesis verilog_input_version verilog_2001
module top_module ( 
    input [2:0] sel,            // 3位选择信号
    input [3:0] data0,          // 4位输入 data0
    input [3:0] data1,          // 4位输入 data1
    input [3:0] data2,          // 4位输入 data2
    input [3:0] data3,          // 4位输入 data3
    input [3:0] data4,          // 4位输入 data4
    input [3:0] data5,          // 4位输入 data5
    output reg [3:0] out        // 4位输出
);

    // 使用 case 语句实现 6-to-1 多路复用器
    always @(*) begin
        case (sel)
            3'd0: out = data0;  // 选择 data0
            3'd1: out = data1;  // 选择 data1
            3'd2: out = data2;  // 选择 data2
            3'd3: out = data3;  // 选择 data3
            3'd4: out = data4;  // 选择 data4
            3'd5: out = data5;  // 选择 data5
            default: out = 4'b0000;  // 如果 sel 超出范围,输出0
        endcase
    end

endmodule

image-20241022215950236

代码详解

  1. 输入/输出定义
    • 输入 sel 是 3 位宽的选择信号,用于选择 6 个 4 位数据输入中的一个。
    • data0data5 是 4 位宽的输入数据。
    • 输出 out 是 4 位宽,存储根据 sel 选择的对应数据。
  2. always @(*)
    • 组合逻辑的 always @(*) 块用于描述多路复用器逻辑。组合逻辑意味着输出应该根据输入信号的变化立即更新。
    • case 语句根据 sel 信号的值选择相应的输入数据赋给 out
    • sel 的值在 05 之间时,对应选择 data0data5 中的某一个。
    • default 分支处理 sel 超出范围的情况,确保输出为 0,避免锁存器推断。
  3. 避免锁存器
    • case 语句的每个条件都对 out 进行了赋值,且使用了 default 分支,保证所有情况下 out 都会被赋值,从而避免锁存器推断。

关键点总结

  1. case 语句的使用
    • case 语句非常适合处理多种条件下的逻辑选择。在多路复用器设计中,使用 case 可以简洁、直观地描述选择逻辑。
  2. 避免锁存器推断
    • 通过确保在 case 语句中处理所有可能的选择信号 sel 的值,或者提供 default 分支,可以避免 Verilog 推断出锁存器。
  3. 多路复用器的实现
    • 6-to-1 MUX 通过选择信号 sel,从 6 个输入数据中选择一个输出。这种设计模式广泛应用于处理数据通道选择和控制流向。

通过 case 语句,我们实现了一个 6-to-1 的多路复用器。该设计通过 sel 信号选择相应的数据输入并输出,使用 default 分支确保在所有情况下输出都有确定的值,避免了锁存器的推断。case 语句在处理多个选择条件时非常方便,尤其适用于多路复用器、解码器等逻辑设计。


Priority encoder

先行的知识讲解

优先编码器(Priority Encoder)是一种组合逻辑电路,它根据输入向量中最高优先级的 1 输出该 1 所在的位置编号。优先编码器通常从高位开始检测输入,当检测到第一个 1 时,忽略低位。优先编码器的输出是第一个 1 出现的位置

Verilog 中的 case 语句

case 语句是描述多路选择逻辑的有力工具,类似于其他编程语言中的 switch 语句。在设计优先编码器时,case 语句可以用于检测特定的输入模式,并输出相应的优先级。

  • case 语句的格式

    case (表达式)
        条件1: begin
            // 执行某些操作
        end
        条件2: begin
            // 执行某些操作
        end
        default: begin
            // 默认操作
        end
    endcase
    
  • 优先编码器与 case:在实现优先编码器时,case 语句能够处理多个输入模式。通过从高位到低位检查输入信号,case 语句可以对特定输入模式进行匹配,并输出相应的编码。

避免锁存器(Latch)

锁存器推断通常是由于组合逻辑中未能处理所有可能的输入情况。如果没有为所有输入分支分配输出,Verilog 会推断出锁存器。在优先编码器中,如果没有输入为 1 时,应该输出 0,确保输出信号始终被赋值。

实现目标

设计一个 4 位优先编码器,检测输入 in[3:0],并输出 pos

  • 当输入向量中有 1 时,输出该 1 的最高位位置。
  • 当输入向量中全为 0 时,输出 0

正确使用 case 实现的 Verilog 代码

// synthesis verilog_input_version verilog_2001
module top_module (
    input [3:0] in,          // 4位输入信号
    output reg [1:0] pos     // 2位输出,表示第一个1的位置
);

    always @(*) begin
        // 使用 case 语句,优先检测高位
        case (1'b1)          // 将检测的表达式固定为 1'b1,以便按优先级匹配
            in[3]: pos = 2'd3;  // 如果 in[3] 为1,则输出位置 3
            in[2]: pos = 2'd2;  // 如果 in[2] 为1,则输出位置 2
            in[1]: pos = 2'd1;  // 如果 in[1] 为1,则输出位置 1
            in[0]: pos = 2'd0;  // 如果 in[0] 为1,则输出位置 0
            default: pos = 2'd0;  // 如果没有任何输入为1,输出0
        endcase
    end

endmodule

image-20241022220237903

代码详解

  1. 输入/输出定义
    • in:4 位输入信号 in[3:0],表示要编码的输入向量。
    • pos:2 位输出,表示输入向量中第一个 1 的位置。
  2. always @(*)
    • 这是一个组合逻辑块,通过 always @(*) 来描述优先编码器的逻辑。每当输入信号 in 变化时,立即重新计算输出。
  3. case 语句的使用
    • case (1'b1) 语法:在 Verilog 中,case 语句中可以将匹配的表达式固定为 1'b1,然后逐个检查每个位的输入。这种方法在优先编码器的设计中非常方便,因为它可以逐位检查 in[3:0],并在第一个检测到 1 时立即输出位置。
    • in[3]in[0]:依次从高位到低位进行检查。如果某位为 1,则立即输出该位对应的位置。
    • default:如果输入向量 in[3:0] 中没有任何 1,输出默认的 0。这样可以确保没有输入信号时不会推断出锁存器。
  4. 锁存器避免
    • default 分支用于处理所有输入为 0 的情况,确保输出信号 pos 始终被赋值,避免锁存器的推断。

为什么使用 case 语句?

if-else 语句相比,case 语句在处理多种不同输入时更加简洁和清晰,特别是当有明确的模式匹配时。使用 case (1'b1) 可以逐位检测输入信号,并根据最高位的 1 来输出相应的编码值。

这段代码实现了一个 4 位优先编码器,使用 case 语句来检测输入信号中第一个出现的 1,并输出其位置编码。通过 case (1'b1) 的语法,我们能够逐位检查输入,从高位到低位进行优先级判断。当所有输入都为 0 时,使用 default 分支确保输出为 0,避免推断出锁存器。这种设计既简单清晰,又符合硬件设计的要求。

If else

// synthesis verilog_input_version verilog_2001
module top_module (
    input [3:0] in,          // 4位输入信号
    output reg [1:0] pos     // 2位输出,表示第一个1的位置
);

    always @(*) begin
        // 默认值,防止锁存器推断
        pos = 2'd0;

        // 根据输入的最高有效位到最低位,按优先级选择
        if (in[3]) 
            pos = 2'd3;     // 如果 in[3] 为1,则输出 3
        else if (in[2]) 
            pos = 2'd2;     // 如果 in[2] 为1,则输出 2
        else if (in[1]) 
            pos = 2'd1;     // 如果 in[1] 为1,则输出 1
        else if (in[0]) 
            pos = 2'd0;     // 如果 in[0] 为1,则输出 0
        // 如果没有任何1,默认 pos = 0
    end

endmodule

Priority encoder with casez

casez 语句 是 Verilog 中的一个特殊 case 语句,用于在多条件分支中进行匹配时忽略不重要的位(don’t care bits)。它可以通过使用 z? 字符表示不关心的位。这样,可以减少 case 项的数量,从而简化优先编码器(Priority Encoder)等逻辑的设计。

casez 的功能

  • z表示“任意值”或“don’t care”,即当 z 位置可以是 01,该 case 条目依然可以匹配。
  • 匹配的顺序casez 会依次检查每个 case 条目,直到找到第一个匹配的条件。即使某个输入同时匹配多个 case 条目,系统只会执行第一个匹配的条目。

优先编码器工作原理

  • 优先编码器用于检测输入向量中最先出现的 1,并输出该 1 所在的位的位置编码。
  • 输入的 8 位向量 in[7:0] 中,从最低位开始检测,找到第一个 1,并输出其位置。如果没有 1,则输出 0

在这个问题中,我们需要使用 casez 来忽略高位的 0,只匹配低位的 1

Verilog 实现 8 位优先编码器

// synthesis verilog_input_version verilog_2001
module top_module (
    input [7:0] in,        // 8位输入信号
    output reg [2:0] pos   // 3位输出信号,表示第一个1的位置
);

    always @(*) begin
        casez (in)
            8'b00000001: pos = 3'd0;  // 如果最低位为1,输出0
            8'b0000001?: pos = 3'd1;  // 如果第1位为1,输出1
            8'b000001??: pos = 3'd2;  // 如果第2位为1,输出2
            8'b00001???: pos = 3'd3;  // 如果第3位为1,输出3
            8'b0001????: pos = 3'd4;  // 如果第4位为1,输出4
            8'b001?????: pos = 3'd5;  // 如果第5位为1,输出5
            8'b01??????: pos = 3'd6;  // 如果第6位为1,输出6
            8'b1???????: pos = 3'd7;  // 如果第7位为1,输出7
            default: pos = 3'd0;      // 如果没有1,输出0
        endcase
    end

endmodule

image-20241022220542191

代码详解

  1. 输入和输出信号
    • in[7:0]:这是 8 位输入向量,其中每一位可以是 01。我们需要检测第一个出现的 1 的位置。
    • pos[2:0]:这是 3 位输出信号,表示输入向量中第一个 1 出现的位位置。
  2. always @(*)
    • 组合逻辑的 always 块,每当输入 in 变化时,输出 pos 重新计算。
  3. casez 语句
    • casez (in):表示输入向量 in 需要逐条与 casez 条件进行匹配。z 位表示不关心的位,也就是在 casez 中被忽略。
    • 每一条 casez 项都从最低有效位开始,检查第一个 1 的位置。当检测到第一个 1 时,输出对应的位置编码。
    • 例如,8'b0000001? 表示输入的低位部分匹配 0000001x 的情况,无论 in[0]0 还是 1,当 in[1]1 时,该条件成立。
  4. 优先级逻辑
    • casez 从低位到高位依次进行匹配,确保检测到最先出现的 1。一旦找到匹配的 case 条目,立即输出对应的位位置,忽略其他低位的 1
    • default 分支用于处理输入向量中没有 1 的情况,输出默认值 0,确保组合逻辑的输出总是有定义的值。

使用 casez 的好处

相比于标准的 case 语句,casez 通过允许 z 位表示不关心的值,使我们能够简洁地处理优先级逻辑。通过逐位检查输入,casez 可以有效地实现优先编码器,避免为每一个可能的输入值都显式编写 case 条目(避免了 256 条 case 的复杂性)。

通过 casez 语句,我们实现了一个简洁的 8 位优先编码器。casezz 位使得我们可以忽略不关心的高位,从而简化代码逻辑,确保逐位检测输入信号,并输出第一个 1 出现的位置。这样设计不仅逻辑清晰,还能确保在所有情况下都能正确输出结果,并避免推断锁存器。


Avoiding latches

在设计硬件时,避免锁存器(latches)的生成是非常重要的。锁存器会在组合逻辑中保留状态,从而引发意外的行为,特别是在时序逻辑设计中。因此,在组合逻辑(如 always @(*) 块)中,确保所有输出在每种可能的输入下都有明确的赋值,是避免锁存器生成的关键。

如何避免锁存器推断

  1. 为所有输出赋默认值:在 caseif-else 语句之前,先为所有输出赋一个默认值,通常为 0。这样即使某些条件没有匹配到,输出也不会保持不变,从而避免锁存器。
  2. 覆盖所有条件:确保 case 语句或 if-else 语句处理了所有可能的情况。如果某些条件未被覆盖,Verilog 将推断出锁存器来记住之前的状态。

本次任务

我们需要根据键盘的扫描码(scancode)识别键盘上的方向键按下了哪个(上、下、左、右)。具体的扫描码和方向键的映射关系如下:

  • 16'he06b -> 左箭头
  • 16'he072 -> 下箭头
  • 16'he074 -> 右箭头
  • 16'he075 -> 上箭头
  • 其他情况 -> 没有方向键按下

设计思路

为了避免锁存器推断,我们可以为每个输出(leftdownrightup)先赋一个默认值(0),然后在 case 语句中根据扫描码的具体情况,设置相应的输出为 1

Verilog 实现

// synthesis verilog_input_version verilog_2001
module top_module (
    input [15:0] scancode,  // 16位输入,表示键盘的扫描码
    output reg left,        // 左方向键输出信号
    output reg down,        // 下方向键输出信号
    output reg right,       // 右方向键输出信号
    output reg up           // 上方向键输出信号
);

    always @(*) begin
        // 为所有输出信号赋默认值,避免锁存器
        left = 1'b0;
        down = 1'b0;
        right = 1'b0;
        up = 1'b0;

        // 根据扫描码设置相应方向键的输出
        case (scancode)
            16'he06b: left = 1'b1;   // 左箭头
            16'he072: down = 1'b1;   // 下箭头
            16'he074: right = 1'b1;  // 右箭头
            16'he075: up = 1'b1;     // 上箭头
            // 不需要 default,因为已经为所有输出赋了默认值
        endcase
    end

endmodule

image-20241022220625251

代码详解

  1. 输入信号 scancode
    • scancode 是一个 16 位宽的输入信号,表示从键盘接收到的扫描码。
  2. 输出信号 leftdownrightup
    • 这四个输出信号表示是否按下了对应的方向键。每个信号是 1 位的布尔信号,当某个方向键被按下时,对应的信号会被置为 1
  3. always @(*)
    • 组合逻辑块,表示当 scancode 发生变化时重新计算输出信号。
    • 在进入 case 语句之前,所有输出信号都被赋予默认值 0,以避免锁存器的推断。
  4. case 语句
    • 根据输入的 scancode,选择对应的方向键输出。
    • scancode16'he06b 时,设置 left = 1 表示按下了左箭头。
    • 类似地,其他扫描码分别对应按下的下箭头、右箭头和上箭头。
    • 不需要 default 分支,因为所有输出信号在 case 语句之前已经被赋默认值为 0,这避免了在没有匹配的 scancode 时推断出锁存器。

如何避免锁存器推断

  1. 默认值的设置
    • case 语句之前为所有输出设置默认值,确保在 scancode 不匹配时,输出保持为 0,从而避免锁存器。
  2. 无锁存器推断的关键
    • 在 Verilog 中,如果某个输出信号在某些输入条件下没有明确赋值,系统可能会推断出一个锁存器来保持先前的状态。通过在 always 块的开头赋默认值,可以避免这种情况。

该设计使用了 case 语句和组合逻辑 always @(*) 块来识别键盘方向键的扫描码。通过为所有输出信号设置默认值,我们有效地避免了锁存器的推断。这样确保了无论输入 scancode 是什么,所有输出信号都会有明确的值,符合组合逻辑的要求。这种设计不仅简洁,而且保证了硬件实现的正确性。