HDLBits_Verilog学习笔记Ⅰ——Verilog Language_Procedures

 作者:脱发秘籍搬运工 https://www.bilibili.com/read/cv10795225?spm_id_from=333.999.list.card_article.click 出处:bilibili 
Verilog在线学习网站:HDLBits

28. Always blocks (combinational)

小知识点:由于数字电路是通过线路将逻辑门连接构成,所以任何数字电路理论上都可以表示为模块和assign语句的某种组合。但在某些时候,这并不是描述电路的最简便的方式。过程块(包括always、initial、task、fuction)则提供了另一种用以描述电路的语法。例如:👇

//以下是两种可综合(综合成硬件电路)的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块,敏感列表总是使用(*)。明确地列出信号容易出错(比如漏掉一个),并且这类错误在硬件综合时会被忽略:如果你明确地列出了敏感列表但漏了一个信号,综合出来的硬件电路仍与使用(*)时一样。但在仿真时,仿真器会按漏了一个信号的情况跑仿真,这会导致仿真结果与原硬件不匹配。(在SystemVerilog中,使用always_comb。)

       关于wire和reg的声明:assign赋值语句的左边必须是net类型(例如wire),而过程赋值语句(在always块中)的左边必须是变量类型(例如reg)。这些类型(wire和reg)与最终综合的硬件无关,只是Verilog作为硬件仿真语言使用时遗留下来的语法习惯。

