JoshuaのFPGA · 2022年10月22日 · 四川

SpinalHDL设计错误总结

简介(Introduction)

SpinalHDL编译器会做很多设计检查,来确保生成的VHDL/Verilog是可仿真的可综合的。基本上,SpinalHDL不会生成破损的VHDL/Verilog设计。以下是SpinalHDL检查的简要概括:

  • 重复赋值
  • 时钟域交叉
  • 层次化违例
  • 组合逻辑环路
  • Latch
  • 未驱动的信号
  • 位宽不匹配
  • 得不到的switch生命

在每个SpinalHDL错误报告中,你会得到栈轨迹(stack trace),能够帮你精准地确定哪里出错。这个设计检查第一次可能会感到有些过犹不及,但一旦你跳脱出传统的硬件描述方式这会变得非常宝贵。

赋值覆盖(Assignment Overlap)

一、简介

SpinalHDL将会检查, 没有任何的信号赋值会完全抹除前面的赋值。

二、例子

下列代码:

class TopLevel extends Component {
 val a = UInt(8 bits)
 a := 42
 a := 66 // Erase the a := 42 assignment
}

将会产生如下报错:

ASSIGNMENT OVERLAP completely the previous one of (toplevel/a :  UInt[8 bits])
  ***
  Source file location of the a := 66 assignment via the stack trace
  ***

可以修复为:

class TopLevel extends Component {
 val a = UInt(8 bits)
 a := 42
 when(something) {
 a := 66
 }
}

但当某些情况下, 如果用户真的需要去覆盖掉之前的赋值(因为在某些时候覆盖是有意义的), 则可以使用如下表达:

class TopLevel extends Component {
 val a = UInt(8 bits)
 a := 42
 a.allowOverride
 a := 66
}

跨时钟域违例(Clock crossing violation)

一、简介

SpinalHDL会检查用户设计中的寄存器只会与相同时钟域的寄存器以组合逻辑方式连接。

二、例子

下述代码:

class TopLevel extends Component {
 val clkA = ClockDomain.external("clkA")
 val clkB = ClockDomain.external("clkB")

val regA = clkA(Reg(UInt(8 bits))) // PlayDev.scala:834
 val regB = clkB(Reg(UInt(8 bits))) // PlayDev.scala:835

val tmp = regA + regA // PlayDev.scala:838
 regB := tmp
}

会报错:

CLOCK CROSSING VIOLATION from (toplevel/regA :  UInt[8 bits]) to (toplevel/regB :  UInt[8 bits]).
- Register declaration at
  ***
  Source file location of the toplevel/regA definition via the stack trace
  ***
- through
      >>> (toplevel/regA :  UInt[8 bits]) at ***(PlayDev.scala:834) >>>
      >>> (toplevel/tmp :  UInt[8 bits]) at ***(PlayDev.scala:838) >>>
      >>> (toplevel/regB :  UInt[8 bits]) at ***(PlayDev.scala:835) >>>

有多种修改方式如下:

  1. 跨时钟域标记(crossClockDomain tag)

    可以利用crossClockDomain来向SpinalHDL编译器传递“不用担心这种跨时钟赋值”的信息:

    class TopLevel extends Component {
    val clkA = ClockDomain.external("clkA")
    val clkB = ClockDomain.external("clkB")
    
    val regA = clkA(Reg(UInt(8 bits)))
    val regB = clkB(Reg(UInt(8 bits))).addTag(crossClockDomain)
    
    val tmp = regA + regA
    regB := tmp
    }
  2. setSyncronousWith

    用户还可以通过使用一个ClockDomain对象的setSynchronousWith方法指定两个时钟域一起同步。

    class TopLevel extends Component {
    val clkA = ClockDomain.external("clkA")
    val clkB = ClockDomain.external("clkB")
    clkB.setSyncronousWith(clkA)
    
    val regA = clkA(Reg(UInt(8 bits)))
    val regB = clkB(Reg(UInt(8 bits)))
    
    val tmp = regA + regA
    regB := tmp
    }
  3. BufferCC

    当交换单比特信号(如Bool类型), 或格雷码时, 可以使用BufferCC安全地跨时钟域。

    警告:不要对多比特信号使用BufferCC, 因为如果时钟是异步的, 接收端有损坏读取的风险。更多信息请参见“Clock Domain”章节.
