HDLBits_Verilog Language_Procedures

HDLBits链接


组合逻辑的always块

tips:考虑到硬件的可综合性,有两种always块是有意义的:

  • 组合:always @(*)
  • 时序:always @(posedge clk)

组合逻辑的always块和assign赋值是等价的,使用哪一种完全看哪一种更方便。always块内可有更丰富的状态,如if-then,case等,但不能有连续赋值语句assign。

如果在always块内指定了特定的信号,但没有的话,和always(*)综合结果相同,此时功能仿真结果和硬件结果就有所差异。

assign赋值语句的左边一般为wire类型,always块中左边的变量一般为reg类型。这些类型与硬件综合无关,仅是verilog语法的要求。

题目描述

使用赋值语句和组合always块两种方式构建与门。

Solution

1
2
3
4
5
6
7
8
9
10
11
module top_module(
input a,
input b,
output wire out_assign,
output reg out_alwaysblock
);
assign out_assign = a & b;
always @(*) begin
out_alwaysblock = a & b;
end
endmodule

时序逻辑的always块

tips:时序always块可像组合always块那样生成电路,同时也会生成一系列的触发器,寄存器等,因为输出要等到下个时钟延才能输出。

阻塞赋值 vs 非阻塞赋值

verilog中有三种赋值方式:

  • 连续赋值(assign x=y;),只能在always块外使用。
  • 阻塞赋值(x=y;),只能在always块内使用。
  • 非阻塞赋值(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
module top_module(
input clk,
input a,
input b,
output wire out_assign,
output reg out_always_comb,
output reg out_always_ff );
assign out_assign = a ^ b;
always @(*) begin
out_always_comb = a ^ b;
end
always @(posedge clk) begin
out_always_ff <= a ^ b;
end
endmodule

IF选择器

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

1

if的两种方式:

1
2
3
4
5
6
7
8
always @(*) begin
if (condition) begin
out = x;
end
else begin
out = y;
end
end
1
assign out = (condition) ? x : y;

题目描述

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
module top_module(
input a,
input b,
input sel_b1,
input sel_b2,
output wire out_assign,
output reg out_always );
assign out_assign=(sel_b1&sel_b2)?b:a;
always @(*) begin
if(sel_b2&sel_b1) begin
out_always = b;
end
else begin
out_always = a;
end
end
endmodule

IF中锁存器问题

tips:如何避免在使用if语句时生成锁存器?

tip:锁存器与触发器的区别?

  • 锁存器是一种对脉冲电平(也就是0或者1)敏感的存储单元电路,而触发器是一种对脉冲边沿(即上升沿或者下降沿)敏感的存储电路。

当我们在设计电路时,不能直接先写成代码然后期望它直接生成为合适的电路,如下典型错误所示:

  • If (cpu_overheated) then shut_off_computer = 1;
  • If (~arrived) then keep_driving = ~gas_tank_empty;

语法上正确的代码并不意味着设计成的电路也是合理的。我们来思考这么一个问题,如上图的错误示例,如果if条件不满足,输出如何变化呢?Verilog给出的解决方法是:保持输出不变。因为组合逻辑电路不能记录当前的状态,所以就会综合出锁存器。

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

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

1
2
3
4
5
6
7
8
9
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
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) begin
shut_off_computer = 1;
end
else begin
shut_off_computer = 0;
end
end

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

endmodule

Case语句

tips:在Verilog中,case语句与if-elseif-else相近,与c语言中的switch差别较大,示例如下:

1
2
3
4
5
6
7
8
9
always @(*) begin     // This is a combinational circuit
case (in)
1'b1: begin
out = 1'b1; // begin-end if statement >1
end
1'b0: out = 1'b0;
default: out = 1'bx;
endcase
end
  • case语句以case开始,每个case的选项以分号结束。
  • 每个case的选项中只能执行一个statement,所以就无需break语句。但如果我们想在一个case选项中执行多个statement,就需要使用begin...end
  • case中可以有重复的case item,首次匹配的将会被执行。

