资讯详情

Chisel教程——14.(完结篇)Scala和Chisel中的数据类型

(完结篇)Scala和Chisel中等数据类型

结束开头的碎念

这是这个系列的最后一篇文章,官方的Chisel-Bootcamp中后面还有FIRRTL相关内容,但设计一个RISC-V CPU这样的目标,依靠本系列文章的内容就足够了。因为这个系列的学习倾向于在实战中学习,难免会有很多遗漏。所以后续会更新一个系列,争取对抗Chisel全面详细地讲解语言。

另外,除了Chisel语言的基础学习也会像以前承诺的那样针对RISC-V系统结构的实现是一个系列。该系列将从只有几个指令的单周期开始CPU逐步实现支持RV64GC复杂的指令集、可操作系统CPU。敬请期待!

目前国内研究体系结构较少,从事或计划相关工作的人不多,各大知识共享平台上的相关文章也不热门,,毕竟冷门研究方向啊。本人为某计科被评为A 硕士期间研究课题与计算机系统结构交集不多,但对系统结构有浓厚的兴趣。所以毕业后一直在考虑继续学习系统结构,所以有了这个系列。

到目前为止,这些文章很少被浏览,但它们总是需要的。在这种情况下,我会继续做这些事情。

动机

言归正传,Scala这是一种强大的编程语言,这是一把双刃剑:坏的方面,很多都可以Python解释和执行的程序在上面Scala中连的编译不能通过,因为Python是动态语言;好的方面,Scala通过编译的程序在运行时会比较Python少犯错误。

这篇文章的目的是让我们更好地理解它Scala中等公民的数据类型(types)。一开始,我们可能会觉得我们写代码的效率很低,但很快,我们就会学会理解编译中的错误信息,并进一步理解如何使用类型系统来构建我们的程序。

静态类型

Scala中的类型

Scala所有对象都有一种类型,通常是这个对象的类型,例如:

println(10.getClass) println(10.0.getClass) println("ten".getClass) 

输出为:

int double class java.lang.String 

当我们声明自己的类别时,它也有相应的类型:

class MyClass { 
             val a = 1 } println(new MyClass().getClass) 

输出为:

defined class MyClass 

若无要求,。这会让Scala编译器捕获函数使用不当。

def double(s: String): String = s   s // 这些句子可以试试 double("hi")      // 使用正确,会返回"hihi" double(10)        // 编译器会报告错误用法的参数类型 double("hi") / 10 // 使用错误的用法,编译器错误地使用返回值(/ 不是String的成员) 

如果函数不返回任何值,则返回类型为Unit

var counter = 0 def increment(): Unit = { 
             counter  = 1 } increment() 

Scala类型 vs. Chisel类型

我们前面讨论过Chisel类型和Scala例如:

val a = Wire(UInt(4.W))
a := 0.U

这个是正确用法,因为0.UUInt类型(一个Chisel类型)的,而:

val a = Wire(UInt(4.W))
a := 0

就不对,因为0Int类型(一个Scala类型)的。

还有布尔值,在Scala里面是Boolean,而在Chisel里面是Bool

val bool = Wire(Bool())
val boolean: Boolean = false
// 正确用法
when (bool) { 
         ... }
if (boolean) { 
         ... }
// 错误用法
if (bool) { 
         ... }
when (boolean) { 
         ... }

如果我们不小心弄混了Scala类型和Chisel类型,Scala编译器会为我们生成捕获到的错误。这是因为Scala是静态类型的,在编译的时候,编译器能够区分Chisel和Scala类型,还能理解if ()里面应该是Booleanwhen ()里面应该是Bool

