跳转至

计算机组成原理实验 2 环境指南(非官方)

以下指南适合有 Linux/macOS 环境的同学使用。

笔者心态:对 Verilog 感到不适,并且想折腾一些花样。

实验需要的工具

  • Verilator
  • Chisel
  • GTKWave

安装 C/C++ 编译环境

实验工具安装需要

  • gcc
  • git
  • g++
  • make

通过以下命令可以安装

# Debian/Ubuntu
sudo apt install build-essential git device-tree-compiler
# Fedora
sudo dnf install build-essential git device-tree-compiler

如果配置环境过程中出现 "command not found." 可能是有依赖的工具没装,此时可以利用搜索引擎。

macOS 需要安装 Homebrew 作为包管理器,请查阅 Tuna Mirrors 的安装指南

安装 Verilator

使用包管理器安装:

# Debian/Ubuntu
sudo apt install verilator
# Fedora/CentOS
sudo dnf install verilator verilator-devel
# macOS
brew install verilator

但是有一些 Linux 的发行版,他会将包拆的比较奇怪,导致没法找到 verilated.mk 之类的情况发生。 亦或者是版本比较老,则可以使用编译安装:

# 先 cd 到一个文件夹中,推荐 cd 到 $HOME/Downloads
git clone https://github.com/verilator/verilator
cd verilator
autoconf # 生成 ./configure 文件
./configure --prefix=$HOME/app/verilator # 配置 makefile 和安装路径
make -j `nproc` # 多核编译
make install  # 这会安装到 $HOME/app/verilator 中
echo 'export verilator_ROOT=$HOME/app/verilator' >> .bashrc # 追加环境变量

如果想要其他支持,请查阅 Verilator 的安装文档

Verilator 在编译过程中,需要用到 GNU 方言,因此推荐使用 Linux。

使用 SDKMAN 安装 Scala/Java/sbt

SDKMAN 是一个用于管理 Java 相关开发环境的软件。可以通过下面这个命令安装:

curl -s "https://get.sdkman.io" | bash

安装过程中,注意看提示,会要求在安装完成后配置环境变量。

通过 SDKMAN 安装 Scala/Java/sbt:

sdk install java
sdk install scala 2.12.13
sdk install sbt

安装 GTKWave

# Debian/Ubuntu
sudo apt install gtkwave
# Fedora
sudo dnf install gtkwave

macOS 请看 附录:macOS-安装-GTKWave

一个 Chisel 与 Verilator 结合的例子

下面这个例子可以在 这里 找到。

创建 Chisel 项目

Chisel 是 Scala 的一个库并且目前为止并没有一个真正的 Chisel IDE。我们可以通过官方的 Chisel-template 创建我们的项目。

git clone https://github.com/chipsalliance/chisel-template.git <my-chisel-project>

编写 Chisel 代码

chisel-template 项目下天然的提供了一个 GCD 硬件 module。

/// src/main/scala/gcd/GCD.scala
package gcd

import Chisel3._
// _root_ disambiguates from package Chisel3.util.circt if user imports Chisel3.util._
import _root_.circt.stage.ChiselStage

/**
  * Compute GCD using subtraction method.
  * Subtracts the smaller from the larger until register y is zero.
  * value in register x is then the GCD
  */
class GCD extends Module {
  val io = IO(new Bundle {
    val value1        = Input(UInt(16.W))
    val value2        = Input(UInt(16.W))
    val loadingValues = Input(Bool())
    val outputGCD     = Output(UInt(16.W))
    val outputValid   = Output(Bool())
  })

  val x  = Reg(UInt())
  val y  = Reg(UInt())

  when(x > y) { x := x - y }
    .otherwise { y := y - x }

  when(io.loadingValues) {
    x := io.value1
    y := io.value2
  }

  io.outputGCD := x
  io.outputValid := y === 0.U
}

object GCD extends App {
  ChiselStage.emitSystemVerilogFile(
    new GCD,
    firtoolOpts = Array("-disable-all-randomization", "-strip-debug-info")
  )
}

上面这个 class GCD 继承了 Chisel 的 Module 抽象类。在这个类里面,我们描述了 GCD 模块的行为。等价于 SystemVerilog 中的 module GCD();object GCD 表示创建了一个名为 GCD 的单例,这个单例继承自 App,起到程序入口的作用,这里用于发射 SystemVerilog 代码。

然后我们通过在命令行中输入 sbt "runMain gcd.GCD" 来启动 SystemVerilog 代码的发射 。最后,我们就可以在项目的根目录下找到生成的 GCD.sv 文件。

// Generated by CIRCT firtool-1.62.0
module GCD(
  input         clock,
                reset,
  input  [15:0] io_value1,
                io_value2,
  input         io_loadingValues,
  output [15:0] io_outputGCD,
  output        io_outputValid
);

  reg [15:0] x;
  reg [15:0] y;
  always @(posedge clock) begin
    if (io_loadingValues) begin
      x <= io_value1;
      y <= io_value2;
    end
    else if (x > y)
      x <= x - y;
    else
      y <= y - x;
  end // always @(posedge)
  assign io_outputGCD = x;
  assign io_outputValid = y == 16'h0;