题目描述: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 // This is a combinational circuit
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=3'b0;
endcase
end

endmodule

优先编码器

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

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

Solution

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

1
2
3
4
5
6
7
8
9
10
11
case(input)

4'b0001: output = 1;

.......

4'b1111:output = 1;

default: output = 0;

endcase

2、如果按上述思路来写,那么更多位的优先编码器如何实现呢?其实有更简单的方法,目前既不用casex也不用casez,这两位要在后面出场。来看另一种思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
module top_module (
input [3:0] in,
output reg [1:0] pos );
always @(*) begin
case(1)
in[0]:pos = 0;
in[1]:pos = 1;
in[2]:pos = 2;
in[3]:pos = 3;
default:pos = 0;
endcase
end
endmodule

根据上面所说的case的性质,case中可以有重复的case item,但首次匹配的才会被执行。再看上面的case语句,即从低到高位去比较in中是否有数据位为1,下面即使有重复为1的也只会执行首次匹配的操作。如此,N位优先编码器只需要N个case分支即可,大大简化代码量。


casez实现优先编码器

题目描述

为8位输入构建优先级编码器。当给定输入位向量时,输出该向量中第一个1的位置。如果输入向量没有高位,则报告为零。 例如,给定输入8’b10010000的8位优先级编码器将输出3’d4,因为bit [4]是高的第一位。

tips:由上一个练习我们知道,case语句中将有256种case item,使用casez以后,我们可以减少需比较的case item,这就是casez的目的,在比较中,将值z的位视作无关位。

所以上题的另一种解法为:

1
2
3
4
5
6
7
8
9
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)是如何匹配多个case项的。选择第一个匹配项(因此4’b1111匹配第一个项目,out = 0,但不匹配后面的任何项目)。

  • 还有类似的casex,它将x和z均无视掉。
  • 符号?是z的同义词,所以2‘bz0=2’b?0

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module top_module (
input [7:0] in,
output reg [2:0] pos );
always @(*) begin
casez(in)
8'bzzzzzzz1:pos=0;
8'bzzzzzz1z:pos=1;
8'bzzzzz1zz:pos=2;
8'bzzzz1zzz:pos=3;
8'bzzz1zzzz:pos=4;
8'bzz1zzzzz:pos=5;
8'bz1zzzzzz:pos=6;
8'b1zzzzzzz:pos=7;
default:pos=0;
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

tips:为避免生成锁存器,所有的输入情况必须要被考虑到。但仅有一个简单的default是不够的,我们必须在case item和default中为4个输出进行赋值,这会导致很多不必要的代码编写。

一种简单的方式就是对输出先进行赋初值的操作,这种类型的代码确保在所有可能的情况下输出都被赋值,除非case语句覆盖了赋值。这也意味着不再需要缺省的default项。如下面示例代码:

1
2
3
4
5
6
always @(*) begin
up = 1'b0; down = 1'b0; left = 1'b0; right = 1'b0;
case (scancode)
... // Set to 1 as necessary.
endcase
end

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module top_module (
input [15:0] scancode,
output reg left,
output reg down,
output reg right,
output reg up );
always @(*) begin
left=0;down=0;right=0;up=0;
case(scancode)
16'he06b:left=1;
16'he072:down=1;
16'he074:right=1;
16'he075:up=1;
endcase
end
endmodule

总结:

  • 学习了组合和时序两种always块,并知晓了两种类型的always块中变量的类型即赋值方式。何时使用wire与reg,何时用阻塞和非阻塞赋值。

  • 学习了如何在组合逻辑中使用if和case语句时避免生成锁存器

    将所有状态均考虑到并为其输出赋值,考虑不全可用else/default。

    在case之前为所有输出赋初值,这样可以不用使用default,除非满足case item进行覆盖赋值,否则仍保持初值。

    时序逻辑下不会生成锁存器。

  • 学习了case语句的妙用,以及使用casez忽略无关位,更简便的实现优先编码器。