Verilog Language: Procedures 程序
程序包括 always 块、initial 块、task 和 function 块。程序允许使用顺序语句(这些语句不能在程序外使用)来描述电路的行为。
- Always blocks (combinational) 组合逻辑的 Always 块
- Always blocks (clocked) 时钟控制的 Always 块
- If statement If 语句
- If statement latches If 语句锁存器
- Case statement Case 语句
- Priority encoder 优先编码器
- Priority encoder with casez 带 casez 的优先编码器
- Avoiding latches 避免锁存器
Always blocks (combinational)

在 Verilog 中,设计硬件逻辑有两种常见的方式:连续赋值和过程赋值,它们的实现方式分别为 assign 语句 和 always @(*) 组合逻辑块。这两种方式在语法和使用场景上有一些差异,但在很多情况下,它们可以实现相同的功能。
1. 连续赋值 (assign 语句)
连续赋值用于描述组合逻辑,可以用来直接表达电路中的逻辑关系。它的主要特性是当赋值的右侧表达式中任何一个信号发生变化时,左侧的输出会立即更新。因此,assign 语句非常适合描述简单的逻辑电路。
- 语法:
assign <wire> = <表达式>; - 使用限制:
assign语句的左边必须是wire类型,因为它代表电路中的信号连接,类似物理上的导线。 - 例子:
assign sum = a + b;这条语句表示将a和b的和赋值给sum,当a或b的值发生变化时,sum也会自动更新。
2. 组合逻辑的 always @(*) 块
与 assign 不同,always 块是一种更为通用的语法结构,可以用来实现组合逻辑,也可以用来实现时序逻辑。对于组合逻辑,通常使用 always @(*),表示当任何输入信号发生变化时重新计算输出。
- 语法:
always @(*) <组合逻辑描述> - 使用限制:
always @(*)块的左边输出必须是reg类型,虽然reg通常用于存储数据,但在这里它仅仅是作为一个变量来存储组合逻辑的结果,并不真正代表时钟触发的寄存器。 - 例子:
always @(*) begin sum = a + b; end表示组合逻辑块,当a或b变化时重新计算sum的值。
3. 区别与应用场景
assign语句:适用于简单、直观的组合逻辑,特别是只涉及单一表达式的逻辑关系。always @(*)组合逻辑块:适合用于需要多条件判断(如if、case语句)或是更复杂的组合逻辑设计。
例如,如果我们想实现一个简单的 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

代码详解
assign语句:assign out_assign = a & b;表示通过assign语句实现组合逻辑的AND操作。当输入信号a或b发生变化时,输出out_assign会立即更新,反映a & b的值。- 左侧的信号类型必须是
wire,因为assign语句会连续驱动该信号。
always @(*)组合逻辑块:always @(*)块描述了同样的AND操作。当a或b变化时,always块会重新计算输出out_alwaysblock的值。- 左侧的信号类型必须是
reg,尽管名字是reg,但它在这里并没有用作时序逻辑中的寄存器,而是作为组合逻辑的输出。
关键点总结
assign和always @(*)的相似性:- 两者都用于描述组合逻辑,它们在电路实现中等效,并且会产生相同的硬件。
- 当输入发生变化时,输出也会立即更新。
- 何时使用
assign:- 适合简单、单一表达式的逻辑设计,代码简洁清晰。
- 左侧信号必须是
wire。
- 何时使用
always @(*):- 适合更复杂的组合逻辑,尤其是需要
if-else或case语句时。 - 左侧信号必须是
reg。
- 适合更复杂的组合逻辑,尤其是需要
Verilog 提供了不同的方式来描述硬件电路,其中 assign 语句和 always @(*) 块在组合逻辑的设计中非常常用。虽然它们在硬件上的行为是相同的,但它们的语法和使用场景有所不同。通过本次练习,可以熟悉这两种方式的不同写法以及它们的适用场景。在实际设计中,选择哪种方式取决于逻辑的复杂性以及代码的可读性。
Always blocks (clocked)
在 Verilog 中,always 块是用于描述硬件行为的核心结构。根据其用途,always 块可以分为两种类型:
- 组合逻辑
always块(always @(*)) - 时序逻辑
always块(always @(posedge clk))
这两种 always 块在 Verilog 设计中的作用非常不同:
- 组合逻辑块用于描述纯粹的组合逻辑,没有时序元素。
- 时序逻辑块用于描述时钟触发的逻辑(如寄存器或触发器),即每当时钟上升沿时,更新输出。
1. 组合逻辑 always 块 (always @(*))
组合逻辑块(always @(*))描述的是纯逻辑关系,当任何输入信号发生变化时,组合逻辑块会立即重新计算输出。这种方式等效于 assign 语句,并且在组合逻辑块内部使用的是阻塞赋值(=)。
- 阻塞赋值:阻塞赋值是一次性的,并且是顺序执行的。它在组合逻辑块中使用,保证操作按照顺序进行,模拟硬件中的组合逻辑。
2. 时序逻辑 always 块 (always @(posedge clk))
时序逻辑块描述时钟触发的逻辑,特别是寄存器。这里的逻辑仅在时钟上升沿时触发更新。这种设计模式用于实现触发器和寄存器,数据只会在时钟的上升沿进行更新。在时序逻辑块中,我们使用非阻塞赋值(<=)。
- 非阻塞赋值:非阻塞赋值允许多个赋值同时发生,这与硬件中的寄存器行为一致。在时序逻辑中,非阻塞赋值确保不会产生竞争,并且可以保证不同逻辑单元的同步更新。
3. 组合逻辑与时序逻辑的区别
组合逻辑的输出在输入变化时立即更新,而时序逻辑的输出只有在时钟沿触发时才会更新,这意味着时序逻辑的输出会相对滞后。时序逻辑通常用于构建状态机、流水线或数据存储单元,而组合逻辑则主要用于实现快速的逻辑计算。
实现 XOR 门
在这个示例中,我们会用三种方式实现一个 XOR 门:
- 使用
assign语句进行连续赋值。 - 使用组合逻辑的
always @(*)块。 - 使用时序逻辑的
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

