数字硬件系统设计之一:Scala快速入门(上)

此文作为spinalHDL数字硬件系统设计的入门篇,有点啰里啰唆,关键是后面的小视频演示。
背景
为啥介绍Scala?Scala是做大数据开发最牛的工具之一, 和数字系统设计有毛关系?这话说来就长了,简单说都是因为美国名校加州伯克利分校的错,才害得我们做电子系统硬件设计的码农们去辛苦研究一种高度抽象、苦涩难懂、一旦掌握又会让你的开发工作非常简单高效的语言:

1979年伯克利分校的大神David Patterson教授提出了RISC的想法,主张处理器硬件设计应该专注加速常用的指令,较为复杂的指令则利用常用指令去组合;上世纪80年代ARM公司就是基于RISC架构开始做芯片设计,最终一步一步崛起,在嵌入式领域、手机应用等领域战胜INTEL,成为现在的移动芯片之王,包括华为麒麟、高通骁龙都是基于ARM架构设计的。随着商业ARM的成功,其授权费用越来越高,几乎只能大企业玩得起了(大企业米多,要交就交呗),对于中小企业和初创企业几乎就关闭了入行的大门,这个时候,有人勇敢地站了出来:2010年伯克利分校的大神David Patterson教授的团队(就是最早提出RISC概念的David Patterson)开始设计着手设计一个全新的处理器指令架构,经过多年研发终于定型为第五版,就是现在准备挑战ARM架构、INTEL X86架构的RISCV架构,RISCV架构是完全免费的,关于RISCV架构可以关注公众号中的RISCV专辑。伯克利分校的大神们想找一种硬件设计语言来实现RISCV原型设计,他们觉得VHDL/verilog效率实在太低、太古老了,用它做设计实在太痛苦, 最后大神们看中了兼容java的scala语言, 决定在scala语言上开发一个专用的硬件设计新语言(即chisel3硬件设计语言), 注意这里使用的是"硬件设计语言",而不是传统的"硬件描述语言",VHDL/verilog最早都不是用于硬件设计的,是用于描述和归档设计文件的,虽然后来发展用于设计,但有很多先天不足!基于scala的全新硬件设计语言还有spinalHDL, 我个人更喜欢spinalHDL, 其风格更接近于大家熟悉的verilog。

关于大神David Patterson教授,我再啰嗦几句,他的名著值得所有计算机、电子行业人员拜读:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
此书多少钱?不贵很值得购买收藏,即使不常看,也可拿出来装逼

人民币269元

说了很多VHDL/verilog的缺点,现在再说说其优点:目前主流商业EDA工具都支持VHDL/verilog, 而且现在主流商业EDA的综合优化功能都做得非常完善和强大,而且VHDL/verilog还是国际标准;目前chisel3、spinalHDL敏捷开发由于缺乏主流商业EDA工具的支持(毕竟其发展年限太短),chisel3/spinalHDL目前只能用于前端设计,还无法直接用于后端的布局布线, 目前的使用方法是:前端可以使用chisel3\spinalHDL设计(当然传统上大都使用verilog),然后chisel3\spinalHDL设计的文件自动转化为RTL级描述的verilog文件,以利用主流EDA的时序优化和后端布局布线功能。

chisel3、spinalHDL代表着一种新兴的发展趋势:敏捷开发,充分利用现代软件的先进技术,达到高效率高可靠性。趋势就是未来,就像十年前C/C++语言出现用于硬件设计一样,当年也没有主流商业EDA软件的支持,但是现在像xilinx的vivado HLS等商业EDA都是支持用C/C++来设计数字硬件系统了。

因为SpinalHDL、Chisel3是由Scala开发的,所以在使用SpinalHDL设计硬件数字系统之前要对Scala语言学习。掌握Scala才能更好地理解SpinalHDL。

Scala是运行在JVM上一门语言。开发效率非常高、语法丰富简洁,三两行Scala代码能搞定Java要写的一大堆代码。有人把java比做C语言,把scala比做C++, scala就是java++

对于Scala搞的那么多语法糖和新概念, 首次接触都是又爱又恨,甚至有点莫名其妙,不过慢慢熟悉之后,就会感觉Scala特性很自然,例如:

1)单例对象(singleton object),scala没有static关键字,用object关键字来新建单例对象

2)伴生对象和伴生类(companion object & companion class)、独立对象(standalone object)

这两个概念是相互关联的。假设有object A 和 class A, 两者刚好同名,都是A,这时候可以说:object A是class A的“伴生对象”;class A是object A的“伴生类”。当一个object B没有同名的class B的时候,object B就叫做“独立对象”。