Scala强制类型转换(asInstanceOf

x.asInstanceOf[T]可以将对象x转换为类型T,如果不能够完成到T类型的转换就会抛出一个异常:

val x: UInt = 3.U
try { 
        
  println(x.asInstanceOf[Int]) // 会抛出异常
} catch { 
        
  case e: java.lang.ClassCastException => println("As expected, we can't cast UInt to Int")
}

// 但是我们可以将x强制转换为Data类型,因为UInt就是从Data继承的
println(x.asInstanceOf[Data])

输出为:

As expected, we can't cast UInt to Int
UInt<2>(3)

Chisel中的类型转换

下面的代码运行的话Scala内核会直接挂掉(为什么不是报错呢?不知道),因为将UInt类型的值赋值给了SInt,这是非法的:

class TypeConvertDemo extends Module { 
        
    val io = IO(new Bundle { 
        
        val in  = Input(UInt(4.W))
        val out = Output(SInt(4.W))
    })
    io.out := io.in
}

test(new TypeConvertDemo) { 
         c =>
      c.io.in.poke(3.U)
      c.io.out.expect(3.S)
      c.io.in.poke(15.U)
      c.io.out.expect(-1.S)
}

Chisel有一组类型转换函数,其中最常用的强制类型转换用的是x.asTypeOf(T),如果把第6行代码改成:

io.out := io.in.asTypeOf(io.out)

问题就解决了,测试通过。有一些Chisel对象还定义了asUInt()asSInt()

类型匹配

匹配操作符

匹配操作符match在前面就已经讲过。类型匹配在写泛型生成器(Type-generic Generator)的时候尤其有用。下面是一个生成器的例子,它可以把两个UIntSInt的字面值相加。后面还会继续对泛型生成器进行阐述。

下面的例子中,chiselTypeOf可以用于获取字面值的Chisel类型。注意:Scala中其实有更好、更安全的办法来写泛型生成器的。

class ConstantSum(in1: Data, in2: Data) extends Module { 
        
  val io = IO(new Bundle { 
        
      val out = Output(chiselTypeOf(in1)) // 获取in1的类型
  })
  (in1, in2) match { 
        
      case (x: UInt, y: UInt) => io.out := x + y
      case (x: SInt, y: SInt) => io.out := x + y
      case _ => throw new Exception("I give up!")
  }
}
println(getVerilogString(new ConstantSum(3.U, 4.U)))
println(getVerilogString(new ConstantSum(-3.S, 4.S)))
println(getVerilogString(new ConstantSum(3.U, 4.S))) // 会触发异常

输出如下:

module ConstantSum(
  input        clock,
  input        reset,
  output [1:0] io_out
);
  assign io_out = 2'h3; // @[MyModule.scala 15:43]
endmodule

module ConstantSum(
  input        clock,
  input        reset,
  output [2:0] io_out
);
  assign io_out = 3'sh1; // @[MyModule.scala 16:43]
endmodule

[error] java.lang.Exception: I give up!

需要记住的是,Chisel类型通常不应该进行值匹配。Scala的match会在电路展开时进行,但是你可能想要的是展开后的比较。下面的例子就会报一个符号错误:

class InputIsZero extends Module { 
        
  val io = IO(new Bundle { 
        
    val in  = Input(UInt(16.W))
    val out = Output(Bool())
  })
  io.out := (io.in match { 
        
    // 这里会触发编译报语法错误
    case (0.U) => true.B
    case _   => false.B
  })
}
println(getVerilogString(new InputIsZero))

Unapply

当进行匹配的时候原理是什么呢?为什么我们可以完成下面这样有意思的匹配呢:

case class Something(a: String, b: Int)
val a = Something("A", 3)
a match { 
        
  case Something("A", value) => value
  case Something(str, 3)     => 0
}

这是因为每个case类都会有个伴生对象被创建,其中包含了一个unapply方法,作为apply方法的补充。那什么是unapply方法呢?

Scala中的unapply方法是另一种语法糖的形式,它可以给匹配语句在匹配中既能匹配类型,也能从类型中的能力。

来看下面这个例子。如果我们给参数说这个生成器流水线化了,那延迟就是3*totalWidth,否则就是2*someOtherWidth。因为case类有定义了的unapply,所以我们可以在case类里进行值匹配,就像这样:

case class SomeGeneratorParameters(
  someWidth: Int,
  someOtherWidth: Int = 10,
  pipelineMe: Boolean = false
) { 
        
  require(someWidth >= 0)
  require(someOtherWidth >= 0)
  val totalWidth = someWidth + someOtherWidth
}

def delay(p: SomeGeneratorParameters): Int = p match { 
        
  case sg @ SomeGeneratorParameters(_, _, true) => sg.totalWidth * 3
  case SomeGeneratorParameters(_, sw, false) => sw * 2
}

println(delay(SomeGeneratorParameters(10, 10)))
println(delay(SomeGeneratorParameters(10, 10, true)))

输出为:

20
60

如果仔细看delay函数的话,应该可以注意到除了进行类型匹配以外,我们还:

  1. 直接引用了参数的内部值;
  2. 直接在参数的内部值上进行比较;

这是编译器实现了unapply方法给予的可能性。要注意的是case类的unapply只是个语法糖,下面这两条语句是等价的:

case p: SomeGeneratorParameters => p.sw * 2
case SomeGeneratorParameters(_, sw, _) => sw * 2

此外,匹配还有其他的语法和风格。下面这两条语句也是等价的,但是第二条语句允许在内部值上进行匹配的同时还引用父值:

case SomeGeneratorParameters(_, sw, true) => sw
case sg@SomeGeneratorParameters(_, sw, true) => sw

我们还可以直接在检查语句中嵌入条件检查,就像下面三条等价的语句演示的一样:

case SomeGeneratorParameters(_, sw, false) => sw * 2
case s@SomeGeneratorParameters(_, sw, false) => s.sw * 2
case s: SomeGeneratorParameters if s.pipelineMe => s.sw * 2

上面所有的语法能用都是因为Scala类的伴生对象中的unapply方法。如果想unapply一个类但又不想把它写成case类,那可以手动实现一个unapply方法。下面的例子就演示了如何手动实现一个类的applyunapply方法:

class Boat(val name: String, val length: Int)
object Boat { 
        
  def unapply(b: Boat): Option[(String, Int)] = Some((b.name, b.length))
  def apply(name: String, length: Int): Boat = new Boat(name, length)
}

def getSmallBoats(seq: Seq[Boat]): Seq[Boat] = seq.filter { 
         b =>
  b match { 
        
    case Boat(_, length) if length < 60 => true
    case Boat(_, _) => false
  }
}

val boats = Seq(Boat("Santa Maria", 62), Boat("Pinta", 56), Boat("Nina", 50))
println(getSmallBoats(boats).map(_.name).mkString(" and ") + " are small boats!")

输出为:

Pinta and Nina are small boats!

偏函数(部分应用函数)

这里只是简单介绍一下,详细的可以自己去找Scala的文档。

偏函数(PartialFunction)是一种只定义在它输入的一个子集上的函数。就跟一个选项一样,偏函数也许对于特定输入不会返回值,这个可以通过isDefinedAt(...)来测试。

偏函数之间可以通过orElse串起来。注意,用未定义的输入调用偏函数会导致运行时错误。比如在给偏函数的输入是用户定义的时候就会发生。所以为了更具类型安全,建议写函数的时候返回一个Option。下面看例子,最后一段的partialFunc3通过or把两个偏函数的定义域取并集了:

// 用来打印的助手函数,不然代码太冗长了
def printAndAssert(cmd: String, result: Boolean, expected: Boolean): Unit = { 
        
  println(s"$cmd = $result")
  assert(result == expected)
}

// 这个偏函数是为..., -1, 2, 5, ...这些数定义的
val partialFunc1: PartialFunction[Int, String] = { 
        
  case i if (i + 1) % 3 == 0 => "Something"
}
// 在定义内的
printAndAssert("partialFunc1.isDefinedAt(2)", partialFunc1.isDefinedAt(2), true)
printAndAssert("partialFunc1.isDefinedAt(5)", partialFunc1.isDefinedAt(5), true)
// 不在定义内的
printAndAssert("partialFunc1.isDefinedAt(1)", partialFunc1.isDefinedAt(1), false)
printAndAssert("partialFunc1.isDefinedAt(0)", partialFunc1.isDefinedAt(0), false)
println(s"partialFunc1(2) = ${ 
          partialFunc1(2)}")	// 可以执行
try { 
        
  println(partialFunc1(0))	// 抛出异常
} catch { 
        
  case e: scala.MatchError => println("partialFunc1(0) = can't apply PartialFunctions where they are not defined")
}

// 这个偏函数是为..., 1, 4, 7, ...这些数定义的
val partialFunc2: PartialFunction[Int, String] = { 
        
  case i if (i + 2) % 3 == 0 => "Something else"
}
// 在定义内的
printAndAssert("partialFunc2.isDefinedAt(1)", partialFunc2.isDefinedAt(1), true)
// 不在定义内的
printAndAssert("partialFunc2.isDefinedAt(0)", partialFunc2.isDefinedAt(0), false)
println(s"partialFunc2(1) = ${ 
          partialFunc2(1)}")	// 可以执行
try { 
        
  println(partialFunc2(0))	// 抛出异常
} catch { 
        
  case e: scala.MatchError => println("partialFunc2(0) = can't apply PartialFunctions where they are not defined")
}

val partialFunc3 = partialFunc1 orElse partialFunc2	// 定义域取并集
// 不在定义内的
printAndAssert("partialFunc3.isDefinedAt(0)", partialFunc3.isDefinedAt(0), false)
// 在定义内的
printAndAssert("partialFunc3.isDefinedAt(1)", partialFunc3.isDefinedAt(1), true)
printAndAssert("partialFunc3.isDefinedAt(2)", partialFunc3.isDefinedAt(2), true)
// 不在定义内的
printAndAssert("partialFunc3.isDefinedAt(3)", partialFunc3.isDefinedAt(3), false)
println(s"partialFunc3(1) = ${ 
          partialFunc3(1)}")
println(s"partialFunc3(2) = ${ 
          partialFunc3(2)}")

输出如下:

partialFunc1.isDefinedAt(2) = true
partialFunc1.isDefinedAt(5) = true
partialFunc1.isDefinedAt(1) = false
partialFunc1.isDefinedAt(0) = false
partialFunc1(2) = Something
partialFunc1(0) = can't apply PartialFunctions where they are not defined
partialFunc2.isDefinedAt(1) = true
partialFunc2.isDefinedAt(0) = false
partialFunc2(1) = Something else
partialFunc2(0) = can't apply PartialFunctions where they are not defined
partialFunc3.isDefinedAt(0) = false
partialFunc3.isDefinedAt(1) = true
partialFunc3.isDefinedAt(2) = true
partialFunc3.isDefinedAt(3) = false
partialFunc3(1) = Something else
partialFunc3(2) = Something

类型安全的连接

Chisel可以检查不同类型的连接,下面这种连接就会报错:

  • Bool/UIntClock

对于其他类型,Chisel会允许我们连接,但是可能会酌情截断或者扩展数据,比如:

  • Bool/UIntBool/UInt
  • BundleBundle

看例子:

class Bundle1 extends Bundle { 
        
  val a = UInt(8.W)
}

class Bundle2 extends Bundle1 { 
        
  val b = UInt(16.W)
}

class BadTypeModule extends Module { 
        
  val io = IO(new Bundle { 
        
    val c = Input(Clock())
    val in = Input(UInt(2.W))
    val out = Output(Bool())

    val bundleIn = Input(new Bundle2)
    val bundleOut = Output(new Bundle1)
  })
  // 报错,failed @: Sink (Bool) and Source (Clock) have different types.
  // io.out := io.c 

  // 可以,但是Chisel会把io.in截断成1位来匹配io.out
  io.out := io.in

  // 可以,但是Chisel只会把他俩的共有部分连接起来
  io.bundleOut := io.bundleIn
}

println(getVerilogString(new BadTypeModule))

输出如下:

module BadTypeModule(
  input         clock,
  input         reset,
  input         io_c,
  input  [1:0]  io_in,
  output        io_out,
  input  [7:0]  io_bundleIn_a,
  input  [15:0] io_bundleIn_b,
  output [7:0]  io_bundleOut_a
);
  assign io_out = io_in[0]; // @[MyModule.scala 31:10]
  assign io_bundleOut_a = io_bundleIn_a; // @[MyModule.scala 34:16]
endmodule

泛型(Type Generics)

Scala中的泛型

Scala的泛型(也叫做polymorphism,多型,多态)很复杂,尤其是和继承耦合到一起的时候。这一小节就简单感受一下,更多内容也还是自己去找文档看。

类的类型可以是多态的,一个很好的例子就是序列,需要知道它们元素的类型:

val seq1 = Seq("1", "2", "3") // 类型为Seq[String]
val seq2 = Seq(1, 2, 3)       // 类型为Seq[Int]
val seq3 = Seq(1, "2", true)  // 类型为Seq[Any]

有时候Scala编译器需要确定一个多态类型,这就需要我们显式指定类型了:

//val default = Seq() // 官方说这里会报错,但实际上测试并没有报错
val default = Seq[String]() // 用户必须告诉编译器这个序列的具体类型
println(Seq(1, "2", "3", true).foldLeft(default){ 
         (strings, next) =>
    next match { 
        
        case s: String => strings ++ Seq(s)
        case _ => strings
    }
})

输出为:

List(2, 3)

函数的输入和输出类型也可以是多态的。下面的例子定义了一个函数,它会对一个代码块运行消耗的时间进行测试。它基于代码块的返回值类型来参数化。注意,=> T语法编码了一个匿名的函数,这个匿名函数没有参数列表。

def time[T](block: => T): T = { 
        
    val t0 = System.nanoTime()
    val result = block
    val t1 = System.nanoTime()
    val timeMillis = (t1 - t0) / 1000000.0
    println(s"Block took $timeMillis milliseconds!")
    result
}

// 从1加到一百万
val int = time { 
         (1 to 1000000).reduce(_ + _) }
println(s"Add 1 through a million is $int")

// 从1到一百万,找到其中对应的16进制字符串包含beef的最大的数字
val string = time { 
        
    (1 to 1000000).map(_.toHexString).filter(_.contains("beef")).last
}
println(s"The largest number under a million that has beef: $string")

输出如下:

Block took 23.394011 milliseconds!
Add 1 through a million is 1784293664
Block took 98.548134 milliseconds!
The largest number under a million that has beef: ebeef
<

标签: 连接器poke

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台