Verilog 最佳实践

Verilog 最佳实践

Published
Published September 21, 2020
Updated
Last updated April 10, 2022
Description
Progress
Author
本文主要记录一些书写 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 blockassign 在实现逻辑复杂的情况下,会写出很长的(一行)代码,很多时候看着有些头疼。然而 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