HDLBits第四章练习及答案

本文介绍了Verilog HDL中的核心概念,包括Always块的组合与时钟行为,If语句的使用及其可能导致的锁存问题,以及Case语句在逻辑设计中的应用。详细讨论了如何避免在设计中意外创建锁存器,并通过实例展示了如何构建多路复用器和优先级编码器。此外,还强调了在编写代码时确保所有条件下的输出都有定义的重要性。
摘要由CSDN通过智能技术生成

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)逻辑运算符:
①逻辑与&&
②逻辑或||
③逻辑非!

真值表:

ab!a!ba && ba ll b
110011
100101
011001
001100

(2)按位运算符:
①按位与&
②按位或|
③按位取反~
④按位异或^
⑤按位同或^~
例如:~a;a & b ;a | b ;a ^ b

按位与真值表:

&01x
0000
101x
x0xx

练习:

构建一个在a和b之间进行选择的 2 对 1 多路复用器。如果两个 sel_b1和sel_b2是真实的,选择b;否则,选择a。同样的事情做两次,一次使用assign语句,一次使用过程if 语句。

sel_b1sel_b2out_assignout_always
00aa
01aa
10aa
11bb

代码实现:

(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’he06bleft arrow
16’he072down arrow
16’he074right arrow
16’he075up arrow
Anything elsenone

您的电路有一个 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

验证结果:
在这里插入图片描述

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值