CO-Verilog

CO-Verilog

Charles Lv7

CO-Verilog

Verilog Summary

verilog的小总结,有部分内容来自于讨论区,在此收纳,防止突然讨论区像教程一样在计组上机结束后就被关了。

1.$signed的注意事项

首先我们要了解,一个表达式由操作数(Operand)、运算符(Operator)组成,且可以递归定义。

针对符号,表达式的最终计算主要有两步:①符号确定向内传播

首先存在如下一些特殊的运算符(包括但不限于),它们的符号仅由其中部分操作数确定:

(1).移位运算符(<<、>>、>>>等)生成的表达式(形如i >>> j)的符号i的符号决定,j恒视作无符号且不参与该表达式的符号确定。

(2).三目运算符生成的表达式(形如 i ? j :k )中的i 不参与该表达式的符号确定。

其次,各类常量的符号性也要注意。

比如0等常量是有符号的,而4b'02'd0等给定基数形式的常量是无符号的(形如4'sd12的除外,为有符号常量,其中s即表示Signed)。

再看对符号确定的解释:
如果一个表达式在排除不参与其符号确定的操作数后,剩下的子表达式中任意一个被确定为无符号,那么整个表达式都被确定为无符号,否则有符号。

符号确定后,现在开始向内传播部分,依然是标准文本:

这部分与教程解释一致:在确定整体表达式的符号性后,将确定好的符号性由外向内地传递给上下文决定的子表达式(自决定的子表达式不受影响)。类似递归的过程,遇到原子表达式则强制转换原子表达式的符号。

如果你还是觉得难以理解,那就直接通过一个例子(简化自896-837)来感受一下:

1
1 ? $signed(a)>>>b : 4'b0

首先Verilog不存在表达式短路这样的优化(至少仿真时没有),所以我们不能把该表达式进一步简化成$signed(a)>>>b,仍然要看整体。

符号确定:首先1不参与符号确定,排除;$signed(a)>>>b4'b0,其中4'b0显然无符号,$signed(a)>>>b符号由$signed(a)决定,是有符号的;则表达式1 ? 有符号 : 无符号,根据Verilog对无符号的偏爱,表达式整体无符号。

向内传播:首先1不受影响,排除;原子表达式4'b0本就是无符号,无需转换;$signed(a)>>>bb不受影响,再次递归进入$signed(a),是有符号原子表达式,强制转化成无符号,即$unsigned($signed(a))

最终表达式等价a >> b,相当于仍对a进行逻辑右移。


简单理解及一些规避方法:

1.扫描表达式中不在$signed()中也不属于不受影响的无符号原子操作数,找到一个则直接鉴定整个表达式无符号。如果这些操作数一个都找不到,恭喜,直接按正常思维就能算对表达式;如果鉴定为无符号,那么就要把表达式中所有不属于不受影响(几乎就是所有)的有符号原子操作数强制转化成无符号再计算。

2.如果你不能确定到底哪些会变成无符号,那就尽一切办法阻止它,具体方法就是多用括号。

1
2
3
assign a = ((b + c) >>> d) + e;  // 原式
assign a = $signed($signed($signed($signed(b) + $signed(c)) >>> $signed(d)) + $signed(e)); // 狂暴法
assign a = (($signed(b) + $signed(c)) >>> d) + $signed(e); // 温和法

此外还有分离变量、自己实现易出问题的部分等方法(见教程,都用于自己写代码)。


最后提一嘴$signed(),与$unsigned()一样是一类系统函数,不仅能强制转换符号性,还能起到类似屏障的作用,函数内部的表达式是单独计算的,既不影响函数外部符号的确定,外部符号向内传播时也不会进入函数内部,这点与教程推荐的分离变量的作用一致。因此可以把$signed(a+b)看成一个原子表达式或变量/常量,但其本身依然有可能受到向内传播的影响,被转换成无符号形式,失去转换效果,但能保住其内部的表达式的计算结果。 比如下面的例子(修改自教程):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// wrong !!!
assign out = op == 0? in_a + in_b:
op == 1? in_a - in_b:
op == 2? in_a >> in_b:
$signed(in_a) >>> in_b;
/*
* 比起给每个变量都加一层$signed()的方法,
* 分离变量法与用$signed包裹敏感的中间量
* 是更优的选择。
*/
// correct 分离变量法
wire [WIDTH-1:0] shift;
assign shift = $signed(in_a) >>> in_b;
assign out = op == 0? in_a + in_b:
op == 1? in_a - in_b:
op == 2? in_a >> in_b:
shift;
// correct 用$signed包裹敏感中间量
assign out = op == 0? in_a + in_b:
op == 1? in_a - in_b:
op == 2? in_a >> in_b:
$signed($signed(in_a) >>> in_b);
// correct 你甚至可以用$unsigned
assign out = op == 0? in_a + in_b:
op == 1? in_a - in_b:
op == 2? in_a >> in_b:
$unsigned($signed(in_a) >>> in_b);

2.wire和reg变量类型

1、从仿真角度来说,HDL语言面对的是编译器,相当于使用软件思路,此时:
   wire对应于连续赋值,如assign;
   reg对应于过程赋值,如always,initial;
2、从综合角度,HDL语言面对的是综合器,相当于从电路角度来思考,此时:
   wire型变量综合出来一般情况下是一根导线。
   reg变量在always中有两种情况,根据是否时钟沿(posedge clk)综合成时序逻辑(会包含触发器)或者组合逻辑,所以reg型并不表示综合出来就是暂存器register,在组合电路中使用reg,综合后只是net(线网)。

3.赋值

在Verilog中有以下三种赋值方法:
  1.连续赋值(assign x=y;):不能在过程块内使用;
  2.过程阻塞性赋值(x=y;):只能在过程块中使用;
  3.过程非阻塞性复制(x<=y):只能在过程块内使用。
在always中不能对wire变量赋值,wire为无逻辑连线,所以输入什么就的输出什么。那 assign c = a & b;不是对wire的赋值吗?不然,虽然称为连续赋值,但可能更多带有连接的意思。综合时是将a & b综合成 a、b经过一个与门,而c只是连接到与门输出线,真正综合出来的是与门&,不是c。

4.always过程块

我们知道数字电路是由导线连接的逻辑门组成,因此任何电路都可以表示为module和assign语句的某种组合。但是,有时候这不是描述电路最简便的方法。过程块(比如always块)提供了一种用于替代assign语句描述电路的方法。有两种always块是可以综合出电路硬件的:

组合逻辑:

1
always @(*)

组合always块相当于assign语句,因此组合电路存在两种表达方法。具体使用哪个主要取决于使用哪个更方便。过程块内的代码与外部的assign代码不同。always过程块中可以使用更丰富的语句(比如if-then,case)。以下两个代码均创造出了相同的组合逻辑电路,只要任何输入(右侧)改变值,两者都将重新计算输出。

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

对于组合always块,敏感变量列表总是使用(*)。如果把所有的输入都列出来也是可以的,但容易出错的(可能少列出了一个),并且在硬件综合时会忽略少列了一个,仍按原电路综合。但仿真器将会按少列一个来仿真,这导致了仿真与硬件不匹配。

时序逻辑:

1
always @(posedge clk)

时序always块也会像组合always块一样生成一系列的组合电路,但同时在组合逻辑的输出还生成了一组触发器(Flip-Flop)或寄存器。该输出在下一个时钟上升沿(posedge clk)后可见,而不是之前的立即可见。

5.阻塞和非阻塞赋值的不同

我们知道在描述组合逻辑的always块中用阻塞赋值,在描述时序逻辑的always块中用非阻塞赋值,为什么一定要这样做呢?
回答是:这是因为要使综合前仿真和综合后仿真一致的缘故。如果不按照上面两个要点来编写Verilog代码,也有可能综合出正确的逻辑,但前后仿真的结果就会不一致。
先做以下定义
RHS– 方程式右手方向的表达式或变量可分别缩写为:RHS表达式或RHS变量。
LHS– 方程式左手方向的表达式或变量可分别缩写为:LHS表达式或LHS变量。
IEEE Verilog标准定义了有些语句有确定的执行时间,有些语句没有确定的执行时间。若有两条或两条以上语句准备在同一时刻执行,但由于语句的排列次序不同(而这种排列次序的不同是IEEE Verilog标准所允许的),却产生了不同的输出结果。这就是造成Verilog模块冒险和竞争现象的原因。为了避免产生竞争,理解阻塞和非阻塞赋值在执行时间上的差别是至关重要的。

阻塞赋值

赋值时先计算一条语句等号右手方向(RHS)部分的值,这时赋值语句不允许任何别的Verilog语句的干扰,直到现行的赋值完成时刻,即把RHS赋值给 LHS的时刻,它才允许别的赋值语句的执行。
如果在一个过程块中阻塞赋值的RHS变量正好是另一个过程块中阻塞赋值的LHS变量,这两个过程块又用同一个时钟沿触发,这时阻塞赋值操作会出现问题,即如果阻塞赋值的次序安排不好,就会出现竞争。若这两个阻塞赋值操作用同一个时钟沿触发,则执行的次序是无法确定的。下面的例子可以说明这个问题:
用阻塞赋值的反馈振荡器

1
2
3
4
5
6
7
8
9
10
11
12
13
module fbosc1 (y1, y2, clk, rst);
output y1, y2;
input clk, rst;
reg y1, y2;

always @(posedge clk or posedge rst)
if (rst) y1 = 0; // reset
else y1 = y2;

always @(posedge clk or posedge rst)
if (rst) y2 = 1; // preset
else y2 = y1;
endmodule

两个always块是并行执行的,与前后次序无关,。如果前一个 always块的复位信号先到 0 时刻,则 y1 和 y2 都会取 1,而如果后一个 always 块的复位信号先到 0 时刻,则y1 和 y2 都会取 0。

非阻塞赋值

在计算非阻塞赋值的RHS表达式和更新LHS期间,其他的Verilog语句,包括其他的Verilog非阻塞赋值语句都能同时计算RHS表达式和更新LHS。非阻塞赋值允许其他的Verilog语句同时进行操作。非阻塞赋值的操作可以看作为两个步骤的过程:
  在赋值时刻开始时,计算非阻塞赋值RHS表达式。
  在赋值时刻结束时,更新非阻塞赋值LHS表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
module fbosc2 (y1, y2, clk, rst);  
output y1, y2;
input clk, rst;
reg y1, y2;

always @(posedge clk or posedge rst)
if (rst) y1 <= 0; // reset
else y1 <= y2;

always @(posedge clk or posedge rst)
if (rst) y2 <= 1; // preset
else y2 <= y1;
endmodule

此时复位信号到0时,y1为1而y2为0是确定的。

Verilog模块编程要点:
使用以下规范可避免综合后仿真中出现的的冒险竞争问题。
  1.时序电路建模时,用非阻塞赋值。
  2.锁存器电路建模时,用非阻塞赋值。
  3.用always块写组合逻辑时,采用阻塞赋值。
  4.在同一个always块中同时建立时序和组合逻辑电路时,用非阻塞赋值。
  5.在同一个always块中不要同时使用非阻塞赋值和阻塞赋值。
  6.不要在多个always块中为同一个变量赋值。

6.Verilog综合出锁存器的问题

Warning (10240): … inferring latch(es)
上述这类警告通常情况下代表错误,除非锁存器是故意生成的。
时序电路无论如何不会产生锁存器(锁存器只能是电平敏感的),组合电路只要条件不完备就会产生锁存器,如if语句未用 else 语句覆盖所有情况,case 语句未使用 default 语句覆盖所有可能的情况,组合电路输出必须在所有输入的情况下都有值,即为语句块中所有的变量赋值。若没else、default语句,电路输出仍需保持不变,这就意味着电路需要记住当前状态,从而产生锁存器。组合逻辑(比如逻辑门)不能记住任何状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
A:    always  @(*)begin
if(d)
a = b;
end

B: always @(*)begin
if(d)
a = b;
else
a = a;
end
C:
always @ (b or d)
case(d)
2’b00: a=b>>1;
2’b11: c=b>>1;
default:
begin
a=b;
c=b;
end
endcase
D: always @(b or d)begin
a=b;
c=b;
case(d)
2’b00: a=b>>1;
2’b11: c=b>>1;
endcase
end

代码A:
是一个always语句块构成的组合逻辑,其中缺少else分支。当d=1’b0时,综合工具会默认保持a的值,即生成锁存器.
代码B:
虽然补全了else分支语句。但是,其代码依然有保持功能,即会生成锁存器;也就是说,组合逻辑是否会生成锁存器,其根本原因是该组合逻辑存在保持功能!
代码C:
在always语句块内使用了case语句,并且case语句中含有default分支。但是,我们可以发现:d=2’b00时,没有说明c的赋值;d=2’b01时,没有说明a的赋值。
代码D:
和代码C的不同是,它在always语句块的开始,对a和c进行了默认赋值;这就类似于软件里面的初始化,当然只是类似而已,不会生成锁存器。

7.X和Z

x是不定态,也就是说,其状态不可知。z是高阻态,在实际电路中,开关断开(没有驱动)就意味着高阻态(断路电阻视为很大)。casexcasez说的是如果情况中的某一位值是x或z,那么就不比较他们了,就比较其他的不是x或z的位。
当声明变量的时候,如果不赋值,那么wire类型初值是z,整数和寄存器都是x,这很符合实际情况,wire没有驱动的时候是高阻态,而综合后与门电路相关的整数和寄存器,都是与门电路相关,而门电路是有驱动的(这里说的是不是输入的驱动),所以应该是不定态x。

8.模块例化引用

模块引用相当于C语言中的函数调用,可将引用的模块看作一个有定向输入输出的黑盒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module top_module (input x, input y, output z);

wire tmp1, tmp2, tmp3, tmp4;
A A1(x, y, tmp1);
B B1(x, y, tmp2);
A A2(x, y, tmp3);
B B2(x, y, tmp4);

assign z = (tmp1 | tmp2) ^ (tmp3 & tmp4);

endmodule

module A (input x, input y, output z);

assign z = (x^y) & x;

endmodule

module B ( input x, input y, output z );

assign z = ~(x ^ y);

endmodule

9.同步复位与异步复位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
同步复位
module top_module (
input clk,
input reset, // Synchronous reset
input [7:0] d,
output reg [7:0] q
);

always @(posedge clk) begin
if(reset)
q <= 8'd0;
else
q <= d;
end
endmodule

异步复位
module top_module (
input clk,
input areset, // active high asynchronous reset
input [7:0] d,
output reg [7:0] q
);
always @(posedge clk, posedge areset) begin//这个需要注意一下
if(areset)
q <= 8'd0;
else
q <= d;
end
endmodule

10.generate 用法

generate 语句可以动态地生成 Verilog 代码,常用于编写许多结构相同但参数不同的赋值语句或逻辑语句,方便参数化模块的生成。generate 语句主要有以下三种用途:
  1.对矢量中的多个位进行重复操作
  2.重复操作多个模块的实例引用
  3.根据参数定义来确定程序中是否应该包括某段 Verilog 代码
举个第二个用途的例子(generate-for结构)
在使用前必须先声明一个 genvar 变量,用于 for 循环判断,需要给generate 块命名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
genvar i;
generate
for (i = 0; i < 15; i++)
begin: xorLoop
xor u_xor(
.dout (dout[i]),
.din0 (din0[i]),
.din1 (din1[i])
);
end
endgenerate
在仿真时,仿真器会将 generate 块代码展开,上面的代码就相当于
xorLoop u_xor0(.dout(dout[0]), .din0(din0[0]), .din1(din1[0]));
xorLoop u_xor1(.dout(dout[1]), .din0(din0[1]), .din1(din1[1]));
// ......
xorLoop u_xor15(.dout(dout[15]), .din0(din0[15]), .din1(din1[15]));

11.有限状态机

三段式

三段式状态机虽然代码会长一些,但能够更方便地修改,并更清晰地表达状态机的跳变与输出规则。
三段式分别指
1.状态跳转逻辑,根据输入信号以及当前状态确定状态的次态状态
2.触发器实现,在时钟边沿实现状态寄存器的跳变以及状态复位
3.输出逻辑,根据当前状态(moore)实现输出
举个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module top_module (
input clk,
input in,
input areset,
output out
);
parameter A=0, B=1;
reg state;
reg next;
always@(*) begin // Combinational always block for state transition logic.
case (state)
A: next = in ? A : B;
B: next = in ? B : A;
endcase
end
always @(posedge clk, posedge areset) begin
if (areset) state <= B; // Reset to state B
else state <= next; // Otherwise, cause the state to transition
end
// Combinational output logic. In this problem, an assign statement is the simplest.
// In more complex circuits, a combinational always block may be more suitable.
assign out = (state==B);
endmodule

Moore型和Mealy型

根据状态机的输出是否与输入条件相关来区分Moore状态机和Mealy状态机.
实现相同功能时,Moore型状态机需要比Mealy型状态机多一个状态,且Moore型状态机的输出比Mealy型延后一个时钟周期。
假设Moore型有3个状态(A,B,C)其中当输入in为1时状态B将转移到状态C,而Mealy型仅仅需要2个状态(A,B),这是因为Moore型的输出仅仅与状态有关,若想输出C状态的值,则必须形成C状态。而对于Mealy型,输出是由状态和输入共同决定的,即如果在状态B时,输入in为1,那么输出电路就可以输出C状态的值了,不需要等到状态C形成,因此会提前一个周期输出,也不需要状态C。
或者可以这么理解,状态机的次态由当前状态和输入决定,Moore的输出其实是由次态(例如上述C状态)决定的,所以需要等待一个时钟沿,输出相较于状态转移滞后一个周期。而mealy型在相同的周期内反应,所以不需要等待时钟。

  • Title: CO-Verilog
  • Author: Charles
  • Created at : 2022-12-27 13:04:49
  • Updated at : 2023-11-05 21:36:02
  • Link: https://charles2530.github.io/2022/12/27/co-verilog/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments