主体内容摘自:https://blog.csdn.net/qq_34291505/article/details/87570908
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。
能够表示具体值的数据类型为UInt、SInt和Bool。实际可综合的电路都是若干个bit,所以只能表示整数,这与Verilog是一致的。要表示浮点数,本质还是用多个bit来构建,而且要遵循IEEE的浮点标准。
要表示值,则必须有相应的字面量。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方法有两个版本:
有字面量(如1.U(32.W)
)的数据类型用于赋值、初始化寄存器等操作,而无字面量(如UInt(32.W)
)的数据类型则用于声明端口、构造向量等。
UInt、SInt和Bool三个类都包含四个方法:asUInt、asSInt、asBool和asBools。
另外,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
。
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)
}
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位赋值,其余不变。
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
。
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
}
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不需要重写该方法。
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。
这里要注意的一点是相等性比较的两个符号是“ === ” 和 “ =/= ”。因为“ == ”和“ != ”已经被Scala占用,所以Chisel另设了这两个新的操作符。按照优先级的判断准则,“===”和“=/=”的优先级以首个字符为“=”来判断,也就是在逻辑操作中,相等性比较的优先级要比与、或、异或都高。
某些操作符会发生位宽的改变,这些返回的结果会生成一个自动推断的位宽。如下表所示:
当把一个短位宽的信号值或硬件结构赋值给长位宽的硬件结构时,会自动扩展符号位。但是反过来会报错,并不是像Verilog那样把多余的高位截断,这需要注意(注:最新的chisel3版本已经可以像Verilog一样自动把高位截断了)。
Chisel本质上还是Scala,所以Chisel的泛型就是使用Scala的泛型语法,这使得电路参数化更加方便。无论是Chisel的函数还是模块,都可以用类型参数和上、下界来泛化。在例化模块时,传入不同类型的参数,就可能会产生不同的电路,而无需编写额外的代码,当然前提是逻辑、类型必须正确。
要熟练使用泛型比较麻烦,所需素材很多,这里就不再介绍。读者可以通过阅读Chisel的源码来学习它是如何进行泛型的。
读者在学习本章后,应该理清Chisel数据类型的关系。常用的类型就五种:UInt、SInt、Bool、Bundle和Vec[T],所以重点学会这五种即可。有关三种值类UInt、SInt和Bool的操作符与Verilog差不多,很快就能理解。