代码详解
assign语句:assign out_assign = a ^ b;使用连续赋值方式实现 XOR 逻辑。这种方式最简洁,直接声明了out_assign是a和b的 XOR 结果。- 输出
out_assign会在a或b变化时立即更新。
- 组合逻辑的
always @(*)块:always @(*)块实现的 XOR 门与assign语句功能上相同,但它使用了过程赋值。- 使用的是阻塞赋值
=,当a或b变化时,组合逻辑块会重新计算out_always_comb的值。 - 这种方式适合更复杂的组合逻辑设计。
- 时序逻辑的
always @(posedge clk)块:always @(posedge clk)块实现的是时钟触发的 XOR 门,使用非阻塞赋值<=。- 每当时钟上升沿发生时,
out_always_ff才会更新为a ^ b的结果。这相当于在 XOR 门之后加了一个触发器,导致out_always_ff的值延迟一个时钟周期。 - 这种方式适用于描述寄存器或触发器行为。
总结
- 组合逻辑 vs. 时序逻辑:
- 组合逻辑:通过
always @(*)或assign实现,输出与输入信号立即相关联,适合描述无时钟依赖的逻辑电路。 - 时序逻辑:通过
always @(posedge clk)实现,输出只有在时钟边沿时才更新,适合描述触发器和寄存器等需要同步的逻辑电路。
- 组合逻辑:通过
- 阻塞赋值 vs. 非阻塞赋值:
- 阻塞赋值:用于组合逻辑,在
always @(*)块中使用。它按顺序执行,所有赋值都会立即生效。 - 非阻塞赋值:用于时序逻辑,在
always @(posedge clk)块中使用。它模拟了寄存器的行为,多个赋值可以在时钟边沿同时发生。
- 阻塞赋值:用于组合逻辑,在
通过这三种方式实现 XOR 门,可以帮助理解 Verilog 中组合逻辑和时序逻辑的不同表达方式,以及阻塞赋值与非阻塞赋值的使用场景。在实际设计中,选择哪种方式取决于电路的功能需求:组合逻辑适合快速计算,而时序逻辑用于数据存储和状态同步。
If statement

在 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_b1 和 sel_b2 的状态来决定选择输入 a 还是输入 b:
- 当
sel_b1和sel_b2都为真时,选择b。 - 否则,选择
a。
我们将通过两种方式实现:
- 使用
assign条件运算符。 - 使用
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

代码详解
- 使用
assign语句:assign out_assign = (sel_b1 & sel_b2) ? b : a;这一行代码使用条件运算符来描述一个 2-to-1 MUX。- 当
sel_b1和sel_b2同时为真时,选择b,否则选择a。 - 这种方式非常简洁,适合简单的组合逻辑设计。
- 使用组合逻辑的
always @(*)块:always @(*)块的作用是通过if-else语句实现同样的 2-to-1 MUX 逻辑。- 当
sel_b1 & sel_b2为真时,选择b,否则选择a。 if-else语句可以处理更复杂的逻辑,因此在需要多分支条件时更加直观。
关键点总结
- 条件运算符 vs. if-else:
- 条件运算符:适合简洁表达选择逻辑,可以直接通过
assign语句实现,语法简洁且易读。 if-else语句:更适合复杂的逻辑选择,可以处理多个条件分支。
- 条件运算符:适合简洁表达选择逻辑,可以直接通过
- 组合逻辑中的
always @(*):- 在
always @(*)块中,务必确保输出信号在所有条件下都被赋值,避免产生锁存器(latch)。 always @(*)块的优势在于可以处理复杂的逻辑表达,比如多重条件选择。
- 在
- 2-to-1 MUX 的实现:
- 使用两种不同的方法实现了一个 2-to-1 多路复用器,效果等效,但在不同场景下可以选择不同的方法以提高代码的简洁性或可读性。
通过这个练习,我们可以看到如何使用两种方式来实现选择逻辑:assign 条件运算符和 if-else 语句。这两者在逻辑上是等效的,但 if-else 语句提供了更大的灵活性,适用于复杂的组合逻辑。另一方面,条件运算符更简洁,适合简单的逻辑设计。
If statement latches

