Verilog HDLBits 第五期:2.4Procedures

目录

前言

2.4.1Always blocks(combinational) (Alwaysblock1)

A bit of practice

Solution:

 2.4.2Always blocks(clocked) (Alwaysblock2)

A bit of practice

Solution:

 2.4.3If statement(Always if)

A bit of practice

Solution:

2.4.4If statement latches(Always if2)

Solution:

2.4.5Case statement (Always case)

A bit of practice

Solution:

2.4.6Priority encoder(Always case2)

Solution:

2.4.7Priority encoder(Always casez)

Solution:

2.4.8Avoiding latches(Always nolatches)

Solution:


前言

HDLbits网站如下

Problem sets - HDLBits (01xz.net)

从本期开始我们继续HDLbits第二章Verilog Language的学习,本期的内容是2.4Procedures


2.4.1Always blocks(combinational) (Alwaysblock1)

由于数字逻辑电路是由用wire连接的逻辑门组成的,因此任何电路都可以表示为模块和赋值语句的组合。然而,有时这不是描述电路最方便的方法。always过程块提供了描述电路的另一种方法。

对于综合硬件,有两种相关的always块:

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

组合always块相当于赋值语句,因此总有可以用两种方式来表示组合电路。选择使用哪种语法主要是看哪个语法更方便。过程块里面代码的语法与外面代码不同。过程块有更丰富的语句集(例如,if-then, case),不能包含连续赋值语句,但也引入了许多新的非直观的出错方法。(*过程连续赋值确实存在,但与连续赋值有些不同,并且不可综合。)

例如,assign 和组合 always 块描述了相同的电路。两者都创建了相同的组合逻辑块。每当任何输入(右侧)更改值时,两者都会重新计算输出。

assign out1 = a & b | c ^ d;
always @(*) out2 = a & b | c ^ d;

Verilog HDLBits 第五期:2.4Procedures_第1张图片

对于组合always块,always的敏感列表使用*包含所有输入信号。明确列出信号容易出错(如果您忘记了),并且在硬件综合时会被忽略。如果您明确指定敏感度列表并遗漏了一个信号,综合的硬件仍然会像指定了 (*) 一样运行,但仿真不会也不匹配硬件的行为。(在SystemVerilog中,使用 always_comb)

关于wire和reg的一点注意事项:assign语句的左操作数必须是线网类型(比如说 wire),而对于过程块赋值(在always块中)左操作数必须是一个可变的类型(比如说 reg)。这些类型(wire 与 reg)与综合的硬件无关,只是 Verilog 用作硬件仿真语言时遗留下来的语法。

A bit of practice

