Chisel教程——07.详解ChiselTest-程序员宅基地

技术标签: fpga开发  Chisel速成班教程  scala  risc-v  Chisel  计算机体系结构  

详解ChiselTest

动机

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

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

还有以下计划:

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

基础的Tester实现

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

iotesters ChiselTest
poke poke(c.io.in1, 6) c.io.in1.poke(6.U)
peek peek(c.io.out1) c.io.out1.peek()
expect expect(c.io.out1, 6) c.io.out1.expect(6.U)
step step(1) c.io.clock.step(1)
initiate Driver.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输入接口,一次一个,知道序列的元素用完了
expectDequeueSeq Decoupled输出接口移出元素,一次一个,并且和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字面值的序列,再作为输入或用于比对的输出。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_43681766/article/details/123931579

智能推荐

matlab学习笔记-程序员宅基地

文章浏览阅读165次。加入水印,采用lsb%Name: Chris Shoemaker%Course: EER-280 - Digital Watermarking%Project: Least Significant Bit Substitution% Watermark Embedingclear all;% save start timestart_time=cputime;% read in the co..._[cover_object,map]=imread(file_name);

ipv6无网络访问权限可行解决方案_ipv6无internet访问权限怎么办-程序员宅基地

文章浏览阅读1.6w次。原文地址:http://www.xitongtiandi.net/wenzhang/win10/12654.htmlWin10专业版下ipv6无网络访问权限解决方案(只在win10专业版下做了测试,win7和win8.1待测试)1、首先打开 https://support.microsoft.com/en-us/kb/929852 选择Re-enable IPv6 相关的_ipv6无internet访问权限怎么办

【C程序设计】——程序=算法+数据结构_程序等于数据结构+算法-程序员宅基地

文章浏览阅读3.1k次,点赞32次,收藏37次。而且每次都要直接使用上一步骤的具体运算结果(如2,6,24等),也不方便,应当能找到一种通用的表示方法。由于数值运算往往有现成的模型,可以运用数值分析方法,因此对数值运算的算法的研究比较深入,算法比较成熟。从图中可以看出:“其他”这一部分,包括不能被4整除的年份,以及能被4整除,又能被100整除,但不能被400整除的那些年份(如1900年),它们都是非闰年。:若year能被4整除,不能被100整除,则输出year的值和“是闰年”。因此,上述算法不仅是正确的,而且是计算机能方便实现的较好的算法。_程序等于数据结构+算法

Keil : Error-Flash Download failed Cortex-M4错误解决方案整理(J-Flash擦除下载教程)_error: flash download failed - "cortex-m4-程序员宅基地

文章浏览阅读2.6w次,点赞18次,收藏88次。记录一下碰到的问题解决方法第一步:首先最先要确定的是芯片和设置是否对应!!!!!!!!!第二步:确定芯片和设置对应无误后,再考虑下面的方法Keil : Error-Flash Download failed Cortex-M4错误解决方案整理在开发 nRF51822/nRF52832/nRF52840时候出现如下如下问题:问题: Keil电子下载时候出现 Error: Flash Download failed - "Cortex-M4"的错误,如下图根据官方教程解释如下,还是发现不容易解决,另_error: flash download failed - "cortex-m4

SQL字符串分割成若干列-程序员宅基地

文章浏览阅读313次。在数据库编程中,很多朋友会碰到分割字符串的需求,一般都是分割成一列多行模式,但也有时会需要分割成多列一行的模式,下面我们来看下如何实现这种需求。首先创建一个辅助函数,来得到生成多列的SQL语句:create function toArray(@str nvarchar(1000),@sym nvarchar(10))returns nvarchar(3000)asbe..._sqlserver 字符串分割成多列 小提琴

配置MM32微控制器引脚复用功能_mm32f单片机的bootloader串行口引脚-程序员宅基地

文章浏览阅读1.3k次。配置MM32微控制器引脚复用功能文章目录配置MM32微控制器引脚复用功能IntroductionAlgorithmGPIOx_CR寄存器GPIOx_AFR寄存器GPIOx_CR & GPIOx_AFR寄存器TIMUARTSPI_MASTERSPI_SLAVEI2CCANADCFSMCQSPIDACCOMPSDIOUSBPraticeConclusionIntroduction使用过NXP(FSL)微控制器的开发者在配置引脚复用功能时,直接在PORT模块中,对应引脚的的PCR寄存器的MUX字段_mm32f单片机的bootloader串行口引脚

随便推点

存储过程学习总结-程序员宅基地

文章浏览阅读134次。[color=darkred][size=x-large]存储过程学习总结[/size][/color][size=medium]1、存储过程基本语法:[/size]create procedure sp_name()begin ......end;[size=medium]2、如何调用:[/size]call sp_name();[size=medium]..._存储过程repeatuse near 'end repeat; close curgra;

动态script加载数据-程序员宅基地

文章浏览阅读97次。Neil Fraser文章看得糊里糊涂,e文不是很好...一般我们加载数据会生成一个script标签,在onload事件里remove掉,或者在jsonp回调函数中remove掉script标签,取得数据这样其实script占用的内存并没有释放,必须:for (var prop in jsFile) { delete jsFile[prop];}ie下不能d..._script加载的数据在哪里

Javascript存放位置约束-程序员宅基地

文章浏览阅读77次。JavaScript 程序应该尽量放在 .js 的文件中,需要调用的时候在 HTML 中以 &lt;script src="filename.js"&gt; 的形式包含进来。JavaScript 代码若不是该 HTML 文件所专用的,则应尽量避免在 HTML 文件中直接编写 JavaScript 代码。因为这样会大大增加 HTML 文件的大小,无益于代码的压缩和缓存的使用。&lt;scr..._js里的下载路径怎么做约束

程序员真正的价值_程序员价值-程序员宅基地

文章浏览阅读604次。作者:池建强网址:macshuo.com微信:sagacity-mac问:池老师,我是个不爱互动的人,但是您所有的文章我都看了,非常感谢您的引导,我入手了人生第一台 MBP。现在问题来了,但是找不到更合适的人解答,只能求助于您了,如果您有时间的话。问题是这样的:我有个32bit unix file(开启一个服务进程),在 Mac 上执行时错误提示是:exec f_程序员价值

测试工作总体流程图-程序员宅基地

文章浏览阅读143次。测试工作总体流程图_测试的工作流程

推荐文章

热门文章

相关标签