endmodule

使用 Verilator C++ 调试 Chisel 生成的 SystemVerilog

我们可以用 C++ 编写一个 golden model 作为对照。

// 一个正确的 gcd 实现
int gcd(uint16_t a, uint16_t b)
{
    while (b != 0) {
        int remainder = a % b;
        a = b;
        b = remainder;
    }
    return a;
}

下面我们进行了硬件模块的仿真。这里的 #include "VGCD.h" 是在使用 Verilator 将 SystemVerilog 代码编译成 C++ 类的过程中自动生成的头文件。这个头文件包含了编译后的 C++ 类,用于在仿真环境中模拟硬件行为。

#include "VGCD.h"
#include "verilated.h"
#include "verilated_vcd_c.h"

int main(int argc, char* argv[])
{
    Verilated::commandArgs(argc, argv);
    Verilated::traceEverOn(true); // 启用波形跟踪

    size_t fail_cnt = 0;
    size_t success_cnt = 0;

    auto dut = std::make_unique<VGCD>();
    VerilatedVcdC* vcd = new VerilatedVcdC();
    dut->trace(vcd, 99); // 设定跟踪级别
    vcd->open("gcd.vcd"); // 打开 VCD 文件

    // 重置设备
    dut->reset = 1;
    dut->clock = 0;
    for (int i = 0; i < 5; i++) {
        dut->clock = !dut->clock;
        dut->eval();
        vcd->dump(10 * i); // 记录时间点
    }
    dut->reset = 0;

    srand(time(NULL));
    uint16_t x = rand();
    uint16_t y = rand();

    // 主仿真循环
    for (int cycle = 0; cycle < 400; cycle++) {
        dut->io_loadingValues = (cycle == 5);
        dut->io_value1 = x;
        dut->io_value2 = y;

        dut->clock = 1;
        dut->eval();
        vcd->dump(10 * cycle + 5);

        dut->clock = 0;
        dut->eval();
        vcd->dump(10 * cycle + 10);

        dut->io_loadingValues = 0;
    }

    // 收集结果和清理
    uint16_t top_z = dut->io_outputGCD;

    dut->final();
    vcd->close(); // 关闭 VCD 文件

    if (uint16_t z = gcd(x, y); z == top_z) {
        std::cout << "success" << std::endl;
    } else {
        std::cout << "fail" << std::endl;
        std::cout << "x: " << (int)x << std::endl;
        std::cout << "y: " << (int)y << std::endl;
        std::cout << "gcd(x, y): " << (int)z << std::endl;
        std::cout << "dut(x, y): " << (int)top_z << std::endl;
    }

    return 0;
}

编译 C++ 和 SystemVerilog

verilator -Wall --cc --trace -Iobj_dir -Wno-UNUSEDSIGNAL GCD.sv --exe tb.cxx # 会生成 obj_dir 文件夹,这是将 SystemVerilog 编译成了 C++ 的 class
make -C obj_dir -f VGCD.mk

这样,我们就使用 Verilator 完成了 C++ 和 SystemVerilog 的编译。我们可以在 obj_dir 文件夹中找到 VGCD 可执行文件。通过在命令行中执行 ./obj_dir/VGCD,我们可以运行这个可执行文件。执行完成后,如果看到输出中显示 success 字样,这表示仿真成功。同时,在项目的根目录下会生成一个名为 gcd.vcd 的文件。我们可以使用 VSCode 的插件 wavetrace(付费) 来打开这个 .vcd 文件,以便查看波形数据。

详细的用法查阅 Verilator 的使用教程

使用 GTKWave 打开波形仿真文件

gtkwave gcd.vcd

使用 tcl 脚本

每次使用 GTKWave 调试波形图时,我们需要执行一系列重复的步骤:打开程序、选择信号、然后使用 append/insert 将信号添加到视图中。 这很不优雅。尤其是:当我们需要频繁的调试:打开 GTKWave -> 选中信号 -> a/i -> 看波形 -> 改代码 -> make run -> 打开 GTKWave -> 选中信号 -> a/i -> 看波形 -> ...

然而,我们可以通过自动化来简化这个流程。具体做法是编写一个 tcl 脚本,这样每次打开 GTKWave 时,脚本会自动将所有必要的信号添加到显示界面中。这样可以提高调试效率。

创建一个 load_all_waves.tcl 文件,写入以下内容,保存。

# load_all_waves.tcl
# Add all signals
set nfacs [ gtkwave::getNumFacs ]
set all_facs [list]
for {set i 0} {$i < $nfacs } {incr i} {
  set facname [ gtkwave::getFacName $i ]
  lappend all_facs "$facname"
}
set num_added [ gtkwave::addSignalsFromList $all_facs ]
puts "num signals added: $num_added"

# Zoom full
gtkwave::/Time/Zoom/Zoom_Full

