本文主要记录一些书写 CPU 时遇到的 Verilog 语法相关的问题以及解决经验。
阻塞赋值 vs 非阻塞赋值
在《自己动手写 CPU》一书中,作者使用以下方式在组合逻辑中赋值:
always @(*) begin if (rst == `RstEnable) begin a = 2'b00 end else if (<XXX CONDITION>) begin a = 2'b00 if (<XXX CONDITION>) begin a = 2'b01 end end // ... end
那么问题来了:为什么不使用
<=
?在这里使用 <=
会有什么区别?首先,
<=
代表非阻塞赋值而 =
代表阻塞赋值。他们的区别在于后者会顺序执行,而前者会在语句执行完毕后统一更新值。// 假设 a/b 为不同的值always @(*) begin b = a // c == a c = b end always @(*) begin b <= a // c == b c <= b end
再回到问题本身:两者没有区别,但是最佳实践是『总是在 always 中使用
<=
』。以上最佳实践来自于 Varilator 的 warning。我推测原因是因为阻塞赋值有可能在中途改变,从而导致编程者需要无时不刻不关注此时变量的值,从而导致编程的心智负担和出错率都大大增高。(另外在
always @(posedge clk)
情况下,使用 =
会有可能导致非预期的行为)always @(*) 与 assign 的区别
尽管两者综合出来的电路几乎没有区别。但从语法的角度,这两个语句分别从不同的角度在描述逻辑。
assign
很简单,就是将输入信号『扁平地』、『简单的』赋值给被赋值信号。而
always @(*)
的本身含义是『监视括号中给出的信号,一旦他们发生变化,就重新执行这个 block 中的语句』;同时 *
的含义是所有 RHS(Right Hand Side) 的变量都作为监视的变量。所以将以上两个调节结合来看后,生成的电路就等价于一个组合逻辑的电路。然而在实际使用上,两者各有各的好处。
因为不能使用
if-else block
,assign
在实现逻辑复杂的情况下,会写出很长的(一行)代码,很多时候看着有些头疼。然而 always @(*)
虽然解决了这个情况,但是却很容易写出无法下板的代码(比如含有锁存器),不得不说更让开发者头疼了。如何(避免)写出一个(组合电路的)锁存器
wire val; always @(*) begin if (rst == `RstEnable) val <= `Zero; else if (<XXX CONDITION>) val <= `VAL_1; else if (<YYY CONDITION>) val <= `VAL_2; else begin // notion here end end
在上面最后一个
else
中,我们可能希望在某些条件下这个值不要变化。但很可惜,这样就写出了一个锁存器。首先,由于锁存器是毛刺敏感的,如果不能保证sel信号的质量,那么会造成输出信号a的不稳定;其次,FPGA芯片中一般没有锁存器这样一个资源,需要使用一个触发器和一些逻辑门来实现,比较浪费资源;第三,锁存器的引入会对时序分析造成困难。
简单来说,这样的代码是无法下板运行的。
修改方法:如果你希望保存一个数据,那么一定需要用到时序逻辑的
always @(posedge clk)
。多个 always 中赋值同一个变量
wire [31:0] val; always @(*) begin if (rst == `RstEnable) val <= `Zero; else if (<XXX CONDITION>) val[1] <= `VAL_1; else if (<YYY CONDITION>) val[2] <= `VAL_2; else begin val[3] <= `VAL_3; end end
不要慌,这是一段正确的代码。那么如果我们在已经知道正确的逻辑下,故意(不小心)将代码分开,会怎样呢?
wire [31:0] val; always @(*) begin if (rst == `RstEnable) val <= `Zero; else if (<XXX CONDITION>) val[1] <= `VAL_1; else begin val[3] <= `VAL_3; end end always @(*) begin if (rst == `RstEnable) val <= `Zero; else if (<YYY CONDITION>) val[2] <= `VAL_2; else begin val[3] <= `VAL_3; end end
嗯,比赛的时候调试了很长时间,发现这样的代码还是无法正常下板。因此,请务必将同一个变量的赋值放在一个
always
中。不要省略声明!不要省略!
Verilog 的模块声明给了编程者很大的灵活性,比如以下几种写法都是合法的。
mod tmp( input wire clk, wire reset, flush, output wire[10:0] port1, wire port2, ) endmodule
是的,除了变量名不能(也无法)省略,其他的部分都可以省略。
但是这样的省略会带来非常严重的后果,比如以上定义中
port2
的长度应该是多少呢?或许你会以为它的长度是 1,但是 Verilator 会提示你,它的长度是
[10:0]
。所以最后只能被迫『显式大于隐式』🤔。
包括
1
也建议写成 1'b1
。