class AsyncFifo extends Component {
    val popToPushGray = Bits(ptrWidth bits)
    val pushToPopGray = Bits(ptrWidth bits)

    val pushCC = new ClockingArea(pushClock) {
        val pushPtr     = Counter(depth << 1)
        val pushPtrGray = RegNext(toGray(pushPtr.valueNext)) init(0)
        val popPtrGray  = BufferCC(popToPushGray, B(0, ptrWidth bits))
        val full        = isFull(pushPtrGray, popPtrGray)
        ...
    }

    val popCC = new ClockingArea(popClock) {
        val popPtr      = Counter(depth << 1)
        val popPtrGray  = RegNext(toGray(popPtr.valueNext)) init(0)
        val pushPtrGray = BufferCC(pushToPopGray, B(0, ptrWidth bits))
        val empty       = isEmpty(popPtrGray, pushPtrGray)
        ...
    }
}

组合环(Combinatorial loop)

一、简介

SpinalHDL会检查设计中没有组合逻辑环的存在。

二、例子

下述代码:

class TopLevel extends Component {
 val a = UInt(8 bits) // PlayDev.scala line 831
 val b = UInt(8 bits) // PlayDev.scala line 832
 val c = UInt(8 bits)
 val d = UInt(8 bits)

a := b
 b := c | d
 d := a
 c := 0
}

会报错:

COMBINATORIAL LOOP :
 Partial chain :
 >>> (toplevel/a : UInt[8 bits]) at ***(PlayDev.scala:831) >>>
 >>> (toplevel/d : UInt[8 bits]) at ***(PlayDev.scala:834) >>>
 >>> (toplevel/b : UInt[8 bits]) at ***(PlayDev.scala:832) >>>
 >>> (toplevel/a : UInt[8 bits]) at ***(PlayDev.scala:831) >>>

Full chain :
 (toplevel/a : UInt[8 bits])
 (toplevel/d : UInt[8 bits])
 (UInt | UInt)[8 bits]
 (toplevel/b : UInt[8 bits])
 (toplevel/a : UInt[8 bits])

可能的修复方式为:

class TopLevel extends Component {
 val a = UInt(8 bits) // PlayDev.scala line 831
 val b = UInt(8 bits) // PlayDev.scala line 832
 val c = UInt(8 bits)
 val d = UInt(8 bits)

a := b
 b := c | d
 d := 42
 c := 0
}

三、假阳性(false-positives)

SpinalHDL检测组合逻辑环的策略是悲观的, 因此可能出现误报的情况。假如发生了误报, 用户可以无效化对某一信号的环检测, 如:

class TopLevel extends Component {
 val a = UInt(8 bits)
 a := 0
 a(1) := a(0) // 因为这行而发生了误报
}

可以被修复为:

class TopLevel extends Component {
 val a = UInt(8 bits).noCombLoopCheck
 a := 0
 a(1) := a(0)
}

层次违例(Hierarchy violation)

一、简介

SpinalHDL将会检查当前层次设计的信号不会访问到该组件的外部区域。

下述信号可以被一个组件内所访问:

  • 在当前组件中定义的所有无方向(directionless)信号
  • 当前组件的所有输入/输出和IO信号
  • 子(child)组件的所有输入/输出和IO信号

同时, 下列信号可以被分配赋值到组件内部:

  • 在当前组件中定义的所有无方向(directionless)信号
  • 当前组件的所有输出和IO信号
  • 子组件的所有输入/输出信号

如果发生了一个HIERARCHY VIOLATION错误, 意味着上述的某一规则被违反。

