Verilog 最佳实践

TL;DR

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