目录
1 Always blocks
由于数字电路是由与导线连接的逻辑门组成,因此任何电路都可以表示为模块和分配语句的某种组合。但是,有时这不是描述电路的最方便的方法。
Procedures过程(always blocks为例)为描述电路提供了另一种语法。
对于综合硬件,两种类型的always blocks是相关的:
- Combinational: always @(*)
- Clocked: always @(posedge clk)
1.1 Combinational
Combinational always blocks等同于赋值语句,因此总可以用两种方式表示组合电路,使用哪种选择主要是哪种语法更方便的问题。
程序块内部代码的语法与外部代码不同。程序块具有更丰富的语句集(例如if-then,case),不能包含连续的赋值,但是还引入了许多新的非直观的错误处理方式。
例如,assign和combinational always block描述了同一电路。两者都创建相同的组合逻辑块。每当任何输入(右侧)更改值时,两者都将重新计算输出:
- assign out1 = a & b | c ^ d;
- always @(*) out2 = a & b | c ^ d;
对于combinational always blocks,请始终使用灵敏度列表(*)。
明确列出信号很容易出错(如果您错过了一个信号),并且在硬件综合中会被忽略。如果您明确指定了灵敏度列表(*)并错过了信号,则合成的硬件仍会像指定了一样运行,但是模拟不会与硬件的行为匹配。
关于wire vs. reg的注释:assign语句的左侧必须为网络类型(例如,wire),而过程赋值的左侧(始终在块中)必须为变量类型(例如reg)。这些类型(wire与reg)与合成的硬件无关,而仅仅是Verilog用作硬件仿真语言时留下的语法。
question:
使用assign语句和组合的always块构建AND门。
solution:
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
1.2 Clocked
Clocked always blocks会产生组合逻辑块,就像combinational always blocks一样,但还会在组合逻辑块的输出处创建一组触发器(或“寄存器”)。而不是立即看到逻辑团(the blob of logic)的输出,而是仅在下一个(posege clk)之后立即显示输出。
Verilog中有三种类型的分配:
- 连续分配(assign x = y;)。仅在不在过程内部时使用(“始终阻止”)。连续分配(分配x = y;)。仅在不在过程内部时使用(“always block”)。
- 程序阻塞分配(Procedural blocking assignment):(x = y;)。只能在过程内使用。
- 程序非阻塞分配(Procedural non-blocking assignment):(x <= y;)。只能在过程内使用。
在combinational always block中,使用阻止分配(blocking assignments)。在 clocked always block中,使用非阻塞分配(non-blocking assignments)。不遵循该规则将导致极难发现不确定性的错误,并且仿真和综合硬件之间的错误也不同。
question:
使用assign语句,组合的Always块和Clocked Always块三种方式构建XOR门。请注意,clocked 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
2 Statement
2.1 If statement
if语句通常创建2对1多路复用器,如果条件为true,则选择一个输入,如果条件为false,则选择另一个输入。
always @(*) begin
if (condition) begin
out = x;
end
else begin
out = y;
end
end
这等效于对条件运算符使用连续赋值:
assign out = (condition) ? x : y;
但是,过程式if语句提供了一种新的出错方法:仅当out是always assigned的值时,电路才是组合电路。
question:
构建一个在a和b之间选择的2对1多路复用器。如果sel_b1和sel_b2均为true,则选择b。否则,选择a。执行两次,一次使用assign语句,一次使用过程式if语句。
solution:
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
2.2 If statement latches
设计电路时,必须首先考虑电路:
- 我想要这个逻辑门
- 我想要一个具有这些输入并产生这些输出的组合逻辑块
- 我想要一组组合的逻辑后跟一组触发器(flip-flops)
一定不要做的是先编写代码,然后希望它生成正确的电路。
- If (cpu_overheated) then shut_off_computer = 1;
- If (~arrived) then keep_driving = ~gas_tank_empty;
语法正确的代码不一定会导致电路合理(组合逻辑+触发器)。通常的原因是:“在您指定的情况以外的情况下会发生什么?”。 Verilog的答案是:保持输出不变。
这种“保持输出不变”的行为意味着需要记住当前状态,从而产生一个锁存器。组合逻辑(例如逻辑门)无法记住任何状态。
注意警告:Warning (10240): ... inferring latch(es)" messages.
除非有意使用闩锁,否则它几乎总是表示错误。在所有条件下,组合回路必须为所有输出分配一个值。这通常意味着您总是需要else子句或分配给输出的默认值。
question:
以下代码包含创建闩锁的不正确行为。试修复错误,以便仅在计算机确实过热时才关闭计算机,并在到达目的地或需要加油时停止驾驶。
always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end
always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
end
solution:
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 | gas_tank_empty)
keep_driving = 0;
else
keep_driving = 1;
end
endmodule
2.3 Case statement
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 item”以冒号结尾。没有“开关”。
- 每个案例项目都可以执行一条语句。这使得C中使用的“中断”变得不必要。但这意味着如果您需要多个语句,则必须使用begin … end。
- 允许重复(和部分重叠)的案例项目。使用第一个匹配的。 C不允许重复的案例项目。
question:
如果存在大量案例,**Case语句比if语句更方便。**因此,在本练习中,创建6对1多路复用器。当sel在0到5之间时,选择相应的数据输入。否则,输出0。数据输入和输出均为4位宽。(注意推断闩锁)
solution:
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 = 0;
endcase
end
endmodule
3 Priority encoder
3.1 Priority encoder
question:
构建一个4位优先级编码器。
优先级编码器是一种组合电路,当给定输入位向量时,输出该向量中前1位的位置。例如,给定输入8’b10010000的8位优先级编码器将输出3’d4,因为bit [4]是高的第一位。
对于此问题,如果所有输入位都不为高(即输入为零),则输出零。请注意,一个4位数字具有16种可能的组合。
module top_module (
input [3:0] in,
output reg [1:0] pos );
always @(*) begin
case(in)
4'h0:pos=0;
4'h1:pos=0;
4'h2:pos=1;
4'h3:pos=0;
4'h4:pos=2;
4'h5:pos=0;
4'h6:pos=1;
4'h7:pos=0;
4'h8:pos=3;
4'h9:pos=0;
4'ha:pos=1;
4'hb:pos=0;
4'hc:pos=2;
4'hd:pos=0;
4'he:pos=1;
4'hf:pos=0;
default:pos=0;
endcase
end
endmodule
3.2 Priority encoder with casez
question:
为8位输入构建优先级编码器。给定一个8位向量,输出应报告向量中的第一位,即1。如果输入向量没有高位,则报告为零。例如,输入8’b10010000应该输出3’d4,因为bit [4]是高的第一位。
根据上一个练习(Priority encoder),case语句中将有256个案例。如果case语句中支持的case项无关位,我们可以减少这种情况(减少到9个case)。
这就是casez的目的:在比较中,将值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 item。选择第一个匹配项(因此4’b1111匹配第一个项,out = 0,但不匹配任何后续项)。
- 还有一个类似的casex,将x和z都视为无关位。我认为在casez上使用它没有太大的目的。
- 数字"?"是z的同义词。所以2’bz0与2’b?0相同。
solution:
module top_module (
input [7:0] in,
output reg [2:0] pos );
always @(*) begin
casez(in) //casez instead of case
8'bzzzzzzz1:pos=0;
8'bzzzzzz10:pos=1;
8'bzzzzz100:pos=2;
8'bzzzz1000:pos=3;
8'bzzz10000:pos=4;
8'bzz100000:pos=5;
8'bz1000000:pos=6;
8'b10000000:pos=7;
default:pos=0;
endcase
end
endmodule
3.3 Avoiding latches
假设您正在建立一个电路来处理游戏的PS / 2键盘的扫描码。给定收到的扫描码的最后两个字节,您需要指示是否已按下键盘上的箭头键之一。这涉及到相当简单的映射,可以将其实现为具有四个案例的case语句(或if-elseif)。
电路具有一个16位输入和四个输出。构建识别这四个扫描代码并断言(asserts)正确输出的电路。
为避免产生锁存器,必须在所有可能的情况下为所有输出分配一个值。
仅具有默认情况是不够的。我们必须在所有四种情况和默认情况下为所有四个输出分配一个值。这可能涉及很多不必要的输入。解决此问题的一种简单方法是在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语句覆盖该分配。这也意味着默认:case项变得不必要。
注意:逻辑合成器生成一个组合电路,其行为与代码描述的等效。硬件不会按顺序“执行”代码行。
solution:
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;
16'he072: down=1;
16'he074: right=1;
16'he075: up=1;
endcase
end
endmodule