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
是什么,所有输出信号都会有明确的值,符合组合逻辑的要求。这种设计不仅简洁,而且保证了硬件实现的正确性。