chisel常见数据类型详解(更新)

主体内容摘自:https://blog.csdn.net/qq_34291505/article/details/87570908

一、Chisel的数据类型

Chisel定义了自己的一套数据类型,读者应该跟Scala的九种基本值类区分开来。而且Chisel也能使用Scala的数据类型,但是Scala的数据类型都是用于参数和内建控制结构,构建硬件电路还是得用Chisel自己的数据类型,在使用时千万不要混淆。当前Chisel定义的数据类型如下图所示,其中绿色方块是class,红色是object,蓝色是trait,箭头指向的是超类和混入的特质:
在这里插入图片描述
所有数据类型都继承自抽象基类Data,它混入了两个特质HasId和NamedComponent。如果读者查看Chisel3的源代码,就会看到很多参数传递时都用下界表明了是Data的子类。在实际硬件构成里,并不会用到Data,读者也不用关心它的具体实现细节。更多的,应该关注Data类的两大子类:聚合类Aggregate和元素类Element。

聚合类Aggregate

常用子类是向量类Vec[T]和包裹类Bundle。

  • Vec[T]类用于包含相同的元素,元素类型T可以是任意的Data子类。因为Vec[T]混入了特质IndexedSeq[T],所以向量的元素能从下标0开始索引访问。

  • Bundle类用于被自定义的类继承,这样自定义的类就能包含任意Data的子类对象,常用于协助构造模块的端口,故而衍生出了一些预定义的端口子类。

  • 混合向量类MixedVec[T]是Chisel3.2以上版本添加的语法,它与Vec[T]的不同在于可以包含不同类型的元素。

Element类

衍生出了Analog、Bits和Clock三个子类,单例对象DontCare和特质Reset。

  • Analog用于在黑盒中模拟inout端口,目前在实际Chisel里并无其他用途。
  • Bits类的两个子类SInt和UInt是最常用的两个数据类型,它们是用补码表示的有符号整数和无符号整数。不仅用来协助定义端口位宽,还用来进行赋值。
  • FixedPoint类提供的API带有试验性质,而且将来可能会发生改变,所以不常用。
  • Bool类是Chisel自己的布尔类型,区别于Scala的Boolean。Bool类是UInt类的子类,因为它可以看成是1bit的UInt,而且它被混入Reset特质,因为复位信号都是用Bool类型的线网或寄存器使能的。此外,Bits类混入了特质ToBoolable,也就是说FixedPoint、SInt和UInt都能转换成多bit的Bool类型。
  • Clock类表示时钟,Chisel里的时钟是专门的一个类型,并不像Verilog里那样是1bit的线网。复位类型Reset也是如此。
  • 单例对象DontCare用于赋值给未驱动的端口或线网,防止编译器报错。

二、数据字面量

能够表示具体值的数据类型为UInt、SInt和Bool。实际可综合的电路都是若干个bit,所以只能表示整数,这与Verilog是一致的。要表示浮点数,本质还是用多个bit来构建,而且要遵循IEEE的浮点标准。

  • 对于UInt,可以构成任意位宽的线网或寄存器。
  • 对于SInt,在Chisel里会按补码解读,转换成Verilog后会使用系统函数$signed,这是可综合的。
  • 对于Bool,转换成Verilog后就是1bit的线网或寄存器。

要表示值,则必须有相应的字面量。Chisel定义了一系列隐式类:fromBigIntToLiteral、fromtIntToLiteral、fromtLongToLiteral、fromStringToLiteral、fromBooleanToLiteral

回顾前面讲述的隐式类的内容,也就是会有相应的隐式转换。以隐式类fromtIntToLiteral为例,存在一个同名的隐式转换,把相应的Scala的Int对象转换成一个fromtIntToLiteral的对象。而fromtIntToLiteral类有两个方法U和S,分别构造一个等值的UInt对象和SInt对象。再加上Scala的基本值类都是用字面量构造对象,所以要表示一个UInt对象,可以写成“1.U”的格式,这样编译器会插入隐式转换,变成“fromtIntToLiteral(1).U”,进而构造出字面值为“1”的UInt对象。同理,也可以构造SInt。还有相同行为的方法asUInt和asSInt

