吃透Chisel语言.07.Chisel基础(四)——Bundle和Vec

Chisel基础(四)——Bundle和Vec

Chisel基础的前面三篇我们学习了数据类型、组合电路操作符和寄存器,虽然已经足够实现很复杂的数字电路了,但还是不够方便。比如我需要构建一个32个寄存器的寄存器组,那么我需要写32个RegInit吗?再比如我要将几个信号打包到一起,我又该怎么实现呢?Chisel中提供了两种构造用于给相关的信号分组,他们就是BundleVec,其中:

  1. Bundle用于将不同类型的信号划分为一组;
  2. Vec用于表示一个可索引的、相同类型的信号的集合;

这一部分我们就来详细讲解它们。

Bundle介绍

Chisel中Bundle用于将多个信号组合到一起,这个Bundle没有什么好的翻译,我本人倾向于翻译为捆绑包,可以理解为将一堆信号捆绑在一起,但是为了不引起误会,后面这个词我就统一不翻译了。

整个一个bundle可以被当成一个整体来引用,而其中的个体字段都可以通过他们的命名来访问。我们通过定义一个类来定义一个bundle(也就是一组信号的集合),这个类拓展自Bundle类,它的每个字段以val的形式在构造块中给出,比如下面的代码:

class Channel() extends Bundle {
    val data = UInt(32.W)
    val valid = Bool()
}

Channel这个bundle就将datavalid这两个信号捆绑到了一起。如果要使用这个bundle的话,我们可以new一个Channel然后把它封装到一个Wire里面(这里的Wire下一篇就会介绍),然后其中的每个字段用.引用就行:

val ch = Wire(new Channel())
ch.data := 123.U
ch.valid := true.B

val b = ch.valid

.记号是面向对象中常用的标记,x.y就表示x是一个对象的引用而y这是这个对象的一个字段。因为Chisel也是面向对象的,所以我们当然可以用.来访问bundle中的字段。

Chisel中的Bundle跟C里面的struct、VHDL里面的record以及SystemVerilog里面的struct都是类似的。一个bundle也可以作为一个整体来引用:

val channel = ch

Vec介绍

Chisel中的Vec表示一组相同类型信号的集合,也就是一个向量。向量中的每个元素都可以通过索引来访问。显然,Chisel里面的这个向量,和其他编程语言里面的数组是类似的,不过Ararry已经是Scala里面的关键字了,所以Chisel里面用的就是Vec

创建向量通过调用Vec的构造器来实现,这个构造器有两个参数,一个是向量中元素的个数,另一个是向量中元素的类型。向量同样也需要封装到一个Wire里面:

val v = Wire(Vec(3, UInt(4.W)))

向量中的每个元素都可以通过(index)来访问:

v(0) := 1.U
v(1) := 3.U
v(2) := 5.U

val idx = 1.U(2.W)
val a = v(idx)

可以这么理解,封装到Wire里面的向量就是个多路选择器了,选择信号就对应着向量的索引,这是打包为Wire的情况。我们也可以把向量打包成Reg,以此来定义一组寄存器。

比如下面这个例子定义了一个处理器的寄存器组,总共有32个32位的寄存器,这是经典的32位RISC处理器的寄存器实现,比如RISC-V的32位版本。代码如下:

val registerFile = Reg(Vec(32, UInt(32.W)))

看到没有,这比傻乎乎地写32个寄存器是不是强多了?

同样,寄存器堆的元素(即某个寄存器)也可以使用索引来访问并且跟正常寄存器的使用是一样的:

registerFile(idx) := dIn
val dOur = registerFile(idx)

BundleVec的使用

我们可以自由地把BundleVec放到一起用。

比如我们可以用一个bundle创建一个向量,我们需要给向量字段传递一个原型,比如对于上面的Channel,我们可以用下面的代码定义一个Channel向量:

val vecBundle = Wire(Vec(8, new Channel()))

Bundle里面同样也可以有向量,比如:

class BundleVec extends Bundle {
    val field = UInt(8.W)
    val vector = Vec(4, UInt(8.W))
}

如果我们希望创建一个有复位值的bundle类型的寄存器时,我们首先得创建那个bundle的Wire,按需设置每个字段的初始值,然后再将它传递给RegInit

val initVal = Wire(new Channle())
initVal.data := 0.U
initVal.valid := false.B

val channelReg = RegInit(initVal)

通过组合使用BundleVec等构造,我们可以定义自己的数据结构,这就是Chisel体现强大抽象能力的地方之一。

部分赋值和Bundle

在Chisel中进行部分赋值是不允许的,虽然在Chisel2中可以,在Verilog和VHDL中也是可以的,比如下面的代码就会在电路展开的时候报错:

val assignWord = Wire(UInt(16.W))
assignWord(7, 0) := lowByte
assignWord(15, 8) := highByte

在这种情况下,使用Bundle就是个很好的解决方案。比如首先创建一个局部bundle,然后给这个bundle创建一个Wire,然后再给每个独立的字段赋值,最后用asUInt()把这个bundle转换成UInt并赋值给目标UInt。注意,我们把这个Bundle定义为局部的数据结构是因为只在这里会用到。代码如下:

val assignWord = Wire(UInt(16.W))

class Split extends Bundle {
    val high = UInt(8.W)
    val low = UInt(8.W)
}

val split = Wire(new Split())
split.low := lowByte
split.high := highByte

assignWord := split.asUInt

不过这种方法有个缺点,那就是我们需要知道bundle里面的字段合并成一个位向量之后的顺序是什么样的,比如这里的highlow就不能弄反了。

另一种方法就是用Bool构造向量,这样的话每个值都可以独立地赋值了,最后一样地转换成UInt就好:

val vecResult = Wire(Vec(4, Bool()))

vecResult(0) := data(0)
vecResult(1) := data(1)
vecResult(2) := data(2)
vecResult(3) := data(3)

val uintResult = vecResult.asUInt

结语

BundleVec在Chisel中非常重要,熟练掌握可以极大提高写代码的效率,应对更复杂、更需要模块化的场景也会更游刃有余。这篇文章里还用到了另一个核心概念Wire,也就是Verilog中的wire,即线网。这个Wire和前面说的UIntSInt啥的不一样,它直接就是个表示硬件的类型,除Wire外,Chisel中RegIO也是直接硬件的类型,下一部分就来谈一谈这三者,进一步说一说如何理解Chisel生成电路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值