在 Verilog 中,组合逻辑的设计必须保证所有可能的条件分支下,输出信号都被赋值。如果某些条件下输出没有被明确赋值,Verilog 将默认保持该输出的上一状态。为了达到这个效果,Verilog 编译器可能会隐式推断出锁存器(latch),而这是通常我们不希望出现的,因为锁存器可能引发意外的时序问题。
锁存器(latch)的推断主要来源于以下原因:
- 设计者没有在所有可能的情况下给组合逻辑的输出赋值。
- 组合逻辑需要记住某些状态,但并没有明确设计为同步逻辑(如寄存器)。
在组合逻辑设计中,我们应该避免使用锁存器,除非这是设计所需的。因此,在 always @(*) 块中,应该保证所有输出在任何情况下都被赋值,以避免 Verilog 编译器推断出锁存器。
锁存器推断的示例
以下是两个可能会推断出锁存器的例子:
-
未在
else分支中赋值:always @(*) begin if (cpu_overheated) shut_off_computer = 1; // 缺少 else 分支,可能会生成锁存器 end -
部分条件未处理:
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
任务分析
shut_off_computer的逻辑:当cpu_overheated为1时,应该关闭计算机,即shut_off_computer = 1。否则,shut_off_computer应该保持0。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

代码详解
shut_off_computer的逻辑:- 在
always @(*)块的开头,将shut_off_computer赋值为0,这意味着默认情况下不关闭计算机。 - 如果
cpu_overheated为1,将shut_off_computer置为1,表示关闭计算机。
- 在
keep_driving的逻辑:- 默认情况下,
keep_driving被赋值为0,表示停止驾驶。 - 如果尚未到达目的地 (
~arrived) 且油箱不为空 (~gas_tank_empty),则将keep_driving设置为1,表示继续驾驶。
- 默认情况下,
关键点总结
- 避免锁存器推断:为了避免锁存器被意外推断,组合逻辑的每个输出信号必须在所有条件下都有明确的赋值。这可以通过
else语句或为输出赋默认值来实现。 - 组合逻辑的行为:在设计组合逻辑时,要确保输出信号是由当前的输入条件完全决定的,而不是依赖于之前的状态。这一点对于避免不必要的锁存器至关重要。
- 改进设计:通过在
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 范围为 0 到 5,选择相应的 4 位数据输入 data0 到 data5。如果 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

代码详解
- 输入/输出定义:
- 输入
sel是 3 位宽的选择信号,用于选择 6 个 4 位数据输入中的一个。 data0到data5是 4 位宽的输入数据。- 输出
out是 4 位宽,存储根据sel选择的对应数据。
- 输入
always @(*)块:- 组合逻辑的
always @(*)块用于描述多路复用器逻辑。组合逻辑意味着输出应该根据输入信号的变化立即更新。 case语句根据sel信号的值选择相应的输入数据赋给out。- 当
sel的值在0到5之间时,对应选择data0到data5中的某一个。 default分支处理sel超出范围的情况,确保输出为 0,避免锁存器推断。
- 组合逻辑的
- 避免锁存器:
case语句的每个条件都对out进行了赋值,且使用了default分支,保证所有情况下out都会被赋值,从而避免锁存器推断。
关键点总结
case语句的使用:case语句非常适合处理多种条件下的逻辑选择。在多路复用器设计中,使用case可以简洁、直观地描述选择逻辑。
- 避免锁存器推断:
- 通过确保在
case语句中处理所有可能的选择信号sel的值,或者提供default分支,可以避免 Verilog 推断出锁存器。
- 通过确保在
- 多路复用器的实现:
- 6-to-1 MUX 通过选择信号
sel,从 6 个输入数据中选择一个输出。这种设计模式广泛应用于处理数据通道选择和控制流向。
- 6-to-1 MUX 通过选择信号
通过 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

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

