组合逻辑环与 UNOPT

上一节中的 SRLatch 是一个典型的组合逻辑环的例子。在 Vivado 中,SRLatch 可以综合和布线,但是布线阶段 Vivado 也会警告代码中的组合逻辑环:

因为组合逻辑环容易导致电路振荡,从而造成 “仿真通过但上板不过” 的情况。因此,请务必消除 Vivado 报告的组合逻辑环

Verilator 对于组合逻辑环通常报告 UNOPT 或者 UNOPTFLAT 警告,字面意思是不好优化,因为组合逻辑环需要多次迭代后才能得到最终的结果(收敛)。这两种警告的区别在于,一个是 Verilator 生成网表(flatten netlist)前报告的,一个是生成网表后报告的,因此这两种警告对我们而言没有区别。下文中两种警告都用 UNOPT 指代。

Verilator 虽然声称忽略这些警告不会影响仿真的正确性,只会影响仿真的性能。但我们的 CPU 中不应该有组合逻辑环。如果有,大概率是有地方写错了,导致仿真的结果和我们的预期不符。根据我们的经验,大家在写流水线的 hazard 部分时比较容易写出组合逻辑环。

对于 Verilator 报告的 UNOPT 警告,某些情况下不一定是真的组合逻辑环。这是因为,出于仿真性能上的考虑,Verilator 并不是按信号的每一位来单独计算的,通常会把很多信号放一起计算(比如都放在一个 uint32_t 内)。你可以简单地认为 Verilator 是在变量层级生成网表。下面将举几个常见的例子。

例子:StructWire

module StructWire (
    input  logic a,
    output logic d
);
    struct packed {
        logic b, c;
    } block;
    assign block.b = a;
    assign block.c = block.b;
    assign d = block.c;
endmodule

这段代码单纯地把 a 的值传给 d,中间经过了 block。很明显,ablock.bblock.cd 没有形成组合逻辑环,但是 Verilator 会把 block 这个变量看作一个整体,于是就出现了 blockblock 这个假的逻辑环。此时你只需使用 /* verilator split_var */ 来消除警告:

    struct packed {
        logic b, c;
    } block /* verilator split_var */;

split_var 告诉 Verilator 要把 block 拆成多个变量(可能是 block__DOT__ablock__DOT__b 之类的)。这样一来 Verilator 就不会报告 UNOPT 了。

注意:/* verilator split_var */ 需要 Verilator 的版本 ≥ 4.0301。如果你用的是 Ubuntu 20.04,那你有可能在使用 4.028。请按照课程仓库首页的指示安装较新版本的 Verilator。我们在测试的时候会使用较高版本的 Verilator。

例子:PartialSum

下面是一个异或前缀和的例子:

module PartialSum (
    input  logic [31:0] a,
    output logic [31:0] b
);
    assign b[0] = a[0];
    for (genvar i = 1; i < 32; i++) begin
        assign b[i] = b[i - 1] ^ a[i];
    end
endmodule

StructWire 一样,从每一位来看,不存在逻辑环。但是 Verilator 会认为有 bb 的组合逻辑环。所以这里也需要将 b 的每一位分开。但与 StructWire 例子中不一样的是,b 是模块的端口,如果你尝试:

module PartialSum (
    input  logic [31:0] a,
    output logic [31:0] b /* verilator split_var */
);

你会收到下面的警告:

%Warning-SPLITVAR: PartialSum-SPLITVAR.sv:3:25: 'b' has split_var metacomment but will not be split because it is public
    3 |     output logic [31:0] b /*verilator split_var*/
      |                         ^
                   ... Use "/* verilator lint_off SPLITVAR */" and lint_on around source to disable this message.
%Error: Exiting due to 1 warning(s)

因为 b 是公开的模块端口,所以 Verilator 拒绝直接将 b 分为多个变量。一个常用的技巧是引入一个私有的中间变量:

    logic [31:0] c /* verilator split_var */;  // 引入中间变量
    // 对 c 而不是 b 做操作
    assign c[0] = a[0];
    for (genvar i = 1; i < 32; i++) begin
        assign c[i] = c[i - 1] ^ a[i];
    end
    assign b = c;  // 再将 c 整体赋值给 b

例子:AlwaysComb

下面这个例子来自某位同学实验 1 的代码。我们对原始代码做了简化,大致的代码结构如下:

module AlwaysComb (
    input  logic a, d,
    output logic c, f
);
    always_comb begin
        // ...
        f = ~a;
        // ...
        c = d ^ f;
        // ...
    end
endmodule

module Top (
    input  logic a, b,
    output logic c
);
    logic d, f;
    assign d = b ^ f;
    AlwaysComb inst(.*);
endmodule

将上面的代码画成原理图,并没有发现逻辑环:

但是 Verilator 依然会报告 UNOPT。这是因为 Verilator 把 always_comb 语句块作为一个整体处理。Verilator 认为上面的 always_comb 块的输入(敏感列表)有 adf,输出有 cf。因此,当 a 发生变化时,触发 always_comb 块,从而 f 也会发生变化。然后 d 也发生变化。由于 dalways_comb 块的输入之一,导致 Verilator 认为形成了 fdf 的逻辑环。但是明显 d 的变化不会导致 f 变化。消除这个 UNOPT 的一种方法就是将 always_comb 一分为二:

    always_comb begin
        // ...
        f = ~a;
        // ...
    end
    always_comb begin
        // ...
        c = d ^ f;
        // ...
    end

事实上这种拆分无论是对 Verilator 还是对一般的事件驱动的仿真器都是有好处的,因为拆分后可以减少不必要的迭代,从而优化仿真性能。从这个例子中可以总结出一个经验:always 块应该避免给变量赋值后使用这个变量,也就是避免变量同时出现在 always 块的输入和输出中。同时我们也鼓励大家不要写过长的 always 块,而是尽量将其分为若干个独立的 always 块。

例子:Box

下面这段代码中有一个真的组合逻辑环,和一个假的组合逻辑环。请尝试用 verilator --lint-only [文件名] 找出这两个逻辑环。

typedef logic [31:0] i32;
typedef struct packed {
    logic valid, write_en;
    i32   addr, data;
} req_t;
typedef struct packed {
    logic data_ok;
    i32   data;
} resp_t;

module Box (
    input  logic  clk, resetn,
    input  req_t  req,
    output resp_t resp
);
    i32 stored;

    assign resp.data_ok = req.valid && req.addr == 32'h19260817;
    always_comb begin
        if (resp.data_ok)
            resp.data = stored;
        else
            resp.data = '0;
    end

    always_ff @(posedge clk)
    if (resetn) begin
        if (req.write_en)
            stored <= resp.data_ok ? req.data : stored;
    end else
        stored <= 32'hdeadbeef;
endmodule

module Fetch (
    input  logic  flush,
    output req_t  req,
    input  resp_t resp
);
    assign {req.valid, req.addr} = {!flush, 32'h19260817};
endmodule

module Core(input logic clk, resetn);
    logic  flush /* verilator public_flat_rd */;
    req_t  req   /* verilator public_flat_rd */;
    resp_t resp  /* verilator public_flat_rd */;
    assign flush = resp.data_ok && resp.data == '0;

    Fetch trump(.*);
    Box biden(.*);
endmodule