1、Always块1(组合)
由于数字电路是由用导线连接的逻辑门组成的,因此任何电路都可以表示为模块和赋值语句的某种组合。然而,有时这并不是描述电路的最方便的方式。过程(其中always块是一个示例)提供了一种用于描述电路的替代语法。
对于综合硬件,有两种类型的 always 块是相关的:
(1)组合:always @(*)
(2)时钟: always @(posedge clk)
组合always块等效于assign语句,因此总有一种方法可以双向表达组合电路。选择使用哪个,主要是哪个语法更方便的问题。程序块内部代码的语法与外部代码不同。程序块具有更丰富的语句集(例如,if-then、case),不能包含连续赋值*,但也引入了许多新的非直观的错误方式。 (*过程连续赋值确实存在,但与连续赋值有些不同,并且不可合成。)
例如,assign 和combinational always 块描述了相同的电路。两者都创建了相同的组合逻辑块。每当任何输入(右侧)更改值时,两者都会重新计算输出。
assign out1 = a & b | c ^ d;
always @(*) out2 = a & b | c ^ d;
对于组合 always 块,始终使用(*)的敏感度列表。明确列出信号容易出错(如果您错过了),并且在硬件综合时会被忽略。如果您明确指定敏感度列表并遗漏了一个信号,合成的硬件仍然会像指定了()的内容一样运行,但模拟不会也不匹配硬件的行为。(在 SystemVerilog 中,使用always_comb。)
关于 wire 与 reg 的说明:
assign 语句的左侧必须是net类型(例如,wire),而过程赋值(在 always 块中)的左侧必须是变量类型(例如,reg)。
这些类型(wire 与 reg)与合成的硬件无关,只是 Verilog 用作硬件模拟语言时遗留下来的语法。
练习:
使用assign 语句和组合always 块构建AND 门。
代码实现:
module top_module(
input a,
input b,
output wire out_assign,
output reg out_alwaysblock
);
assign out_assign = a & b;
always @(*)
out_alwaysblock = a & b;
endmodule
验证结果:
2、Always块2(时钟)
对于硬件综合,有两种相关的always块:
组合: always @(*)
时钟: always @(posedge clk)
时钟always块创建组合逻辑块,就像组合always块一样,但也在组合逻辑块的输出处创建一组触发器(或“寄存器”)。逻辑块的输出不是立即可见,而是仅在下一个 (posedge clk) 之后立即可见。
阻塞与非阻塞分配
Verilog 中有三种类型的赋值:
(1)连续赋值(assign x = y;):只能当不在过程内部时使用(“始终阻塞”)。
(2)程序阻塞赋值:( x = y; ) :只能在程序内部使用。
(3)程序非阻塞赋值:( x <= y; ) :只能在程序内部使用。
在组合always块中,使用阻塞分配;在时钟always块中,使用非阻塞分配。充分理解为什么对硬件设计不是特别有用,需要很好地理解 Verilog 模拟器如何跟踪事件。不遵循此规则会导致极难发现仿真和综合硬件之间的不确定性和不同的错误。
练习:
以三种方式构建 XOR 门,使用赋值语句、组合的 always 块和时钟的 always 块。请注意,时钟always块产生与其他两个不同的电路:有一个触发器,因此输出被延迟。
代码实现:
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@(*)
out_always_comb = a ^ b;
always@(posedge clk)
out_always_ff <= a ^ b;
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语句提供了一种新的出错方式。只有当out总是被分配一个值时,电路才是组合的。
拓展:
(1)逻辑运算符:
①逻辑与&&
②逻辑或||
③逻辑非!
真值表:
a | b | !a | !b | a && b | a ll b |
---|---|---|---|---|---|
1 | 1 | 0 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 0 | 1 |
0 | 1 | 1 | 0 | 0 | 1 |
0 | 0 | 1 | 1 | 0 | 0 |
(2)按位运算符:
①按位与&
②按位或|
③按位取反~
④按位异或^
⑤按位同或^~
例如:~a;a & b ;a | b ;a ^ b
按位与真值表:
& | 0 | 1 | x |
---|---|---|---|
0 | 0 | 0 | 0 |
1 | 0 | 1 | x |
x | 0 | x | x |
练习:
构建一个在a和b之间进行选择的 2 对 1 多路复用器。如果两个 sel_b1和sel_b2是真实的,选择b;否则,选择a。同样的事情做两次,一次使用assign语句,一次使用过程if 语句。
sel_b1 | sel_b2 | out_assign | out_always |
---|---|---|---|
0 | 0 | a | a |
0 | 1 | a | a |
1 | 0 | a | a |
1 | 1 | b | b |
代码实现:
(1)逻辑与运算符&&
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==1 && sel_b2==1) ? b : a;
always @(*) begin
if (sel_b1==1 && sel_b2==1) begin
out_always = b;
end
else begin
out_always = a;
end
end
endmodule
(2)按位与运算符&
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_b1 & sel_b2) begin
out_always = b;
end
else begin
out_always = a;
end
end
endmodule
验证结果:
4、if 语句锁存
错误的常见来源:如何避免产生锁存
设计电路时,你必须首先想到在电路方面:
(1)我想要这个逻辑门;
(2)我想要一个具有这些输入并产生这些输出的逻辑组合块;
(3)我想要一个逻辑组合块,然后是一组触发器。
你不能做的是先写代码,然后希望它生成一个合适的电路。
If (cpu_overheated) then shut_off_computer = 1;
If (~arrived) then keep_driving = ~gas_tank_empty;
语法正确的代码不一定会产生合理的电路(组合逻辑 + 触发器)。通常的原因是:“在您指定的情况之外的情况下会发生什么?”。Verilog 的答案是:保持输出不变。
这种“保持输出不变”的行为意味着需要记住当前状态,从而产生一个锁存器。组合逻辑(例如,逻辑门)无法记住任何状态。注意Warning (10240): … inferring latch(es)消息。除非锁存是故意的,否则它几乎总是表明存在错误。组合电路必须在所有条件下为所有输出分配一个值。这通常意味着您总是需要else子句或分配给输出的默认值。
例如:
以下代码包含创建锁存的错误行为。修复错误,以便您仅在计算机确实过热时才关闭计算机,并在到达目的地或需要加油时停止驱动。
错误代码:
always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end
always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
end
正确代码:
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
(1)case 语句以case开头,每个“case item”以冒号结尾。没有“switch”。
(2)每个 case 项只能执行一个语句。这使得 C语言 中使用的“break”变得不必要。但这意味着如果您需要多个语句,则必须使用begin … end。
(3)允许重复(和部分重叠)的case item,使用第一个匹配的case 。C语言不允许重复的 case 项。
练习:
如果有大量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'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;
endcase
end
endmodule
验证结果:
6、优先级编码器
优先级编码器是一个组合电路,给定一个输入位向量时,输出第一的位置1中的向量位。例如,给定输入8’b100 1 0000的 8 位优先级编码器将输出3’d4,因为 bit[4] 是第一个高位。
构建一个 4 位优先级编码器。对于这个问题,如果没有一个输入位为高(即输入为零),则输出零。请注意,一个 4 位数字有 16 种可能的组合。
代码实现:
(1)case 语句实现
module top_module (
input [3:0] in,
output reg [1:0] pos );
always@(*) begin
case(in[3:0])
4'b0000: pos = 2'b00;
4'b0001: pos = 2'b00;
4'b0010: pos = 2'b01;
4'b0011: pos = 2'b00;
4'b0100: pos = 2'b10;
4'b0101: pos = 2'b00;
4'b0110: pos = 2'b01;
4'b0111: pos = 2'b00;
4'b1000: pos = 2'b11;
4'b1001: pos = 2'b00;
4'b1010: pos = 2'b01;
4'b1011: pos = 2'b00;
4'b1100: pos = 2'b10;
4'b1101: pos = 2'b00;
4'b1110: pos = 2'b01;
4'b1111: pos = 2'b00;
default: pos = 2'b00;
endcase
end
endmodule
(2)casez 语句实现
module top_module (
input [3:0] in,
output reg [1:0] pos );
always @(*) begin
casez (in[3:0])
4'bzzz1: pos = 2'b00;
4'bzz1z: pos = 2'b01;
4'bz1zz: pos = 2'b10;
4'b1zzz: pos = 2'b11;
default: pos = 2'b00;
endcase
end
endmodule
验证结果:
7、casez的优先级编码器
构建一个8 位输入优先编码器。给定一个 8 位向量,输出应报告向量中的第一个位1。如果输入向量没有高位,则报告零。例如,输入8’b100 1 0000应该输出3’d4,因为 bit[4] 是第一个高位。
从上一个练习中,case 语句中有 256 个 case。如果 case 语句中的 case 项支持 don’t-care 位,我们可以减少这种情况(减少到 9 个 case)。这就是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)如何匹配多个 case 项。选择第一个匹配项(因此4’b1111匹配第一项out = 0,但不匹配后面的任何项)。
还有一个类似的casex将x和z 都视为无关紧要。在casez上使用casex没有多大意义。
?是z的同义词。所以2’bz0和2’b?0是一样的。
代码实现:
module top_module (
input [7:0] in,
output reg [2:0] pos );
always @(*) begin
casez (in[7:0])
8'bzzzzzzz1: pos = 3'b000;
8'bzzzzzz1z: pos = 3'b001;
8'bzzzzz1zz: pos = 3'b010;
8'bzzzz1zzz: pos = 3'b011;
8'bzzz1zzzz: pos = 3'b100;
8'bzz1zzzzz: pos = 3'b101;
8'bz1zzzzzz: pos = 3'b110;
8'b1zzzzzzz: pos = 3'b111;
default: pos = 3'b000;
endcase
end
endmodule
验证结果:
8、避免锁存
假设您正在构建一个电路来处理来自游戏的 PS/2 键盘的扫描码。给定接收到的最后两个字节的扫描码,您需要指出是否按下了键盘上的箭头键之一。这涉及一个相当简单的映射,它可以实现为具有四个 case 的 case 语句(或 if-elseif)。
Scancode [15:0] | Arrow key |
---|---|
16’he06b | left arrow |
16’he072 | down arrow |
16’he074 | right arrow |
16’he075 | up arrow |
Anything else | none |
您的电路有一个 16 位输入和四个输出。构建此电路以识别这四个扫描码并断言正确的输出。
为避免创建锁存器,必须在所有可能的条件下为所有输出分配一个值。仅仅有一个默认情况是不够的。您必须在所有四种情况和默认情况下为所有四个输出分配一个值。这可能涉及许多不必要的打字。解决此问题的一种简单方法是在 case 语句之前为输出分配一个“默认值” :
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 语句覆盖分配。这也意味着default: case 项变得不必要了。
提醒:逻辑合成器生成一个组合电路,其行为与代码描述的相同。硬件不会按顺序“执行”代码行。
代码实现:
module top_module (
input [15:0] scancode,
output reg left,
output reg down,
output reg right,
output reg up );
always @(*) begin
up = 1'b0; down = 1'b0; left = 1'b0; right = 1'b0;
case (scancode)
16'he06b: left = 1'b1;
16'he072: down = 1'b1;
16'he074: right = 1'b1;
16'he075: up = 1'b1;
endcase
end
endmodule
验证结果: