always 块、if/case 语句与优先编码器(Procedures)
1.Always blocks(组合)
由于数字电路由逻辑门通过导线连接组成,任何电路都可以表示为模块的组合并赋予语句。然而,有时这并不是描述电路最方便的方式。过程(其中始终以块为例)提供了描述电路的另一种语法。
对于硬件综合来说,有两种类型的始终块是相关的:
Combinational: always @(*) 组合:始终 @(*)
Clocked: always @(posedge clk) 时钟:始终 @(posedge clk)
组合始终块等价于赋值语句,因此总有方法可以双向表达组合电路。选择使用哪种语法主要取决于哪个语法更方便。程序块内代码的语法与外部代码不同。程序式块拥有更丰富的语句集(例如,if-then、case),不能包含连续赋值*,但也引入了许多新的非直观错误方式。 (*程序连续赋值确实存在,但与连续赋值有所不同,且不可综合。)
assign out1 = a & b | c ^ d;
always @(*) out2 = a & b | c ^ d;
例如,assign和组合始终块描述的是同一个电路。两者都形成了同一团组合逻辑。每当右侧任何输入值发生变化时,两者都会重新计算输出。

对于组合型始终块,始终使用敏感度列表(*)。明确列出信号容易出错(如果漏掉一个),因此硬件合成时会被忽略。如果你明确指定了灵敏度列表但漏掉了信号,合成后的硬件仍然会表现得像(*)被指定一样,但仿真不会与硬件的行为匹配。(在SystemVerilog中,使用always_comb。)关于线路vs. reg的说明:assign语句的左侧必须是net类型(例如wire),而过程式赋值的左侧(在始终块中)必须是变量类型(例如reg)。这些类型(线vs. reg)与合成的硬件无关,只是Verilog作为硬件仿真语言时遗留下来的语法。
// synthesis verilog_input_version verilog_2001
module top_module (
input a,
input b,
// 提醒:想想 out_alwaysblock 需要什么数据类型?
output out_assign,
output reg out_alwaysblock
);
// 上半部分
// 提示:声明 wire,使用 assign 语句实现 a & b
assign out_assign = a & b;
// 下半部分
// 提示:声明 reg,使用 always @(*) 块实现 a & b
always @(*) begin
out_alwaysblock = a & b;
end
endmodule

2.Always blocks(记时)
时钟始终块会像组合逻辑块一样创建一组组合逻辑,但同时在组合逻辑块的输出处生成一组触发器(或称"寄存器")。逻辑块的输出不再立即可见,而是仅在下一个输出之后立即可见(posedge clk)。Verilog 中有三种作业类型:
(assign x = y;). 连续赋值(赋值 x = y;)。仅在不在过程内使用("始终阻挡")。
assignment: (x = y;). 程序性分段分配:(x = y;)。只能在手术中使用。
assignment: (x <= y;). 程序性非阻塞赋值:(x <= y;)。只能在手术中使用。
在组合型始终阻挡中,使用阻挡任务。在时钟始终分区中,使用非分区赋值。全面理解原因对硬件设计帮助不大,需要对Verilog模拟器如何跟踪事件有良好理解。不遵循此规则会导致极难发现既非确定性又在仿真与合成硬件之间存在差异的错误。用assign语句、组合始终块和时钟始终块三种方式构建一个XOR门。注意,时钟始终模块产生的电路与其他两个不同:存在触发器,因此输出会被延迟。

// synthesis verilog_input_version verilog_2001
module top_module(
input clk,
input a,
input b,
output wire out_assign,
output reg out_always_comb,
output reg out_always_ff );
// 提示:声明 wire,使用 assign 语句实现 a ^ b
assign out_assign = a ^ b;
// 提示:声明 reg,使用 always @(*) 块实现 a ^ b
always @(*) begin
out_always_comb = a ^ b;
end
// 提示:声明 reg,使用 always @(posedge clk) 块实现 a ^ b
always @(posedge clk) begin
out_always_ff = a ^ b;
end
endmodule

