写在前面

SpinalHDL这个语言,我是在了解了香山处理器Chisel才知道的,一直想用一用,SpinalHDL和Chisel师承一脉,都是基于Scala来进行电路描述。其本质上仍旧是HDL而非HLS,在设计之前依旧需要有清晰的电路结构,在电路描述上更加方便和快捷。
本文主要对spinal HDL的参考文档进行了翻译,后期打算应用spinal HDL进行简单的模块的改写。本文中内容主要对scala编程进行了简单的介绍。

环境搭建

参考大佬知乎文章搭建环境:
环境搭建文章参考

数据类型

在 Scala 中,有 5 种主要数据类型:

类型示例描述
Booleantrue, false
Int3, 0x3232位整数
Float3.14f32 位浮点数
Double3.1464 位浮点数
String“Hello world”UTF-16 字符串

变量

在 Scala 中,可以使用 var 关键字定义变量:

var number : Int = 0 
number = 6 
number += 4 
println(number) // 10

Scala 能够自动推断类型。 如果变量是在声明时分配的,则不需要指定它:

var number = 0 //'number'的类型在编译时被推断为Int。

但在 Scala 中使用 var 并不常见。 相反,通常使用 val 定义的常量值:

val two   = 2 
val three = 3 
val six   = two * three

函数

例如,如果您想定义一个函数,如果它的两个参数之和大于零,则返回 true,您可以执行以下操作:

def sumBiggerThanZero(a: Float, b: Float): Boolean = {
  return (a + b) > 0
}

然后,要调用此函数,可以按顺序进行传参数,或者按名字进行传参数:

sumBiggerThanZero(2.3f, 5.4f)

or

sumBiggerThanZero(
  a = 2.3f,
  b = 5.4f
)

Return

不需要 return 关键字。 如果没有它,Scala 将函数的最后一条语句作为返回值。返回类型推断 Scala 能够自动推断返回类型,不需要指定。

def sumBiggerThanZero(a: Float, b: Float) = {
  (a + b) > 0
}

如果你的函数只包含一个语句,Scala 函数不需要花括号。同时可以为函数的每个参数指定一个默认值:

def sumBiggerThanZero(a: Float, b: Float = 0.0f) = (a + b) > 0

不返回任何内容的函数 如果您希望函数不返回任何内容,则应将返回类型设置为 Unit。 它等效于 C/C++ void 类型。

def printer(): Unit = {
  println("1234")
  println("5678")
}

Apply

apply 的函数很特别,因为可以调用它们而无需键入它们的名称:

class Array() {
  def apply(index: Int): Int = index + 3
}

val array = new Array()
val value = array(4)   //array(4) is interpreted as array.apply(4) and will return 7

这个概念也适用于 Scala 对象(static):

object MajorityVote {
  def apply(value: Int): Int = ...
}

val value = MajorityVote(4) // Will call MajorityVote.apply(4)

Object

在 Scala 中,没有 static 关键字。 取而代之的是对象。 对象定义中定义的所有内容都是静态的。

下面的例子定义了一个名为 pow2 的静态函数,它接受一个浮点值作为参数并返回一个浮点值。

object MathUtils {
  def pow2(value: Float): Float = value * value
}

然后你可以通过写来调用它:

MathUtils.pow2(42.0f)

入口点(main)

Scala 程序的入口点(主函数)应该在对象内部定义为名为 main 的函数。

object MyTopLevelMain{
  def main(args: Array[String]) {
    println("Hello world")
  }
}

class

类语法与 Java 非常相似。 想象一下,您要定义一个 Color 类,该类将三个 Float 值 (r,g,b) 作为构造参数:

class Color(r: Float, g: Float, b: Float) {
  def getGrayLevel(): Float = r * 0.3f + g * 0.4f + b * 0.4f
}

然后,实例化上一个示例中的类并使用其 getGrayLevel 函数:

val blue = new Color(0, 0, 1)
val grayLevelOfBlue = blue.getGrayLevel()

注意,如果你想从外部访问类的一个构造参数,这个构造参数应该定义为一个val:

class Color(val r: Float, val g: Float, val b: Float) { ... }
...
val blue = new Color(0, 0, 1)
val redLevelOfBlue = blue.r

继承

