SpinalHDL本质上来讲是Scala语言的一个库,所以需要先学习Scala,才能在此基础上学习SpinalHDL。
文章目录
- Scala 基础
- Scala 数据类型(5种:Boolean、Int、Float、Double、String)
- Scala Variables
- Scala Functions
- Return
- Return类型推断
- 花括号
- 返回值为空的函数
- 参数默认值
- Apply 函数
- Object(静态类)
- 入口函数main
- class
- 继承(Inheritance):override
- Case class
- 模板 / 类型参数化(Templates / Type parameterization)
- Scala 编码规范
- 使用class还是case class
- [case] class:所有类名应以大写字母开头
- companion object(伴生对象):伴生对象应该以大写字母开头
- function:函数名始终以小写字母开头
- instances(实例):类的实例应该始终以小写字母开头
- if / when
- switch
- Parameters
- Scala 交互
- 介绍
- SpinalHDL在API背后的工作原理
- 一切皆为引用
- 硬件类型
- RGB example
- 生成的 RTL 中的信号名称
- Scala 用于elaboration,SpinalHDL 用于硬件描述(如:可以使用Boolean,而无法使用Bool)
- Scala elaboration能力(if,for,函数式编程)
变量和函数应该定义在object、class或function中,不能在 Scala 文件的根目录下定义。
Scala 基础
Scala 数据类型(5种:Boolean、Int、Float、Double、String)
Scala Variables
在Scala中,您可以使用var关键字定义变量:
var number : Int = 0
number = 6
number += 4
println(number) // 10
Scala 能够自动推断类型。如果变量在声明时被赋值,您无需指定其类型:
var number = 0 //The type of 'number' is inferred as an Int during compilation
然而,在Scala中使用var并不是很常见。相反,通常会使用由val定义的常量值:
val two = 2
val three = 3
val six = two * three
Scala Functions
例如,如果您想定义一个函数,该函数返回true,如果其两个参数的和大于零,则可以按照以下方式进行:
def sumBiggerThanZero(a: Float, b: Float): Boolean = {
return (a + b) > 0
}
然后,要调用这个函数,你可以写:
sumBiggerThanZero(2.3f, 5.4f)
你也可以通过名称指定参数,如果有很多参数的话这是非常有用的:
sumBiggerThanZero(
a = 2.3f,
b = 5.4f
)
Return
return关键字并不是必须的。如果没有它,Scala会将函数中最后一个语句作为返回值。
def sumBiggerThanZero(a: Float, b: Float): Boolean = {
(a + b) > 0
}
Return类型推断
Scala 能够自动推断返回类型,您不需要指定它:
def sumBiggerThanZero(a: Float, b: Float) = {
(a + b) > 0
}
花括号
如果你的函数只包含一条语句,Scala 函数不需要花括号:
def sumBiggerThanZero(a: Float, b: Float) = (a + b) > 0
返回值为空的函数
如果你想让一个函数返回空值,那么它的返回类型应该设置为 Unit。这相当于 C/C++ 中的 void 类型。
def printer(): Unit = {
println("1234")
println("5678")
}
参数默认值
您可以为函数的每个参数指定默认值:
def sumBiggerThanZero(a: Float, b: Float = 0.0f) = {
(a + b) > 0
}
Apply 函数
名为apply的函数很特殊,因为你可以在不必输入它们的名称的情况下调用它们:
class Array() {
def apply(index: Int): Int = index + 3
}
val array = new Array()
val value = array(4) //array(4) 被解释为 array.apply(4),将返回 7。
Object(静态类)
在Scala中,没有静态关键字。取而代之的是object。在对象定义内部定义的所有内容都是静态的。
以下示例定义了一个名为pow2的静态函数,它以浮点值作为参数,并返回一个浮点值。
object MathUtils {
def pow2(value: Float): Float = value * value
}
//可以使用如下方式调用它:
MathUtils.pow2(42.0f)
入口函数main
Scala程序的入口点(即主函数)应该定义在一个 object 内,作为名为main的函数
object MyTopLevelMain{
def main(args: Array[String]) {
println("Hello world")
}
}
class
类语法与Java非常相似。假设您想定义一个颜色类,该类以三个浮点值(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
继承(Inheritance):override
举个例子,假设你想定义两个类,矩形和正方形,它们都继承自Shape类:(注意使用其中的override
)
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 class
case class是声明类的另一种方式。
case class Rectangle(width: Float, height: Float) extends Shape {
override def getArea() = width * height
}
case class和普通calss之间有一些区别:
- case class不需要使用 new 关键字进行实例化。
- 构造参数可以从外部访问;您无需将它们定义为
val
。
在 SpinalHDL 中,这解释了编码规范的原因:通常建议使用case class而不是普通class,以减少输入量并增加连贯性。
模板 / 类型参数化(Templates / Type parameterization)
假设你想设计一个给定数据类型的队列类,那么你需要为该类提供一个类型参数:
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() }
Scala 编码规范
使用class还是case class
当您定义Bundle或Component时,最好将其声明为case class。原因如下:
- 它避免了使用new关键字。
- case类提供了一个clone函数。这在SpinalHDL中非常有用,例如,在定义新的Reg或某种新的Stream时需要克隆Bundle。
- 构造参数可以直接从外部看到。
[case] class:所有类名应以大写字母开头
class Fifo extends Component {
}
class Counter extends Area {
}
case class Color extends Bundle {
}
companion object(伴生对象):伴生对象应该以大写字母开头
object Fifo {
def apply(that: Stream[Bits]): Stream[Bits] = {...}
}
object MajorityVote {
def apply(that: Bits): UInt = {...}
}
这个规则的一个例外是当伴生对象被用作函数(仅适用于内部),并且这些apply函数不会生成硬件时:
object log2 {
def apply(value: Int): Int = {...}
}
function:函数名始终以小写字母开头
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 bits), initialContent = sinTable)
instances(实例):类的实例应该始终以小写字母开头
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 {
...
}
例外情况包括:
- 在 otherwise 前省略点是可以的。
- 如果将 if/when 语句压缩到一行上可以使代码更易读,则可以这样做。
switch
SpinalHDL switch通常应按以下方式编写:
switch(value) {
is(key) {
}
is(key) {
}
default {
}
}
如果将is/default语句压缩成一行可以使代码更易读,则可以这样做。
Parameters
将 Component/Bundle 中的分组参数放在一个case class中通常是受欢迎的,因为:
- 更容易携带/操作(carry/manipulate)以配置设计
- 更好的可维护性
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 bits)
val g = UInt(c.gWidth bits)
val b = UInt(c.bWidth bits)
}
但这并不适用于所有情况。例如:在FIFO中,将dataType参数与fifo的depth参数分组是没有意义的,因为通常dataType是与设计相关的内容,而depth则是与设计配置相关的内容。
class Fifo[T <: Data](dataType: T, depth: Int) extends Component {
}
Scala 交互
介绍
SpinalHDL实际上不是一种语言:它是一个普通的Scala库。这乍一看可能很奇怪,但它是一个非常强大的组合。
您可以使用整个Scala世界来帮助您通过SpinalHDL库描述硬件,但为了做到这一点,重要的是要理解SpinalHDL如何与Scala交互。
SpinalHDL在API背后的工作原理
当您执行SpinalHDL硬件描述时,每次使用SpinalHDL functions、operators或classes时,它都会构建一个表示设计电路的内存图形(in-memory graph)。
然后,在**完成实例化顶层组件类的工作(即elaboration)**之后,SpinalHDL将对构建的图进行一些处理,并且如果一切正常,则将该图刷新到VHDL或Verilog文件中。
一切皆为引用
例如,如果您定义了一个以Bits类型参数为输入的Scala函数,在调用它时,该参数将作为引用传递。因此,如果在函数内部分配该参数,则对底层Bits对象产生的影响与在函数外部进行分配相同。
硬件类型
SpinalHDL中的硬件数据类型是两个部分的组合:
- 给定Scala类型的实例
- 该实例的配置
例如,Bits(8位)是Scala类型Bits和其8位配置(作为构造参数)的组合。
RGB example
让我们以 Rgb bundle 类为例:
case class Rgb(rWidth: Int, gWidth: Int, bWidth: Int) extends Bundle {
val r = UInt(rWidth bits)
val g = UInt(gWidth bits)
val b = UInt(bWidth bits)
}
硬件数据类型在这里是Scala Rgb类及其rWidth、gWidth和bWidth参数化的组合。
这是一个用法示例:【cloneOf 有点意思!!!】
// 定义一个 RGB 信号
val myRgbSignal = Rgb(5, 6, 5)
// 定义与前面相同数据类型的另一个 RGB 信号
val myRgbCloned = cloneOf(myRgbSignal)
你还可以使用函数来定义各种类型工厂(typedef):
// 定义一个类型工厂函数
def myRgbTypeDef = Rgb(5, 6, 5)
// 使用该类型工厂创建一个 RGB 信号。
val myRgbFromTypeDef = myRgbTypeDef
生成的 RTL 中的信号名称
为了在生成的 RTL 中命名信号,SpinalHDL 使用 Java 反射来遍历整个组件层次结构,收集存储在class属性中的所有引用,并使用它们的属性名称进行命名。
这就是为什么在function内定义的每个信号的名称都会丢失:
def myFunction(arg: UInt) {
val temp = arg + 1 // 在生成的 RTL 中,您将无法检索到“temp”信号
return temp
}
val value = myFunction(U"000001") + 42
如果您想保留生成的 RTL 中内部变量的名称,一种解决方案是使用 Area
:
def myFunction(arg: UInt) new Area {
val temp = arg + 1 // 您将无法在生成的 RTL 中检索到临时信号。
}
val myFunctionCall = myFunction(U"000001") // 将生成名为`myFunctionCall_temp`的`temp`
val value = myFunctionCall.temp + 42
Scala 用于elaboration,SpinalHDL 用于硬件描述(如:可以使用Boolean,而无法使用Bool)
例如,如果您编写一个Scala for循环来生成一些硬件,它将在VHDL / Verilog中生成展开的结果。
另外,如果您想要一个常量,您不应该使用SpinalHDL硬件字面值而是Scala的。例如:
// 这是错误的,因为您不能将硬件Bool用作构造参数(它会导致层次结构违规)。
class SubComponent(activeHigh: Bool) extends Component {
// ...
}
// 没错,你可以使用Scala世界中的所有内容来参数化你的硬件。
class SubComponent(activeHigh: Boolean) extends Component {
// ...
}
Scala elaboration能力(if,for,函数式编程)
Scala的所有语法都可以用于详细说明硬件设计,例如,Scala if语句可用于启用或禁用硬件生成:
val counter = Reg(UInt(8 bits))
counter := counter + 1
if(generateAClearWhenHit42) { // elaboration test,就像在VHDL中生成if语句一样
when(counter === 42) { // Hardware test
counter := 0
}
}
Scala的for循环也是如此:
val value = Reg(Bits(8 bits))
when(something) {
// 使用Scala for循环设置值的所有位(在硬件elaboration期间评估)
for(idx <- 0 to 7) {
value(idx) := True
}
}
此外,函数式编程技术可以与许多SpinalHDL类型一起使用:
val values = Vec(Bits(8 bits), 4)
val valuesAre42 = values.map(_ === 42)
val valuesAreAll42 = valuesAre42.reduce(_ && _)
val valuesAreEqualToTheirIndex = values.zipWithIndex.map{ case (value, i) => value === i }