HDLBits_Verilog Language_Procedures

HDLBits链接


组合逻辑的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
2
3
4
5
6
7
8
9
10
11
12
13
14
module top_module(
input a,
input b,
output wire out_assign,
output reg out_alwaysblock
);
// 方式1: assign连续赋值
assign out_assign = a & b;

// 方式2: 组合always块
always @(*) begin
out_alwaysblock = a & b;
end
endmodule

时序逻辑的always块

时序always块会生成触发器、寄存器等时序电路,输出会等到下个时钟沿才能更新。这就像同步工作的流水线,只有时钟节拍到来时,数据才会往下流动。

阻塞赋值 vs 非阻塞赋值

这是Verilog中最容易混淆的概念之一!让我们把它讲清楚。

Verilog中有三种赋值方式:

  1. 连续赋值: assign x = y; —— 只能在always块外使用
  2. 阻塞赋值: x = y; —— 只能在always块内使用
  3. 非阻塞赋值: x <= y; —— 只能在always块内使用

该用哪一种?这里有一条黄金法则:

组合逻辑的always块中(always @(*)),使用阻塞赋值=

时序逻辑的always块中(always @(posedge clk)),使用非阻塞赋值<=

为什么要这样?简单来说:

  • 阻塞赋值是顺序执行的,就像软件代码,一行执行完才会执行下一行
  • 非阻塞赋值是并行执行的,所有赋值都在时钟沿同时发生,这更符合真实硬件的行为

题目:三种方式构建异或门

题目描述:使用赋值语句、组合always块和时序always块三种方式构建一个异或门。

Solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module top_module(
input clk,
input a,
input b,
output wire out_assign,
output reg out_always_comb,
output reg out_always_ff
);
// 方式1: 连续赋值(组合逻辑)
assign out_assign = a ^ b;

// 方式2: 组合always块,使用阻塞赋值
always @(*) begin
out_always_comb = a ^ b;
end

// 方式3: 时序always块,使用非阻塞赋值
always @(posedge clk) begin
out_always_ff <= a ^ b;
end
endmodule

IF选择器

一个if语句会产生一个2选1的数据选择器。需要注意的是:并不是if选择的那路数据才被实现成电路,而是if和else两路都会被实现为电路,然后用选择器选择输出。

打个比方,if-else就像一个单刀双掷开关,两条路都已经铺好了,开关只是选择走哪一条而已。

1

if的两种写法:

1
2
3
4
5
6
7
8
9
// 写法1: always块 + if-else
always @(*) begin
if (condition) begin
out = x;
end
else begin
out = y;
end
end
1
2
// 写法2: 三目运算符,更简洁
assign out = (condition) ? x : y;

题目: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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module top_module(
input a,
input b,
input sel_b1,
input sel_b2,
output wire out_assign,
output reg out_always
);
// 方式1: 三目运算符
assign out_assign = (sel_b1 & sel_b2) ? b : a;

// 方式2: if-else
always @(*) begin
if (sel_b1 & sel_b2) begin
out_always = b;
end
else begin
out_always = a;
end
end
endmodule

IF中锁存器问题

这是本章的重点!很多初学者就是在这里不小心生成了锁存器。

先搞清楚:锁存器 vs 触发器

  • 锁存器(latch): 对脉冲电平(也就是0或者1)敏感的存储单元
  • 触发器(flip-flop): 对脉冲边沿(即上升沿或者下降沿)敏感的存储电路

为什么要避免生成锁存器?在ASIC设计中,除了CPU高速电路或者RAM这种对面积很敏感的电路,一般不提倡用锁存器,因为:

  1. 锁存器对毛刺敏感,无异步复位端,不能让芯片在上电时处在确定的状态
  2. 锁存器会使静态时序分析变得很复杂,不利于设计的可重用

锁存器是怎么生成的?

当我们在设计电路时,不能直接写成代码然后期望它直接生成为合适的电路。看下面两个典型错误:

1
2
3
4
5
6
7
// 错误示例1
if (cpu_overheated)
shut_off_computer = 1;

// 错误示例2
if (~arrived)
keep_driving = ~gas_tank_empty;

