HDLBits——Procedures
Problem 28: Always blocks(combinational) (Alwaysblock1)
Requirement:
过程块(比如 always 块)提供了一种用于替代 assign 语句描述电路的方法。分别使用 assign 语句和组合 always 块来构建与门。
Solution:
// synthesis verilog_input_version verilog_2001
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
PS:
-
有两种 always 块是可以综合出电路硬件的:
综合逻辑:always @(*)
时序逻辑:always @(posedge clk)
-
组合 always 块相当于 assign 语句,因此组合电路存在两种表达方法,具体使用哪个主要取决于使用哪个更方便。过程块内的代码与外部的assign代码不同。过程块中可以使用更丰富的语句(比如 if-then,case),但不能包含连续赋值。
例如,assign 和组合 always 块描述相同的电路,只要任何输入改变值,两者都将重新计算输出。
assign out1 = a & b | c ^ d; always @(*) out2 = a & b | c ^ d;
对于组合always块,敏感变量列表总是使用(*)。如果把所有的输入都列出来也是可以的,但容易出错(可能少列出了一个),在硬件综合时会忽略少列了一个,仍按原电路综合,但仿真器将会按少列一个来仿真,这导致了仿真与硬件不匹配。
-
关于 wire 与 reg 的说明:assign 语句的左侧必须是 net 类型(例如,
wire
),而过程赋值(在 always 块中)的左侧必须是 variable 类型(例如,reg
)。
Timing Diagram:
Problem 29: Always blocks(clocked) (Alwaysblock2)
Requirement:
使用 assign 语句,组合 always 块和时序 always 块这三种方式来构建异或门。 请注意,时序 always 块生成了与另外两个不同的电路,多了一个触发器,因此输出会有一定的延迟。
Solution:
// synthesis verilog_input_version verilog_2001
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
PS:
-
上一节提到有两种 always 块,时序 always 块也会像组合 always 块一样生成一系列的组合电路,但同时生成了一组触发器(或寄存器)。该输出在下一个时钟上升沿(posedge clk)后可见,而不是之前的立即可见。
-
在 Verilog 中有以下三种赋值方法:
- 连续赋值(assign x=y;):不能在过程块内使用;
- 过程阻塞性赋值(x=y;):只能在过程块中使用;
- 过程非阻塞性赋值(x<=y):只能在过程块内使用。
在组合 always 块中,使用阻塞性赋值。在时序 always 块中,使用非阻塞性赋值。具体为什么对设计硬件用处不大,还需要理解Verilog模拟器如何跟踪事件。不遵循此规则会导致极难发现非确定性错误,并且在仿真和综合出来的硬件之间存在差异。
Timing Diagram:
从仿真的波形图可以看出,out_always_ff 比其他两个输出延迟了一个时钟周期,因为上升沿才触发赋值。
Problem 30: If statement(Always if)
Requirement:
构建一个可以在 a 和 b 之间选择的二选一多路复用器。如果 sel_b1 和 sel_b2 都为真,输出 b,其他情况输出 a。请使用两种方法作答,一次使用 assign 赋值,一次使用 if 语句。
sel_b1 | sel_b2 | out_assign out_always |
---|---|---|
0 | 0 | a |
0 | 1 | a |
1 | 0 | a |
1 | 1 | b |
Solution:
// synthesis verilog_input_version verilog_2001
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
PS:if 语句通常对应一个二选一多路复用器,如果条件为真,则选择其中一个输入作为输出;如果条件为假,则选择另一个输入作为输出,if 语句必须在过程块内使用。与使用**条件运算符(问号冒号语句)+ 连续赋值(assign)**的语句是等价的。
注意:冒号前面是满足条件的结果。
assign 语句也可写作 assign out_assign = sel_b1 ? (sel_b2 ? b:a):a; 即优先判断 sel_b1 的值,sel_b1 为 0 直接输出 a,为 1 的时候再判断 sel_b2 是否为 1,此时,sel_b1 和 b2 均为 1 时,才输出 b,否则均输出 a。但不如上面的简单。
Timing Diagram:
Problem 31: If statement latches(Always if2)
Requirement:
设计电路时除了指定的情况,输出保持不变。这意味着电路需要记住当前状态,从而产生锁存器(latch),组合逻辑不能记住任何状态。除非锁存器是故意生成的,组合电路输出必须在所有输入的情况下都必须有值,这意味着需要 else 子句或输出默认值 default。
以下代码包含生成锁存器的错误,请修复错误,以便真正过热时才关闭计算机,同时使得到达目的地或者需要加油时,停止驾驶。
always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end
always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
end
Solution:
// synthesis verilog_input_version verilog_2001
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 begin
shut_off_computer = 0;
end
end
always @(*) begin
if (~arrived & ~gas_tank_empty)
keep_driving = 1;
else begin
keep_driving = 0;
end
end
endmodule
//停车也可改为
always @(*) begin
if (~arrived)
keep_driving = ~gas_tank_empty;
else
keep_driving = ~arrived;
end
Timing Diagram:
Problem 32: Casestatement(Always case)
Requirement:
创建一个 6 选 1 的多路复用器。当 sel 介于 0 和 5 之间时,选择相应的数据输入。 其他情况输出0。数据输入和输出均为 4 位宽。注意:不要生成锁存器。
Solution:
// synthesis verilog_input_version verilog_2001
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)
0: begin out = data0; end
1: begin out = data1; end
2: begin out = data2; end
3: begin out = data3; end
4: begin out = data4; end
5: begin out = data5; end
default: begin out = 4'b0000; end
endcase
end
endmodule
PS:Verilog 中的 case 语句几乎等同于 if-else if-else 序列,写在逻辑 always 块中,它将一个表达式与其他表达式列表进行比较。它的语法和功能与 C 语言中的 switch 语句稍有不同:
- case 语句以 case 开头,每个 case 项以冒号结束。而 switch 语句没有。
- 每个 case 项只执行一个语句。 这样就不需要 C 语言中 break 来跳出 switch。但这也意味着如果需要多个语句,则必须使用 begin … end。
- case 项允许重复和部分重叠,执行程序匹配到的第一个,而 C 语言不允许重复的 case 项目。
注意:这里一定要用 default 声明一下不在 case 项里的输出,否则会生成不必要的寄存器,影响电路的功能。
Timing Diagram:
Problem 33: Priority encoder(Always case2)
Requirement:
构建一个4位优先编码器。如果输入均为零,则输出零。
优先编码器:组合电路,当给定输入时,输出输入向量中的右边第一个1的位置。例如,输入8’b10010000的,则优先编码器将输出3’d4,因为位[4]是从右数第一个1。出现的第一个数字把后面的数字屏蔽掉了,第一个数字具有较高的优先级,所以叫做优先译码器。
Solution:
// synthesis verilog_input_version verilog_2001
module top_module (
input [3:0] in,
output reg [1:0] pos
);
always @(*) begin
if (in[0]) pos = 0;
else if (in[1]) pos = 1;
else if (in[2]) pos = 2;
else if (in[3]) pos = 3;
else pos = 0;
end
endmodule
Timing Diagram:
Problem 34: Priority encoder with casez(Always casez)
Requirement:
构建一个 8 输入的优先编码器。给定一个 8 位向量,输出输入向量中左数第一个 1 的位置。如果输入均为 0,则输出零。
Solution:
// synthesis verilog_input_version verilog_2001
module top_module (
input [7:0] in,
output reg [2:0] pos );
always @(*) begin
casez (in)
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
**casez:**如果 case 语句中的 case 项与某些输入无关,就可以减少列出的 case 项(在本题中减少到 4 个)。这就是 casez 的用途:它在比较中将具有值 z 的位视为无关项(即输入01都会匹配到)。
注意:符号 “?” 是 z 的同义词,8’bzzzz1000 也可写作 8’b???1000。
**casex:**与 casez 类似,将输入的 x 和 z 都视为无关。
Timing Diagram:
Problem 35: Always nolatches(Always nolatches)
Requirement:
假设写一个来处理用于游戏 PS/2 键盘扫描码的电路。给出接收到扫描码的最后的两个字节,需要判断是否有按键被按下。这是一个相当简单的映射,可以使用 case 语句或者 if-else 语句实现,一共有如下四种情况。
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 位的输入和 4 个输出,请描述此电路,识别这四个按键的扫描码并输出。
Solution:
// synthesis verilog_input_version verilog_2001
module top_module (
input [15:0] scancode,
output reg left,
output reg down,
output reg right,
output reg up );
always @(*) begin
case (scancode)
16'he06b: begin left = 1'b1;down = 1'b0;right = 1'b0;up = 1'b0; end
16'he072: begin left = 1'b0;down = 1'b1;right = 1'b0;up = 1'b0; end
16'he074: begin left = 1'b0;down = 1'b0;right = 1'b1;up = 1'b0; end
16'he075: begin left = 1'b0;down = 1'b0;right = 1'b0;up = 1'b1; end
default: begin left = 1'b0;down = 1'b0;right = 1'b0;up = 1'b0; end
endcase
end
endmodule
PS:always@(*) 综合器会生成一个组合电路,其行为与代码描述相同,硬件不会按顺序“执行”代码。
改进:为避免生成不必要的锁存器,必须在所有条件下为所有的输出赋值(参见Problem 31),导致打很多字,使代码变得冗长。 一个简单的方法是在 case 语句之前为输出分配一个“默认值”,除非 case 语句覆盖赋值,否则这种代码样式可确保在所有可能的情况下输出 0。 此时 case 的 default 项变得不必要,综合器会报一个 case 语句没有 default 的 warning。
// synthesis verilog_input_version verilog_2001
module top_module (
input [15:0] scancode,
output reg left,
output reg down,
output reg right,
output reg up );
always @(*) begin
left = 1'b0;down = 1'b0;right = 1'b0;up = 1'b0;
case (scancode)
16'he06b: begin left = 1'b1; end
16'he072: begin down = 1'b1; end
16'he074: begin right = 1'b1; end
16'he075: begin up = 1'b1; end
endcase
end
endmodule
Timing Diagram:
PS:天气是阴沉的,房间内没有阳光射入,好疲惫。