从几个隐式类的名字就可以看出,可以通过BigInt、Int、Long和String四种类型的Scala字面量来构造UInt和SInt

按Scala的语法,其中BigInt、Int、Long三种类型默认是十进制的,但可以加前缀“0x”或“0X”变成十六进制。

对于String类型的字面量,Chisel编译器默认也是十进制的,但是可以加上首字母“h”、“o”、“b”来分别表示十六进制、八进制和二进制。此外,String字面量可以用下划线间隔。

可以通过Boolean类型的字面量——true和false——来构造fromBooleanToLiteral类型的对象,然后调用名为B和asBool的方法进一步构造Bool类型的对象。

1.U       // decimal 1-bit lit from Scala Int.
0x100.U   // hexadecimal 9-bit lit
"ha".U    // hexadecimal 4-bit lit from string.
"o12".U   // octal 4-bit lit from string.
"b1010".U // binary 4-bit lit from string.
"h_dead_beef".U   // 32-bit lit of type UInt

5.S    // signed decimal 4-bit lit from Scala Int.
-8.S   // negative decimal 4-bit lit from Scala Int.
5.U    // unsigned decimal 3-bit lit from Scala Int.

8.U(4.W) // 4-bit unsigned decimal, value 8.
-152.S(32.W) // 32-bit signed decimal, value -152.

true.B // Bool lits from Scala lits.
false.B

三、数据宽度

默认情况下,数据的宽度按字面值取最小,例如字面值为“8”的UInt对象是4位宽,SInt就是5位宽。但是也可以指定宽度。在Chisel2里,宽度是由Int类型的参数表示的,而Chisel3专门设计了宽度类Width。还有一个隐式类fromIntToWidth,就是把Int对象转换成fromIntToWidth类型的对象,然后通过方法W返回一个Width对象

方法U、asUInt、S和asSInt都有一个重载的版本,接收一个Width类型的参数,构造指定宽度的SInt和UInt对象注意,Bool类型固定是1位宽。例如:

1.U              // 字面值为“1”、宽度为1bit的UInt对象
1.U(32.W)   // 字面值为“1”、宽度为32bit的UInt对象

UInt、SInt和Bool都不是抽象类,除了可以通过字面量构造对象以外,也可以直接通过apply工厂方法构造没有字面量的对象。UInt和SInt的apply方法有两个版本:

  • 一个版本接收Width类型的参数构造指定宽度的对象,
  • 另一个则是无参版本构造位宽可自动推断的对象。

有字面量(如1.U(32.W))的数据类型用于赋值、初始化寄存器等操作,而无字面量(如UInt(32.W))的数据类型则用于声明端口、构造向量等。

四、类型转换

UInt、SInt和Bool三个类都包含四个方法:asUInt、asSInt、asBool和asBools

  • 其中asUInt和asSInt分别把字面值按无符号数和有符号数解释,并且位宽不会变化,要注意转换过程中可能发生符号位和数值的变化。例如,3bit的UInt值“b111”,其字面量是“7”,转换成SInt后字面量就变成了“-1”。
  • asBool会把1bit的“1”转换成Bool类型的true,“0”转换成false。
  • 如果位宽超过1bit,则用asBools转换成Bool类型的序列Seq[Bool]。

另外,Bool类还有一个方法asClock,把true转换成电压常高的时钟,false转换成电压常低的时钟。

Clock类只有一个方法asUInt,转换成对应的0或1。

val bool: Bool = false.B        // always-low wire
val clock = bool.asClock        // always-low clock

clock.asUInt                    // convert clock to UInt (width 1)
clock.asUInt.asBool             // convert clock to Bool (Chisel 3.2+)

五、向量与混合向量

①向量的定义

如果需要一个集合类型的数据,除了可以使用Scala内建的数组、列表、集等数据结构外,还可以使用Chisel专属的Vec[T]。T必须是Data的子类,而且每个元素的类型、位宽必须一样。

