形式化验证

介绍

SpinalHDL 允许生成 SystemVerilog 断言 (SVA) 的子集。主要是断言(assert)、假设(assume)、覆盖(cover)和其他一些内容。

此外,它还提供了形式化验证后端,允许直接在开源 Symbi-Yosys 工具链中运行形式化验证。

形式化验证后端

您可以通过以下方式运行组件的形式化验证:

import spinal.core.formal._
FormalConfig.withBMC(15).doVerify(new Component {
    // Toplevel to verify
})

目前支持3种模式:

  • withBMC(depth)

  • withProve(depth)

  • withCover(depth)

安装要求

要安装 Symbi-Yosys,您有几种选择。您可以在以下位置获取预编译包:

或者你可以从头开始编译:

示例

外部断言

这是一个简单计数器和相应的形式化测试代码的示例。

import spinal.core._

//Here is our DUT
class LimitedCounter extends Component {
  //The value register will always be between [2:10]
  val value = Reg(UInt(4 bits)) init(2)
  when(value < 10) {
    value := value + 1
  }
}

object LimitedCounterFormal extends App {
  // import utilities to run the formal verification, but also some utilities to describe formal stuff
  import spinal.core.formal._

  // Here we run a formal verification which will explore the state space up to 15 cycles to find an assertion failure
  FormalConfig.withBMC(15).doVerify(new Component {
    // Instantiate our LimitedCounter DUT as a FormalDut, which ensure that all the outputs of the dut are:
    // - directly and indirectly driven (no latch / no floating wire)
    // - allows the current toplevel to read every signal across the hierarchy
    val dut = FormalDut(new LimitedCounter())

    // Ensure that the state space start with a proper reset
    assumeInitial(ClockDomain.current.isResetActive)

    // Check a few things
    assert(dut.value >= 2)
    assert(dut.value <= 10)
  })
}

内部断言

如果您愿意,可以将形式化验证语句直接嵌入到 DUT 中:

class LimitedCounterEmbedded extends Component {
  val value = Reg(UInt(4 bits)) init(2)
  when(value < 10) {
    value := value + 1
  }

  // That code block will not be in the SpinalVerilog netlist by default. (would need to enable SpinalConfig().includeFormal. ...
  GenerationFlags.formal {
    assert(value >= 2)
    assert(value <= 10)
  }
}

object LimitedCounterEmbeddedFormal extends App {
  import spinal.core.formal._

  FormalConfig.withBMC(15).doVerify(new Component {
    val dut = FormalDut(new LimitedCounterEmbedded())
    assumeInitial(ClockDomain.current.isResetActive)
  })
}

外部激励

如果您的 DUT 有输入,您需要从测试代码中驱动它们。您可以使用所有常规硬件语句来执行此操作,但您也可以使用形式化验证中的 anyseqanyconstallseqallconst 语句:

class LimitedCounterInc extends Component {
  //Only increment the value when the inc input is set
  val inc = in Bool()
  val value = Reg(UInt(4 bits)) init(2)
  when(inc && value < 10) {
    value := value + 1
  }
}

object LimitedCounterIncFormal extends App {
  import spinal.core.formal._

  FormalConfig.withBMC(15).doVerify(new Component {
    val dut = FormalDut(new LimitedCounterInc())
    assumeInitial(ClockDomain.current.isResetActive)
    assert(dut.value >= 2)
    assert(dut.value <= 10)

    // Drive dut.inc with random values
    anyseq(dut.inc)
  })
}

更多关于断言/past(以前某个时钟内的状态)的例子

例如,我们可以检查该值是否在正向计数(如果尚未达到 10):

FormalConfig.withBMC(15).doVerify(new Component {
  val dut = FormalDut(new LimitedCounter())
  assumeInitial(ClockDomain.current.isResetActive)

  // Check that the value is incrementing.
  // hasPast is used to ensure that the past(dut.value) had at least one sampling out of reset
  when(pastValid() && past(dut.value) =/= 10) {
    assert(dut.value === past(dut.value) + 1)
  }
})