二、例子

下列代码:

class TopLevel extends Component {
 val io = new Bundle {
 val a = in UInt(8 bits)
 }
 val tmp = U"x42"
 io.a := tmp
}

会产生如下报错:

HIERARCHY VIOLATION : (toplevel/io_a : in UInt[8 bits]) is driven by (toplevel/tmp :  UInt[8 bits]), but isn't accessible in the toplevel component.
  ***
  Source file location of the `io.a := tmp` via the stack trace
  ***

修复如下:

class TopLevel extends Component {
 val io = new Bundle {
 val a = out UInt(8 bits) // in转为out
 }
 val tmp = U"x42"
 io.a := tmp
}

还应该指出, 诸如(a(1):= a(0))这样的赋值会使Verilator等工具无法匹配。在这种情况下, 使用Vec(Bool, 8)可能会更好。

IO包

一、简介

SpinalHDL会检查每个IO包定义内都只有in/out/inout信号。

二、例子

下述代码:

class TopLevel extends Component {
 val io = new Bundle {
 val a = UInt(8 bits) 
}
}

将会报错:

IO BUNDLE ERROR : A direction less (toplevel/io_a :  UInt[8 bits]) signal was defined into toplevel component's io bundle
  ***
  Source file location of the toplevel/io_a definition via the stack trace
  ***

修复如下:

class TopLevel extends Component {
 val io = new Bundle {
 val a = in UInt(8 bits)
 }
}

但是可能对于元(meta)硬件的设计真的需要io.a是无方向的, 则可以用:

class TopLevel extends Component {
 val io = new Bundle {
 val a = UInt(8 bits)
 }
 a.allowDirectionLessIo
}

锁存器检测

一、简介

SpinalHDL会检查在综合时没有组合逻辑信号会引入锁存器。换句话说, 这是检查没有组合信号被部分赋值。

二、例子

下述代码:

class TopLevel extends Component {
 val cond = in(Bool)
 val a = UInt(8 bits)

when(cond) {
 a := 42
 }
}

会产生如下报错:

LATCH DETECTED from the combinatorial signal (toplevel/a :  UInt[8 bits]), defined at
  ***
  Source file location of the toplevel/io_a definition via the stack trace
  ***

修复为:

class TopLevel extends Component {
 val cond = in(Bool)
 val a = UInt(8 bits)
 a := 0
 when(cond) {
 a := 42
 }
}

无驱动检测(no driver on)

一、简介

SpinalHDL将会检查设计中所有使用的组合逻辑信号都有相应的赋值连线声明。

二、例子

下述代码:

class TopLevel extends Component {
 val result = out(UInt(8 bits))
 val a = UInt(8 bits)
 result := a
}

会产生如下报错:

NO DRIVER ON (toplevel/a :  UInt[8 bits]), defined at
  ***
  Source file location of the toplevel/a definition via the stack trace
  ***

修复如下:

class TopLevel extends Component {
 val result = out(UInt(8 bits))
 val a = UInt(8 bits)
 a := 42
 result := a
}

排除空指针(NullPointerException)

一、简介

NullPointerException是一个Scala运行时的报错, 它会在一个变量在初始化前就被访问时发生。

二、例子

下述代码:

class TopLevel extends Component {
 a := 42
 val a = UInt(8 bits)
}

将会报错:

Exception in thread "main" java.lang.NullPointerException
  ***
  Source file location of the a := 42 assignment via the stack trace
  ***

修复为:

class TopLevel extends Component {
 val a = UInt(8 bits)
 a := 42
}

问题解释(Issue explanation)

SpinalHDL不是一门语言, 它是一个Scala库, 这意味着它遵守与Scala通用编程语言相同的规则。

当运行上面的SpinalHDL硬件描述来生成相应的VHDL/Verilog时, SpinalHDL硬件描述将作为一个Scala程序执行, 并且a将是一个空引用, 直到程序执行val a = UInt(8 bits), 所以在此之前试图给它赋值将导致NullPointerException

