verilog(常见)错误总结(来自p1和推荐题)

陈子祥

前言

我在写P1之前,已经好久都没写过verilog代码了,这导致我在写P1的时候频繁出错,都是些非常小的错误,难受😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞,关键是这些小错还要一段时间检查,在考试的时候花时间在这些小错误上实在太不值得了.标题为什么要在常见两字打上括号,因为这仅是我在P1和推荐题做题时,遇到的我自认为可能是"常见"的小错误,仅代表我个人观点.这些错误是绝对不该犯的.写这篇文章的缘由,是想给大家分享这些错误和一些良好思维习惯并做警示作用.还有,就是提升一下自己在讨论区的活跃度😜

有关变量定义和使用问题

这种问题主要分为三大种:

  1. 未定义变量就直接使用
  2. 定义变量忘记限制位宽,直接进行赋值操作
  3. 在某些语句块中定义局部变量

在做P1和推荐题时,我手生疏到一道题同时犯前两个错误​🤣​​ 我在pre时,有思考过第三个问题,再此也提一提

未定义变量就直接使用

这是一类非常典型的问题,我在P1.Q2题目中,为了图方便,将功能选择信号ALUOp赋值给了o变量,我是直接这么写的

1
assign o = ALUOp;

这里的问题是未定义变量o便直接对其进行赋值,ise不会报错,所以一开始我没有注意这个问题.在verilog语言中,若是对未定义的变量进行连续赋值,会将其默认认定为wire型1位变量,在这道题中这么写很显然会出现错误.

可用的解决方案

使用如下代码