语法上正确的代码并不意味着设计出的电路也是合理的。我们来思考这么一个问题:如果if条件不满足,输出如何变化呢?

Verilog给出的解决方法是:保持输出不变。但组合逻辑电路不能记忆当前的状态,所以为了实现”保持不变”,综合工具就会综合出锁存器!

所以当我们使用if语句或者case语句时,必须考虑到所有情况并给对应情况的输出进行赋值,这意味着:

  • if语句一定要有else分支
  • case语句一定要有default分支

题目:找BUG,修复锁存器问题

题目描述:找BUG,解决下面的代码中包含的创建锁存的不正确行为。

1
2
3
4
5
6
7
8
9
10
// 有问题的代码
always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end

always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
end

2

Solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module top_module (
input cpu_overheated,
output reg shut_off_computer,
input arrived,
input gas_tank_empty,
output reg keep_driving
);
// 修复1: 加上else分支,覆盖所有情况
always @(*) begin
if (cpu_overheated) begin
shut_off_computer = 1'b1;
end
else begin
shut_off_computer = 1'b0;
end
end

// 修复2: 加上else分支
always @(*) begin
if (~arrived) begin
keep_driving = ~gas_tank_empty;
end
else begin
keep_driving = 1'b0;
end
end

endmodule

Case语句

在Verilog中,case语句与if-elseif-else结构相近,与C语言中的switch差别较大。让我们看看示例:

1
2
3
4
5
6
7
8
9
always @(*) begin     // 这是一个组合电路
case (in)
1'b1: begin
out = 1'b1; // 如果语句超过1条,要用begin-end
end
1'b0: out = 1'b0;
default: out = 1'bx;
endcase
end

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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'b000: out = data0;
3'b001: out = data1;
3'b010: out = data2;
3'b011: out = data3;
3'b100: out = data4;
3'b101: out = data5;
default: out = 4'b0000; // 不要忘记default!
endcase
end

endmodule

优先编码器

优先编码器是一种组合电路,当给定输入位向量时,输出该向量中第一个1的位置。例如,给定输入8’b10010000的8位优先级编码器将输出3’d4,因为bit[4]是高电平的第一位。


题目:4位优先编码器

题目描述:构建一个4位优先级编码器。对于此问题,如果所有输入位都不为高(即输入为零),则输出零。请注意,一个4位数字具有16种可能的组合。

思路1:根据惯性思维使用case语法,列出每一种情况,然后列出其对应的输出。

1
2
3
4
5
6
7
// 这种方法太繁琐了!
case(input)
4'b0001: output = 1;
...
4'b1111: output = 1;
default: output = 0;
endcase

思路2: 如果按上述思路来写,那么更多位的优先编码器如何实现呢?其实有更简单的方法,这里既不用casex也不用casez,我们来看另一种思路——反向case!

Solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top_module (
input [3:0] in,
output reg [1:0] pos
);
always @(*) begin
case(1) // 注意这里:我们在找"1"的位置!
in[0]: pos = 2'd0; // 如果in[0]是1,匹配这一项
in[1]: pos = 2'd1; // 如果in[1]是1,匹配这一项
in[2]: pos = 2'd2;
in[3]: pos = 2'd3;
default: pos = 2'd0;
endcase
end
endmodule

为什么这样能工作?根据前面所说的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
2
3
4
5
6
7
8
9
always @(*) begin
casez (in[3:0])
4'bzzz1: out = 2'd0; // in[3:1]可以是任意值
4'bzz1z: out = 2'd1;
4'bz1zz: out = 2'd2;
4'b1zzz: out = 2'd3;
default: out = 2'd0;
endcase
end

case语句的行为就好像是按顺序检查每个项一样(实际上,它所做的事情更像是生成一个巨大的真值表,然后进行门操作)。注意某些输入(例如4’b1111)是如何匹配多个case项的。选择第一个匹配项(因此4’b1111匹配第一个项目,out=0,但不匹配后面的任何项目)。

  • 还有类似的casex,它将x和z均视为无关位
  • 符号?是z的同义词,所以2'bz0 = 'b?0

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module top_module(
input [7:0] in,
output reg [2:0] pos
);
always @(*) begin
casez(in)
8'bzzzzzzz1: pos = 3'd0;
8'bzzzzzz1z: pos = 3'd1;
8'bzzzzz1zz: pos = 3'd2;
8'bzzzz1zzz: pos = 3'd3;
8'bzzz1zzzz: pos = 3'd4;
8'bzz1zzzzz: pos = 3'd5;
8'bz1zzzzzz: pos = 3'd6;
8'b1zzzzzzz: pos = 3'd7;
default: pos = 3'd0;
endcase
end
endmodule