浅出:在理论上,通过之前学的module模块和assign语句就能表示所有数字电路了,但在实际操作中,某些行为较复杂的电路很难通过上述方式来表示(可以想象一下,复杂电路通过与、或门等来表示会有多麻烦)。所以,Verilog提供了一种从行为级来描述电路的方式——always等过程块。这些过程块可以让我们方便地利用if、case和for循环等高级语法来描述电路的行为,综合器会自动把这些语句块综合成相应的硬件电路。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Practice: Build an AND gate using both an assign statement and a combinational always block. (Since ...

大白话:根据下图,分别用assign语句和always块两种方式来写一个与门。(这题还体现不出always的优势,等到第30题才能体现出来)

转自HDLBits
答案(先做再看哦,且不唯一,仅供参考):

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
BB几句:说几句题外话,虽然always等过程块提供了一种高效描述电路的方式,但是这种方式也有缺点。比如你用if-else和for循环嵌套写了一个复杂的电路,后面实际综合出来的电路结构可能会让你很惊(beng)讶(kui)。这就是用高级语法进行行为级描述的缺陷,它让你很难把控电路结构的细节,全靠EDA工具去综合,而assign和逻辑门等“低级”描述方式就能有效避免这个缺陷。另外,还有软硬件编程思维之类的差别就不细说了,总之对于初学者来说,要记住一点,for循环之类的要慎用(循环虽好,可不要贪杯噢🤭)。

29. Always blocks (clocked)

小知识点:从硬件的综合角度来看,存在两种always块类型:

组合逻辑: always @(*)
时序逻辑: always @(posedge clk)
       时序always块也会像组合always块那样生成一系列的组合逻辑电路,但同时又会在组合逻辑的输出口生成一组触发器(或寄存器)。所以该输出只在下一个时钟上升沿(posedge clk)后可见,而不是之前组合逻辑的立即可见。

       阻塞赋值和非阻塞赋值:Verilog中有以下三种类型的赋值方式:

连续赋值: (assign x=y;) // 不能在过程块(always块)内使用;
过程阻塞赋值: (x=y;) // 只能在过程块内使用;
过程非阻塞赋值: (x<=y;) // 只能在过程块内使用。
       在组合always块中,使用阻塞赋值。在时序always块中,使用非阻塞赋值。具体理解为什么这么规定对设计硬件帮助不大,这还需要理解Verilog模拟器是如何跟踪事件的。不遵循此规则会导致一些极难发现的错误,即仿真结果以及综合出来的硬件都会存在不确定性,并且两者之间也会存在差异。

浅出:时序逻辑always块相对于组合逻辑always块的不同点在于输出端多了一组触发器或寄存器(学过数字电路的应该都知道触发器和寄存器是构成时序电路的重要元器件)。Verilog有三种赋值方式,并且规定组合always块使用阻塞赋值,时序always块使用非阻塞赋值,初学者不需要理解为什么,只需记住(可以把非阻塞赋值的符号“<=”中的<记成一个触发器😄)。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Practice: Build an XOR gate three ways, using an assign statement, a combinational always block, and a clocked always block. Note that ...

大白话:根据下图,用前面讲的三种方式写一个异或门。需要注意的是,时序always块产生的电路与其余两个不同,它有一个触发器,因此输出会有延迟。

转自HDLBits
答案(先做再看噢,且不唯一,仅供参考):

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
BB两句:从下面的波形图中可以看到,尽管输入a或b的值已经发生了变化,但时序逻辑的输出out_always_ff还是要在下一个时钟上升沿到来时才会改变,所以它要比另外两个输出慢一拍(迟一个时钟周期)。另外再说句题外话,可以试着思考一下,我在某些模块内不想看到always块,那该怎么写时序逻辑?(答案是:你可以例化DFF模块呀,hhh~😄)。

仿真波形图

30. If statement
小知识点:通常通过if语句来描述一个二选一的多路复用器(数据选择器):当条件为真时,选择其中一个输入;当条件为假时,选择另一个输入。

always @(*) begin
if (condition) begin
out = x;
end
else begin
out = y;
end
end
       这相当于使用条件运算符进行连续赋值:

assign out = (condition) ? x : y;
       但是,过程if语句使用不当可能会引入新的错误,只有输出在所有条件下都被赋值才能生成正确的组合电路(具体可参考下一题中的latch)。


Practice: Build a 2-to-1 mux that chooses between a and b. Choose b if both ...

大白话:根据下面的“真值表”来写一个二选一的数据选择器。用assign和if两种方式。


答案(先做再看,且不唯一,仅供参考):

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

31. If statement latches


小知识点:如何避免产生latch(锁存器)。在设计电路时,必须先从电路的角度考虑:

· 我想实现一个逻辑门;
· 我想实现一个具有输入且产生输出的组合逻辑块;
· 我想实现一个组合逻辑块,并接着一组触发器。
       你不能上来就直接写代码,然后希望它生成一个合适的电路。语法正确的代码不一定会产生合理的电路(组合逻辑+触发器)。通常是因为:“在你指定的那些情况之外会发生什么?”,Verilog的回答是“保持输出不变”。

       这种“保持输出不变”的行为意味着需要记住当前的状态,从而会导致产生锁存器。组合逻辑(如逻辑门)不能记住任何状态。注意:类似“Warning (10240): ... inferring latch(es)”这样的警告信息基本上代表有故障,除非这个latch是故意生成的。组合电路必须为所有输出在所有条件下分配一个值。这通常意味着您总是需要为输出分配else子句或默认值。

       以下代码就存在生成锁存器的错误行为:

always @(*) begin
if (cpu_overheated)
shut_off_computer = 1;
end
具体可参考下图:

转自HDLBits
浅出:如果不写全所有情况下的输出,综合工具就会默认将原值赋给输出,即保持输出不变。具体的电路可以参考上图:它直接把输出shut_off_computer连到了多选器的其中一个输入端上了。这会导致当cpu_overheated=1时,shut_off_computer=1;当cpu_overheated=0时,shut_off_computer=shut_off_computer(=1或不确定值x),也就是在这个例子中,当cpu_overheated拉高一次后,不论后面选择信号cpu_overheated怎么变,shut_off_computer都大概率始终等于1(刚开始是一个不确定值)。这种结构就是一个锁存器。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Practice: Fix the bugs so that you will shut off the computer only if it's really overheated, and stop driving if you've arrived at your destination or you need to refuel.

大白话:修复上面代码的bug(只有当电脑真的过热时,你才会关闭电脑;如果你已经到达目的地或需要加油,你就会停止开车)。

答案(先做再看,且不唯一,仅供参考):

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 = ~arrived;
end

endmodule
BB两句:组合always块中的latch问题一定要避免,这在后面的DC综合时可能还会产生error,使你不得不去debug,而且它还容易引起竞争冒险。


32. Case statement

小知识点:Verilog中的Case语句几乎等价于一个if-else if-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项。
       如果选择信号存在好几种情况(case项),则case语句比if语句更方便。 

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Practice: Case statements are more convenient than if statements if there are a large number of cases. So, in this exercise, create a 6-to-1 multiplexer. When sel ...

大白话:写一个6-to-1的数据选择器,根据选择信号sel的值来选择相对应的数据输入,否则输出0。

答案(先做再看,且不唯一,仅供参考):

module top_module (
... // 信号太多,就省略了
);
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'b0000; // 所有其它没有写明的情况都包含在这项中
endcase
end

endmodule
BB两句:组合always块的case也会生成latch,所有最后的default也要赋值。另外case只是几乎等同于if-else if-else,最大的区别就在于,if-else有不一样的优先级顺序,而case语句中所有被判断的分支项都具有一样的优先级。


33. Priority encoder

小知识点:优先编码器是一种组合电路,当给定一个输入位向量时,输出该向量从右往左数(从低位到高位)第一个1的位置。例如,输入8'b10010000时,8位优先级编码器将输出3'd4,因为位[4]是从低到高第一个为1的位。(注:从右到左,最低的那位是第0位。)

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Practice: Build a 4-bit priority encoder. For this problem, if ...

大白话:构建一个4位优先编码器。对于这个问题,如果没有一个输入位是高的(即输入全为零),则输出为零。注意,4位数字有16种可能的组合。

答案(先做再看,且不唯一,仅供参考):

module top_module (
input [3:0] in,
output reg [1:0] pos
);
always@(*) begin
case(in) // 用十六进制可以少打一些字,用二进制更直观,各有优劣
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; // 这个例子中,16种情况都遍历了,此项可省略
endcase
end
endmodule

34. Priority encoder with casez


小知识点:如果按上一题的方式来写一个8位输入的优先编码器的话,case语句中将有256个case项。如果case语句中的case项与某些输入无关,就可以减少列出的case项(在本题中减少到9个)。这就是casez的用途:它在比较中将具有值z的位视为无关项。具体可参考下面对上一题的casez写法:

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匹配第一个case项,即out = 0)。

