「BUAA CO」Verilog易错点

「BUAA CO」Verilog易错点

Squirrel7ang Lv3

在P4以及后续的设计过程中,Verilog的语法不过关可能会带来十分糟糕的体验,因此列出了本人犯的错误和舍友们犯的错误。内容仅供参考交流,如有错误,欢迎指正。

Verilog可以在HDLbits 进行练习,也可以参考菜鸟教程

1、关于$signed()

Verilog中默认一切未声明有符号的整型数均为无符号数,并默认有符号数与无符号数运算时,将有符号数自动转化为无符号数。比如:

1
2
wire [3 : 0] outcome;
assign outcome = (opCode == 1'b1) ? signed(a) * signed(b) : a * b;

上述代码中,signed(a) * signed(b)a*b通过三目运算符进行运算,因此默认将第二位操作数(signed(a) * signed(b))转换为无符号的乘法。解决方案在官方教程中有提及。

上面这个例子很容易看出问题,但是当运用多重三目运算符进行嵌套的时候,结构复杂,很可能会遗漏有符号数和无符号数的问题。

此外还有一点与C代码不同的是,在Verilog中,不能将“=”视为一种运算符。也就是说如果我在某一语句块里这么写:

1
2
3
always @(*) begin
outcome = $signed(a) * $signed(b);
end

outcome仍然是两个有符号数做乘法的运算结果。=应当和<=视为一种赋值符

2、关于条件运算和浮空

使用三目运算符时,当第一位操作数,即条件,为浮空值的时候,表达式返回值也是浮空值。 比方说:

1
2
wire outcome;
assign outcome = (1'bx) ? 1'b1 : 1'b0;

在上述代码中,outcome会得到1位的浮空值。Verilog不知道条件是真是假,因此无法给出结果。

但是如果这样写:

1
2
wire [1 : 0] outcome;
assign outcome = (1'bx) ? 2'b01 : 2'b00;

outcome就会得到2'b0x的值,理由是尽管Verilog不知道结果是第二个操作数还是第三个操作数,但是不论是哪一个,其第1位一定是0,但是无法确定其第0位究竟是0还是1,因此第0位浮空。

if-else语句中则不一样。比如:

1
2
3
4
if (a == 1'b1) 
b <= 1'b1;
else
b <= 1'b0;

在上述代码中,如果a为一位浮空值,程序仍然会执行else语句块内的代码。if-else是安全的。

在一些情况下,我们希望判断语句能够更加保险,即能够对浮空值也进行判断。这是可以使用===!===,这双目两个运算符可以连同高阻态z和浮空值x一并进行判断。

三目运算符在进行简单的if-else判断是非常好用的,主要是写起来和读起来都比较舒服。但是不建议用于多重if-else判断,尤其是当不论if还是else里面都有if-else语句的时候。

3、关于”~”和”!”以及“自动类型转换”

省流版就是:不要用位宽不同的两个数进行相互赋值,理由是Verilog在进行位扩展的时候会进行一些很诡异的操作。

下面讲一个具体的例子。下例由室友LJC提出,愣是把我看傻了。

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
module test(
);
reg [1 : 0] a;
reg [3 : 0] c;
reg clk;

always @(posedge clk) begin
c = ~a;
$display("when a = 1'b%b, c = ~ a = 4'b%b", a, c);
c = !a;
$display("when a = 1'b%b, c = ! a = 4'b%b", a, c);
end

initial begin
a = 2'b00;
c = 4'b00;
clk = 0;

#2 a = 2'b01;
#2 a = 2'b10;
#2 a = 2'b11;
#2 $finish;
end

always #1 clk <= ~clk;

endmodule

使用iverilog进行编译和仿真会得到:

1
2
3
4
5
6
7
8
9
10
11
$ iverilog -o a.out test.v
$ vvp a.out
when a = 1'b00, c = ~ a = 4'b1111
when a = 1'b00, c = ! a = 4'b0001
when a = 1'b01, c = ~ a = 4'b1110
when a = 1'b01, c = ! a = 4'b0000
when a = 1'b10, c = ~ a = 4'b1101
when a = 1'b10, c = ! a = 4'b0000
when a = 1'b11, c = ~ a = 4'b1100
when a = 1'b11, c = ! a = 4'b0000
test3.v:23: $finish called at 8 (1s)

可以看出,~a!a在进行位扩展时十分抽象,编译时会先进行1扩展或者0扩展再进行赋值。因此当一个稍微复杂的表达式中出现了位宽的改变时,强烈建议用vector也就是花括号{,}手动进行扩展。

P.S.样例中在时序逻辑中进行阻塞赋值是不合规范的,请勿模仿。

4、位宽

下例来自舍友ZHX,这种事情一旦发生,debug可就困难了。大家可以试着找找下图中的错误。
位宽错误

Answer

不声明位宽时,默认1位。这与整数不同,后者默认32位。
Addr是32位的地址,但是声明成了1位。

5、关于循环

在P4以及后续的上机题中,可能会需要统计一个32位数中的1的个数,或者32位数中是否存在连续的4个1。这时需要使用循环写组合逻辑。比如前者可写为:

1
2
3
4
5
6
7
8
9
integer i;
reg [31: 0] sum;
wire [31: 0] target;
always @(*) begin
sum = 0
for (i=0; i<32; i=i+1) begin
sum = sum + target[i];
end
end

6、关于切片

对于wire类型或者reg类型变量,我们会经常用到[:]取出特定位宽,再利用{,}进行拼接。但是当我们想取出的位置随变量改变时,就会出现问题,比如:

1
2
3
4
5
6
7
module test(
);
//...
integer i = 1;
wire [3 : 0] c;
assign c = c[(i + 1):i]
//...

iverilog编译会报错。

1
test.v:6: error: Part select expressions must be constant.

个人猜测Verilog要求返回值的位宽必须是恒定的,因此要求用常数选择位宽。

解决方案有三。第一个是利用位移运算去挪它,再用常数选出来;第二个是用vector把每一位取出来并进行拼接,如assign c = {{c[i+1]}, {c[i]}};

第三个方案更简洁,利用System Verilog中的切片完成。比如上述例子可写成:

1
2
3
4
5
6
7
1     module test(
2 );
...
12 integer i = 1;
13 wire [3 : 0] c;
14 assign c = c[(i + 1) -: 2]
...

不要用[i+:2],这会返回[i:i+1]

7、关于阻塞和非阻塞

阻塞赋值和非阻塞赋值看似简单,但实际上在编译过程中又不是那么简单。个人建议将阻塞赋值和非阻塞赋值分开,即在always @(*)中进行阻塞赋值,在always @(posedge clk)中进行非阻塞赋值。请不要在阻塞赋值中掺杂非阻塞赋值。比如我的天才构思,实现了在时钟上升沿的瞬间利用上升沿后的数据对寄存器值进行更新:

1
2
3
4
5
6
7
8
9
always @(posedge clk) begin
flag = 0;
// process coming data...
flag = 1;
end

always @(posedge flag) begin
// do sth...
end

我不知道第一个语句块中是否真的读到了上升沿后的数据,因为仿真得到的全是浮空值。

非阻塞赋值是在利用上升沿之前的数据在上升沿对寄存器进行一次更新,所以请不要在同一个上升沿对同一个寄存器更新两次,即不要在同一次always中执行多次对同一个reg类型变量的非阻塞赋值操作。如果需要用上升沿之后的数据输出的话,可以写一个Mealy机。

待补充~

  • 标题: 「BUAA CO」Verilog易错点
  • 作者: Squirrel7ang
  • 创建于 : 2024-01-01 10:19:01
  • 更新于 : 2024-01-13 00:25:56
  • 链接: https://redefine.ohevan.com/2024/01/01/CO/verilog_error/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论