Chisel项目构建、运行和测试(五)——Chisel测试之波形测试(Waveform)和printf调试大法
我们已经学习了ScalaTest
和ChiselTest
这两种测试框架,他们在小规模数字设计和单元测试(Unit Testing)的时候很好用,就跟软件测试是一样的。单元测试集合也用于回归测试(Regression Testing)。但是为了调试更复杂的数字设计,我们需要一次性观测多个信号。数字设计中经典的调试方法就是把信号用波形(Waveform)的方法展现出来,波形中的信号会随着时间的推移显示。再者,用printf
输出信息来进行调试也是其他编程语言中常用的方法,Chisel中也是可以用printf
的,因为内容较少,前面也用到过,就放在这篇文章里一起讲了。那我们首先从波形调试开始。
波形(Waveform)
Chisel测试器可以生成包含所有寄存器和IO信号的波形。下面我们直接举例子,我们就给前面写的二位按位与写一个波形测试器。为了给测试生成波形,应该给测试传递定义writeVcd=1
到测试,比如对于之前的测试代码(一点都没有修改过):
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
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))
})
io.out := io.a & io.b
}
class SimpleTest extends AnyFlatSpec with ChiselScalatestTester {
"DUT" should "pass" in {
test(new DeviceUnderTest) { dut =>
dut.io.a.poke(0.U)
dut.io.b.poke(1.U)
dut.clock.step()
println("Result is: " + dut.io.out.peek().toString)
dut.io.a.poke(3.U)
dut.io.b.poke(2.U)
dut.clock.step()
println("Result is: " + dut.io.out.peek().toString)
}
}
}
用下面的命令就可以在默认的test_run_dir/DUT_should_pass
文件夹下生成DeviceUnderTest.vcd
文件:
sbt "testOnly SimpleTest -- -DwriteVcd=1"
那我们有了.vcd
文件,就可以用各种软件来看波形了,比如GTKWave或者ModelSim,在软件中打开.vcd
文件就行了。软件中可以选择我们要观察的信号进行整个时间维度上的观测,因为这不在本系列的讨论范围内,这里就不展开讲了。
我们也可以不用命令选项的方式生成波形文件,修改测试代码就行。只需要给test()
函数传递一个WriteVcdAnnotation
注解就行了。直接放代码:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
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))
})
io.out := io.a & io.b
}
class WaveformTest extends AnyFlatSpec with ChiselScalatestTester {
"Waveform" should "pass" in {
test(new DeviceUnderTest)
.withAnnotations(Seq(WriteVcdAnnotation)) { dut =>
dut.io.a.poke(0.U)
dut.io.b.poke(0.U)
dut.clock.step()
dut.io.a.poke(0.U)
dut.io.b.poke(1.U)
dut.clock.step()
dut.io.a.poke(1.U)
dut.io.b.poke(0.U)
dut.clock.step()
dut.io.a.poke(1.U)
dut.io.b.poke(1.U)
dut.clock.step()
}
}
}
可以看到,我们现在没有读取输出了,也没有用expect
把输出和预期值比较。我们把WriteVcdAnnotations
注解加入到了选项中,这样运行测试的时候就可以直接生成.vcd
波形文件了,只需要执行:
sbt "testOnly WaveformTest"
不过上面的代码中,我们还是显式地枚举了a
和b
的几种取值,这种一是很麻烦,而是扩展性不好,覆盖所有取值的情况会在位宽变大的时候指数增长。所以我们可以利用Scala代码来驱动DUT,比如循环。下面的测试器就通过嵌套的两层循环枚举了两个二位信号的所有可能的取值并进行测试,不仅覆盖了所有可能性,代码也变得很简洁:
import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec
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))
})
io.out := io.a & io.b
}
class WaveformTestWithIteration extends AnyFlatSpec with ChiselScalatestTester {
"WaveformIteration" should "pass" in {
test(new DeviceUnderTest)
.withAnnotations(Seq(WriteVcdAnnotation)) { dut =>
for (a <- 0 until 4) {
for (b <- 0 until 4) {
dut.io.a.poke(a.U)
dut.io.b.poke(b.U)
dut.clock.step()
}
}
}
}
}
运行下面的命令即可:
sbt "testOnly WaveformIteration"
printf
调试大法
用printf
输出关键信息是调试中另一种常见的手法,因为好用且简单,我们都亲切地称呼它为“printf
调试大法”。这种方法来源于C语言,就是简单地把感兴趣的变量通过printf
语句打印出来而已。Chisel电路的测试中也是可以用的,之前我们其实用过println
函数来在测试中输出信息,但这里我们说的是printf
,两者是有区别的。
区别在于println
是Scala内置的输出函数,是不可以在Chisel模块中执行的,而printf
是Chisel中定义的,可以在模块中执行。如果在Chisel模块的定义中使用了printf
语句,不管这条语句在什么地方都会执行,而且每次执行都在时钟的上升沿。使用方法就是简单地把printf
插到模块定义里就行:
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))
})
io.out := io.a & io.b
printf("dut: %d %d %d\n", io.a, io.b, io.out)
}
现在我们再运行WaveformIteration
测试,因为它会在每种可能的数值上迭代,所以会通过printf
输出每次的结果,进而可以验证我们模块的正确性。重新运行测试,结果如下:
而如果我们在模块中使用的是Scala中内置的println
,比如插入println(io.a, io.b, io.out)
这一行代码,那么输出是这样的:
(DeviceUnderTest.io.a: IO[UInt<2>],DeviceUnderTest.io.b: IO[UInt<2>],DeviceUnderTest.io.out: IO[UInt<2>])
可以看到,输出的并不是a``b
和out
的值,而是它们在Scala中定义的类型,而且只输出了一次。如果尝试用toString()
函数来强制转换,比如println(io.a.toString(), io.b.toString(), io.out.toString())
,结果是一样的。
因此可以总结得到这样的结论,printf
是和Chisel硬件模块交互执行的,会在每个时钟的上升沿输出信息,输出的值是端口的值,而println
仅在实例化模块的时候输出信息,输出的信息是Scala中的Chisel对象的类型。
结语
这一篇文章讲了如何在Chisel中输出波形文件用于调试,提到了利用Scala语言的特性方便调试的方法,还介绍了可以在模块中执行的printf
语句。这样一来,可能会用到的测试框架就都介绍完了,关于Chisel项目构建、运行和测试的内容就告一段落了。下一大部分将会介绍Chisel中的基本组件——Module,即模块,这是构建Chisel项目的基础,也是模块化、层级化数字设计的关键,敬请期待。