Vec[T]的伴生对象里有一个apply工厂方法,接收两个参数,第一个是Int类型,表示元素的个数,第二个是元素。它属于可索引的序列,下标从0开始。例如:

val myVec = Wire(Vec(3, UInt(32.W)))

还有一个工厂方法VecInit[T],通过接收一个Seq[T]这里的Seq包括seq、array、list、tuple、queue等集合)作为参数来构造向量,或者是多个重复参数。不过,这个工厂方法常把有字面值的数据作为参数,用于初始化寄存器组、ROM、RAM等,或者用来构造多个模块。

  val Vec1 = VecInit(1.U, 2.U, 3.U, 4.U)//重复参数
  val Vec2 = VecInit(Seq.fill(8)(0.U(8.W)))//序列

因为Vec[T]也是一种序列,所以它也定义了诸如map、flatMap、zip、foreach、filter、exists、contains等方法。尽管这些方法应该出现在软件里,但是它们也可以简化硬件逻辑的编写,减少手工代码量。


②混合向量的定义

混合向量MixedVec[T]与普通的向量Vec[T]类似,只不过包含的元素可以不全都一样,比如位宽不一样。它的工厂方法是通过重复参数或者序列作为参数来构造的:

  val Vec1 = MixedVec(UInt(8.W), UInt(16.W), UInt(32.W))//重复参数
  或者
  val Vec2 = MixedVec(Array(UInt(8.W), UInt(16.W), UInt(32.W)))//序列

并且也有一个叫MixedVecInit[T]的单例对象,也是通过重复参数或者序列作为参数来构造的:

 val Vec1 = MixedVecInit(1.U, 2.U, 3.U, 4.U)//重复参数
 或者
 val Vec2 = MixedVecInit(Seq.fill(8)(0.U(8.W)))//序列

从上面也看出来了,对于可以传入序列的向量,它们的序列参数并不一定要逐个手写,可以通过Scala的函数,比如fill、map、flatMap、to、until等来生成。如下所示:

val mixVec = MixedVec((1 to 10) map { i => UInt(i.W) })//序列
val mixVecinit = MixedVecInit(Seq.fill(8)(0.U(8.W)))//序列
val vecinit= VecInit(Seq.fill(4)(4.U(8.W)))//序列

注:关于向量定义需要注意的

Vec和MixedVec定义的时候,最好不要直接给确切的值,只需要给出Chisel type,之后再给元素单独赋值即可,如上面的UInt(32.W)即可,否则会报错。原因是:vec接收的是数据类型,而带字面量的数据如 1.U 会被认为是硬件类型,就会报错,你自己可以试一下。

同样,VecInit和MixedVecInit必须给出确切的初始化的值,不能只给Chisel type。原因是:vecinit接收的是硬件类型,而Chisel type如UInt(32.W)是数据类型,如果传入就会报错,你自己也可以试一下。


③Vec和UInt的互相转换

Vec和UInt的转换,需要借助Bool类型的数据。所以中间需要使用到asBools和asUInt

  • 使用asBools将UInt转换成Vec
import chisel3._

class Foo extends RawModule {
  val uint = 0xc.U
  val vec = VecInit(uint.asBools)

  printf(p"$vec") // Vec(0, 0, 1, 1)

  // Test
  assert(vec(0) === false.B)
  assert(vec(1) === false.B)
  assert(vec(2) === true.B)
  assert(vec(3) === true.B)
}
  • 使用asUInt将Vec转换成UInt
import chisel3._

class Foo extends RawModule {
  val vec = VecInit(true.B, false.B, true.B, true.B)
  val uint = vec.asUInt

  printf(p"$uint") // 13

  // Test
  // (remember leftmost Bool in Vec is low order bit)
  assert(0xd.U === uint)

}

④向量和混合向量的维度与索引

myVec 其实是一个二维数据,因为每个元素都是32位宽的数据,每个bit都可以被索引到,如下:

val myVec = Wire(Vec(3, UInt(32.W)))

myVec(0)(5)//索引vec第一个元素的第6个bit

myVec(0)(3,0)//索引vec第一个元素的低4位