伴生带来的特权就是:它们可以互相访问私有成员。

class和object的区别:

a、单例对象不能用new来实例化。

b、单例对象不能带参数。

c、单例对象在第一次调用的时候才初始化。

d、当单例对象与某个类同名时,它也叫该类的伴生对象

   是不是觉得这概念名字有点多而乱?这不算啥,还有更闹心的,这就是学习门槛,迈过去了就不闹心了,而变成了舒心:
  1. 略坑的函数调用语法糖

a.神奇的点号省略

 //无参数  
"hello" toUpperCase  
"hello".toUpperCase  
"hello".toUpperCase()  


//一个参数  
"hello".indexOf "h"  
"hello" indexOf "h"   

//多个参数  

"hello world" substring (0, 3)   


//全部搞在一起就是下面这样,这是什么鬼?这可是合法的语法哈

 "hello world" substring (0, 3)  toUpperCase() indexOf "h"  

//配合上匿名函数  

Array(1,2,3) map ((i:Int)=> i+1)  

 1 to 3 map ((i:Int)=> i+1)  

点号省略可以省略也可不省略,当省略时有时真让人犯晕!

b. 神奇的for

for(i <- 1 to 4) println(i)  

for(i <- 1 to 4 if i > 1) println(i)  

 for(i <- 1 to 4 if i > 1; if i < 4; if i % 2 ==0) println(i)  

 这3个for都是对的,for里面居然带if !

c. 神奇的花括号{}代替小括号()语法

如果函数调用只传入一个参数,花括号可以代替小括号:

println("hello")  

println{“hello”} // 都对,是不是有点晕?

def xx(i:Int)(j:Int) = i+j  

 xx(1){2} //result: 3  

(xx(1)_)(3) //curry化调用,不信你不懵  
 

//看了上面的还没懵?那就再来个  

def xx(i:Int)(j:(Int)=>Int) = j(i)  

xx(1){i=> i+10}  

d. 神奇的byName函数和调用

为了让自己定义的方法看起来和scala内建语法一样“和谐”,搞出了一 个byName参数和byName函数

e. 类型的上下界

class foo[T <% A]{…} //弱上界<%。关系较弱:T能够隐式转换为Ordered[T]

class foo[T <: A]{…} //上界 <:。T必须是A的子类,A是T的类型上界。

class foo[T >: A]{…} //下界 >:。T必须是A的父类,A是T类型的下界。

这在c/c++里好像没见过

f.协变和逆变

+T: 协变:class Queue[+T] {}。如果C<:A,则Queue[C] <: Queue[A]

-T: 逆变 : class Queue[-T] {}。如果C<:A,则Queue[C] >: Queue[A]

这在c/c++里也好像没见过

g.隐式转换

隐式转换,是对应于显式的转换来说的。比如有"1234".toInt,这就是显式的转换,不直接调用toInt方法转换的就是隐式转换

implicit def str2Int(s:String):Int = Integer.parseInt(s) //隐式str转int

def add(a:Int, b:Int) = a+b

add(“1”,2) //先把"1"隐式转换为1,再加起来。

h. case类

case类和一般类的不同点:

1.会有和类名一样的工厂方法。不需要使用new来创建这个类对象

 case class Var(name:String)  

val v1 = Var("peter") //和类名一样的工厂方法  

2.case类的参数列表默认是val的。

3.自动添加了toString,hashCode和equals的自然实现。

4.case类的最大的用途就是用来做模式匹配。

下面我们就来具体介绍Scala,只能做为基础扫盲,但对一般的spinalHDL应用开发已经足够了,遇见犯晕的语法,还得去查查scala的网站https://www.scala-lang.org/

面向对象特性
Scala是一种纯面向对象的语言,每个值都是对象。

函数式编程
Scala也是一种函数式语言,其函数也能当成值来使用。Scala提供了轻量级的语法用以定义匿名函数,支持高阶函数,允许嵌套多层函数,并支持柯里化。Scala的case class及其内置的模式匹配相当于函数式编程语言中常用的代数类型。

可以利用Scala的模式匹配,编写类似正则表达式的代码处理数据。

scala即是纯面向对象的语言,又支持函数式语言!很奇葩呀

1.声明变量、常用类型
scala代码会被编译成字节码,然后交给Java虚拟机执行。
不强制指定变量的类型时,编译器会自动推断变量类型。
scala> 65
res0: Int= 30
val定义的值无法改变它的内容。在Scala中,鼓励使用val。SpinalHDL中基本都是使用val, 其与硬件连接关系相对固定不变刚好对应。
scala> val answer = 6
5
answer: Int= 30
scala> answer = 20
:8: error: reassignment to val
answer = 20

