启动仿真器

简介

下面是一个硬件定义+测试平台的示例:

import spinal.core._

// Identity takes n bits in a and gives them back in z
class Identity(n: Int) extends Component {
  val io = new Bundle {
    val a = in Bits(n bits)
    val z = out Bits(n bits)
  }

  io.z := io.a
}
import spinal.core.sim._

object TestIdentity extends App {
  // Use the component with n = 3 bits as "dut" (device under test)
  SimConfig.withWave.compile(new Identity(3)).doSim{ dut =>
    // For each number from 3'b000 to 3'b111 included
    for (a <- 0 to 7) {
      // Apply input
      dut.io.a #= a
      // Wait for a simulation time unit
      sleep(1)
      // Read output
      val z = dut.io.z.toInt
      // Check result
      assert(z == a, s"Got $z, expected $a")
    }
  }
}

配置

SimConfig 将返回一个默认的仿真配置实例,您可以在该实例上调用多个函数来配置您的仿真过程:

语法

描述

withWave

打开仿真波形捕获与存储(默认格式)

withVcdWave

打开仿真波形捕获与存储(VCD格式)

withFstWave

打开仿真波形捕获与存储(FST 二进制格式)

withConfig(SpinalConfig)

指定用于生成硬件的 SpinalConfig

allOptimisation

启用所有 RTL 编译优化以减少仿真时间(会增加编译时间)

workspacePath(path)

更改生成的仿真文件存放的文件夹

withVerilator

使用 Verilator 作为后台仿真器(默认)

withGhdl

使用GHDL作为后台仿真器

withIVerilog

使用 Icarus Verilog 作为后台仿真器

withVCS

使用Synopsys VCS作为后台仿真器

然后你可以调用 compile(rtl) 函数来编译硬件并预热仿真器。该函数将返回一个 SimCompiled 实例。

在此 SimCompiled 实例上,您可以使用以下函数进行仿真:

doSim[(simName[, seed])]{dut => /* main stimulus code */}

运行仿真,直到主线程运行完成并退出/返回。如果仿真完全卡住,它将检测并报告错误。只要例如时钟正在运行,仿真过程可以永远持续下去,因此建议使用 SimTimeout(cycles) 来限制可能的运行时间。

doSimUntilVoid[(simName[, seed])]{dut => ...}

运行仿真,直到通过调用 simSuccess()simFailure() 结束。主激励线程可以继续或提前退出。只要有事件需要处理,仿真就会继续。如果完全卡住,仿真器将报告错误。

以下测试平台模板将使用以下顶层设计:

class TopLevel extends Component {
   val counter = out(Reg(UInt(8 bits)) init (0))
   counter := counter + 1
}

这是一个包含多个仿真配置的模板:

val spinalConfig = SpinalConfig(defaultClockDomainFrequency = FixedFrequency(10 MHz))

SimConfig
  .withConfig(spinalConfig)
  .withWave
  .allOptimisation
  .workspacePath("~/tmp")
  .compile(new TopLevel)
  .doSim { dut =>
    SimTimeout(1000)
    // Simulation code here
}

这是一个模板,其中仿真过程在主仿真线程执行完成后结束:

SimConfig.compile(new TopLevel).doSim { dut =>
  SimTimeout(1000)
  dut.clockDomain.forkStimulus(10)
  dut.clockDomain.waitSamplingWhere(dut.counter.toInt == 20)
  println("done")
}

这是一个模板,其中仿真过程通过显式调用 simSuccess() 结束:

SimConfig.compile(new TopLevel).doSimUntilVoid{ dut =>
  SimTimeout(1000)
  dut.clockDomain.forkStimulus(10)
  fork {
    dut.clockDomain.waitSamplingWhere(dut.counter.toInt == 20)
    println("done")
    simSuccess()
  }
}

注意它是否等同于:

SimConfig.compile(new TopLevel).doSim{ dut =>
  SimTimeout(1000)
  dut.clockDomain.forkStimulus(10)
  fork {
    dut.clockDomain.waitSamplingWhere(dut.counter.toInt == 20)
    println("done")
    simSuccess()
  }
  simThread.suspend() // Avoid the "doSim" completion
}

默认情况下,仿真文件应存放在 $WORKSPACE/$COMPILED 指定的目录中。

  • $WORKSPACE 的默认值是 simWorkspace,但你可以利用 SPINALSIM_WORKSPACE 环境变量来指定一个不同的工作空间路径。

  • $COMPILED 变量代表的是当前进行仿真的顶层模块的名称。

  • 波形文件的存放路径会根据所选用的后端工具有所变化。在 Verilator 的情况下,它通常会被保存在默认的路径 $WORKSPACE/$COMPILED/$TEST 下。

  • 对于 Verilator 后端,可以使用 SimConfig.setTestPath(path) 函数设置测试文件夹位置。

  • 在进行仿真时,要确定测试文件夹的当前路径,可以调用 currentTestPath() 方法来获取。

在同一硬件上运行多个测试

val compiled = SimConfig.withWave.compile(new Dut)

compiled.doSim("testA") { dut =>
   // Simulation code here
}

compiled.doSim("testB") { dut =>
   // Simulation code here
}

从线程中抛出仿真成功或失败结果

在仿真过程中的任何时刻,您都可以调用 simSuccesssimFailure 来结束它。

当仿真时间太长时,可能会导致仿真失败,例如,因为测试平台正在等待从未发生的条件。为此,请调用 SimTimeout(maxDuration) 函数设置超时时间,其中 maxDuration 是仿真应被视为失败的时间(以仿真时间单位表示)。

例如,要使仿真在持续 1000 个时钟周期后失败:

val period = 10
dut.clockDomain.forkStimulus(period)
SimTimeout(1000 * period)

在失败之前捕获给定时间窗内的波形

如果您有一个很长时间的仿真,并且您不想捕获所有波形(太多错误,太慢),那么您主要有两种方法可以做到这一点。

要么您已经知道模拟在哪个 simTime 失败,在这种情况下您可以在测试平台中执行以下操作:

disableSimWave()
delayed(timeFromWhichIWantToCapture)(enableSimWave())

或者,您可以运行双步锁仿真,其中一个仿真的运行比另一个仿真的运行稍有延迟,一旦领先的模拟出现故障,它将开始记录波形。

为此,您可以使用 DualSimTracer 实用程序,其中包含已编译硬件的参数、故障前要捕获的时间窗大小以及随机种子。

这是一个例子:

package spinaldoc.libraries.sim

import spinal.core._
import spinal.core.sim._
import spinal.lib.misc.test.DualSimTracer

class Toplevel extends Component{
  val counter = out(Reg(UInt(16 bits))) init(0)
  counter := counter + 1
}

object Example extends App {
  val compiled = SimConfig.withFstWave.compile(new Toplevel())

  DualSimTracer(compiled, window = 10000, seed = 42){dut=>
    dut.clockDomain.forkStimulus(10)
    dut.clockDomain.onSamplings{
      val value = dut.counter.toInt

      if(value % 0x1000 == 0){
        println(f"Value=0x$value%x at ${simTime()}")
      }

      // Throw a simulation failure after 64K cycles
      if(value == 0xFFFF){
        simFailure()
      }
    }
  }
}

这将生成以下文件结构:

  • simWorkspace/Toplevel/explorer/stdout.log : stdout of the simulation which is ahead

  • simWorkspace/Toplevel/tracer/stdout.log : stdout of the simulation doing the wave tracing

  • simWorkspace/Toplevel/tracer.fst : Waveform of the failure

scala 终端将显示仿真结果到标准输出。