3.IF命题
if语句通常会创建一个2对1的多路复用器,条件为真时选择一个输入,如果条件为假则选择另一个输入。

模板:
always @(*) begin
if (condition) begin
out = x;
end
else begin
out = y;
end
end
这等价于使用带有条件算子的连续赋值:
assign out = (condition) ? x : y;
然而,程序式if语句提供了一种新的错误方式。电路只有在输出总是被赋予一个值时才是组合电路。
作业:构建一个2对1的多路复用器,选择A和b。如果sel_b1和sel_b2都为真,则选择b。否则,请选择一个。重复两次,一次用assign语句,一次用过程式if语句。
eg:4对1复用器的写法
module mux4to1 (
input [3:0] d, // d[3]=d3, d[2]=d2, d[1]=d1, d[0]=d0
input [1:0] s,
output y
);
assign y = d[s]; // s=0 -> d0, s=1 -> d1, s=2 -> d2, s=3 -> d3
endmodule
| sel_b1 | sel_b2 | out_assign out_always |
|---|---|---|
| 0 | 0 | a |
| 0 | 1 | a |
| 1 | 0 | a |
| 1 | 1 | b |
module top_module (
input a,
input b,
input sel_b1,
input sel_b2,
output out_assign,
output reg out_always
);
assign out_assign = (sel_b1 & sel_b2) ? b : a;
always @(*) begin
if (sel_b1 & sel_b2)
out_always = b;
else
out_always = a;
end
endmodule

4.如果语句
常见错误来源:如何避免锁扣
设计电路时,首先要考虑电路:我想要这个逻辑门,我想要一个组合逻辑块,包含这些输入并产生这些输出。我想要一团逻辑组合,后面跟着一堆触发器。语法正确的代码不一定会产生合理的电路(组合逻辑+触发器)。通常的原因是:"除了你指定的那些情况,其他情况会发生什么?"Verilog 的回答是:保持输出不变。这种"保持输出不变"的行为意味着需要记住当前状态,从而产生锁存。组合逻辑(例如逻辑门)无法记住任何状态。注意警告(10240):...推断锁闭"信息。除非闩是故意的,否则几乎总是提示有bug。组合电路必须在所有条件下为所有输出分配一个值。这通常意味着你总是需要给输出分配else子句或默认值。

以下代码包含了错误行为,导致锁扣。修复漏洞,确保只有在电脑过热严重时才关闭,到达目的地或需要加油时就停止驾驶。
always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end
always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
end

对代码进行修改
// 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
);
always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
else
shut_off_computer = 0; // ← 补上
end
always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
else
keep_driving = 0; // ← 补上:到站了就停车
end
endmodule

5.case语句
Verilog 中的case语句几乎等价于一系列 if-elseif-else,将一个表达式与其他表达式进行比较。其语法和功能与 C 语言中的 switch 语句不同。
always @(*) begin // This is a combinational circuit
case (in)
1'b1: begin
out = 1'b1; // begin-end if >1 statement
end
1'b0: out = 1'b0;
default: out = 1'bx;
endcase
end
case陈述以case开头,每个"case条目"以冒号结尾。没有"开关"。
每个案例项只能执行一个语句。这使得C语言中的"断开"变得多余。但这意味着如果你需要多个语句,你必须使用开始......结束。允许重复(部分重叠)的案件项目。第一个匹配的就用。C不允许重复的案件物品。
如果case数众多,case陈述比if陈述更方便。
作业:所以,在这个练习中,创建一个6比1的多路复用器。当sel在0和5之间时,选择对应的数据输入。否则输出为0。数据输入和输出都是4位宽的。要小心推断门闩。
module top_module (
input [2:0] sel,
input [3:0] data0,
input [3:0] data1,
input [3:0] data2,
input [3:0] data3,
input [3:0] data4,
input [3:0] data5,
output reg [3:0] out
);
always @(*) begin
case (sel)
3'd0: out = data0;
3'd1: out = data1;
3'd2: out = data2;
3'd3: out = data3;
3'd4: out = data4;
3'd5: out = data5;
3'd6: out = 4'b0; // 默认情况
3'd7: out = 4'b0; // 默认情况
endcase
end
endmodule