val一旦定义赋值,是不能重新赋值的,这与硬件连接关系相对固定不变刚好对应
如果要声明其值可变的变量,用var。
scala> var counter = 0
counter: Int= 0
scala> counter = 20
counter: Int= 20
变量或函数的类型总是写在变量或函数名称后面,与Java的习惯不同。
scala> val greeting : String= “Hello”
greeting: String= Hello
如果一行只有一条语句,一行结尾可以不使用分号;当同一行代码中存在多条语句时需要分号隔开不同的语句。
常用的数据类型与Java一样: Byte、Char、Short、Int、Long、Float、Double及Boolean,这些都是预定义好的类。
在基本类型和包装类型之间的转换是由Scala编译器来完成的。
±*/%等操作符实际上是方法。
对于BigInt和BigDecimal对象,可以以常规的方式使用数学操作符(但在Java中同样的操作要写成x.multiply(x))。
scala> val x : BigInt= 123
x: BigInt= 123
scala> x * x
res1: scala.math.BigInt= 15129
不带参数的Scala方法通常可以省略方法后面的圆括号,一般没有参数且不改变当前对象的方法不带圆括号。

2.控制结构与函数
表达式有值,语句执行动作。
Scala中,几乎所有构造出来的语法结构都有值,不像Java中把表达式和语句(if语句)分为两类。
在这里if表示式有值。
代码块也有值,最后一个表达式就是值。
语句中,分号不是必需的。
函数式中不使用return。

条件表达式
在Scala中if/else表达式有值,这个值就是在if或else之后的表达式的值。
scala> var x = 10
x: Int= 10
scala> val r = if(x > 0) 1else-1
r: Int= 1
scala> var x = 0
x: Int= 0
scala> val r = if(x > 0) 1else-1
r: Int= -1
可能if没有输出值,但在Scala中,每个表达式都有某种值。
scala> var x = 0
x: Int= 0
scala> val r = if(x > 0) 1
r: AnyVal= ()

块表达式和赋值
在Scala中{}块包含一系列表达式,其结果也是一个表达式。块中最后一个表达式的值就是块的值。
当某个val变量的初始化需要分多步完成时,使用{}来实现很方便。
val dis = {val dx = x - x0; val dy = y - y0; sqrt(dx * dx + dy * dy)}

循环
while与Java中的循环一样。
while(n > 0) {
r = r * n
n -= 1
}

Scala的for不是for(初始化; 检查变量是否满足; 更新变量)的结构。
for(i <- 1 to n) {
r = r * i
}

1 to n 表达式表示:返回数据1到n(包含n)的区间[1,n]。
1 until n 表达式表示:返回数据1到n(不包含n)的区间[1,n)。

增强for循环和for推导式
可以以 变量<-表达式的形式提供多个生成器,用分号将他们隔开

scala> for(i <- 1 to 3; j <- 1 to 3) print((10* i + j) + " ")
11
12
13
21
22
23
31
32
33

每个生成器都可以带一个守卫,以if开头的Boolean表达式 (if前并没有分号)

scala> for(i <- 1 to 3; j <- 1 to 3if i != j) print((10* i + j) + " ")
12
13
21
23
31
32

for推导式:for循环的循环以yield开始,则该循环会构造出一个集合,每次迭代生成集合中的一个元素
scala> for(i <- 1 to 10) yield i % 3
res0: IndexedSeq[Int] = Vector(1, 2, 0, 1, 2, 0, 1, 2, 0, 1)

函数
函数定义:需要给出函数名、参数和函数体,例如下
def abs(x: Double) = if(x >= 0) x else-x
必须给出所有参数的类型
递归函数必须指定返回值类型
def fac(n: Int) : Int= if(n <= 0) 1else n * fac(n - 1)
不需要 return语句
有 = 等号连接函数体
默认参数和带名参数
scala> def decorate(str: String, left: String= “[”, right: String= “]”) = left + str + right
decorate: (str: String, left: String, right: String)String
scala> decorate(“Hello World”)
res3: String= [HelloWorld]
scala> decorate(“Hello World”, “<”, “>”)
res4: String=
也可以在提供参数值时指定参数名,这样就可与函数定义参数列表的顺序不一致
scala> decorate(left = “<<”, str = “Hello Scala”, right = “>>”)
res5: String= <>

可以混用未命名参数和带名参数,只要未命名的参数排在前面即可

scala> decorate(“Hello Spark”, right = “]<<”)

res6: String= [HelloSpark]<<
相当于
scala> decorate(“Hello Spark”, “[”, “]<<”)