使用assign 语句和组合always 块创建与门。(由于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@(*)
        out_alwaysblock = a & b ;

endmodule

 2.4.2Always blocks(clocked) (Alwaysblock2)

对于综合硬件,有两种相关的always块:

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

时序always块像组合always块一样创建一些组合逻辑,但是也在组合逻辑的输出生成一组触发器(或者寄存器)。逻辑块的输出不是立即可见的,而是仅在下一个(posedge clk)到来之后可见。

阻塞赋值和非阻塞赋值

Verilog中有三种不同的赋值语句:

  • 连续赋值语句(assign x = y;)只能不在always块中使用
  • 过程阻塞赋值语句(x = y ;)只能在过程块中使用
  • 过程非阻塞赋值语句(x = y ;)只能在过程块中使用

在组合always块中,使用阻塞赋值语句。在时序always块中,使用非阻塞赋值语句。充分理解为什么对硬件设计不是特别有用,需要很好地理解 Verilog 模拟器如何跟踪事件。不遵循此规则会导致极难发现仿真和综合硬件之间的不确定性和不同的错误。

A bit of practice

用以下三种方法来创建一个异或门:assign语句、组合always块和时序always块。注意时序always块生成了一个与其他两个不一样的电路:生成了一个触发器所以导致输出延迟了。

Verilog HDLBits 第五期:2.4Procedures_第2张图片        

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 @ (*)
        out_always_comb = a ^ b;
    
    always @ (posedge clk)
        out_always_ff <= a ^ b;

endmodule

Verilog HDLBits 第五期:2.4Procedures_第3张图片

从输出时序图看到,由于非阻塞赋值,out_always_ff 比其他两个输出延迟了一个时钟周期。


 2.4.3If statement(Always 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 总是被赋值时,电路才是组合的。

A bit of practice

创建一个2选1mux来选择a和b。如果sel_b1 和 sel_b2 都为真选择b。其他情况,选择a。同样的事情做两次,一次使用assign 语句,一次使用过程if 语句。

Verilog HDLBits 第五期:2.4Procedures_第4张图片        

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) 
            out_always = b ;
        else 
            out_always = a ;
    end
endmodule

2.4.4If statement latches(Always if2)

一个很常见的错误:如何避免产生锁存器

设计电路时,首先要从电路方面考虑:

  • 我想实现一个逻辑门
  • 我想要一个具有这些输入并产生这些输出的组合逻辑块
  • 我想实现一组组合逻辑,紧接着一组触发器

你不能只是先写代码,然后就希望它生成一个合适的电路。

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

语法正确的代码不一定会产生合理的电路(组合逻辑 + 触发器)。通常的原因是:“在您指定的情况之外的情况下会发生什么?”。 Verilog 的回答是:保持输出不变。

这种“保持输出不变”的行为意味着需要记住当前状态,从而产生一个锁存器。组合逻辑(比如逻辑门)不能存储任何状态。注意warning(10240)消息:... inferring latch(es) 。通常代表出现了bug,除非锁存器是故意生成的。在所有情况下,组合电路必须赋值给所有输出。这通常意味着您总是需要 else 语句或分配给输出的默认值。

Demonstration演示

以下代码包含生成锁存器的错误行为。修复错误,以便您仅在计算机确实过热(cpu_overheated)时才关闭计算机(shut_off_computer);并在到达目的地或需要加油时才停止驾驶。

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
           shut_off_computer = 0;
    end

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

endmodule

2.4.5Case statement (Always 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
  • case语句以case开头,每个case项都以分号结尾。而switch语句没有。
  • 每个case项只执行一个语句。这就不需要C语言中break来跳出switch。但是这也意味着,如果你需要多个语句,则必须使用 begin ... end 
  • case语句允许重复和部分重叠,执行匹配到的第一个。而C语言不允许重复的case项。

A bit of practice

如果有很多case项的话,case语句比if语句方便很多。所以在本练习中,创建一个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)
            3'd0: out = data0;
            3'd1: out = data1;
            3'd2: out = data2;
            3'd3: out = data3;
            3'd4: out = data4;
            3'd5: out = data5;
            default: out = 4'd0;
        endcase
    end

endmodule

注意:2的十进制表示是 3'd2,二进制表示是3'b10 (写成3'b2就会语法错误)


2.4.6Priority encoder(Always case2)

优先编码器是组合电路,当给定输入向量时,输出输入向量中第一个1的位置。比如,一个8位优先编码器的输入是8’b10010000,将会输出3'd4,因为第四位是右数第一个1。

创建1个4位优先编码器。对于本问题,如果输入每一位都为0,则输出0。注意4位二进制数有16种可能的组合。

Hint:使用十六进制(4'hb)或十进制(4'd11)与二进制(4'b1011)相比更节省打字量。

Solution:

module top_module (
	input [3:0] in,
	output reg [1:0] pos
);

	always @(*) begin			// Combinational always block
		case (in)
			4'h0: pos = 2'h0;	// 我喜欢用16进制,因为它节省了打字量
			4'h1: pos = 2'h0;
			4'h2: pos = 2'h1;
			4'h3: pos = 2'h0;
			4'h4: pos = 2'h2;
			4'h5: pos = 2'h0;
			4'h6: pos = 2'h1;
			4'h7: pos = 2'h0;
			4'h8: pos = 2'h3;
			4'h9: pos = 2'h0;
			4'ha: pos = 2'h1;
			4'hb: pos = 2'h0;
			4'hc: pos = 2'h2;
			4'hd: pos = 2'h0;
			4'he: pos = 2'h1;
			4'hf: pos = 2'h0;
			default: pos = 2'b0;	// Default 项可以不写,因为已经包括了所以16种组合
		endcase
	end
	
	// 下一题(always_casez)中给出了更简单的 方法。
endmodule

注意:2的十进制表示是 3'd2,二进制表示是3'b10 (写成3'b2就会语法错误)


2.4.7Priority encoder(Always casez)

创建8位优先编码器。给定8位输入向量,输出第一个1出现的位置。如果输入向量为0,则输出0。比如,输入是8’b10010000,将会输出3'd4,因为第四位是右数第一个1。

对于前一题(always_case2),将有256条case语句。如果case语句中的case项支持无关项,我们可以减少到9条case语句。这就是 casez 的用途:它在比较中将值为 z 的位视为无关位。

举个栗子,下面代码将会实现之前的4位优先编码器:

always @(*) begin
    casez (in[3:0])
        4'bzzz1: out = 0;   // in[3:1] 可以是任何值
        4'bzz1z: out = 1;
        4'bz1zz: out = 2;
        4'b1zzz: out = 3;
        default: out = 0;
    endcase
end

case 语句的行为就像是按顺序检查每个case项(实际上,它的作用更像是生成一个巨大的真值表然后生成门电路)。请注意某些输入(例如 4'b1111)匹配了多个 case 项。程序选择第一个匹配项(因此4'b1111匹配第一项,out=0,而不匹配任何后面的case项)。

  • 还有一个类似的casex,它将x和z都视作无关项。我认为casex相较于casez而言,没有多大的意义。
  • 数字 ? 是z的同义词。所以2'bz0与2'b?0相同。

Hint:在这里必须使用二进制形式,因为你需要为某些位指定z。

优先编码器是组合电路,当给定输入向量时,输出输入向量中第一个1的位置。比如,一个8位优先编码器的输入是8’b10010000,将会输出3'd4,因为第四位是右数第一个1。

创建1个4位优先编码器。对于本问题,如果输入每一位都为0,则输出0。注意4位二进制数有16种可能的组合。

Hint:使用十六进制(4'hb)或十进制(4'd11)与二进制(4'b1011)相比更节省打字量。

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 = 3'd0;
                8'bzzzzzz1z:pos = 3'd1;
                8'bzzzzz1zz:pos = 3'd2;
                8'bzzzz1zzz:pos = 3'd3;
                8'bzzz1zzzz:pos = 3'd4;
                8'bzz1zzzzz:pos = 3'd5;
                8'bz1zzzzzz:pos = 3'd6;
                8'b1zzzzzzz:pos = 3'd7;
                default    :pos = 3'd0;              
            endcase
        end
endmodule

2.4.8Avoiding latches(Always nolatches)

假设您正在构建一个电路来处理来自游戏的 PS/2 键盘的扫描码。给定接收到的最后两个字节的扫描码,您需要指出是否按下了键盘上的箭头键之一。这涉及一个相当简单的映射,它可以实现为具有四个 case 的 case 语句(或 if-elseif)。

Verilog HDLBits 第五期:2.4Procedures_第5张图片

 电路有1个16位输入、4个输出。创建此电路以识别这四个按键的扫描码并输出。

为了避免生成锁存器,在所有可能情况下,所有输出都必须被赋值。仅仅有一个默认情况是不够的。您必须在所有四种情况和默认情况下为所有四个输出赋一个值。这可能涉及许多不必要的打字。解决此问题的一种简单方法是在 case 语句之前为输出分配一个“默认值”:

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

除非case语句覆盖赋值,否则这种代码样式可确保在所有可能的情况下输出0。 这也意味着case的default项变得不必要。

提醒:逻辑综合器生成一个组合电路,其行为与代码描述的内容相同。硬件不会按顺序“执行”代码行。

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 
            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

注意: up = 1'b0; down = 1'b0; left = 1'b0; right = 1'b0;  应该放在always块里!


2.4终于结束了,笔者在这期的学习中受益匪浅,我们下期再见!

你可能感兴趣的:(HDLbits,嵌入式硬件,verilog,fpga开发,硬件,fpga)