启动仿真(Boot a Simulation)

一、介绍(Introduction)

这是一个硬件定义+testbench的例子:

//你的硬件顶层
import spinal.core._
class TopLevel extends Component {
  ...
}

//你的测试顶层
import spinal.sim._
import spinal.core.sim._

object DutTests {
  def main(args: Array[String]): Unit = {
    SimConfig.withWave.compile(new TopLevel).doSim{ dut =>
      //仿真代码
    }
  }
}

二、配置(Configuration)

SimConfig会返回默认的仿真配置实例, 通过该实例能调用各种函数对仿真进行配置:

语句 描述
withWave 启用仿真波形捕获(默认模式)
withVcdWave 启用仿真波形捕获(VCD文本模式)
withFstWave 启用仿真波形捕获(二进制FST模式)
withConfig(SpinalConfig) 指定用来产生硬件的SpinalConfig
allOptimisation 启用所有的RTL编译优化来减少仿真时间(会增加编译时间)
workspacePath(path) 改变生成的sim文件的文件夹位置
withVerilator 用Verilator作为仿真后端(默认)
withGhdl 用withGhdl作为仿真后端
withIVerilog 用IVerilog作为仿真后端
withVCS 用VCS作为仿真后端

之后你可以调用compile(rtl)函数对硬件编译并为仿真器做准备。这个函数会返回SimCompiled实例。

在这个SimCompiled实例中你可以用以下函数运行你的仿真:

语句 描述
doSim[(simName[, seed])]{dut => ...} 一直运行仿真指导主线程完成(不等待分支线程)或直到所有线程卡住
doSimUntilVoid[(simName[, seed])]{dut => ...} 一直运行仿真知道所有线程完成或卡住

例如:

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

SimConfig
  .withConfig(spinalConfig)
  .withWave
  .allOptimisation
  .workspacePath("~/tmp")
  .compile(new TopLevel)
  .doSim { dut =>
    //仿真代码
}

注意默认情况下, 仿真文件会被替换成simWorkspace/xxx文件夹。你可以通过设置SPINALSIM_WROKSPACE环境变量重写simWorkspace的位置。

三、在同一硬件上运行多个测试用例(Running multiple tests on the same hardware)

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

compiled.doSim("testA") { dut =>
   //仿真代码
}

compiled.doSim("testB") { dut =>
   //仿真代码
}

四、从线程中报告仿真的成功或失败(Throw Success or Failure of the simulation from a thread)

在仿真的任何一个时刻你都可以调用simSuccesssimFailure来终止它。

如果仿真太大, 很可能会产生仿真失败, 例如因为testbench在等待从未发生的条件的判断。为此, 调用SimTimeout(maxDuration)其中maxDuration所仿真应该会发生失败的时间(以仿真时间为单位)。

例如, 在1000个时钟周期后终止仿真:

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

访问仿真信号(Accessing signals of the simulation)

一、读写信号(Read and write signals)

每个顶层的接口信号都可以通过Scala读写:

语句 描述
Bool.toBoolean 读出硬件Bool信号作为ScalaBoolean
Bits/UInt/SInt.toInt 读出硬件BitVector信号作为ScalaInt
Bits/UInt/SInt.toLong 读出硬件BitVector信号作为ScalaLong
Bits/UInt/SInt.toBigInt 读出硬件BitVector信号作为ScalaBigInt
SpinalEnumCraft.toEnum 读出硬件SpinalEnumCraft信号作为ScalaSpinalEnumElement
Bool #= Boolean 用ScalaBoolean值赋值给硬件Bool信号
Bits/UInt/SInt #= Int 用ScalaInt值赋值给硬件BitVector信号
Bits/UInt/SInt #= Long 用ScalaLong值赋值给硬件BitVector信号
Bits/UInt/SInt #= BigInt 用ScalaBigInt值赋值给硬件BitVector信号
SpinalEnumCraft #= SpinalEnumElement 用ScalaSpinalEnumElement值赋值给硬件SpinalEnumCraft信号
Data.randomize() 给SpinalHDL值赋随机值
dut.io.a #= 42
dut.io.a #= 42l
dut.io.a #= BigInt("101010", 2)
dut.io.a #= BigInt("0123456789ABCDEF", 16)
println(dut.io.b.toInt)

二、在模块层次访问信号(Accessing signals inside the component’s hierarchy)

为了访问在模块层次内部的信号, 你应该先把信号设置成simPublic

你可以直接在硬件描述中增加simPublic标签:

object SimAccessSubSignal {
  import spinal.core.sim._

  class TopLevel extends Component {
    val counter = Reg(UInt(8 bits)) init(0) simPublic() //这里给counter寄存器增加simPublic标签让其可被访问
    counter := counter + 1
  }

  def main(args: Array[String]) {
    SimConfig.compile(new TopLevel).doSim{dut =>
      dut.clockDomain.forkStimulus(10)

      for(i <- 0 to 3) {
        dut.clockDomain.waitSampling()
        println(dut.counter.toInt)
      }
    }
  }
}

或者你可以在完成对顶层例化后, 在仿真时增加标签

object SimAccessSubSignal {
  import spinal.core.sim._
  class TopLevel extends Component {
    val counter = Reg(UInt(8 bits)) init(0)
    counter := counter + 1
  }

  def main(args: Array[String]) {
    SimConfig.compile {
      val dut = new TopLevel
      dut.counter.simPublic()
      dut
    }.doSim{dut =>
      dut.clockDomain.forkStimulus(10)

      for(i <- 0 to 3) {
        dut.clockDomain.waitSampling()
        println(dut.counter.toInt)
      }
    }
  }
}