实现一个可以接受可变长参数列表的函数

scala> def sum(args: Int*) = {

| var result = 0

| for(arg <- args) result += arg

| result

| }

sum: (args: Int*)Int

scala> val s = sum(1, 3, 5, 7)
s: Int= 16

scala> def echo(args: String*) =
for (arg <- args) println(arg)
echo: (String*)Unit

scala> echo()
scala> echo(“one”)
one
scala> echo(“hello”, “world!”)
hello
world!
可以使用 _*将一个整数区间转换成参数序列
直接使用会抛出如下错误:
scala> val ss = sum(1 to 5)
:8: error: type mismatch;
found : scala.collection.immutable.Range.Inclusive
required: Int
val ss = sum(1 to 5)

// 正确的使用方式
scala> val ss = sum(1 to 5: _*)
ss: Int= 15

如果函数体包含在花括号当中,但没有前面的 =号,返回类型是Unit,这样的函数被称做过程。过程不返回值,调用它仅仅是为了它的副作用。
scala> def sayHello(name: String) = "Hello, " + name

sayHello: (name: String)String

    scala> def sayHello(name: String): Unit = "Hello, " + name
                sayHello: (name: String)Unit

scala> def sayHello(name: String) { print(“Hello, " + name); “Hello, " + name }
^
warning: procedure syntax is deprecated: instead, add : Unit = to explicitly declare sayHello's return type
sayHello: (name: String)Unit
当val被声明为lazy时,它的始始化将被推迟,直到首次对它取值。
lazy val words = scala.io.Source.fromFile(”/usr/share/dict/words”).mkString
可以故意把文件名写错,试一下在初始化语句被执行的时候会不会报错(只有访问words时才提示文件未找到)

3.常用数组操作
长度固定使用Array,长度可变使用ArrayBuffer
提供初始值时不要使用new
用()来访问元素
for(elem <- arr)遍历元素
for(elem <- arr if …) yield …将原数组转为新数组
定长数组
10个整数的数组,所有元素初始为0
scala> val nums = new ArrayInt
nums: Array[Int] = Array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
10个元素的字符中数组,所有元素初始化为null
scala> val str = new ArrayString
str: Array[String] = Array(null, null, null, null, null, null, null, null, null, null)
提供初始值就不需要new,长度为2的Array[String],类型是推断出来的
scala> val str1 = Array(“Hello”, “Scala”)
str1: Array[String] = Array(Hello, Scala)
使用()来访问元素
scala> val s = str1(0)
s: String = Hello
变长数组
与Java中ArrayList功能等效的数据结构ArrayBuffer
初始化一个空的可变长数组,准备存入整数
scala> import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.ArrayBuffer
scala> val b = ArrayBufferInt
b: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer()
用+=在尾添加元素或多个用括号包都来的元素
scala> b += 1
res0: b.type = ArrayBuffer(1)
scala> b += (1, 2, 3)
res1: b.type = ArrayBuffer(1, 1, 2, 3)
用++=操作符追加任何集合
scala> b ++= Array(6, 8, 9)
res2: b.type = ArrayBuffer(1, 1, 2, 3, 6, 8, 9)
移除最后2个元素
scala> b.trimEnd(2)
scala> b
res4: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 1, 2, 3, 6)
可在任意位置插入或移除元素(不高效,所有在那个位置后面的元素都必须被平移)
// 在下标2之前插入,下标从0开始
scala> b.insert(2, 4)
scala> b
res6: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 1, 4, 2, 3, 6)
// 在下标2之前插入多个元素,下标从0开始
scala> b.insert(2, 4, 5)
scala> b
res8: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 1, 4, 5, 4, 2, 3, 6)
定长数组与变长数据转换
// 转成定长数组
scala> b.toArray
res9: Array[Int] = Array(1, 1, 4, 5, 4, 2, 3, 6)
// 转成变长数组
scala> b.toBuffer
res10: scala.collection.mutable.Buffer[Int] = ArrayBuffer(1, 1, 4, 5, 4, 2, 3, 6)

遍历数组
// 使用下标访问
scala> for (i <- 0 until b.length)
| println(i + “:” + b(i))

// 不使用下标
scala> for(elem <- b)
| println(elem)

数组转换
for推导式,从一个数组转换,生成一个全新的数组
scala> val a = Array(2, 3, 5, 7)
a: Array[Int] = Array(2, 3, 5, 7)
scala> val res = for(elem <- a) yield 2 * elem
res: Array[Int] = Array(4, 6, 10, 14)
scala> a
res13: Array[Int] = Array(2, 3, 5, 7)
for推导式,从一个ArrayBuffer转换,生成一个全新的ArrayBuffer
scala> a.toBuffer
res14: scala.collection.mutable.Buffer[Int] = ArrayBuffer(2, 3, 5, 7)
scala> val res = for(elem <- res14) yield 2 * elem
res: scala.collection.mutable.Buffer[Int] = ArrayBuffer(4, 6, 10, 14)

