Chisel教程——07.详解ChiselTest

详解ChiselTest

动机

Chisel团队给测试框架做了很多工作,ChiselTest提供了以下改进:

  1. 既可以进行单元测试也可以进行系统集成测试;
  2. 为可组合的抽象和分层设计;
  3. 高度可用,通过让单元测试更简单、更无痛(避免样板(boilerplate)和其他没意义的事情)和更有用来鼓励单元测试;

还有以下计划:

  1. 具备面向多种后端和模拟器的能力(如果测试向量不是静态的,或者使用有限的测试构建API子集,则可能需要在综合时链接到Scala);
  2. 将会包含在基础的Chisel3中,以避免封装和依赖带来的蛋疼;

基础的Tester实现

ChiselTestiotesters一样都是从基本操作开始讲起,下面是一个简要的总结,关于旧的iotesters和新的ChiselTest中的基本功能特性的对应关系:

iotestersChiselTest
pokepoke(c.io.in1, 6)c.io.in1.poke(6.U)
peekpeek(c.io.out1)c.io.out1.peek()
expectexpect(c.io.out1, 6)c.io.out1.expect(6.U)
stepstep(1)c.io.clock.step(1)
initiateDriver.execute(...) { c =>test(...) { c =>

下面还是先看之前写的一个简单的pass:

// 通过传入参数指定端口宽度的Chisel代码
class PassthroughGenerator(width: Int) extends Module { 
  val io = IO(new Bundle {
    val in = Input(UInt(width.W))
    val out = Output(UInt(width.W))
  })
  io.out := io.in
}

如果使用旧风格的测试,那么应该是这样的:

val testResult = Driver(() => new Passthrough()) {
  c => new PeekPokeTester(c) {
    poke(c.io.in, 0)     // Set our input to value 0
    expect(c.io.out, 0)  // Assert that the output correctly has 0
    poke(c.io.in, 1)     // Set our input to value 1
    expect(c.io.out, 1)  // Assert that the output correctly has 1
    poke(c.io.in, 2)     // Set our input to value 2
    expect(c.io.out, 2)  // Assert that the output correctly has 2
  }
}
assert(testResult)   // Scala Code: if testResult == false, will throw an error
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

现在用新风格写(方便起见,这里把测试放在主程序里):

import chisel3._
import chisel3.util._
import chisel3.experimental._
import chisel3.experimental.BundleLiterals._
import chisel3.tester._
import chisel3.tester.RawTester.test

class MyModule(width: Int) extends Module {
  val io = IO(new Bundle {
    val in  = Input(UInt(width.W))
    val out = Output(UInt(width.W))
  })

  io.out := io.in
}

object MyModule extends App {
  test(new MyModule(16)) { c =>
    c.io.in.poke(0.U)     // 输入设为0
    c.clock.step(1)    // 时钟步进
    c.io.out.expect(0.U)  // 输出应当为0
    c.io.in.poke(1.U)     // 输入设为1
    c.clock.step(1)    // 时钟步进
    c.io.out.expect(1.U)  // 输出应当为1
    c.io.in.poke(2.U)     // 输入设为2
    c.clock.step(1)    // 时钟步进
    c.io.out.expect(2.U)  // 输出应当为2
  }
}

这里测试通过,但是会提示一个警告:

在这里插入图片描述

那就在sbt run之前运行一句set scalacOptions += "-deprecation",想看看详情,结果直接就不显示警告了。

以上的例子中,需要注意以下几点。

ChiselTest的测试方法需要的样板更少,以前的PeekPokeTester已经内置到进程中了。

pokeexpect方法现在是每个单独的io元素的方法,这样可以给测试人员提供重要的提示,来更好地检查类型。peekstep操作现在已是io元素上的方法。

还有个区别是pokeexpect的值时Chisel字面值。虽然这里的例子很简单,但后面更多高级有趣的例子会展示更强大的检查功能。未来的改进中通过指定Bundle字面值的能力可以进一步增强这一点。

具备Decoupled(解耦合)接口的模块

这一部分会了解tester2中的一些Decoupled接口的工具。Decoupled接收一个Chisel数据类型并给他提供readyvalid信号。ChiselTest提供一些很棒的工具来自动化并可靠地测试这些接口。

一个队列的例子

QueueModule传递由ioType确定类型的数据。在QueueModule内部有entries状态元素,这意味着可以在推出数据之前容纳这么多元素。

case class QueueModule[T <: Data](ioType: T, entries: Int) extends MultiIOModule {
  val in = IO(Flipped(Decoupled(ioType)))
  val out = IO(Decoupled(ioType))
  out <> Queue(in, entries)
}

注意这里的case类修饰符一般是不需要的,例子中的是为了在Jupyter环境的多个单元格中复用。

enqueueNowexpectDequeueNow

ChiselTest有一些内置的方法来处理在IO中有解耦合接口的电路。这里展示怎么向queue中插入或从queue中提取值。

方法描述
enqueueNow添加(排队)一个元素到一个Decoupled输入接口
expectDequeueNow移出(出列)一个元素自一个Decoupled输出接口

注意,这里需要一些样板如initSourcesetSourceClock等,以此来确保readyvalid字段都在测试开始时正确初始化了。

import chisel3._
import chisel3.util._
import chisel3.experimental._
import chisel3.experimental.BundleLiterals._
import chisel3.tester._
import chisel3.tester.RawTester.test

case class QueueModule[T <: Data](ioType: T, entries: Int) extends MultiIOModule {
  val in = IO(Flipped(Decoupled(ioType)))
  val out = IO(Decoupled(ioType))
  out <> Queue(in, entries)
}

object MyModule extends App {
  test(QueueModule(UInt(9.W), entries = 200)) { c =>
    c.in.initSource()
    c.in.setSourceClock(c.clock)
    c.out.initSink()
    c.out.setSinkClock(c.clock)
    
    val testVector = Seq.tabulate(200){ i => i.U }

    testVector.zip(testVector).foreach { case (in, out) =>
      c.in.enqueueNow(in)
      c.out.expectDequeueNow(out)
    }
  }
}

教程这里还是有很多东西没讲清楚的,这里补充一下:

  1. Decoupled(gen: Data):给gen包装一个ready-valid协议,测试中就是给UInt(9.W)类型包装了readyvalid信号;

  2. Flipped[T <: Data](source:T)是把参数列表全都翻转过来,即输出变成输入,输入变成输出,比如:

    class MyModule[T <: Data](ioType: T) extends MultiIOModule {
      val in = IO(Flipped(Decoupled(ioType)))
      val out = IO(Decoupled(ioType))
      out <> in
    }
    
    object MyModule extends App {
      println(getVerilogString(new MyModule(UInt(9.W))))
    }
    

    输出的Verilog代码如下:

    module MyModule(
      input        clock,
      input        reset,
      output       in_ready,
      input        in_valid,
      input  [8:0] in_bits,
      input        out_ready,
      output       out_valid,
      output [8:0] out_bits
    );
      assign in_ready = out_ready; // @[MyModule.scala 11:7]
      assign out_valid = in_valid; // @[MyModule.scala 11:7]
      assign out_bits = in_bits; // @[MyModule.scala 11:7]
    endmodule
    

    可以看到in的三个接口分别为outputinputinput,与out是反的。从这里也可以看到,通过Decoupled包装的ready信号是输入,valid是输出。

  3. <>表示整体连接,这里就是把inout的三个端口分别连接起来,很方便;

  4. Queue(enq:DecoupleIO, entries:Int)是一个Chisel硬件模块,创建一个entries个元素的enq的队列;

enqueueSeqexpectDequeueSeq

现在介绍两个新方法来以单个操作完成排队和出列操作:

方法描述
enqueueSeq持续从一个Seq添加元素到Decoupled输入接口,一次一个,知道序列的元素用完了
expectDequeueSeqDecoupled输出接口移出元素,一次一个,并且和Seq的下一个元素进行比较

下面这个例子还行,但是就像写的那样,enqueueSeq必须在expectDequeueSeq 开始之前完成,如果testVector大于队列的深度,那么这个运行就会出问题,因为队列会被填满没法插入新的元素,可以试试失败是啥样的。

import chisel3._
import chisel3.util._
import chisel3.experimental._
import chisel3.experimental.BundleLiterals._
import chisel3.tester._
import chisel3.tester.RawTester.test

case class QueueModule[T <: Data](ioType: T, entries: Int) extends MultiIOModule {
  val in = IO(Flipped(Decoupled(ioType)))
  val out = IO(Decoupled(ioType))
  out <> Queue(in, entries)
}

object MyModule extends App {
  test(QueueModule(UInt(9.W), entries = 200)) { c =>
    c.in.initSource()
    c.in.setSourceClock(c.clock)
    c.out.initSink()
    c.out.setSinkClock(c.clock)
    
    val testVector = Seq.tabulate(100){ i => i.U }

    c.in.enqueueSeq(testVector)
    c.out.expectDequeueSeq(testVector)
  }
}

现在把entries改成100,队列改成200试试:

在这里插入图片描述

排队排不上,报了个超时错误,下一节会将怎么解决这个问题。

还有需要注意的是,enqueueNowenqueueSeqexpectDequeueNowexpectDequeueSeq这些刚看到的函数并不是ChiselTest中复杂的特殊情况逻辑,相反,他们是ChiselTest鼓励大家从ChiselTest原语中构建的,具体怎么使用这些方法可以看这里的定义:chiseltest/TestAdapters.scala at d199c5908828d0be5245f55fce8a872b2afb314e · ucb-bar/chiseltest · GitHub

ChiselTest中的forkjoin

这一部分将会介绍怎么同时运行一个单元测试的各个部分,因此首先要介绍两个testers2的新特性:

方法描述
fork发射一个并发的代码块,额外的forks(分支)可以通过.fork附加到前一个代码块的结尾来同时执行
join将多个相关的分支变成中

下面的例子有两个分支连在一起,然后join到一起。在第一个fork块中enqueueSeq会继续添加元素直到耗尽,第二个fork会在数据可用时,在每个时钟周期expectDequeueSeq

fork创建的线程以确定的顺序运行,主要根据代码指定的顺序执行,并且某些依赖于其他线程的容易出bug的操作会在运行时检查中禁止。

import chisel3._
import chisel3.util._
import chisel3.experimental._
import chisel3.experimental.BundleLiterals._
import chisel3.tester._
import chisel3.tester.RawTester.test

case class QueueModule[T <: Data](ioType: T, entries: Int) extends MultiIOModule {
  val in = IO(Flipped(Decoupled(ioType)))
  val out = IO(Decoupled(ioType))
  out <> Queue(in, entries)
}

object MyModule extends App {
  test(QueueModule(UInt(9.W), entries = 200)) { c =>
    c.in.initSource()
    c.in.setSourceClock(c.clock)
    c.out.initSink()
    c.out.setSinkClock(c.clock)
    
    val testVector = Seq.tabulate(200){ i => i.U }
    
    fork {
      c.in.enqueueSeq(testVector)
    }.fork {
      c.out.expectDequeueSeq(testVector)
    }.join()
  }
}

forkjoin实现GCD

这一部分用forkjoin方法实现GCD(Greatest Common Denominator,最大公约数)的测试。首先定义IO bundle,这里准备添加一点样板来允许使用Bundle字面值,希望可以支撑支持字面值的代码的自动生成。

// 输入bundle
class GcdInputBundle(val w: Int) extends Bundle {
  val value1 = UInt(w.W)
  val value2 = UInt(w.W)
}

// 输出bundle
class GcdOutputBundle(val w: Int) extends Bundle {
  val value1 = UInt(w.W)
  val value2 = UInt(w.W)
  val gcd    = UInt(w.W)
}

现在来看GCD的Decoupled版本,这里也可以使用Decoupled包装器来给两个bundle添加readyvalid信号。Flipped包装器接收一个会被默认创建为输出的Decoupled GcdInputBundle并将每个字段都转换为相反的方向(递归的),这点在前面的补充中提到过的。Decoupled的捆绑参数的数据元素放置在顶级字段bits中。

/**
  * 用辗转相减法计算GCD
  * 两个寄存器xy中,用大的数减去小的数,小的数和差再存入寄存器,重复此过程直到两个数的差为0
  * 此时寄存器x的值即为最大公约数
  * 返回一个包,包含两个输入值和他们的GCD
  */
class DecoupledGcd(width: Int) extends MultiIOModule {

  val input = IO(Flipped(Decoupled(new GcdInputBundle(width))))
  val output = IO(Decoupled(new GcdOutputBundle(width)))

  val xInitial    = Reg(UInt())
  val yInitial    = Reg(UInt())
  val x           = Reg(UInt())
  val y           = Reg(UInt())
  val busy        = RegInit(false.B)
  val resultValid = RegInit(false.B)

  input.ready := ! busy
  output.valid := resultValid
  output.bits := DontCare  // DontCare是一个单例对象,用于赋值给未驱动的端口或线网,防止编译器报错

  when(busy)  {
    // 保证在计算的时候始终是大数减去小数
    when(x > y) {
      x := x - y
    }.otherwise {
      y := y - x
    }
    when(y === 0.U) {
      // 当y值为0的时候结束计算
      // 如果output已经准备好了,那就把有效的数据发送到output
      output.bits.value1 := xInitial
      output.bits.value2 := yInitial
      output.bits.gcd := x
      resultValid := true.B
      busy := ! output.ready
    }
  }.otherwise {
    when(input.valid) {
      // 有效数据可用且没有正在进行的计算,获取新值并开始
      val bundle = input.deq()
      x := bundle.value1
      y := bundle.value2
      xInitial := bundle.value1
      yInitial := bundle.value2
      busy := true.B
      resultValid := false.B
    }
  }
}

现在这个测试看起来和前面的Queue差不多了,但是还有一些事情要做。因为计算需要多个周期,因此在计算每个GCD是,输入的排队过程会阻塞。不过好消息是这方面的测试和之前的Decoupled是一样简单且一致的。

这里还需要引入的是Chisel3中的Bundle字面值符号,看下面这一行:

new GcdInputBundle(16).Lit(_.value1 -> x.U, _.value2 -> y.U)

上面定义的GcdInputBundle有两个字段value1value2,我们通过先创建一个Bundle再调用它的.Lit()方法来创建Bundle字面值。这个方法接收一个键值对的变量参数列表,这里的键(key,这里是_.value1)是字段名,值(value,这里是x.U)是一个Chisel硬件字面值,Scala中的Intx被转换到Chisel中的UInt字面值,字段名前的_.是必要的,不然不知道是这个Bundle里面的。

这可能不是完美的符号,但是在广泛的开发讨论中,他被视为最小化样板代码和Scala中可用的符号限制之间的最贱平衡。

完整的代码如下:

import chisel3._
import chisel3.util._
import chisel3.experimental._
import chisel3.experimental.BundleLiterals._
import chisel3.tester._
import chisel3.tester.RawTester.test

class GcdInputBundle(val w: Int) extends Bundle {
  val value1 = UInt(w.W)
  val value2 = UInt(w.W)
}

class GcdOutputBundle(val w: Int) extends Bundle {
  val value1 = UInt(w.W)
  val value2 = UInt(w.W)
  val gcd    = UInt(w.W)
}

class DecoupledGcd(width: Int) extends MultiIOModule {

  val input = IO(Flipped(Decoupled(new GcdInputBundle(width))))
  val output = IO(Decoupled(new GcdOutputBundle(width)))

  val xInitial    = Reg(UInt())
  val yInitial    = Reg(UInt())
  val x           = Reg(UInt())
  val y           = Reg(UInt())
  val busy        = RegInit(false.B)
  val resultValid = RegInit(false.B)

  input.ready := ! busy
  output.valid := resultValid
  output.bits := DontCare

  when(busy)  {
    when(x > y) {
      x := x - y
    }.otherwise {
      y := y - x
    }
    when(y === 0.U) {
      output.bits.value1 := xInitial
      output.bits.value2 := yInitial
      output.bits.gcd := x
      resultValid := true.B
      busy := ! output.ready
    }
  }.otherwise {
    when(input.valid) {
      val bundle = input.deq()
      x := bundle.value1
      y := bundle.value2
      xInitial := bundle.value1
      yInitial := bundle.value2
      busy := true.B
      resultValid := false.B
    }
  }
}

object MyModule extends App {
  test(new DecoupledGcd(16)) { dut =>
    dut.input.initSource().setSourceClock(dut.clock)
    dut.output.initSink().setSinkClock(dut.clock)

    val testValues = for { x <- 1 to 10; y <- 1 to 10} yield (x, y)
    val inputSeq = testValues.map { case (x, y) =>
        (new GcdInputBundle(16)).Lit(_.value1 -> x.U, _.value2 -> y.U)
    }
    val resultSeq = testValues.map { case (x, y) =>
        new GcdOutputBundle(16).Lit(_.value1 -> x.U, _.value2 -> y.U, _.gcd -> BigInt(x).gcd(BigInt(y)).U)
    }

    fork {
        dut.input.enqueueSeq(inputSeq)
    }.fork {
        for (expected <- resultSeq) {
        dut.output.expectDequeue(expected)
        dut.clock.step(5) // 在接收到下一输出前等待几个周期来创建backpressure
        }
    }.join()
  }
}

注意以下几点:

  1. 这个test里面的dut和前面的c是一样的,代表被测试的对象,即DUT(Device Under Test,被测器件),起什么名字都行;

  2. 这里的初始化有两种写法:

    dut.input.initSource()
    dut.input.setSourceClock(dut.clock)
    
    dut.input.initSource().setSourceClock(dut.clock)
    

    本质上是一样的,都是先初始化然后再设置时钟;

  3. 简单来说,上面的过程就是先创建Scala的值,然后转换为Bundle字面值的序列,再作为输入或用于比对的输出。

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值