【SpinalHDL快速入门】3、Scala 快速入门

SpinalHDL本质上来讲是Scala语言的一个库,所以需要先学习Scala,才能在此基础上学习SpinalHDL。

变量和函数应该定义在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 }
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ReCclay

如果觉得不错,不妨请我喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值