要开始学习更有趣的Chisel代码,我们首先需要学习如何编译Chisel程序,如何生成在FPGA中执行的Verilog代码,以及如何编写调试测试并验证我们的电路是正确的。
Chisel是用Scala编写的,因此任何支持Scala的构建过程都可以使用Chisel项目。Scala的一个流行的构建工具是sbt,它代表Scala交互式构建工具。除了驱动构建和测试过程之外,sbt还下载正确版本的Scala和Chisel库。
使用sbt创建项目
表示Chisel和Chisel测试程序的Scala库在构建过程中会自动从Maven存储库下载。这些库由build.sbt引用。可以使用latest.release配置build.sbt,以始终使用Chisel的最实际版本。但是,这意味着在每次构建时,都要从Maven存储库中查找版本。此查找需要Internet连接才能使生成成功。最好在build.sbt中使用专用版本的Chisel和所有其他Scala库。也许有时能够编写硬件代码并在没有互联网连接的情况下进行测试也是很好的。比如在飞机上做硬件设计很酷。
文件组织
sbt从Maven构建自动化工具继承了源代码约定。Maven还组织了开源Java库的存储库。
图3.1显示了一个典型Chisel项目的源代码树的组织。项目的根目录是项目主目录,其中包含build.sbt。它还可能包括构建过程的Makefile、自述文件和LICENSE文件。文件夹src包含所有源代码。从那里,它被划分为main(包含硬件源代码)和test(包含测试程序)。Chisel继承自Scala,而Scala本身继承自Java,即包中的源代码组织。包将Chisel代码组织到命名空间中。包还可以包含子包。文件夹目标包含类文件和其他生成的文件。我建议也使用一个文件夹来存放生成的Verilog文件,通常是调用generated。
要在Chisel中使用命名空间的功能,您需要声明一个类/模块是在一个包中定义的,在这个例子中是在mypack中:
package mypack
import chisel3._
class Abc extends Module{
val io = IO(new Bundle{})
}
注意,在这个例子中,我们看到导入chisel3包来使用Chisel类。
为了在不同的上下文中(包名称空间)使用模块Abc,需要导入mypack包的组件。下划线(_)充当通配符,意味着导入mypack的所有类。
import mypack._
class AbcUser extends Module{
val io =IO(new Bundle{})
val abc = new Abc()
}
也可以不从mypack中导入所有类型,而是使用完全限定名mypack.abc来引用包mypack中的模块Abc。
class AbcUser2 extends Module{
val io =IO(new Bundle{})
val abc = new mypack.Abc()
}
也可以只导入一个类并创建它的实例:
import mypack.Abc
class AbcUser3 extends Module {
val io = IO(new Bundle{})
val abc = new Abc()
}
Runing sbt
Chisel项目可以通过一个简单的sbt命令编译和执行:
$ sbt run
这个命令将从源代码树编译所有Chisel代码,并搜索包含一个包含main方法的对象的类,或者更简单的扩展App的类。如果有多个这样的对象,则会列出所有对象,并且可以选择一个。您也可以直接指定要作为sbt参数执行的对象:
$ sbt “runMain mypacket.MyObject”
默认情况下,sbt只搜索源代码树的主要部分,而不搜索测试部分。要执行基于ChiselTest和ScalaTest的测试,您只需使用:
$ sbt test
如果你有一个不遵循ChiselTest约定的测试,并且它包含一个main函数,但被放置在源代码树的测试部分,你可以用下面的sbt命令执行它:
$ sbt “test:runMain mypacket.MyMainTest”
生成verilog
为了综合FPGA或ASIC的Chisel代码,我们需要将Chisel翻译成综合工具可以理解的硬件描述语言。使用Chisel,我们可以生成电路的可合成Verilog描述。
要生成Verilog描述,我们需要一个应用程序。扩展App的Scala对象是一个应用程序,它隐式地生成应用程序启动时的main函数。该应用程序的唯一操作是创建一个新的Hello对象并将其传递给ChiselemitVerilog()函数。下面的代码将生成Verilog文件Hello.v
object Hello extends App {
emitVerilog(new Hello())
}
使用默认版本的emitVerilog()会将生成的文件放到我们项目的根文件夹中(我们在这里运行sbt命令)。要将生成的文件放到子文件夹中,我们需要指定emitVerilog()的选项。建议指定一个生成的文件夹,如图3. 1所示。构建选项可以设置为第二个参数,它是一个字符串数组,下面的代码将在生成的子文件夹中生成Verilog文件Hello.v。
object HelloOption extends App{
emitVerilog(new Hello(), Array("--target-dir","generated"))
}
您也可以将Verilog代码作为Scala String请求,而无需编写文件。您可以简单地打印出字符串进行测试。
object HelloString extends App{
val s = getVerilogString(new Hello())
println(s)
}
工具流程
图3.2显示了凿的工具流程。数字电路在示出为Hello.scala的Chisel类中描述。Scala编译器将这个类与Chisel和Scala库一起编译,并生成可以由标准Java虚拟机(JVM)执行的Java类Hello.class。使用Chisel驱动程序执行此类会生成所谓的RTL灵活中间表示(FIRRTL),这是数字电路的中间表示。在我们的示例中,文件是Hello.fir。FIRRTL编译器对电路执行转换。
Treadle是一个FIRRTL解释器,用于模拟电路。它与Chisel Tester一起可用于Chisel电路的调试和测试。通过断言,我们可以提供测试结果。Treadle还可以生成波形文件(Hello.vcd),可以用波形查看器(例如,免费查看器GTKWave或Modelsim)。
一个FIRRTL转换,Verilog发射器,生成用于合成的Verilog代码(Hello.v)。电路合成工具(例如,Intel Quartus、AMD/Xilinx Vivado或ASIC工具)合成电路。在FPGA设计流程中,工具生成用于配置FPGA的FPGA比特流,例如,Hello.bit.
Chisel Testing
硬件设计的测试通常称为测试台。测试台实例化被测设计(design under test,DUT),驱动输入端口,观察输出端口,并将它们与期望值进行比较。
Chisel的一个优点是它可以使用Scala的全部功能来编写这些测试台。例如,可以在软件模拟器中对硬件的预期功能进行编码,并将硬件的模拟与软件模拟进行比较。在测试处理器的实现时,这种方法非常有效
ChiselTest
sbt test执行所有可用的测试,这对回归测试很有用。但是,如果您只想运行单个测试(套件),则可以使用以下命令执行:
$ sbt “testOnly ExampleTest”
测试电路包含(至少)两个组件:被测器件(通常称为the device under test, DUT)和测试逻辑,也称为test benth。测试使用sbt test。不需要带有main函数的对象。
下面的代码显示了我们的简单设计测试。它包含两个输入端口(2位宽度)和两个输出端口,一个2位宽度和一个Bool。该电路对其输入a和b进行逐位AND运算,并将结果输出到out,并测试两个信号是否相等:
class DeviceUnderTest extends Module{
val io = IO(new Bundle {
val a =Input(UInt(2.W))
val b =Input(UInt(2.W))
val out = Output(UInt(2.W))
val equ = Output(Bool())
})
io.out := io.a & io.b
io.equ := io.a === io.b
在Chisel里,所有对象都应该由val类型的变量来引用,因为硬件电路的不可变性。因此,一个变量一旦初始化时绑定了一个对象,就不能再发生更改。但是,引用的对象很可能需要被重新赋值。例如,输出端口在定义时使用了“=”与端口变量名进行了绑定,那等到驱动该端口时,就需要通过变量名来进行赋值操作,更新数据。很显然,此时“=”已经不可用了,因为变量在声明的时候不是var类型。即使是var类型,这也只是让变量引用新的对象,而不是直接更新原来的可变对象。
为了解决这个问题,几乎所有的Chisel类都定义了方法“:=”,作为等号赋值的代替。所以首次创建变量时用等号初始化,如果变量引用的对象不能立即确定状态或本身就是可变对象,则在后续更新状态时应该用“:=”。从前面讲的操作符优先级来判断,该操作符以等号结尾,而且不是四种逻辑比较符号之一,所以优先级与等号一致,是最低的。
DUT的输入和输出端口通过dut.io访问。您可以通过端口上的poke设置值,该poke将输入端口的Chisel类型的值作为参数。输出端口可以通过在端口上调用peek()来读取,它将返回Chisel类型的值。toString方法将该值转换为字符串。测试仪使用dut.clock.step()将模拟提前一个时钟周期。为了将模拟提前几个时钟周期,我们可以为step()提供一个参数。我们可以使用println()打印输出的值。
除了手动检查打印输出(这是一个很好的起点)之外,我们还可以通过在输出端口上调用expect(value)并将期望值作为参数来表达测试台本身的期望。
波形
第一种方法:要生成测试波形,请将writeVcd=1的定义传递给测试,如以下sbt命令所示:
sbt “testOnly SimpleTest – -DwriteVcd=1”
第二种方法:使用 WriteVcdAnnotation()具体方法:
test(new DeviceUnderTest).withAnnotations(Seq(WriteVcdAnnotation))