定义为组件输入的寄存器(Register defined as component input)

一、简介

在SpinalHDL中, 用户不被允许定义一个将寄存器作为输入的组件。原因是为了预防可能发生的意外, 如用户会尝试着将寄存器信号作为子组件的输入驱动。如果确实需要一个寄存器输入, 用户可以在io包中先定义一个非寄存器输入, 随后在组件内部对其添加寄存器。

二、例子

下述代码:

class TopLevel extends Component {
 val io = new Bundle {
 val a = in(Reg(UInt(8 bits)))
 }
}

会产生如下报错:

REGISTER DEFINED AS COMPONENT INPUT : (toplevel/io_a : in UInt[8 bits]) is defined as a registered input of the toplevel component, but isn't allowed.
  ***
  Source file location of the toplevel/io_a definition via the stack trace
  ***

修复为:

class TopLevel extends Component {
 val io = new Bundle {
 val a = in UInt(8 bits)
 }
}

如果需要一个寄存器信号a, 可以按照如下方式使用:

class TopLevel extends Component {
 val io = new Bundle {
 val a = in UInt(8 bits)
 }
 val a = RegNext(io.a)
}

作用域违例(Scope violation)

一、简介

SpinalHDL将会检查没有信号会在超出其定义的作用域之外被赋值使用。这个错误不容易触发, 因为它需要一些特定的元硬件描述技巧。

二、例子

下述代码:

class TopLevel extends Component {
 val cond = Bool()

var tmp : UInt = null
 when(cond) {
 tmp = UInt(8 bits)
 }
 tmp := U"x42"
}

会报错:

SCOPE VIOLATION : (toplevel/tmp :  UInt[8 bits]) is assigned outside its declaration scope at
  ***
  Source file location of the tmp := U"x42" via the stack trace
  ***

修复为:

class TopLevel extends Component {
 val cond = Bool()

var tmp : UInt = UInt(8 bits)
 when(cond) {

}
 tmp := U"x42"
}

Spinal无法克隆类(Spinal can’t clone class)

一、简介

当SpinalHDL想要通过cloneOf函数创建一个新的数据类型实例, 但是不能这样做时, 就会发生这个错误。出现这种情况的原因总是因为它无法检索Bundle的构造参数。

二、例子1

下述代码:

// cloneOf(this)无法检索用于构造自身的位宽
class RGB(width : Int) extends Bundle {
 val r, g, b = UInt(width bits)
}

class TopLevel extends Component {
 val tmp = Stream(new RGB(8)) // Stream需要cloneOf(new RGB(8))的功能
}

将会报错:

*** Spinal can't clone class spinal.tester.PlayDevMessages$RGB datatype
*** You have two way to solve that :
*** In place to declare a "class Bundle(args){}", create a "case class Bundle(args){}"
*** Or override by your self the bundle clone function
  ***
  Source file location of the RGB class definition via the stack trace
  ***

修复为:

case class RGB(width : Int) extends Bundle {
 val r, g, b = UInt(width bits)
}

class TopLevel extends Component {
 val tmp = Stream(RGB(8))
}

三、例子2

下列代码:

case class Xlen(val xlen: Int) {}

case class MemoryAddress()(implicit xlenConfig: Xlen) extends Bundle {
    val address = UInt(xlenConfig.xlen bits)
}

class DebugMemory(implicit config: Xlen) extends Component {
    val io = new Bundle {
        val inputAddress = in(MemoryAddress())
    }

    val someAddress = RegNext(io.inputAddress) // -> ERROR *****************************
}

报错:

[error] *** Spinal can't clone class debug.MemoryAddress datatype

在这种情况下, 一种解决方案是覆盖克隆函数来传播隐式参数。

case class MemoryAddress()(implicit xlenConfig: Xlen) extends Bundle {
 val address = UInt(xlenConfig.xlen bits)

override def clone = MemoryAddress()
}