索引到所需bit后可以将其赋值给其他变量,但是最后一维的子字是只读的,也即你不能对其赋值,如:

myVec(0)(3,0) := 1.U(4.W)

上面的代码会报错!!!如果想要对最后一维进行赋值,可以参考以下⑤中的方法。需要注意的是,这里说的是最后一维,其实和下面说的Bits类型是一致的,因为向量的最后一维对应的就是Bits类型,比如SInt和UInt。

假如现在有一个vec1,只想对它的第一个元素的低4位赋值,其余不变。

  • 可行的办法1
val Vec1 = Wire(Vec(3, UInt(32.W)))

Vec1(0) := 1.U(32.W)
Vec1(1) := 1.U(32.W)
Vec1(2) := 1.U(32.W)

val Vec2 = Wire(Vec(3, UInt(32.W)))

Vec2(0) := 1.U(32.W)
Vec2(1) := 1.U(32.W)
Vec2(2) := 1.U(32.W)

val bools = VecInit(Vec1(0).asBools)

val seq = 1.U(4.W).asBools

for (i <- 0 until 4){ 
    
    bools(i) := seq(i)
    
}

Vec2(0) := bools.asUInt
Vec1(0) := Vec2(0)

需要注意的是,bools之所以加上了VecInit,是为了后面使用asUInt方法,否则seq是没有该方法的,我们就没办法转换成UInt

  • 可行的办法2
val Vec1 = Wire(Vec(3, UInt(32.W)))

Vec1(0) := 1.U(32.W)
Vec1(1) := 1.U(32.W)
Vec1(2) := 1.U(32.W)

val Vec2 = Wire(Vec(3, UInt(32.W)))

Vec2(0) := 1.U(32.W)
Vec2(1) := 1.U(32.W)
Vec2(2) := 1.U(32.W)
Vec1(0) := Cat(Vec2(0)(31,4),1.U(4.W))

之所以定义了和Vec1完全一样的中间变量Vec2,是因为上述操作涉及到了vec1的组合逻辑loop循环错误,也即等号两边都有vec1,这对wire变量来说是不可以的。除非你定义成reg变量,但是这就脱离了原本的组合逻辑。

当然,中间变量如何定义,如何操作,根据自己的需求决定即可,但避免上述所说的错误是必须的。

⑤子字赋值

在Verilog中,可以直接给向量的某几位赋值。同样,Chisel受限于Scala,不支持直接给Bits(FixedPoint、SInt和UInt)类型的某几位赋值。子字赋值的可行办法是先调用Bits类型的asBools方法。该方法根据调用对象的0、1排列返回一个相应的Seq[Bool]类型的结果,并且低位在序列里的下标更小,比如第0位的下标就是0、第n位的下标就是n。然后用这个Seq[Bool]对象配合VecInit构成一个向量,此时就可以给单个比特赋值。

注意,必须都是Bool类型,要注意赋值前是否需要类型转换。子字赋值完成后,元素为Bool的VecInit向量再调用asUInt、asSInt方法转换回来。例如:

class TestModule extends Module {
   val io = IO(new Bundle {
       val in = Input(UInt(10.W))
       val bit = Input(Bool())
       val out = Output(UInt(10.W))
   })
   val bools = VecInit(io.in.asBools)
   bools(0) := io.bit
   io.out := bools.asUInt
}

六、包裹Bundle

1、Bundle 基本介绍

抽象类Bundle很像C语言的结构体(struct),用户可以编写一个自定义类来继承自它,然后在自定义的类里包含其它各种Data类型的字段。它可以协助构建线网或寄存器,例如:

class MyFloat extends Bundle {
  val sign        = Bool()
  val exponent    = UInt(8.W)
  val significand = UInt(23.W)
}

class ModuleWithFloatWire extends RawModule {
  val x  = Wire(new MyFloat)
  val xs = x.sign
}

但是最常见的用途是用于构建一个模块的端口列表,或者一部分端口。例如:

class MyModule extends Module {
   val io = IO(new Bundle {
       val in = Input(UInt(32.W))
       val out = Output(UInt(32.W))
   })

Bundle可以和UInt进行相互转换。Bundle类有一个方法asUInt,可以把所含的字段拼接成一个UInt数据,并且前面的字段在高位。例如:

class MyBundle extends Bundle {
   val foo = UInt(4.W)  // 高位
   val bar = UInt(4.W)  // 低位
}

val bundle = Wire(new MyBundle)
bundle.foo := 0xc.U
bundle.bar := 0x3.U
val uint = bundle.asUInt  // 12*16 + 3 = 195

我们还可以使用asTypeOf方法将UInt转换成Bundle类型,例如:

class MyBundle extends Bundle {
   val foo = UInt(4.W)  // 高位
   val bar = UInt(4.W)  // 低位
}

val uint = 0xb4.U
val bundle = uint.asTypeOf(new MyBundle)  // foo = 11, bar = 4

2、使用Bundle拆包一个值(给拼接变量赋值)
在Verilog中,左侧的赋值对象可以是一个拼接起多个变量的值,例如:

wire [1:0] a;
wire [3:0] b;
wire [2:0] c;
wire [8:0] z = [...];
assign {a, b, c} = z;

在Chisel里不能直接这么赋值。最简单的做法是先定义一个a、b、c组成的Bundle,高位定义在前面,然后创建线网z。线网z可以被直接赋值,被赋值后,z再调用方法asTypeOf。该方法接收一个Data类型的参数,可以把调用对象强制转换成参数的类型并返回,在这里也就是把a、b、c组成的Bundle作为参数。注意,返回结果是一个新对象,并没有直接修改调用对象z。强制转换必须保证不会出错。例如:

class MyBundle extends Bundle {
  val a = UInt(2.W)
  val b = UInt(4.W)
  val c = UInt(3.W)
}

val z = Wire(UInt(9.W))
z := ...
val unpacked = z.asTypeOf(new MyBundle)
unpacked.a
unpacked.b
unpacked.c

3、参数化的Bundle

因为Chisel是基于Scala和JVM的,所以当一个Bundle类的对象用于创建线网、IO等操作时,它并不是把自己作为参数,而是交出自己的一个复制对象,也就是说编译器需要知道如何来创建当前Bundle对象的复制对象。Chisel提供了一个内部的API函数cloneType,任何继承自Data的Chisel对象,要复制自身时,都是由cloneType负责返回该对象的复制对象。它对应的用户API则是chiselTypeOf

当自定义的Bundle的主构造方法没有参数时,Chisel会自动推断出如何构造Bundle对象的复制,原因很简单,因为构造一个新的复制对象不需要任何参数,仅仅使用关键字new就行了。但是,如果自定义的Bundle带有参数列表,那么Chisel就无法推断了,因为传递进去的参数可以是任意的,并不一定就是完全地复制。此时需要用户自己重写Bundle类的cloneType方法,其形式为:

override def cloneType = (new CustomBundle(arguments)).asInstanceOf[this.type]

例如:

class ExampleBundle(a: Int, b: Int) extends Bundle {
   val foo = UInt(a.W)
   val bar = UInt(b.W)
   override def cloneType = (new ExampleBundle(a, b)).asInstanceOf[this.type]
}

class ExampleBundleModule(btype: ExampleBundle) extends Module {
   val io = IO(new Bundle {
       val out = Output(UInt(32.W))
       val b = Input(chiselTypeOf(btype))
   })
   io.out := io.b.foo + io.b.bar
}

class Top extends Module {
   val io = IO(new Bundle {
       val out = Output(UInt(32.W))
       val in = Input(UInt(17.W))
   })
   val x = Wire(new ExampleBundle(31, 17))
   x := DontCare
   val m = Module(new ExampleBundleModule(x))
   m.io.b.foo := io.in
   m.io.b.bar := io.in
   io.out := m.io.out
}

例子中的ExampleBundle有两个参数,编译器无法在复制它的对象时推断出这两个参数是什么,所以重写的cloneType方法需要用户手动将两个参数传入,而且用asInstanceOf[this.type]保证返回对象的类型与this对象是一样的。

如果没有这个重写的cloneType的方法,编译器会提示把ExampleBundle的参数变成固定的和可获取的,以便cloneType方法能被自动推断,即非参数化Bundle不需要重写该方法。