4.常用映射和元组操作
映射是键值对的集合
元组是n个对象(并不一定要相同类型的对象)的集合
映射
构造一个不可变(默认)映射(其值不能被改变)
scala> val scores = Map(“Alice” -> 90, “Bob” -> 88)
scores: scala.collection.immutable.Map[String,Int] = Map(Alice -> 90, Bob -> 88)
构造一个可变映射
scala> val scores1 = scala.collection.mutable.Map(“Alice” -> 90, “Bob” -> 88)
scores1: scala.collection.mutable.Map[String,Int] = Map(Bob -> 88, Alice -> 90)
构造一个空映射,需要选定一个映射实现并给出类型参数(注意最后是 方括号[] )
scala> val scores2 = new scala.collection.mutable.HashMap[String, Int]
scores2: scala.collection.mutable.HashMap[String,Int] = Map()
使用()获取映射中的值某个键对应的值
scala> val aliceScore = scores(“Alice”)
aliceScore: Int = 90
判断映射中是否包括某个指定键的值,用contains方法
scala> val bobScore = if(scores.contains(“Bob”)) scores(“Bob”) else 0
bobScore: Int = 88
// 简洁写法
scala> val bobScore1 = scores.getOrElse(“Bob”, 0)
bobScore1: Int = 88
可变映射中更新某个映射中的值或添加一个新的映射关系
scala> val scores1 = scala.collection.mutable.Map(“Alice” -> 90, “Bob” -> 88)
scores1: scala.collection.mutable.Map[String,Int] = Map(Bob -> 88, Alice -> 90)
scala> scores1(“Bob”) = 99
scala> scores1
res1: scala.collection.mutable.Map[String,Int] = Map(Bob -> 99, Alice -> 90)
// 如果key不存在,添加一个新的映射关系
scala> scores1(“Fred”) = 79
scala> scores1
res3: scala.collection.mutable.Map[String,Int] = Map(Bob-> 99, Fred-> 79, Alice-> 90)
使用+=操作来添加多个关系(key不存在就添加,存在就更新)
scala> scores1 += (“Yezhiwei” -> 100, “Fred” -> 90)
res4: scores1.type = Map(Bob -> 99, Fred -> 90, Alice -> 90, Yezhiwei -> 100)
scala> scores1
res5: scala.collection.mutable.Map[String,Int] = Map(Bob-> 99, Fred-> 90, Alice-> 90, Yezhiwei-> 100)
使用-=操作来移除某个键值对
scala> scores1 -= “Bob”
res6: scores1.type = Map(Fred -> 90, Alice -> 90, Yezhiwei -> 100)
scala> scores1
res7: scala.collection.mutable.Map[String,Int] = Map(Fred-> 90, Alice-> 90, Yezhiwei-> 100)
不可变的映射可以有同样的操作,只是返回一个新的映射,而不会更新原对象
遍历映射中的所有键值对
scala> for((k, v) <- scores)
| println(k + “:” + v)
// 运行结果
Alice:90
Bob:88
像Java一样,keySet和values方法访问键或值
scala> scores1.keys
res9: Iterable[String] = Set(Fred, Alice, Yezhiwei)
scala> for(v <- scores.values) println (v)
90
88

元组
元组是不同类型的值的聚集,元组的值是通过将单个的值包含在圆括号中构成的
scala> (1, 3.14, “Fred”)
res11: (Int, Double, String) = (1,3.14,Fred)
元组的各组元从1开始的,不是0。与数组和字符串中的位置索引不同
scala> (1, 3.14, “Fred”)
res11: (Int, Double, String) = (1,3.14,Fred)
scala> res11._1
res12: Int = 1
使用模式匹配来获取元组的组元
scala> val (first, second, third) = res11
first: Int = 1
second: Double = 3.14
third: String = Fred
如果不需要所有的值,可以在相应位置上使用_占位
scala> val (first, second, ) = res11
first: Int = 1
second: Double = 3.14
元组可以用于函数需要返回不止一个值的情况
scala> “Hello Scala”.partition(
.isUpper)
res13: (String, String) = (HS,ello cala)

作为(上)篇的结束,我们做个小小的演示视频说明:

spinalHDL

关注微信公众号:RiscV与IC设计

在这里插入图片描述

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值