HDLBits答案(4)_如何避免生成锁存器
HDLBits_Verilog Language_Procedures
组合逻辑的always块
在Verilog中,有两种方式描述组合逻辑:assign连续赋值和always块。使用哪一种完全看哪一种更方便。
考虑到硬件的可综合性,有两种always块是有意义的:
- 组合逻辑:
always @(*)—— 只要敏感列表里的信号变化,就执行 - 时序逻辑:
always @(posedge clk)—— 只在时钟上升沿执行
打个比方:
- assign就像一根导线,右边的变化会立即传到左边,始终连续工作
- 组合always块就像一个纯组合电路,输入一变,输出马上跟着变
- 时序always块就像触发器,只在时钟沿才会更新状态
组合逻辑的always块和assign赋值是等价的。always块内可以有更丰富的语句结构,如if-then、case等,但不能使用连续赋值语句assign。
重要提示: 如果在always块内只对某些情况的信号赋值,其他情况保持不变,这就会综合出锁存器!此时功能仿真结果和综合后的硬件结果可能不一致。
另外还有个小细节:
- assign赋值语句的左边一般为wire类型
- always块中左边的变量一般为reg类型
- 但这些类型与硬件综合无关,仅是Verilog语法的要求! 不要以为用了reg就一定会生成寄存器。
题目:两种方式构建与门
题目描述:使用赋值语句和组合always块两种方式构建与门。
Solution:
1 | module top_module( |
时序逻辑的always块
时序always块会生成触发器、寄存器等时序电路,输出会等到下个时钟沿才能更新。这就像同步工作的流水线,只有时钟节拍到来时,数据才会往下流动。
阻塞赋值 vs 非阻塞赋值
这是Verilog中最容易混淆的概念之一!让我们把它讲清楚。
Verilog中有三种赋值方式:
- 连续赋值:
assign x = y;—— 只能在always块外使用 - 阻塞赋值:
x = y;—— 只能在always块内使用 - 非阻塞赋值:
x <= y;—— 只能在always块内使用
该用哪一种?这里有一条黄金法则:
在组合逻辑的always块中(
always @(*)),使用阻塞赋值=在时序逻辑的always块中(
always @(posedge clk)),使用非阻塞赋值<=
为什么要这样?简单来说:
- 阻塞赋值是顺序执行的,就像软件代码,一行执行完才会执行下一行
- 非阻塞赋值是并行执行的,所有赋值都在时钟沿同时发生,这更符合真实硬件的行为
题目:三种方式构建异或门
题目描述:使用赋值语句、组合always块和时序always块三种方式构建一个异或门。
Solution:
1 | module top_module( |
IF选择器
一个if语句会产生一个2选1的数据选择器。需要注意的是:并不是if选择的那路数据才被实现成电路,而是if和else两路都会被实现为电路,然后用选择器选择输出。
打个比方,if-else就像一个单刀双掷开关,两条路都已经铺好了,开关只是选择走哪一条而已。

if的两种写法:
1 | // 写法1: always块 + if-else |
1 | // 写法2: 三目运算符,更简洁 |
题目:2选1选择器
题目描述:
2选1选择器
| sel_b1 | sel_b2 | out_assign/out_always |
|---|---|---|
| 0 | 0 | a |
| 0 | 1 | a |
| 1 | 0 | a |
| 1 | 1 | b |
Solution:
1 | module top_module( |
IF中锁存器问题
这是本章的重点!很多初学者就是在这里不小心生成了锁存器。
先搞清楚:锁存器 vs 触发器
- 锁存器(latch): 对脉冲电平(也就是0或者1)敏感的存储单元
- 触发器(flip-flop): 对脉冲边沿(即上升沿或者下降沿)敏感的存储电路
为什么要避免生成锁存器?在ASIC设计中,除了CPU高速电路或者RAM这种对面积很敏感的电路,一般不提倡用锁存器,因为:
- 锁存器对毛刺敏感,无异步复位端,不能让芯片在上电时处在确定的状态
- 锁存器会使静态时序分析变得很复杂,不利于设计的可重用
锁存器是怎么生成的?
当我们在设计电路时,不能直接写成代码然后期望它直接生成为合适的电路。看下面两个典型错误:
1 | // 错误示例1 |
语法上正确的代码并不意味着设计出的电路也是合理的。我们来思考这么一个问题:如果if条件不满足,输出如何变化呢?
Verilog给出的解决方法是:保持输出不变。但组合逻辑电路不能记忆当前的状态,所以为了实现”保持不变”,综合工具就会综合出锁存器!
所以当我们使用if语句或者case语句时,必须考虑到所有情况并给对应情况的输出进行赋值,这意味着:
- if语句一定要有else分支
- case语句一定要有default分支
题目:找BUG,修复锁存器问题
题目描述:找BUG,解决下面的代码中包含的创建锁存的不正确行为。
1 | // 有问题的代码 |

Solution:
1 | module top_module ( |
Case语句
在Verilog中,case语句与if-elseif-else结构相近,与C语言中的switch差别较大。让我们看看示例:
1 | always @(*) begin // 这是一个组合电路 |
case语句的特点:
- case语句以
case开始,每个case的选项以冒号结束 - 每个case的选项中只能执行一个statement,所以无需break语句。但如果想在一个case选项中执行多个statement,就需要使用
begin...end - case中可以有重复的case item,首次匹配的将会被执行
题目:6选1数据选择器
题目描述:6选1数据选择器
Be careful of inferring latches!(需添加Default)
Solution:
1 | module top_module ( |
优先编码器
优先编码器是一种组合电路,当给定输入位向量时,输出该向量中第一个1的位置。例如,给定输入8’b10010000的8位优先级编码器将输出3’d4,因为bit[4]是高电平的第一位。
题目:4位优先编码器
题目描述:构建一个4位优先级编码器。对于此问题,如果所有输入位都不为高(即输入为零),则输出零。请注意,一个4位数字具有16种可能的组合。
思路1:根据惯性思维使用case语法,列出每一种情况,然后列出其对应的输出。
1 | // 这种方法太繁琐了! |
思路2: 如果按上述思路来写,那么更多位的优先编码器如何实现呢?其实有更简单的方法,这里既不用casex也不用casez,我们来看另一种思路——反向case!
Solution:
1 | module top_module ( |
为什么这样能工作?根据前面所说的case的性质,case中可以有重复的case item,但首次匹配的才会被执行。上面的case语句是从低到高位去比较in中是否有数据位为1,即使后面有重复为1的也只会执行首次匹配的操作。
如此,N位优先编码器只需要N个case分支即可,大大简化代码量!
casez实现优先编码器
题目描述:
为8位输入构建优先级编码器。当给定输入位向量时,输出该向量中第一个1的位置。如果输入向量没有高位为1,则报告为零。例如,给定输入8’b10010000的8位优先级编码器将输出3’d4,因为bit[4]是高电平的第一位。
思路提示:由上一个练习我们知道,普通case语句中将有256种case item,使用casez以后,我们可以减少需要比较的case item。这就是casez的目的——在比较中,将值为z的位视作无关位(don’t care)。
所以上题的另一种解法为:
1 | always @(*) begin |
case语句的行为就好像是按顺序检查每个项一样(实际上,它所做的事情更像是生成一个巨大的真值表,然后进行门操作)。注意某些输入(例如4’b1111)是如何匹配多个case项的。选择第一个匹配项(因此4’b1111匹配第一个项目,out=0,但不匹配后面的任何项目)。
- 还有类似的
casex,它将x和z均视为无关位 - 符号
?是z的同义词,所以2'bz0 = 'b?0
Solution
1 | module top_module( |
小贴士: 该题也可以按Priority encoder中的解法2来写,复杂度甚至更低,可以试试看!
避免生成锁存器
题目描述:
假设构建一个电路来处理游戏的PS/2键盘上的扫描代码。对于收到的最后两个字节的扫描码,我们需要指示是否按下了键盘上的一个方向键。这涉及到一个相当简单的映射,它可以实现为一个case语句(或if-elseif),包含四个case。
电路有一个16位输入和四个输出。建立能识别这四种扫描码并正确输出的电路。
| Scancode[15:0] | Arrow key |
|---|---|
| 16’he06b | left arrow |
| 16’he072 | down arrow |
| 16’he074 | right arrow |
| 16’he075 | up arrow |
| Anything else | none |
一个更优雅的技巧
为避免生成锁存器,所有的输入情况必须要被考虑到。但仅有一个简单的default是不够的,我们必须在case item和default中为4个输出进行赋值,这会导致很多不必要的代码编写。
一种简单的方式就是对输出先进行赋初值的操作!这种类型的代码确保在所有可能的情况下输出都被赋值,除非case语句覆盖了赋值。这也意味着不再需要缺省的default项。
1 | always @(*) begin |
这种方法是不是很优雅?既避免了锁存器,又减少了代码量!
Solution
1 | module top_module ( |
入门者避坑指南
在这一章,我们学习了如何避免锁存器,这是初学者最容易犯错的地方。让我们总结一下常见的坑:
错误1: 组合逻辑中if没有else
错误表现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15module bad_example (
input [1:0] sel,
input [3:0] a, b, c,
output reg [3:0] y
);
always @(*) begin
if (sel == 2'b00)
y = a; // 只有if,没有else!
else if (sel == 2'b01)
y = b;
else if (sel == 2'b10)
y = c;
// 漏掉了sel == 2'b11的情况!
end
endmodule
错误原因:
当sel等于2’b11时,y没有被赋值,Verilog会保持y不变,这就综合出了锁存器。
正确做法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16module good_example (
input [1:0] sel,
input [3:0] a, b, c,
output reg [3:0] y
);
always @(*) begin
if (sel == 2'b00)
y = a;
else if (sel == 2'b01)
y = b;
else if (sel == 2'b10)
y = c;
else
y = 4'd0; // 补上else分支
end
endmodule
或者用更优雅的方法——先赋初值:
1 | module good_example2 ( |
调试技巧:
- 写完always块后,问自己一个问题:是否所有可能的输入情况下,所有输出都被赋值了?
- 如果不确定,那就用”先赋初值”的方法,最稳妥!
错误2: 组合逻辑中case没有default
错误表现:1
2
3
4
5
6
7
8
9
10
11
12
13
14module bad_example (
input [2:0] sel,
input [3:0] data0, data1, data2,
output reg [3:0] y
);
always @(*) begin
case(sel)
3'b000: y = data0;
3'b001: y = data1;
3'b010: y = data2;
// 漏掉了default! 还有5种情况没覆盖
endcase
end
endmodule
错误原因:
3位sel有8种可能的取值,只覆盖了3种,剩下5种情况下y没有被赋值,会生成锁存器。
正确做法:1
2
3
4
5
6
7
8
9
10
11
12
13
14module good_example (
input [2:0] sel,
input [3:0] data0, data1, data2,
output reg [3:0] y
);
always @(*) begin
case(sel)
3'b000: y = data0;
3'b001: y = data1;
3'b010: y = data2;
default: y = 4'd0; // 加上default
endcase
end
endmodule
或者还是用”先赋初值”的方法:
1 | module good_example2 ( |
调试技巧:
- 记住: N位输入有2^N种可能,你的case覆盖全了吗?
- 用”先赋初值”是万能良药,强烈推荐!
错误3: 时序逻辑用了阻塞赋值
错误表现:1
2
3
4
5
6
7
8
9
10
11
12module bad_example (
input clk,
input d,
output reg q
);
reg temp;
always @(posedge clk) begin
temp = d; // 错误!时序逻辑用了阻塞赋值
q = temp;
end
endmodule
虽然这个简单例子可能不会出问题,但在更复杂的情况下,阻塞赋值会导致仿真和综合结果不一致!
错误原因:
在时序逻辑中使用阻塞赋值,可能会导致竞态条件,不同的仿真器可能有不同的行为。
正确做法:1
2
3
4
5
6
7
8
9
10
11
12module good_example (
input clk,
input d,
output reg q
);
reg temp;
always @(posedge clk) begin
temp <= d; // 正确:非阻塞赋值
q <= temp;
end
endmodule
调试技巧:
- 记住黄金法则:组合逻辑用
=,时序逻辑用<= - 不要混用!在一个always块里,要么全用
=,要么全用<=
错误4: 组合逻辑用了非阻塞赋值
错误表现:1
2
3
4
5
6
7
8module bad_example (
input a, b,
output reg y
);
always @(*) begin
y <= a & b; // 错误!组合逻辑用了非阻塞赋值
end
endmodule
错误原因:
虽然有些综合工具也能处理这种情况,但这不符合标准做法,可能会导致仿真问题。
正确做法:1
2
3
4
5
6
7
8module good_example (
input a, b,
output reg y
);
always @(*) begin
y = a & b; // 正确:阻塞赋值
end
endmodule
错误5: 敏感列表不完整
错误表现:1
2
3
4
5
6
7
8
9module bad_example (
input a, b, c,
output reg y
);
// 错误!敏感列表里漏掉了c
always @(a, b) begin
y = a & b & c;
end
endmodule
错误原因:
当c变化时,always块不会执行,y不会更新!这会导致仿真和综合结果不一致。
正确做法:1
2
3
4
5
6
7
8
9module good_example (
input a, b, c,
output reg y
);
// 用@(*),自动包含所有敏感信号
always @(*) begin
y = a & b & c;
end
endmodule
调试技巧:
- 组合逻辑always块,直接用
@(*)就对了!不要手动列敏感信号,容易漏掉。
本章小结
这一章我们学习了如何避免生成锁存器,这是数字设计中非常重要的知识:
两种always块:
- 组合逻辑:
always @(*),用阻塞赋值= - 时序逻辑:
always @(posedge clk),用非阻塞赋值<=
- 组合逻辑:
避免锁存器的两个方法:
- 方法1: 确保所有分支都给输出赋值(if要有else,case要有default)
- 方法2(推荐): 在always块开头先给所有输出赋初值!
记住: 时序逻辑下不会生成锁存器,锁存器的问题只存在于组合逻辑中
case的妙用:
- 用反向case实现优先编码器
- 用casez忽略无关位
避免锁存器是每个硬件设计者的基本功,一定要掌握好!




