前言

Chisel是一种用于硬件设计的语言. 它是一种HDL(Hardware Description Language), 而非HLS(High-Level Synthesis)语言. Chisel在纯Verilog设计的基础上引入了更多面向对象的特性, 以及更强大的类型系统. 虽然Chisel基于Scala这种高级语言, 如果此前只接触过高级语言而没有接触过Verilog的话, 可能看起来觉得Chisel比Verilog更好上手. 但实际上并非如此, 仍然建议先有一定的Verilog基础再学习Chisel.

到目前为止, Chisel的工业上的应用范围仍然极窄, 主要集中在FPGA设计和RISC-V指令集芯片上. 大多数开发工具链都不原生支持Chisel(基本就没有), 需要将Chisel代码编译成Verilog/SystemVerilog代码后才能进行综合和实现. 所以直到现在都不能说学了Chisel就有很直接的作用, 现阶段它更像是一个玩具, 或者给你提供一些硬件的代码架构设计思路.

安装Scala

Chisel是一个Scala的编译器插件, 它依赖于Scala实现. 目前Chisel仍然只支持到Scala 2.12.x版本, Scala 3.x版本仍然不在支持范围内.

安装步骤参考Scala Install. 推荐在Linux上或者WSL上使用Scala(对于大多数非特定于Windows平台的开发来讲, Linux的开发体验都更好)

Scala要求Java开发环境, 需要先安装JDK. 推荐版本17或者21或更高.

sudo apt install openjdk-21-jdk
sudo pacman -S jdk21-openjdk # On Arch-based Distros

接着下载Scala官方的版本管理器(类似rustup, nvm).

curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz | gzip -d > cs && chmod +x cs && ./cs setup

以上命令会在当前目录中下载一个名为cs的可执行文件, 然后自动开始安装过程. 期间建议保持梯子连接(需要从Maven仓库下载依赖包). 安装完成后程序会提示将~/.local/share/coursier/bin添加到环境变量中, 记得确认添加.

cs会自动安装scalasbt包管理器, 安装完成后通过查看版本号确认安装成功.

scala --version
# Scala code runner version: 1.5.4
# Scala version (default): 3.6.4
sbt --version
# sbt runner version: 1.10.11

配置开发环境

VSCode上通过Metals插件提供Scala的语言服务器支持. 另外推荐安装Scala Syntax插件, 提供更好的语法高亮和代码补全.

安装好这两个插件后能自动识别当前工作区内的build.sbt文件, 并进行项目配置. 它会在检测到build.sbt文件时右下角弹窗提示是否导入工程("Import Build"), 选择导入工程即可. 然后它会自动下载build.sbt中指定的编译器插件, 依赖包等内容, 并完成项目配置.

但根据我的经验, Metals的开发体验并不是很好, 主要是它的语言服务器太容易崩了. 崩溃的情景包括不限于: 使用scalafmt格式化代码时整个语言服务器挂掉, 单个文件中语法错误过多时直接挂掉, 使用中毫无预兆突然挂掉. 而且对于低版本的Scala支持不好, 偏偏很多Chisel版本依赖相对较旧的Scala版本.

所以个人觉得最好的开发环境是Intellij IDEA, 然后在上面安装JetBrains的Scala插件. 这个插件同样能自动检测工作区的sbt配置并加载项目, 而且提供了稳定得多的语言服务器支持.

至于IDEA怎么配置就不多说了, 网上教程很多, 和CLion, GoLand这些JB家其它产品类似的. 学生认证可以免费用. 当然如果你说电脑内存不够, IDEA上来先吃2G内存顶不住, 那用VSCode也无妨, Metals挂掉的时候手动重启VSCode工作区就行了.

创建Chisel项目

截止本文撰写时, Chisel的最新版本已经到了6.7.0, 并且Chisel7已经有两个RC版本了. 但网上的大部分文档, 包括Chisel Bootcamp都没有更新, 还是基于Chisel 3.x版本的. 建议使用较新的Chisel版本, 因此本系列文章都会基于Chisel 6.7.0版本进行编写.

一个典型的Chisel项目结构如下:

  • chisel-project
    • .git
    • src
      • main
        • scala
          • adder
            • Adder.scala
      • test
        • scala
          • adder
            • AadderSpec.scala
    • .gitignore
    • build.sbt

略显麻烦, 但Java系的语言都是按照这个文件结构组织项目的, 也没什么办法. Adder.scala是Chisel的模块设计文件, 这个文件中的顶层模块名称应该和文件名一致. AdderSpec.scala是Chisel的测试文件, 对应的是Adder模块的测试. 不过仿真和波形一般不直接用Chisel提供的ChiselSim框架, 而采用Verilator进行仿真(至少我的习惯是这样的). Verilator在后面会提及, 它的仿真速度和功能要远胜ChiselSim.

build.sbt是Scala的构建工具sbt的配置文件, 类似cargo.toml. 如果要使用Git进行版本控制, .gitignore中至少应该包含以下目录