  • 此外,变量x必须要用Wire包住ExampleBundle的对象,否则x在传递给ExampleBundleModule时,编译器会提示应该传入一个硬件而不是裸露的Chisel类型,并询问是否遗漏了Wire()或IO()。
  • 与之相反,“Input(chiselTypeOf(btype))”中的chiselTypeOf方法也必不可少,因为此时传入的btype是一个硬件,编译器会提示Input的参数应该是Chisel类型而不是硬件,需要使用方法chiselTypeOf解除包住ExampleBundle对象的Wire。

这个例子中,cloneType在构造复制对象时,仅仅是传递了对应的参数,这就会构造一个一模一样的新对象。为了进一步说明cloneType的作用,再来看一个“别扭”的例子:

class TestBundle(a: Int, b: Int) extends Bundle {
  val A = UInt(a.W)
  val B = UInt(b.W)
  override def cloneType = (new TestBundle(5*b, a+1)).asInstanceOf[this.type]
}

class TestModule extends Module {
  val io = IO(new Bundle {
    val x = Input(UInt(10.W))
    val y = Input(UInt(5.W))
    val out = Output(new TestBundle(10, 5))
  })

  io.out.A := io.x
  io.out.B := io.y
}

这里,cloneType在构造复制对象前,先把形参a、b做了一些算术操作,再传递给TestBundle的主构造方法使用。按常规思路,代码“Output(new TestBundle(10, 5))”应该构造两个输出端口:10bit的A和5bit的B。但实际生成的Verilog如下:

module TestModule(
  input         clock,
  input         reset,
  input  [9:0]  io_x,
  input  [4:0]  io_y,
  output [24:0] io_out_A,
  output [10:0] io_out_B
);
  assign io_out_A = {{15'd0}, io_x};
  assign io_out_B = {{6'd0}, io_y};
endmodule 

也就是说,“Output(new TestBundle(10, 5))”的真正形式应该是“Output((new TestBundle(10, 5)).cloneType)”,即Output的真正参数是对象TestBundle(10, 5)的cloneType方法构造出来的对象。而cloneType方法是用实参“5 * 5(b)”和“10(a) + 1”来分别赋予形参a和b,因此得出A的实际位宽是25bit,B的实际位宽是11bit。

七、Chisel的内建操作符

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里要注意的一点是相等性比较的两个符号是“ === ” 和 “ =/= ”。因为“ == ”和“ != ”已经被Scala占用,所以Chisel另设了这两个新的操作符。按照优先级的判断准则,“===”和“=/=”的优先级以首个字符为“=”来判断,也就是在逻辑操作中,相等性比较的优先级要比与、或、异或都高。

八、位宽推断

某些操作符会发生位宽的改变,这些返回的结果会生成一个自动推断的位宽。如下表所示:
在这里插入图片描述
当把一个短位宽的信号值或硬件结构赋值给长位宽的硬件结构时,会自动扩展符号位。但是反过来会报错,并不是像Verilog那样把多余的高位截断,这需要注意(注:最新的chisel3版本已经可以像Verilog一样自动把高位截断了)。

九、Chisel泛型

Chisel本质上还是Scala,所以Chisel的泛型就是使用Scala的泛型语法,这使得电路参数化更加方便。无论是Chisel的函数还是模块,都可以用类型参数和上、下界来泛化。在例化模块时,传入不同类型的参数,就可能会产生不同的电路,而无需编写额外的代码,当然前提是逻辑、类型必须正确。

要熟练使用泛型比较麻烦,所需素材很多,这里就不再介绍。读者可以通过阅读Chisel的源码来学习它是如何进行泛型的。

十、总结

读者在学习本章后,应该理清Chisel数据类型的关系。常用的类型就五种:UInt、SInt、Bool、Bundle和Vec[T],所以重点学会这五种即可。有关三种值类UInt、SInt和Bool的操作符与Verilog差不多,很快就能理解。

  • 12
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

耐心的小黑

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值