前言
我在写P1之前,已经好久都没写过verilog代码了,这导致我在写P1的时候频繁出错,都是些非常小的错误,难受😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞😞,关键是这些小错还要一段时间检查,在考试的时候花时间在这些小错误上实在太不值得了.标题为什么要在常见两字打上括号,因为这仅是我在P1和推荐题做题时,遇到的我自认为可能是"常见"的小错误,仅代表我个人观点.这些错误是绝对不该犯的.写这篇文章的缘由,是想给大家分享这些错误和一些良好思维习惯并做警示作用.还有,就是提升一下自己在讨论区的活跃度😜
有关变量定义和使用问题
这种问题主要分为三大种:
- 未定义变量就直接使用
- 定义变量忘记限制位宽,直接进行赋值操作
- 在某些语句块中定义局部变量
在做P1和推荐题时,我手生疏到一道题同时犯前两个错误🤣 我在pre时,有思考过第三个问题,再此也提一提
未定义变量就直接使用
这是一类非常典型的问题,我在P1.Q2题目中,为了图方便,将功能选择信号ALUOp赋值给了o变量,我是直接这么写的
1 | assign o = ALUOp; |
这里的问题是未定义变量o便直接对其进行赋值,ise不会报错,所以一开始我没有注意这个问题.在verilog语言中,若是对未定义的变量进行连续赋值,会将其默认认定为wire型1位变量,在这道题中这么写很显然会出现错误.
可用的解决方案
使用如下代码
1 | `default_nettype none |
若对未定义的变量赋值,会直接进行报错.(一般我不会写这行代码,因为嫌麻烦,而且没必要,(总觉得自己不会犯这种低级失误)如果跟我有一样想法的,那你就要注意这个问题了)
定义变量忘记限制位宽,直接进行赋值操作
说来惭愧,还是P1.Q2,当我发现了第一个问题的时候,我是如下改的,又错了🤣
1 | wire o; |
这里的问题是,不定义位宽,,位宽默认为1.在将位宽较大的变量赋给位宽较小的变量时,会将高位截断,只取低位,ise不会报错,所以一开始我又没有注意到这个问题.一般来讲,除非故意利用这一机制,否则最好是位宽相同的赋值.正确的代码应是:
1 | wire [2:0] o; |
解决这一问题的最好方法,就是养成定义变量时,先想类型,再想位宽,再想标识符的思维习惯.
在某些语句块中定义局部变量
我忘记是在写pre的时候还是写hdlbits的时候,我有想过要在语句块中定义局部变量的问题,例如,要在always块中使用for循环语句,定义一个局部变量 integer i,但是发现会报错.下面用一个简单的例子说明问题(这个例子是构造出来的,虽然真正写代码时没必要这么写,但是能很好的说明问题)
1 | module testpart( |
这段代码会报错,报错原因是
1 | Declarations not allowed in unnamed block |
声明语句是不允许在非命名块出现的
这里有一个叫做命名块的概念
命名块,顾名思义,就是一个具有名字的块.那么如何给一个语句块赋值呢,要用如下格式
顺序块
1 | begin : block_name |
并行块
1 | fork : block_name |
为什么需要一个命名块呢?
因为命名块是设计层次的一部分. 这一点非常重要,这意味着我们可以通过层次名引用进行访问,这也是为什么局部变量只能在命名块声明的原因之一.
回到testpart代码,我们只需要如此改动,就可以通过编译
1 | module testpart( |
算术右移
算术右移在左操作数一定加上 $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 | #reg inr; |
很多人很快能想到,此时的out仅是比我们需求的慢一个周期,事实真的如此吗?通过仿真,设out初值为0,我们发现每个clk上升沿到来时,out仅比上个周期加1,无论in为何非零值.
这里仔细分析一下便知道原因.
这里设in == 3;
首先,for循环赋值语句实际上只是一种大量重复语句的简化写法,在in == 3的前提下,所有的赋值语句是
1 | begin |
我们希望他们串行执行而不是并行执行,但上述代码是并行赋值,所以肯定出现问题了.这三个语句同时给一个out赋值,但右操作数又各不相同,出现了矛盾,似乎出现了竞争.根据实验,我们知道最终out被赋值out + 1;
其中的缘由涉及到了层次化事件队列的知识,这里就不详细说明相关知识了.我们用层次化事件队列解释上述现象
首先,上段代码显示有三个在顺序块的非阻塞赋值,在动态事件队列中,按语句先后顺序依次计算<=号右边的值,然后在非阻塞事件队列按语句先后顺序更新out的值,最终out的值为out + 1
那本题该如何解决呢?
用组合逻辑写就好了,思想类似与有限状态机的输出逻辑部分
1 | #reg inr; |
解决方案
首先,还是提前想好组合和时序选哪个的问题,然后记得调试仿真,因为这个问题不容易被发现.最后,学会使用层次化事件队列分析
结语
上述问题都是我在写P1和推荐题遇到的很基础的问题,希望大家引以为戒,不要再犯这些低级错误.另外,记得做题保持手感,这个真的很重要🤣祝大家P1课上顺利通过