代码详解
- 输入和输出信号:
in[7:0]:这是 8 位输入向量,其中每一位可以是0或1。我们需要检测第一个出现的1的位置。pos[2:0]:这是 3 位输出信号,表示输入向量中第一个1出现的位位置。
always @(*)块:- 组合逻辑的
always块,每当输入in变化时,输出pos重新计算。
- 组合逻辑的
casez语句:casez (in):表示输入向量in需要逐条与casez条件进行匹配。z位表示不关心的位,也就是在casez中被忽略。- 每一条
casez项都从最低有效位开始,检查第一个1的位置。当检测到第一个1时,输出对应的位置编码。 - 例如,
8'b0000001?表示输入的低位部分匹配0000001x的情况,无论in[0]是0还是1,当in[1]为1时,该条件成立。
- 优先级逻辑:
casez从低位到高位依次进行匹配,确保检测到最先出现的1。一旦找到匹配的case条目,立即输出对应的位位置,忽略其他低位的1。default分支用于处理输入向量中没有1的情况,输出默认值0,确保组合逻辑的输出总是有定义的值。
使用 casez 的好处
相比于标准的 case 语句,casez 通过允许 z 位表示不关心的值,使我们能够简洁地处理优先级逻辑。通过逐位检查输入,casez 可以有效地实现优先编码器,避免为每一个可能的输入值都显式编写 case 条目(避免了 256 条 case 的复杂性)。
通过 casez 语句,我们实现了一个简洁的 8 位优先编码器。casez 的 z 位使得我们可以忽略不关心的高位,从而简化代码逻辑,确保逐位检测输入信号,并输出第一个 1 出现的位置。这样设计不仅逻辑清晰,还能确保在所有情况下都能正确输出结果,并避免推断锁存器。
Avoiding latches
在设计硬件时,避免锁存器(latches)的生成是非常重要的。锁存器会在组合逻辑中保留状态,从而引发意外的行为,特别是在时序逻辑设计中。因此,在组合逻辑(如 always @(*) 块)中,确保所有输出在每种可能的输入下都有明确的赋值,是避免锁存器生成的关键。
如何避免锁存器推断
- 为所有输出赋默认值:在
case或if-else语句之前,先为所有输出赋一个默认值,通常为0。这样即使某些条件没有匹配到,输出也不会保持不变,从而避免锁存器。 - 覆盖所有条件:确保
case语句或if-else语句处理了所有可能的情况。如果某些条件未被覆盖,Verilog 将推断出锁存器来记住之前的状态。
本次任务
我们需要根据键盘的扫描码(scancode)识别键盘上的方向键按下了哪个(上、下、左、右)。具体的扫描码和方向键的映射关系如下:
16'he06b-> 左箭头16'he072-> 下箭头16'he074-> 右箭头16'he075-> 上箭头- 其他情况 -> 没有方向键按下
设计思路
为了避免锁存器推断,我们可以为每个输出(left、down、right、up)先赋一个默认值(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

代码详解
- 输入信号
scancode:scancode是一个 16 位宽的输入信号,表示从键盘接收到的扫描码。
- 输出信号
left、down、right、up:- 这四个输出信号表示是否按下了对应的方向键。每个信号是 1 位的布尔信号,当某个方向键被按下时,对应的信号会被置为
1。
- 这四个输出信号表示是否按下了对应的方向键。每个信号是 1 位的布尔信号,当某个方向键被按下时,对应的信号会被置为
always @(*)块:- 组合逻辑块,表示当
scancode发生变化时重新计算输出信号。 - 在进入
case语句之前,所有输出信号都被赋予默认值0,以避免锁存器的推断。
- 组合逻辑块,表示当
case语句:- 根据输入的
scancode,选择对应的方向键输出。 - 当
scancode是16'he06b时,设置left = 1表示按下了左箭头。 - 类似地,其他扫描码分别对应按下的下箭头、右箭头和上箭头。
- 不需要
default分支,因为所有输出信号在case语句之前已经被赋默认值为0,这避免了在没有匹配的scancode时推断出锁存器。
- 根据输入的
如何避免锁存器推断
- 默认值的设置:
- 在
case语句之前为所有输出设置默认值,确保在scancode不匹配时,输出保持为0,从而避免锁存器。
- 在
- 无锁存器推断的关键:
- 在 Verilog 中,如果某个输出信号在某些输入条件下没有明确赋值,系统可能会推断出一个锁存器来保持先前的状态。通过在
always块的开头赋默认值,可以避免这种情况。
- 在 Verilog 中,如果某个输出信号在某些输入条件下没有明确赋值,系统可能会推断出一个锁存器来保持先前的状态。通过在
该设计使用了 case 语句和组合逻辑 always @(*) 块来识别键盘方向键的扫描码。通过为所有输出信号设置默认值,我们有效地避免了锁存器的推断。这样确保了无论输入 scancode 是什么,所有输出信号都会有明确的值,符合组合逻辑的要求。这种设计不仅简洁,而且保证了硬件实现的正确性。