例如,假设您要定义两个类,Rectangle 和 Square,它们扩展了类 Shape:

class Shape {
  def getArea(): Float
}

class Square(sideLength: Float) extends Shape {
  override def getArea() = sideLength * sideLength
}

class Rectangle(width: Float, height: Float) extends Shape {
  override def getArea() = width * height
}

案例类

Case 类是声明类的另一种方式。

case class Rectangle(width: Float, height: Float) extends Shape {
  override def getArea() = width * height
}

然后 case class 和 class 之间有一些区别:

  • case 类不需要 new 关键字来实例化。
  • 参数可从外部访问; 您不需要将它们定义为 val。

在 SpinalHDL 中,这解释了编码约定背后的原因:通常建议使用案例类而不是类,以减少输入和提高一致性。

模板/类型参数化

想象一下,你想设计一个类,它是给定数据类型的队列,在这种情况下,你需要为类提供一个类型参数:

class  Queue[T](){
  def push(that: T) : Unit = ...
  def pop(): T = ...
}

如果要将 T 类型限制为给定类型(例如 Shape)的子类,可以使用 <:Shape 语法:

class Shape() {
    def getArea(): Float
}
class Rectangle() extends Shape { ... }

class  Queue[T <: Shape]() {
  def push(that: T): Unit = ...
  def pop(): T = ...
}

函数也是如此:

def doSomething[T <: Shape](shape: T): Something = { shape.getArea() }

编程约定

类与案例类

定义 aBundle或 a Component时,最好将其声明为 case 类。

原因是:

  • 它避免使用new关键字。在某些情况下,永远不必使用它比有时更好。
  • 一个案例类提供了一个克隆功能。 当需要克隆 Bundle 时,这在 SpinalHDL 中很有用,例如,当您定义新的 Reg 或某种新的 Stream 时。
  • 参数从外面直接可见。

[case]class

所有类名都应该以大写字母开头。

class Fifo extends Component {
}

class Counter extends Area {
}

case class Color extends Bundle {
}

伴生对象

一个伴生对象应以大写字母开头。

object Fifo {
  def apply(that: Stream[Bits]): Stream[Bits] = {...}
}

object MajorityVote {
  def apply(that: Bits): UInt = {...}
}

此规则的一个例外是当伴随对象用作函数时(仅apply在内部),并且这些apply函数不生成硬件:

object log2 {
  def apply(value: Int): Int = {...}
}

函数

函数应始终以小写字母开头:

def sinTable = (0 until sampleCount).map(sampleIndex => {
  val sinValue = Math.sin(2 * Math.PI * sampleIndex / sampleCount)
  S((sinValue * ((1 << resolutionWidth) / 2 - 1)).toInt, resolutionWidth bits)
})

val rom =  Mem(SInt(resolutionWidth bit), initialContent = sinTable)

例化

类的实例化应始终以小写字母开头:

val fifo   = new Fifo()
val buffer = Reg(Bits(8 bits))

if / when

Scala if 和 SpinalHDL when 通常应按以下方式编写:

if(cond) {
  ...
} else if(cond) {
  ...
} else {
  ...
}

when(cond) {
  ...
}.elseWhen(cond) {
  ...
}.otherwise {
  ...
}

switch

SpinalHDLswitch通常应按以下方式编写:

switch(value) {
  is(key) {

  }
  is(key) {

  }
  default {

  }
}

如果可以使代码更具可读性,则可以将is/default语句压缩到一行中。

参数

通常在 case 类中对Component/Bundle的参数进行分组是比较实用的,因为:

  • 更易于携带/操作以配置设计
  • 更好的可维护性
case class RgbConfig(rWidth: Int, gWidth: Int, bWidth: Int) {
  def getWidth = rWidth + gWidth + bWidth
}

case class Rgb(c: RgbConfig) extends Bundle {
  val r = UInt(c.rWidth bit)
  val g = UInt(c.gWidth bit)
  val b = UInt(c.bWidth bit)
}

但这不应该适用于所有情况。例如:在 FIFO 中,将dataType参数与depthfifo的参数分组是没有意义的,因为一般来说,thedataType与设计相关,而 thedepth与设计配置相关。

class Fifo[T <: Data](dataType: T, depth: Int) extends Component {
}

更多推荐