6.优先编码器
优先编码器是一种组合电路,当给定输入比特向量时,会输出向量中第一个1位的位置。例如,给定输入8'b1001 0000的8位优先编码器,输出3'd4,因为位4是第一个高电位。
作业:做一个4位优先编码器。对于这个问题,如果没有任何输入位高电平(即输入为零),输出为零。注意,一个4位数字有16种可能的组合。
使用十六进制(4'hb)或十进制(4'd11)字面量可以比二进制(4'b1011)字面量更省人。
列出真值表:
| in3 | in2 | in1 | in0 | 说明 | pos1:0 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 全0 | 00 |
| 0 | 0 | 0 | 1 | 只有in0是1 | 00 |
| 0 | 0 | 1 | x | in1是第一个1 | 01 |
| 0 | 1 | x | x | in2是第一个1 | 10 |
| 1 | x | x | x | in3是第一个1 | 11 |
从表里能看出来,当 in3=1 时,输出永远是 2'b11,这体现了什么逻辑?(优先级最高,无视低位)。
做题思路是:从最高位 in3 往最低位 in0 依次检查,第一个为 1 的位就是输出值。case 语句按书写顺序匹配,所以把高位情况写在前面就自然实现了优先级。
module top_module (
input [3:0] in,
output reg [1:0] pos
);
always @(*) begin
casez (in)
4'b???1: pos = 2'd0; // in[0]=1,最低位优先
4'b??10: pos = 2'd1; // in[1]=1,且 in[0]=0
4'b?100: pos = 2'd2; // in[2]=1,且 in[1:0]=00
4'b1000: pos = 2'd3; // 只有 in[3]=1
default: pos = 2'd0; // 4'b0000
endcase
end
endmodule

7.带case优先编码器
作业:为8位输入构建一个优先编码器。给定一个8位向量,输出应报告向量中第一个(最低有效)位1。如果输入向量没有高频位,则报告为零。 例如,输入8'b10010000应该输出3'd4,因为bit4是第一个高电位。根据上一次练习(always_case2),案例陈述中将包含256个案例。如果案例语句中的案例项目支持"不在乎"位,我们可以将这种情况减少到9个案例。这就是z 的情况:它在比较中将值为 z 的比特视为不在意。例如,这将实现上一个练习中的4输入优先编码器:
always @(*) begin
casez (in[3:0])
4'bzzz1: out = 0; // in[3:1] can be anything
4'bzz1z: out = 1;
4'bz1zz: out = 2;
4'b1zzz: out = 3;
default: out = 0;
endcase
end
case语句的行为就像每个项都被顺序检查(实际上是一个大型组合逻辑函数)。注意某些输入(例如4'b1111)会匹配多个案例项。选择第一个匹配(因此4'b1111与第一个项目匹配,出 = 0,但不匹配后续任何一个)。
还有类似的casex,把x和z都当作不在乎。我觉得用它而不是Casez没什么用处。
数字?是Z的同义词。那么2'bz0和2'b0是一样的吗?
明确指定优先级行为可能更不易出错,而不是依赖案件项的顺序。例如,如果部分案例项重新排序,以下情况仍会保持相同,因为任何比特模式最多只能匹配一个案例项:
casez (in[3:0])
4'bzzz1: ...
4'bzz10: ...
4'bz100: ...
4'b1000: ...
default: ...
endcase
8位优先编码器真值表
| in7 | in6 | in5 | in4 | in3 | in2 | in1 | in0 | pos2:0 | 说明 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | x | x | x | x | x | x | x | 3'd7 | in7 是第一个高电平 |
| 0 | 1 | x | x | x | x | x | x | 3'd6 | in6 是第一个高电平 |
| 0 | 0 | 1 | x | x | x | x | x | 3'd5 | in5 是第一个高电平 |
| 0 | 0 | 0 | 1 | x | x | x | x | 3'd4 | in4 是第一个高电平 |
| 0 | 0 | 0 | 0 | 1 | x | x | x | 3'd3 | in3 是第一个高电平 |
| 0 | 0 | 0 | 0 | 0 | 1 | x | x | 3'd2 | in2 是第一个高电平 |
| 0 | 0 | 0 | 0 | 0 | 0 | 1 | x | 3'd1 | in1 是第一个高电平 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 3'd0 | in0 是第一个高电平 |
| 0 | 0 | 0 | 0 | 0 | 0 | 0 |
module top_module (
input [7:0] in,
output reg [2:0] pos
);
always @(*) begin
casez (in)
8'bzzzzzzz1: pos = 3'd0; // in[0]=1
8'bzzzzzz10: pos = 3'd1; // in[1]=1, in[0]=0
8'bzzzzz100: pos = 3'd2; // in[2]=1, in[1:0]=00
8'bzzzz1000: pos = 3'd3; // in[3]=1, in[2:0]=000
8'bzzz10000: pos = 3'd4;
8'bzz100000: pos = 3'd5;
8'bz1000000: pos = 3'd6;
8'b10000000: pos = 3'd7;
default: pos = 3'd0; // in = 8'b00000000
endcase
end
endmodule

用for循环语句来写
module top_module (
input [7:0] in,
output reg [2:0] pos
);
integer i;
always @(*) begin
pos = 3'd0; // 默认输出 0,同时处理全 0 情况
for (i = 7; i >= 0; i = i - 1) begin
if (in[i])
pos = i[2:0]; // 从高位往低位扫,最后留下的就是最低位的 1
end
end
endmodule
这段也能编译成功
8.避免锁扣
假设你正在构建一个电路,用来处理游戏中PS/2键盘的扫描码。根据收到的最后两个字节扫描码,你需要指示键盘上的某个方向键是否被按下。这涉及一个相当简单的映射,可以作为有四个案例的case语句(或if-elseif)实现。
作业:你的电路只有一个16位输入和四个输出。构建一个识别这四个扫描码并断言正确输出的电路。为避免创建锁存,所有输出必须在所有可能条件下都被赋予一个值(参见 always_if2)。仅仅有默认案件是不够的。你必须在所有四个情况下和默认情况下都赋予四个输出一个值。这可能涉及大量不必要的打字。一个简单的方法是在案例语句前为输出分配一个"默认值":
always @(*) begin
up = 1'b0; down = 1'b0; left = 1'b0; right = 1'b0;
case (scancode)
... // Set to 1 as necessary.
endcase
end
这种代码风格确保输出在所有可能情况下都被赋予值(0),除非案例语句覆盖了赋值。这也意味着默认:case项变得不再必要。
提醒:逻辑合成器生成一个组合电路,其行为与代码描述的相同。硬件不会按顺序"执行"代码行。
// synthesis verilog_input_version verilog_2001
module top_module (
input [15:0] scancode,
output reg left,
output reg down,
output reg right,
output reg up );
module top_module (
input [15:0] scancode,
output up,
output down,
output left,
output right
);
// 题目要求:使用 always @(*) 块,并且优先赋默认值防止锁存
always @(*) begin
// 第一步:统一赋默认值 0
up = 1'b0;
down = 1'b0;
left = 1'b0;
right = 1'b0;
// 第二步:使用 case 语句识别特定扫描码
case (scancode)
16'hE075: up = 1'b1; // 向上
16'hE072: down = 1'b1; // 向下
16'hE06B: left = 1'b1; // 向左
16'hE074: right = 1'b1; // 向右
// 因为上面已经赋了默认值 0,所以这里可以省略 default 分支,
// 如果加上 default: ; 也不会有语法错误。
endcase
end
endmodule
