吃透Chisel语言.13.Chisel项目构建、运行和测试(五)——Chisel测试之波形测试(Waveform)和printf调试大法

Chisel项目构建、运行和测试(五)——Chisel测试之波形测试(Waveform)和printf调试大法

我们已经学习了ScalaTestChiselTest这两种测试框架,他们在小规模数字设计和单元测试(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"

不过上面的代码中,我们还是显式地枚举了ab的几种取值,这种一是很麻烦,而是扩展性不好,覆盖所有取值的情况会在位宽变大的时候指数增长。所以我们可以利用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``bout的值,而是它们在Scala中定义的类型,而且只输出了一次。如果尝试用toString()函数来强制转换,比如println(io.a.toString(), io.b.toString(), io.out.toString()),结果是一样的。

因此可以总结得到这样的结论,printf是和Chisel硬件模块交互执行的,会在每个时钟的上升沿输出信息,输出的值是端口的值,而println仅在实例化模块的时候输出信息,输出的信息是Scala中的Chisel对象的类型。

结语

这一篇文章讲了如何在Chisel中输出波形文件用于调试,提到了利用Scala语言的特性方便调试的方法,还介绍了可以在模块中执行的printf语句。这样一来,可能会用到的测试框架就都介绍完了,关于Chisel项目构建、运行和测试的内容就告一段落了。下一大部分将会介绍Chisel中的基本组件——Module,即模块,这是构建Chisel项目的基础,也是模块化、层级化数字设计的关键,敬请期待。

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值