小贴士: 该题也可以按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
2
3
4
5
6
7
always @(*) begin
// 先给所有输出赋初值
up = 1'b0; down = 1'b0; left = 1'b0; right = 1'b0;
case (scancode)
... // 需要时再覆盖为1
endcase
end

这种方法是不是很优雅?既避免了锁存器,又减少了代码量!


Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module top_module (
input [15:0] scancode,
output reg left,
output reg down,
output reg right,
output reg up
);
always @(*) begin
// 先赋初值: 默认所有输出都是0
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

入门者避坑指南

在这一章,我们学习了如何避免锁存器,这是初学者最容易犯错的地方。让我们总结一下常见的坑:


错误1: 组合逻辑中if没有else

错误表现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module 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
16
module 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module good_example2 (
input [1:0] sel,
input [3:0] a, b, c,
output reg [3:0] y
);
always @(*) begin
y = 4'd0; // 先赋初值
if (sel == 2'b00)
y = a;
else if (sel == 2'b01)
y = b;
else if (sel == 2'b10)
y = c;
// 即使漏掉了情况,也有初值,不会生成锁存器
end
endmodule

调试技巧:

  • 写完always块后,问自己一个问题:是否所有可能的输入情况下,所有输出都被赋值了?
  • 如果不确定,那就用”先赋初值”的方法,最稳妥!

错误2: 组合逻辑中case没有default

错误表现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module 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
14
module 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module good_example2 (
input [2:0] sel,
input [3:0] data0, data1, data2,
output reg [3:0] y
);
always @(*) begin
y = 4'd0; // 先赋初值
case(sel)
3'b000: y = data0;
3'b001: y = data1;
3'b010: y = data2;
// 不需要default了
endcase
end
endmodule

调试技巧:

  • 记住: N位输入有2^N种可能,你的case覆盖全了吗?
  • 用”先赋初值”是万能良药,强烈推荐!

错误3: 时序逻辑用了阻塞赋值

错误表现:

1
2
3
4
5
6
7
8
9
10
11
12
module 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
12
module 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
8
module bad_example (
input a, b,
output reg y
);
always @(*) begin
y <= a & b; // 错误!组合逻辑用了非阻塞赋值
end
endmodule

错误原因:
虽然有些综合工具也能处理这种情况,但这不符合标准做法,可能会导致仿真问题。

正确做法:

1
2
3
4
5
6
7
8
module good_example (
input a, b,
output reg y
);
always @(*) begin
y = a & b; // 正确:阻塞赋值
end
endmodule


错误5: 敏感列表不完整

错误表现:

1
2
3
4
5
6
7
8
9
module 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
9
module good_example (
input a, b, c,
output reg y
);
// 用@(*),自动包含所有敏感信号
always @(*) begin
y = a & b & c;
end
endmodule

调试技巧:

  • 组合逻辑always块,直接用@(*)就对了!不要手动列敏感信号,容易漏掉。

本章小结

这一章我们学习了如何避免生成锁存器,这是数字设计中非常重要的知识:

  1. 两种always块:

    • 组合逻辑: always @(*),用阻塞赋值=
    • 时序逻辑: always @(posedge clk),用非阻塞赋值<=
  2. 避免锁存器的两个方法:

    • 方法1: 确保所有分支都给输出赋值(if要有else,case要有default)
    • 方法2(推荐): 在always块开头先给所有输出赋初值!
  3. 记住: 时序逻辑下不会生成锁存器,锁存器的问题只存在于组合逻辑中

  4. case的妙用:

    • 用反向case实现优先编码器
    • 用casez忽略无关位

避免锁存器是每个硬件设计者的基本功,一定要掌握好!