.idea/
.vscode/
.metals/
.bloop/
out/
target/
generated # Chisel编译出的Verilog文件位置

接下来创建一个新的Chisel项目, 首先配置sbt工具.

ThisBuild / scalaVersion := "2.13.16"
ThisBuild / version := "1.0.0"
ThisBuild / organization := "%ORGNIZATION%" // 组织名称

val chiselVersion = "6.7.0"
val scalatestVersion = "3.2.19"

lazy val root = (project in file("."))
	.settings(
		name := "chisel-project", // 项目名称
		libraryDependencies ++= Seq(
			"org.chipsalliance" %% "chisel" % chiselVersion,
			"org.scalatest" %% "scalatest" % scalatestVersion % "test",
		),
		scalacOptions ++= Seq(
			"-language:reflectiveCalls",
			"-deprecation",
			"-feature",
			"-Xcheckinit",
			"-Ymacro-annotations",
		),
		addCompilerPlugin("org.chipsalliance" %% "chisel-plugin" % chiselVersion cross CrossVersion.full),
	)

你也可以自己指定Chisel版本和Scala版本, 但要确保这两者版本匹配, 否则sbt没法从Maven上下载到正确的包. 要确定某个版本组合是否合理, 可以直接去Maven仓库看. 其中chisel-plugin是Chisel的编译器插件. 举个例子, 在chisel-plugin 2.13.16目录下只有6.7.0这一个版本, 表明Scala 2.13.16只能搭配Chisel 6.7.0使用. 如果没有特殊需求, 建议用最新的Chisel版本.

接下来在src/main/scala/adder目录下创建一个新的Scala文件, 例如Adder.scala, 内容如下:

package adder // 这里的包名和文件夹名要一致

import chisel3._
import circt.stage.ChiselStage._

class Adder extends RawModule {
	val io = IO(new Bundle {
		val a = Input(UInt(8.W))
		val b = Input(UInt(8.W))
		val sum = Output(UInt(8.W))
	})

	io.sum := io.a + io.b
}

object Main extends App {
	emitSystemVerilogFile(new Adder, args = Array("--target-dir", "generated"))
}

然后在src/test/scala/adder目录下创建一个新的Scala文件, 例如AdderSpec.scala, 内容如下:

package adder

import chisel3._
import chisel3.simulator.EphemeralSimulator._
import org.scalatest.flatspec.AnyFlatSpec

class AdderSpec extends AnyFlatSpec {
	behavior of "Adder"
	it should "add two numbers" in {
		val testCases = Seq(
			(1.U, 2.U, 3.U),
			(0.U, 0.U, 0.U),
			(255.U, 1.U, 0.U), // overflow test
		)

		simulate(new Adder) { dut => 
			testCases.foreach { case (a, b, sum) => 
				dut.io.a.poke(a)
				dut.io.b.poke(b)
				dut.io.sum.expect(sum)
				println(s"Test case: a = $a, b = $b, sum = ${dut.io.sum.peek().litValue}")
			}
			println("All test cases passed.")
		}
	}
}

关于这些代码的具体含义, 我们留到后面再讲. 现在只要知道为了将Chisel代码编译成Verilog代码, 需要在Main对象中调用emitSystemVerilogFile方法. 这个方法的第一个参数是要编译的Chisel对象, 第二个参数是编译选项, 这里我们指定了输出目录为generated.

接下来在终端输入sbt runMain命令, 会运行Main对象中的代码. 如果一切顺利, 会在generated目录下生成一个Adder.sv文件, 这个文件就是Chisel编译后的Verilog代码.

sbt runMain adder.Main # adder是包名, Main是对象名
# 或者
sbt run

为了运行测试, 可以输入sbt test命令, 这会运行src/test/scala/adder/AdderSpec.scala中的测试代码. 如果测试通过, 会输出代码中的通过提示.

sbt testOnly adder.AdderSpec
# 或者
sbt test

如果一切顺利, 现在你应该能理解Chisel的基本项目文件结构了. 接下来将正式开始介绍Chisel的基本语法和用法.

没有必要每次都重启`sbt`

sbt需要依赖JVM, 如果每次编译完Chisel/测试完都关闭sbt, 下一次再重新启动可能会浪费一些性能和时间在JVM上. 所以建议在终端中直接输入sbt进入sbt命令行界面, 每次要编译Chisel的时候输入run即可, 要测试的时候输入test即可. 如果你使用的是VSCode/IDEA提供的"构建"按钮, 它们就是每次构建/测试都要重启sbt的. 如果在意每次都重启JVM多花的时间, 手动在sbt命令行中输入run/test即可.

可选: 配置Verilator