# Print
# set dumpname [ gtkwave::getDumpFileName ]
# gtkwave::/File/Print_To_File PDF {Letter (8.5" x 11")} Minimal $dumpname.pdf

然后我们可以通过下面这个命令打开.tcl.vcd

# 可以通过 gtkwave -h 知道参数的含义:
# -S, --script=FILE. specify Tcl command script file for execution
# -f, --dump=FILE. specify dumpfile name
gtkwave -S load_all_waves.tcl -f gcd.vcd

附录

macOS 安装 GTKWave

macOS 下安装 GTKWave 是一件非常令人疑惑的事情。macOS 有着地狱一般的向下兼容问题。

GTKWave 并没有被 Homebrew 官方收录,但可以使用第三方仓库 randomplum/gtkwave 安装:

brew tap randomplum/gtkWave
brew install --HEAD randomplum/gtkwave/gtkwave

然而,此仓库中维护的 GTKWave 并不包含 TCL 脚本支持,无法使用上文提到的便利方法,因此并不推荐使用这种方式安装。

Nix 是 Mac 上不同于 Homebrew 的其他包管理器,可以用于安装带 TCL 脚本支持的 GTKWave:

Nix

Nix 是 Linux 发行版 NixOS 的包管理器,具有强大而可复现的环境管理功能; 它也可以脱离 NixOS, 单独作为一些其他 Linux distro 以及 macOS 的包管理器使用。

通过这行命令安装 Nix。安装过程中请注意看提示。

sh <(curl -L https://nixos.org/nix/install)

下面是通过 Nix 安装 GTKWave 的方法:

# i 表示 install
# A 表示 attr 这告诉 nix-env 通过它的属性名来选择软件包
nix-env -iA nixpkgs.gtkwave

实际上 Nix 不仅仅能起到包管理的作用,它几乎可以完成任何项目的完整环境复现。对 Nix 和 NixOS 感兴趣的同学可以额外阅读 Nix 中文指南

远程开发的 GTKWave 的问题

远端机器上通常没有桌面环境,当我们连接到远端机器开发时,肯定是不能指望直接在远端机器上执行 gtkwave <.vcd> 就能在本地弹出一个窗口出来的。 自然,我们可以为远端机器安装桌面环境和配置 X-Forward。 但倘若只是想用 GTKWave 查看波形,那么将波形文件以某种方式下载到本地,再使用本地的 GTKWave 打开查看显然是更合理的选择。 但每次重新运行仿真都要手动通过 VSCode 或 scp 下载波形文件确实令人烦躁,那么应该如何快捷地将远端的某个文件下载或同步到本地呢? 我们可以使用 SSHFS 或者基于 SSHFS 的其他软件(例如 CyberDuck, MountainDuck 等)将远端的目录直接挂载到本地, 每次本地打开挂载点内文件时自动从远端拉取文件内容,从而便利我们在本地使用 GTKWave 查看波形。

安装 SSHFS

Windows/Linux

# Windows
winget install -h -e --id "WinFsp.WinFsp"
# Ubuntu/Debian
sudo apt install sshfs
# Fedora/Centos
sudo dnf install sshfs

macOS 安装 SSHFS

macOS 安装 SSHFS 相对来说比较复杂,需要开启 macOS 的内核拓展。

可以在这里下载 macOS 下 SSHFS 的相关软件 macFUSE 和 SSHFS

使用 macFUSE 需要开启内核拓展,请看这个教程 Getting Started with macFUSE

使用 SSHFS

sshfs <user>@<ip>:<远端目录> <挂载点>

例如在笔者的电脑上是这样挂载的

sshfs wangfiox@192.168.1.1:/home/wangfiox/Documents/organ ~/Document/workspace/organ

我们可以通过下面这个命令验证

ll organ
# 输出结果如下
# total 48
# -rw-r--r--  1 wangfiox  staff     9B May 25 19:30 README.md
# drwxr-xr-x  1 wangfiox  staff    32B May 26 19:04 archive
# drwxr-xr-x  1 wangfiox  staff    16B May 26 18:49 hitsz
# drwxr-xr-x  1 wangfiox  staff   162B May 25 19:30 lab1
# drwxr-xr-x  1 wangfiox  staff   272B May 29 20:22 lab2
# drwxr-xr-x  1 wangfiox  staff    96B May 25 19:30 rars

Chisel(Scala) IDE 的选择

Chisel 只是 Scala 中的一个库。因此,只要 IDE 能支持 Scala,那么自然也是支持 Chisel 了。 但是一些 IDE 如 VSCode/JetBrains IDEA 会对 Chisel 语法有更加好的 highlight 支持。

经观察:对于稍微大一些的 Chisel 项目 (小学期级别), VSCode + Metals 会很卡,建议使用 JetBrain IDEA

VSCode 对 Chisel(Scala) 的支持

下面是笔者使用的 VSCode 插件

  • Chisel Syntax
  • Scala (Metals)
  • Scala Snippets
  • Scala Syntax (official)

JetBrains IDEA 对 Chisel(Scala) 的支持

下面是笔者使用的 JetBrain IDEA 插件

  • Scala