假设内存中的内容

这是一个示例,我们希望防止值 1 出现在内存中:

class DutWithRam extends Component {
  val ram = Mem.fill(4)(UInt(8 bits))
  val write = slave(ram.writePort)
  val read = slave(ram.readAsyncPort)
}

object FormalRam extends App {
  import spinal.core.formal._

  FormalConfig.withBMC(15).doVerify(new Component {
    val dut = FormalDut(new DutWithRam())
    assumeInitial(ClockDomain.current.isResetActive)

    // assume that no word in the ram has the value 1
    for(i <- 0 until dut.ram.wordCount) {
      assumeInitial(dut.ram(i) =/= 1)
    }

    // Allow the write anything but value 1 in the ram
    anyseq(dut.write)
    clockDomain.withoutReset() { //As the memory write can occur during reset, we need to ensure the assume apply there too
      assume(dut.write.data =/= 1)
    }

    // Check that no word in the ram is set to 1
    anyseq(dut.read.address)
    assert(dut.read.data =/= 1)
  })
}

实用工具和原语

断言/时钟/复位

断言(assert)是一直被时钟驱动的,但在复位期间被禁用。这也适用于假设(assume)和覆盖(cover)。

如果您想在复位期间保持断言被检查,您可以执行以下操作:

ClockDomain.current.withoutReset() {
  assert(wuff === 0)
}

指定信号的初始值

例如,对于当前时钟域的复位信号(在顶部有用)

ClockDomain.current.readResetWire initial(False)

指定初始假设

assumeInitial(clockDomain.isResetActive)

内存内容(Mem)检查

如果您的设计中有 Mem,并且想要检查其内容,可以通过以下方式进行:

// Manual access
for(i <- 0 until dut.ram.wordCount) {
  assumeInitial(dut.ram(i) =/= X) //No occurence of the word X
}

assumeInitial(!dut.ram.formalContains(X)) //No occurence of the word X

assumeInitial(dut.ram.formalCount(X) === 1) //only one occurence of the word X

在复位的时候进行断言检查,可以这样做

ClockDomain.current.duringReset {
  assume(rawrrr === 0)
  assume(wuff === 3)
}

形式化验证的原语

语法

返回类型

描述

assert(Bool)

assume(Bool)

cover(Bool)

past(that : T, delay : Int)
past(that : T)

T

返回 delay 周期以前的 ``that``值 。 (默认1个周期)

rose(that : Bool)

Bool

that 从 False 变为 True 时返回 True

fell(that : Bool)

Bool

that 从 True 变为 False 时返回 True

changed(that : Bool)

Bool

that 当前值与上一个周期相比发生变化时返回 True

stable(that : Bool)

Bool

that 当前值与上一个周期相比没有改变时返回 True

initstate()

Bool

第一个周期时返回 True

pastValid()

Bool

当过去的值有效时返回 True(第一个周期为 False)。建议在 past, rose, fell, changedstable 每次使用的地方均使用它。

pastValidAfterReset()

Bool

与“pastValid”类似,唯一的区别是这会考虑重置。可以理解为 pastValid & past(!reset),同步逻辑中建议使用。

请注意,您可以对past的返回值使用 init 语句:

when(past(enable) init(False)) { ... }

局限性

不支持非时钟驱动的断言。但它们在第三方形式化验证示例中有这样使用,似乎主要与代码风格相关。

命名策略

所有与形式验证相关的函数都返回 Area 或 Composite(首选),并命名为formalXXXX。 formalContext 可用于创建形式相关逻辑,还有可能是 formalAssertsformalAssumesformalCovers

对于组件

证明模式中需要的, Component 内部所需的最少断言可以命名为 formalAsserts

对于实现 IMasterSlave的接口

可能存在以 formalAssertsMaster, formalAssertsSlave, formalAssumesMaster, formalAssumesSlave or formalCovers 命名的函数。 Master/Slave 是目标接口类型,因此,formalAssertsMaster 可以理解为“主接口的形式化验证断言”。