Chisel教程——13.Scala的面向对象编程特性和Chisel中的Module

Scala的面向对象编程特性和Chisel中的Module

动机

Scala和Chisel都是面向对象的编程语言,这意味着代码可以被组织为对象。Scala是在Java上编译的,继承了Java中很多面向对象的特性,不过下面我们会看到还是有些区别的。Chisel的硬件模块和Verilog中的模块类似,尤其是他们可以被实例化和连线为单个或多个示例。用好面向对象特性,那基本可以轻松点解决项目中绝大多数问题了。

面向对象编程的Scala

这一节概括了Scala是如何实现面向对象编程范式的。到目前为止,我们已经看到很多Scala中的类了,但不止类,Scala中还有以下特性:

  • 抽象类(Abstract Classes)
  • 特质(Traits)
  • 对象(Objects)
  • 伴生对象(Companion Objects)
  • Case对象(Case Classes)

抽象类(abstract class

抽象类和其他编程语言中的实现是一样的。它们可以定义很多没实现的值,而它们的子类必须实现。任何对象都能且只能从一个父抽象类直接继承。下面看例子:

abstract class MyAbstractClass {
  def myFunction(i: Int): Int
  val myValue: String
}
class ConcreteClass extends MyAbstractClass {
  def myFunction(i: Int): Int = i + 1
  val myValue = "Hello World!"
}
// 可以取消注释试试看
// val abstractClass = new MyAbstractClass() // 会报错,不能从抽象类实例化
val concreteClass = new ConcreteClass()      // 这样是可以的

特质(trait)和多重继承

特质和抽象类很相似,因为他们能够定义未实现的值,但有两点不同:

  1. 类可以从多个特质继承;
  2. 特质不能有构造器参数;

特质是Scala实现多重继承的方法,下面就给出了一个例子。MyClassHasFunctionHasValue这两个特质extends而来:

trait HasFunction {
  def myFunction(i: Int): Int
}
trait HasValue {
  val myValue: String
  val myOtherValue = 100
}
class MyClass extends HasFunction with HasValue {
  override def myFunction(i: Int): Int = i + 1
  val myValue = "Hello World!"
}
// 可以取消注释试试看
// val myTraitFunction = new HasFunction() // 会报错,不能从特质实例化
// val myTraitValue = new HasValue()       // 会报错,不能从特质实例化
val myClass = new MyClass()                // 这样是可以的

要继承多个特质,直接用with连起来就行:

class MyClass extends HasTrait1 with HasTrait2 with HasTrait3 ...

一般来说,用特质比用抽象类多得多,除非你确定你想强制使用抽象类的单继承限制。

对象(object

Scala对于这些单类(Singleton Classes)有一个语言特性,叫作对象。我们无法从一个对象创建实例(不需要new),直接简单地引用就行。这个和Java里面的静态类是差不多的。

object MyObject {
  def hi: String = "Hello World!"
  def apply(msg: String) = msg
}
println(MyObject.hi)
println(MyObject("This message is important!")) // 等价于MyObject.apply(msg)

伴生对象(Companion Objects)

当一个类和一个对象名字一样且在同一文件中定义的时候,这个对象就是个伴生对象。如果使用在类名前使用new的话,就会实例化一个类,如不使用new的话,就会直接引用对象。

object Lion {
    def roar(): Unit = println("I'M AN OBJECT!")
}
class Lion {
    def roar(): Unit = println("I'M A CLASS!")
}
new Lion().roar()
Lion.roar()

伴生对象通常用于以下场合:

  1. 为了包含类相关的常量;
  2. 为了在类构造器之前或之后执行代码;
  3. 为了给单个类创建多个构造器;

下面的例子就实例化了几个Animal的实例,现在我们想要每个animal都有名字,并知道实例化时的顺序。最后,如果没有给定名字的话,就会给一个默认值。

object Animal {
    val defaultName = "Bigfoot"
    private var numberOfAnimals = 0
    def apply(name: String): Animal = {
        numberOfAnimals += 1
        new Animal(name, numberOfAnimals)
    }
    def apply(): Animal = apply(defaultName)
}
class Animal(name: String, order: Int) {
  def info: String = s"Hi my name is $name, and I'm $order in line!"
}

val bunny = Animal.apply("Hopper") // 调用Animal工厂方法
println(bunny.info)
val cat = Animal("Whiskers")       // 调用Animal工厂方法
println(cat.info)
val yeti = Animal()                // 调用Animal工厂方法
println(yeti.info)

输出如下:

Hi my name is Hopper, and I'm 1 in line!
Hi my name is Whiskers, and I'm 2 in line!
Hi my name is Bigfoot, and I'm 3 in line!

下面分析一下代码:

  1. 我们的Animal伴生对象定义了Animal类相关的一个常量val defaultName = "Bigfoot"

  2. 同时也定义了一个私有的整数变量private var numberOfAnimals = 0来跟踪Animal实例的顺序;

  3. 还定义了两个方法,即众所周知的工厂方法,来返回Animal类的实例。其中第一个用参数name创建实例,然后和numberOfAnimals一起用来调用Animal类构造器;第二个不接受任何参数,而是使用了defaultName 来调用第一个apply方法;

  4. 这些工厂方法可以简单地调用:

    val bunny = Animal.apply("Hopper")
    
  5. 这样就可以不使用new关键词了,而更神奇的是,编译器在任何时候看到实例或对象后面的圆括号时,都会默认要应用apply方法,因此可以直接这么写:

    val cat = Animal("Whiskers")
    
  6. 工厂方法通常通过伴生对象提供,这样就允许用可选的方法来表达实例创建,允许给构造器参数提供额外的测试、转换,还可以消除new关键词。要注意的是,必须调用伴生对象的apply方法来让numberOfAnimals自增。

Chisel里面用到了很多的伴生对象,比如Module就是。在写这种代码的时候:

val myModule = Module(new MyModule)

就只在调用Module伴生对象,所以Chisel可以在实例化MyModule前或之后运行后台代码,后面会讲到关于Module的一些点。

Case类(Case Class

Case类是Scala中的一种特殊类型,提供了一下很牛掰的额外特性。它们在Scala编程中用的很多,所以这里简单概括一下它们有用的特性:

  1. 运行对类参数的外部访问;
  2. 消除了在实例化时使用new关键词的必要性;
  3. 会自动创建一个unapply方法,用于提供对所有类参数的访问;
  4. 不能以它们构造子类;

下面的例子中,我们声明了三个不同的类,NailScrewStaple

class Nail(length: Int) // 常规class
val nail = new Nail(10) // 需要关键词new
// println(nail.length) // 报错,因为类构造器的参数不能被外部访问

class Screw(val threadSpace: Int) // 通过使用val,参数threadSpace就对外可见了
val screw = new Screw(2)          // 需要关键词new
println(screw.threadSpace)

case class Staple(isClosed: Boolean) // 构造器的参数是默认对外可见的,无需val
val staple = Staple(false)           // 不需要关键词new
println(staple.isClosed)

Staple不需要new的原因是,编译器会自动为case类创建一个伴生对象,这个伴生对象包含了一个apply方法。

Case类对于有很多参数的生成器来说是个好的容器,因为构造器会给你单独的地方来定义导出的参数并验证输入,构造生成器需要的时候就可以直接访问这个case类的参数了:

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

Chisel中的继承和Module

前面我们已经用了很多次ModuleBundle了,但是还是非常有必要理解一下背后的原理。我们创建的每个Chisel模块都是从基本类型Moduleextends出来的类。我们创建的每个Chisel的IO端口都是从基本类型Bundleextends出来的类(在某些特殊场合,是从Bundle的超类型Record创建的)。Chisel中的硬件类型比如UIntBundle都是Data作为超类型的。我们这里将会探索怎么用面向对象编程来创建一个分层的硬件块并探索对象的复用。关于类型和Data相关的内容会在下一篇里面讲,也就是这个系列的最后一篇。

只要你想在Chisel中创建一个硬件对象,都需要将Module作为超类。继承可能不总是复用的好办法(组合胜过继承是个通用原则),但继承依然很强大。下面的例子中,我们会创建一个Module,这个Module将多个实例有层次地连接到一起。

我们现在创建一个硬件格雷码编码/解码器(格雷码_百度百科 (baidu.com)),执行编码还是解码的选择是硬件可编程的:

import scala.math.pow

// 创建一个module
class GrayCoder(bitwidth: Int) extends Module {
  val io = IO(new Bundle{
    val in = Input(UInt(bitwidth.W))
    val out = Output(UInt(bitwidth.W))
    val encode = Input(Bool()) // 为假的时候进行解码
  })
  
  when (io.encode) { // 编码,右移后按位与就行
    io.out := io.in ^ (io.in >> 1.U)
  } .otherwise { // 解码就很复杂了,具体怎么回事就不解释了
    io.out := Seq.fill(log2Ceil(bitwidth))(Wire(UInt(bitwidth.W))).zipWithIndex.fold((io.in, 0)){
      case ((w1: UInt, i1: Int), (w2: UInt, i2: Int)) => {
        w2 := w1 ^ (w1 >> pow(2, log2Ceil(bitwidth)-i2-1).toInt.U)
        (w2, i1)
      }
    }._1	// ._1表示两元素元组的第一个元素,._2表示第二个
  }
}

跑起来,测试一下:

// 测试
val bitwidth = 4
test(new GrayCoder(bitwidth)) { c =>
  def toBinary(i: Int, digits: Int = 8) = {
    String.format("%" + digits + "s", i.toBinaryString).replace(' ', '0')
  }
  println("Encoding:")
  for (i <- 0 until pow(2, bitwidth).toInt) {
    c.io.in.poke(i.U)
    c.io.encode.poke(true.B)
    c.clock.step(1)
    println(s"In = ${toBinary(i, bitwidth)}, Out = ${toBinary(c.io.out.peek().litValue.toInt, bitwidth)}")
  }

  println("Decoding:")
  for (i <- 0 until pow(2, bitwidth).toInt) {
    c.io.in.poke(i.U)
    c.io.encode.poke(false.B)
    c.clock.step(1)
    println(s"In = ${toBinary(i, bitwidth)}, Out = ${toBinary(c.io.out.peek().litValue.toInt, bitwidth)}")
  }

}

测试通过。

格雷码常用于异构的接口。通常使用格雷码计数器而不是全特征编码/解码器,但我们将用下面的模块来做简化。下面是一个AsyncFIFO(异步FIFO队列)的例子,利用前面的格雷码来构造。控制逻辑和测试这里就省略了,现在主要是看我们的格雷码模块是怎么多次实例化并连接在一起的。

代码如下:

class AsyncFIFO(depth: Int = 16) extends Module {
  val io = IO(new Bundle{
    // write inputs
    val write_clock = Input(Clock())
    val write_enable = Input(Bool())
    val write_data = Input(UInt(32.W))
    
    // read inputs/outputs
    val read_clock = Input(Clock())
    val read_enable = Input(Bool())
    val read_data = Output(UInt(32.W))
    
    // FIFO status
    val full = Output(Bool())
    val empty = Output(Bool())
  })
  
  // 添加额外的位来检查满/空状态
  assert(isPow2(depth), "AsyncFIFO needs a power-of-two depth!")
  val write_counter = withClock(io.write_clock) { Counter(io.write_enable && !io.full, depth*2)._1 }
  val read_counter = withClock(io.read_clock) { Counter(io.read_enable && !io.empty, depth*2)._1 }
  
  // 编码,这里实例化了一个然后连在了write_counter上
  val encoder = new GrayCoder(write_counter.getWidth)
  encoder.io.in := write_counter
  encoder.io.encode := true.B
  
  // 同步,编码器的输出接到移位寄存器
  val sync = withClock(io.read_clock) { ShiftRegister(encoder.io.out, 2) }
  
  // 解码,这里也实例化了一个然后连在了sync上
  val decoder = new GrayCoder(read_counter.getWidth)
  decoder.io.in := sync
  decoder.io.encode := false.B
  
  // 状态逻辑转换啥的,省略了
  
}

可以看出,new一个自定义的模块就可以在其他自定义模块里面使用了,而且可以多次使用。在实现一个复杂电路系统的时候,可能有很多基础的构建块是可以复用的,自定义一个Module然后再有层级地集成到一起,可以让代码更简洁,可读性更强。

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值