本文主要记录一些书写 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
。