1
`default_nettype none

若对未定义的变量赋值,会直接进行报错.(一般我不会写这行代码,因为嫌麻烦,而且没必要,(总觉得自己不会犯这种低级失误)如果跟我有一样想法的,那你就要注意这个问题了)

定义变量忘记限制位宽,直接进行赋值操作

说来惭愧,还是P1.Q2,当我发现了第一个问题的时候,我是如下改的,又错了🤣​​

1
2
wire o;
assign o = ALUOp;

这里的问题是,不定义位宽,,位宽默认为1.在将位宽较大的变量赋给位宽较小的变量时,会将高位截断,只取低位,ise不会报错,所以一开始我又没有注意到这个问题.一般来讲,除非故意利用这一机制,否则最好是位宽相同的赋值.正确的代码应是:

1
2
wire [2:0] o;
assign o = ALUOp;

解决这一问题的最好方法,就是养成定义变量时,先想类型,再想位宽,再想标识符的思维习惯.

在某些语句块中定义局部变量

我忘记是在写pre的时候还是写hdlbits的时候,我有想过要在语句块中定义局部变量的问题,例如,要在always块中使用for循环语句,定义一个局部变量 integer i,但是发现会报错.下面用一个简单的例子说明问题(这个例子是构造出来的,虽然真正写代码时没必要这么写,但是能很好的说明问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module testpart(
input clk,
output reg out
);
always @(clk)
begin
integer i;
i=0;
if(clk)
out = i + 1;
else
out = i + 2;
end
endmodule

这段代码会报错,报错原因是

1
Declarations not allowed in unnamed block

声明语句是不允许在非命名块出现的

这里有一个叫做命名块的概念

命名块,顾名思义,就是一个具有名字的块.那么如何给一个语句块赋值呢,要用如下格式

顺序块

1
2
3
begin : block_name
statements
end

并行块

1
2
3
fork : block_name
statements
join

为什么需要一个命名块呢?

因为命名块是设计层次的一部分. 这一点非常重要,这意味着我们可以通过层次名引用进行访问,这也是为什么局部变量只能在命名块声明的原因之一.

回到testpart代码,我们只需要如此改动,就可以通过编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module testpart(
input clk,
output reg out
);
always @(clk)
begin : block
integer i;
i=0;
if(clk)
out = i + 1;
else
out = i + 2;
end
endmodule

算术右移

算术右移在左操作数一定加上 $signed() ,否则依旧补零

有关有符号数问题,若表达式复杂,不建议使用三目运算符,采用case语句. 如果非要使用三目运算符,将带有符号数的表达式赋给一个变量,用该变量替代三目表达式的对应位置.我才用的是后者(我使三目表达式使惯了)

常量与位拼接运算符

我在做P1.Q3时遇到了这个问题. 当EOp = 3时,需要的是低位补零功能,于是我是这样写的代码

1
assign ext = {imm, {16{0}}};

我发现仿真时,ext 一直是0.后来我注意到了ise报的警告,才意识到了问题.

位拼接运算符当中不允许有未指明位数的量,在上述代码,0的位数未指明,但ise不会报错,因为 常数未指明位数默认32位 , 这也解释了上述代码ext一直是0的原因,可以算出,等号右边的量总位数为16 + 16 * 32 = 528位, 赋值需要截取低位16位, 故ext始终为0. 正确的代码如下

1
assign ext = {imm,{16{1'b0}}};

这里强烈推荐,以后写常数时标号位数和进制,这是一个好习惯,当然你可以不采纳

阻塞运算符与非阻塞运算符

这是我在写P1_L1_vote_plus推荐题遇到了一个问题,这个我自认为是很容易错的问题,也不是简单的问题

正常来讲,只要在组合逻辑只使用阻塞运算符,在时序逻辑只使用非阻塞运算符就好了

但是在这道题中,我由于判断失误,误把一个本该放到组合逻辑的一部分放到了时序逻辑的一部分,引发了一系列问题.为了方便叙述,我将问题拆解一下,如下:

给定数组in[7:0]作为输入,out[3:0]作为输出,在每个时钟上升沿更新in中1的个数,作为输出

若误将计算1个数逻辑写到时序逻辑,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#reg inr;
#integer i;
...
always @(posedge clk)
begin
...
inr <= in;
out <= 0;
for (i = 0; i < 8; i = i + 1)
begin
if(in[i]) out<=out+1;
end
end

很多人很快能想到,此时的out仅是比我们需求的慢一个周期,事实真的如此吗?通过仿真,设out初值为0,我们发现每个clk上升沿到来时,out仅比上个周期加1,无论in为何非零值.

这里仔细分析一下便知道原因.

这里设in == 3;

首先,for循环赋值语句实际上只是一种大量重复语句的简化写法,在in == 3的前提下,所有的赋值语句是

1
2
3
4
5
begin
out<=0;
out<=out+1;
out<=out+1;
end

我们希望他们串行执行而不是并行执行,但上述代码是并行赋值,所以肯定出现问题了.这三个语句同时给一个out赋值,但右操作数又各不相同,出现了矛盾,似乎出现了竞争.根据实验,我们知道最终out被赋值out + 1;

其中的缘由涉及到了层次化事件队列的知识,这里就不详细说明相关知识了.我们用层次化事件队列解释上述现象

首先,上段代码显示有三个在顺序块非阻塞赋值,在动态事件队列中,按语句先后顺序依次计算<=号右边的值,然后在非阻塞事件队列按语句先后顺序更新out的值,最终out的值为out + 1

那本题该如何解决呢?

用组合逻辑写就好了,思想类似与有限状态机的输出逻辑部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#reg inr;
#integer i;
...
always @(posedge clk)
begin
...
inr <= in;
end

always @(*)
begin
out = 0;
for (i = 0; i < 8; i = i + 1)
begin
if(inr[i]) out = out + 1;
end
end

解决方案

首先,还是提前想好组合和时序选哪个的问题,然后记得调试仿真,因为这个问题不容易被发现.最后,学会使用层次化事件队列分析

结语

上述问题都是我在写P1和推荐题遇到的很基础的问题,希望大家引以为戒,不要再犯这些低级错误.另外,记得做题保持手感,这个真的很重要🤣祝大家P1课上顺利通过


林语 回复 陈子祥

同学,你的最后一部分让我学到了很多,但是有一个小问题image.png

这里的应该是赋值需要截取低32位


陈子祥 回复 林语

感谢指正

0%