注意:我们需要克隆的是硬件的单元, 而不是最终在其中赋值的值

注意:另一种方法是使用ScopeProperty(请见”其他语言特性”章节中)

未分配的寄存器(Unassigned register)

一、简介

SpinalHDL将会检查所有设计中有用的寄存器都被分配了。

二、例子

下述代码:

class TopLevel extends Component {
 val result = out(UInt(8 bits))
 val a = Reg(UInt(8 bits))
 result := a
}

会报错:

UNASSIGNED REGISTER (toplevel/a :  UInt[8 bits]), defined at
  ***
  Source file location of the toplevel/a definition via the stack trace
  ***

可以修复为:

class TopLevel extends Component {
 val result = out(UInt(8 bits))
 val a = Reg(UInt(8 bits))
 a := 42
 result := a
}

三、只有初始化的寄存器(Register with only init)

在某些情况下, 可能由于硬件参数化, 生成只有init描述但无分配的寄存器也是有意义的。

class TopLevel extends Component {
 val result = out(UInt(8 bits))
 val a = Reg(UInt(8 bits)) init(42)

if(something)
 a := somethingElse //有问题,这个可综合
 result := a
}

会报错:

UNASSIGNED REGISTER (toplevel/a :  UInt[8 bits]), defined at
  ***
  Source file location of the toplevel/a definition via the stack trace
  ***

为了修复它, 可以让SpinalHDL转换该只有init描述但未分配的寄存器为一个组合逻辑:

class TopLevel extends Component {
 val result = out(UInt(8 bits))
 val a = Reg(UInt(8 bits)).init(42).allowUnsetRegToAvoidLatch

if(something)
 a := somethingElse
 result := a
}

无法实现的is表述(Unreachable is statement)

一、简介

SpinalHDL会检查并确保switch中的所有is语句是可实现的

二、例子

下述代码:

class TopLevel extends Component {
 val sel = UInt(2 bits)
 val result = UInt(4 bits)
 switch(sel) {
 is(0){ result := 4 }
 is(1){ result := 6 }
 is(2){ result := 8 }
 is(3){ result := 9 }
 }
}

会报错:

UNREACHABLE IS STATEMENT in the switch statement at
  ***
  Source file location of the is statement definition via the stack trace
  ***

可修复为:

class TopLevel extends Component {
 val sel = UInt(2 bits)
 val result = UInt(4 bits)
 switch(sel) {
 is(0){ result := 4 }
 is(1){ result := 6 }
 is(2){ result := 8 }
 is(3){ result := 9 }
 }
}

位宽不匹配(Width mismatch)

一、简介

SpinalHDL会检查赋值两侧的操作和信号有相同的位宽。

二、分配赋值例子

下述代码:

class TopLevel extends Component {
 val a = UInt(8 bits)
 val b = UInt(4 bits)
 b := a
}

将会报错:

WIDTH MISMATCH on (toplevel/b :  UInt[4 bits]) := (toplevel/a :  UInt[8 bits]) at
  ***
  Source file location of the OR operator via the stack trace
  ***

可以修复为:

class TopLevel extends Component {
 val a = UInt(8 bits)
 val b = UInt(4 bits)
 b := a.resized
}

三、操作实例

下述代码:

class TopLevel extends Component {
 val a = UInt(8 bits)
 val b = UInt(4 bits)
 val result = a | b
}

将会报错:

WIDTH MISMATCH on (UInt | UInt)[8 bits]
- Left  operand : (toplevel/a :  UInt[8 bits])
- Right operand : (toplevel/b :  UInt[4 bits])
  at
  ***
  Source file location of the OR operator via the stack trace
  ***

可修复为:

class TopLevel extends Component {
 val a = UInt(8 bits)
 val b = UInt(4 bits)
 val result = a | (b.resized)
}
推荐阅读
关注数
9
内容数
6
FPGAer
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息