· 还有一个类似的casex,将x和z都视为无关项。不认为casex相比casez有什么特别的意义。(z和x状态的问题涉及电路的基本知识)
· 符号"?"是z的同义词,所以2'bz0与2'b?0是相同的。
       显式地指定优先级行为可能比依赖于用case项的顺序更不容易出错。例如,如果重新排序一些case项,下面的代码仍然会以同样的方式运行,因为任何位组合最多只能匹配其中一个case项:

casez (in[3:0])
4'bzzz1: ...
4'bzz10: ...
4'bz100: ...
4'b1000: ...
default: ...
endcase
浅出:最后这段话还是有必要解释一下。注意,casez是有优先级的!比如在上面的例子中,4'b1111能匹配4'bzzz1、4'bzz1z、4'bz1zz、4'b1zzz四项中的任一项,但是为什么最终out输出0,因为4'bzzz1写在最前面(第一个case项),所以它的优先级最高,4'b1111按out=0输出。如果把四个case项改写成4'bzzz1、4'bzz10、4'bz100、4'b1000,那4'b1111只能匹配4'bzzz1,所以不管把4'bzzz1放第几个,4'b1111都会按4'bzzz1这一项的out=0来输出。以此类推,同学们可以思考一下4'b1110等例子。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Practice: Build a priority encoder for 8-bit inputs. Given an 8-bit vector, the output ...

大白话:类似于上一题,不同的是,这题的优先编码器是8位的(如果还按上一题的方式写,则需要写256个case项)。

答案(先做再看,且不唯一,仅供参考):

module top_module (
input [7:0] in,
output reg [2:0] pos );

always@(*) begin
casez(in) // 这题就只能用二进制了
8'bzzzzzzz1: pos = 3'b000;
8'bzzzzzz10: pos = 3'b001;
8'bzzzzz100: pos = 3'b010;
8'bzzzz1000: pos = 3'b011;
8'bzzz10000: pos = 3'b100;
8'bzz100000: pos = 3'b101;
8'bz1000000: pos = 3'b110;
8'b10000000: pos = 3'b111;
default: pos = 3'b000;
endcase
end

endmodule
BB两句:虽然casez在这两个例子中很好用,而且也可综合,但还是不推荐使用。一般有优先级用if-else,没优先级就用case。


35. Avoiding latches


小知识点:为避免生成不必要的锁存器,必须在所有可能的情况下为所有的输出赋值(参见31.If statement latches)。这可能涉及许多不必要的输入,会多打很多字。 一个简单的解决方法是在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项变得不必要了。提醒:逻辑综合器会综合生成一个组合电路,其行为与代码描述的相同。硬件描述语言不会按顺序去“执行”代码行。

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Practice: Suppose you're building a circuit to process scancodes from a PS/2 keyboard for a game. Given the last two bytes of ...

大白话:假设你正在为一款游戏构建一个处理PS/2键盘扫描码的电路。给定接收到的扫描码的最后两个字节,您需要判断键盘上的某一个arrow键是否被按下。这涉及到一个相当简单的映射,可以用一个case语句(或if-else if)实现,共有如下四种情况:


       所设计的电路有一个16位输入和四个输出。描述这个电路,识别这四个扫描码,并给出正确的输出。(避免生成不必要的锁存器...)

答案(先做再看,且不唯一,仅供参考):

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: left = 1'b1;
16'he072: down = 1'b1;
16'he074: right = 1'b1;
16'he075: up = 1'b1;
endcase
end

endmodule

重点:①过程块的分类:always、initial等;

          ②组合逻辑always块:always @(*)

             时序逻辑always块:always @(posedge clk);

        ③阻塞赋值(=)和非阻塞赋值(<=);

          ④if-else、case、casez条件语句的用法与注意事项;

          ⑤锁存器(latch)的概念与避免方法;

          ⑥优先编码器的概念。

 
posted @ 2022-07-21 13:19  学习记录本  阅读(12)  评论(0)    收藏  举报