Verilator是一个用来进行仿真, 功能验证和查看波形的开源工具. 最大的特点之一是它将Verilog代码转换为C++代码, 并且允许用C++写Testbench. 这样就让Testbench的扩展性非常强, 你可以轻松将硬件设计融入一整个Workflow中. 而且由于仿真程序是用C++编译出来的, 它的仿真速度非常快(对于一些复杂设计而言很有优势). 但缺点是对于各家私有IP核支持一般, 如果厂商提供了IP的Verilator C++库, 那仿真功能不会受到影响, 否则完全无法对设计中的IP核部分进行仿真. 不过Verilator还是一个很通用的工具, 不少公司内部也有在使用.

要使用Verilator, 需要先安装CMake和GCC编译器(也可以选择手写Makefile, 不推荐使用Clang系编译器)

sudo apt install build-essential cmake
sudo pacman -S --needed base-devel cmake # On Arch-based Distros

接着安装Verilator

sudo apt install verilator
sudo pacman -S verilator # On Arch-based Distros

Verilator可以对手写的Verilog代码进行静态检查(Lint), 有助于发现硬件设计中的潜在问题. 不过这里主要讲Chisel, 不会直接手写Verilog代码, 所以这里可能用不到这个功能, 只是顺带一提.

verilator --lint-only /path/to/verilog_file.v

Verilator仿真程序的编译流程通过CMake进行管理, 大概可以有这样的文件目录结构:

  • test
    • src
      • rtl
        • Adder.sv
      • tb
        • Adder_tb.cpp
    • CMakeLists.txt

其中Adder.sv是RTL设计文件, 然后在CMakeLists.txt中配置Verilator. 这是一个最简示例, 根据需要可以添加更多CMake选项.

cmake_minimum_required(VERSION 3.10)
project(AdderTestbench)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # for clangd

find_package(verilator HINTS $ENV{VERILATOR_ROOT} REQUIRED)

set(VERILOG_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/rtl/Adder.sv)
set(TB_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/tb/Adder_tb.cpp)

add_executable(tb ${TB_SRC})

verilate(tb
	TRACE # 打印波形
	TOP_MODULE Adder # 指定顶层模块名称
	SOURCES ${VERILOG_SRC}
	VERILATER_ARGS -O2
)

为了让CMake正确配置项目, 你需要先有一个Adder.sv文件, 否则CMake会报错找不到文件. 配置项目的过程中Verilator会为Verilog设计文件生成对应的头文件, 在Testbench中会用到.

一个Testbench文件通常可以有以下的结构:

#include "verilated.h"
#include "verilated_vcd_c.h"
#include "VAdder.h" // Verilator生成的头文件
#include <iostream>

#define MAX_SIM_TIME 1000
#define VTOP VAdder // 顶层模块名称
uint64_t sim_time = 0;

int main(int argc, char **argv, char **env) {
	Verilated::commandArgs(argc, argv);
	Verilated::traceEverOn(true); // 打开波形输出

	VTOP *top = new VTOP;
	VerilatedVcdC *tfp = new VerilatedVcdC;
	top->trace(tfp, 99); // 设置波形追踪层数
	tfp->open("waveform.vcd"); // 打开波形文件

	bool failed = false;
	while (sim_time < MAX_SIM_TIME) {
		// 向顶层模块输入信号, 例如
		// top->a = 1;
		// top->b = 1;
		top->eval(); // 评估顶层模块
		tfp->dump(sim_time); // 记录波形
		if (!/* 检查功能是否正确的条件, 比如 top->sum = 2 */) {
			std::cout << "Test failed at time " << sim_time << '\n';
			std::cout << "Expected: " << /* 预期值 */ << '\n';
			std::cout << "Got: " << /* 实际值 */ << '\n';	
			failed = true;
		}
	}

	top->final();
	tfp->close(); // 关闭波形文件
	// 清理资源
	delete top;
	delete tfp;
	// 通过返回值表明测试是否通过, 这在工具链中很有用
	return failed;
}

如果已经准备好了Adder.sv文件和Adder_tb.cpp文件, 那可以编译仿真程序了.

mkdir build && cd build
cmake ..
cmake --build . -j 0 # 0表示使用所有CPU核心进行编译
./tb # 运行仿真程序

然后应该能看到仿真程序的输出, 测试通过还是没通过. 然后在tb所在的目录下会生成一个waveform.vcd文件, 这个文件就是Verilator生成的波形文件. 可以用GTKWave等工具打开查看波形.

sudo apt install gtkwave
sudo pacman -S gtkwave # On Arch-based Distros
gtkwave waveform.vcd

到此应该大致清楚Verilator进行硬件设计仿真的流程了. 可以把Chisel和Verilator配合起来使用, 用Chisel生成Verilog文件, 再将Verilog文件添加到Verilator项目中进行仿真.

在GitHub上我提供了这一章用到的源代码文件Chisel-Bootcamp, 里面还添加了一些CMake自定义目标和一个顶层Makefile方便代码编译和运行. 你可以在Makefile所在目录中执行make compile编译Chisel代码, make sim对编译出的代码进行仿真, make show查看波形. 或者可以直接执行make编译Chisel代码并进行仿真. 希望这有助于你对这一节的整个工作流程(Workflow)的理解.