Scala和Chisel中的Collections(集合类实现RISC-V寄存器文件)
动机
无论是数量可变的对象,生成器都经常处理IO接口、模块或测试向量。(Collections)这是处理这种情况的一个非常重要的节将介绍Scala集合,并介绍如何用它们来实现Chisel生成器。最后,本节介绍的内容将用于实现RISC-V寄存器文件(Register File)。
首先要注意的是,我们需要添加一个新的引用,因为mutable.ArrayBuffer
在scala.collection
里面。
import scala.collection._
生成器和集合
这一小节,将着重讲解生成器的概念,并使用Scala集合作为实现生成器的工具。Chisel代码被视为一个电路实例,即对特定电路的描述,而是作为电路的生成器。
class My4ElementFir(b0: Int, b1: Int, b2: Int, b3: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W)) val out = Output(UInt(8.W)) }) val x_n1 = RegNext(io.in, 0.U) val x_n2 = RegNext(x_n1, 0.U) val x_n3 = RegNext(x_n2, 0.U) io.out := io.in * b0.U(8.W) x_n1 * b1.U(8.W) x_n2 * b2.U(8.W) x_n3 * b3.U(8.W) }
这个电路是一种简单的生成器,因为可以生成不同系数的四拍FIR滤波器电路。但是如果你想要更多的电路呢?我们需要通过以下步骤来解决:
- 构建可配置拍数的可配置拍数FIR的Scala软件模型;
- 重新设计模型测试,确认模型有效;
- 重构我们的
My4ElementFir
允许拍数配置; - 用新的测试套件测试新的电路;
用Scala构建可配置的FIR软件模型
下面是一个Scala编写的FIR路的软件实现:
/** * A naive implementation of an FIR filter with an arbitrary number of taps. */
class ScalaFirFilter(taps: Seq[Int]) {
var pseudoRegisters = List.fill(taps.length)(0)
def poke(value: Int): Int = {
pseudoRegisters = value :: pseudoRegisters.take(taps.length - 1)
var accumulator = 0
for(i <- taps.indices) {
accumulator += taps(i) * pseudoRegisters(i)
}
accumulator
}
}
从以下几个步骤来看:
Seq
:注意taps
的类型变成了Seq[Int]
,这意味着创建这个类的实例时可以传递任意长度的Int
序列;Registers
:利用var pseudoRegisters = List.fill(taps.length)(0)
,我们创建了一个List
来存放前面周期的值。选择List
是因为加一个元素到头部和从尾部移出一个元素很容易。Scala集合类族的几乎任何成员都可能会被用到。这里我们还讲列表全都初始化为0了;poke
:这个类还添加了一个poke
函数,用来模拟将一个输入放入滤波器并循环一个时钟周期的过程;- 寄存器更新:
pseudoRegisters = value :: pseudoRegisters.take(taps.length - 1)
这一行使用了take
方法将列表的最后一个元素取出来,然后使用::
列表连接运算符将value
加到缩短后的列表的头部; - 计算输出:最后就是一个简单的循环来计算每个元素和对应拍的系数的乘积的累加和。最后一行
accumulator
返回计算结果;
将测试适配到新的软件FIR模型上
现在需要验证我们的模型是正确的,之前的测试代码只需要改一点点就行了:
val filter = new ScalaFirFilter(Seq(1, 1, 1, 1))
var out = 0
out = filter.poke(1)
println(s"out = $out")
assert(out == 1) // 1, 0, 0, 0
out = filter.poke(4)
assert(out == 5) // 4, 1, 0, 0
println(s"out = $out")
out = filter.poke(3)
assert(out == 8) // 3, 4, 1, 0
println(s"out = $out")
out = filter.poke(2)
assert(out == 10) // 2, 3, 4, 1
println(s"out = $out")
out = filter.poke(7)
assert(out == 16) // 7, 2, 3, 4
println(s"out = $out")
out = filter.poke(0)
assert(out == 12) // 0, 7, 2, 3
println(s"out = $out")
这里的软件FIR模型和之前的My4ElementFir
运行效果一致,测试通过。
用软件FIR模型测试FIR电路
现在我们有足够的理由相信我们的软件模型了,我们可以重写测试代码,比较FIR电路的输出和软件模型输出是否一致。显而易见的好处就是,不需要再手动写测试案例了,很快啊!
val goldenModel = new ScalaFirFilter(Seq(1, 1, 1, 1))
test(new My4ElementFir(1, 1, 1, 1)) {
c =>
for(i <- 0 until 100) {
val input = scala.util.Random.nextInt(8)
val goldenModelResult = goldenModel.poke(input)
c.io.in.poke(input.U)
c.io.out.expect(goldenModelResult.U, s"i $i, input $input, gm $goldenModelResult, ${
c.io.out.peek().litValue}")
c.clock.step(1)
}
}
测试通过,这里共运行了100个周期,检查了用两种方法实现的模型在每一个周期的行为都是同步的。
要注意的事
- 要在正确的位置驱动
step
,软件和硬件的执行是不一样的,很容易就会出错; - 这个测试很弱,因为对IO端口和寄存器的尺寸是很敏感的。实现一个在任意数据位宽上观察封装行为的软件黄金模型是很复杂的。这里我们只能保证在这个值上通过测试了。
参数化的FIR生成器
下面我们创建了一个新的滤波器类,MyManyElementFir
接收一个常数Seq
用于每拍的系数。这个序列可以有任意数量的元素。此外,还添加了一个bitWidth
来控制电路可以处理的数据宽度。为了应对可变长度,我们不得不重构寄存器和创建和连接。代码中所有的方法是用集合函数可用的库的一个简单的子集。后面的部分还会展示更简洁的方法来表达这种行为,以更清晰看出干了些啥。
class MyManyElementFir(consts: Seq[Int], bitWidth: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(bitWidth.W))
val out = Output(UInt(bitWidth.W))
})
val regs = mutable.ArrayBuffer[UInt]()
for(i <- 0 until consts.length) {
if(i == 0) regs += io.in
else regs += RegNext(regs(i - 1), 0.U)
}
val muls = mutable.ArrayBuffer[UInt]()
for(i <- 0 until consts.length) {
muls += regs(i) * consts(i).U
}
val scan = mutable.ArrayBuffer[UInt]()
for(i <- 0 until consts.length) {
if(i == 0) scan += muls(i)
else scan += muls(i) + scan(i - 1)
}
io.out := scan.last
}
下面讲一下这段代码做了哪些事情:
首先,代码里有三个并行的代码段,分别从第7、13和18行开始。这里使用了一个Scala集合类型ArrayBuffer
。ArrayBuffer
允许使用+=
操作符附加元素到末尾。我们开始创建了一个regs
,这个ArrayBuffer
的元素是UInt
类型。然后在tap上迭代,添加输入作为第一个元素,后面使用RegNext
来把后续元素的输入连接到上一个元素(regs(i-1)
)并初始化为0(0.U
)。这些寄存器会存放输入的先前的值,因为需要他们进行计算。
第二部分,我们创建了muls
,这是另一个ArrayBuffer
,也是个UInt
的队列。每个元素都会称为一个结点,第i
个元素的值等于regs(i)
和const(i)
的乘积。
第三部分是scan
,这一部分累加muls
的元素,最后使用scan.last
方法获取scan
的最后一个元素。这种方法在regs
构造过程中比使用regs(i-1)
更加优雅一些。
测试参数化的FIR生成器
现在看看新的FIR生成器好不好用,我们用它创建一个类似My4ElementFir
的实例,然后测试更多的数据:
val goldenModel = new ScalaFirFilter(Seq(1, 1, 1, 1))
test(new MyManyElementFir(Seq(1, 1, 1, 1), 8)) {
c =>
for(i <- 0 until 100) {
val input = scala.util.Random.nextInt(8)
val goldenModelResult = goldenModel.poke(input)
c.io.in.poke(input.U)
c.io.out.expect(goldenModelResult.U, s"i $i, input $input, gm $goldenModelResult, ${
c.io.out.peek().litValue}")
c.clock.step(1)
}
}
测试通过。
更多不同尺寸FIR滤波器的测试
首先创建一个方法r
,用来获取一个随机数;然后是方法runOneTest
,会为指定taps的滤波器创建一个软件模型和一个硬件模拟;然后至少运行拍数两倍以上的数据经过滤波器:
/** a convenience method to get a random integer */
def r(): Int = {
scala.util.Random.nextInt(1024)
}
/** * run a test comparing software and hardware filters * run for at least twice as many samples as taps */
def runOneTest(taps: Seq[Int]) {
val goldenModel = new ScalaFirFilter(taps)
test(new MyManyElementFir(taps, 32)) {
c =>
for(i <- 0 until 2 * taps.length) {
val input = r()
val goldenModelResult = goldenModel.poke(input)
c.io.in.poke(input.U)
c.io.out.expect(goldenModelResult.U, s"i $i, input $input, gm $goldenModelResult, ${
c.io.out.peek().litValue}")
c.clock.step(1)
}
}
}
for(tapSize <- 2 until 100 by 10) {
val taps = Seq.fill(tapSize)(r()) // create a sequence of random coefficients
runOneTest(taps)
}
测试通过。
500个taps的滤波器(就是玩儿)
下面这行代码会在一个500拍的FIR滤波器上运行测试,可能需要等很久运行才能结束:
runOneTest(Seq.fill(500)(r()))
硬件集合
运行时可配置taps的FIR滤波器
下面的代码添加了一个额外的consts
向量到我们FIR生成器的IO接口,允许在电路生成后从外部更改系数。这是通过Chisel中的集合类型Vec
完成的。Vec
支持很多Scala集合的方法,但是只能包含Chisel的硬件元素。Vec
应该只被用于Scala集合不好使的情况,一般有以下两种情形:
- 需要在Bundle中创建集合,典型的是要被用作IO接口的Bundle;
- 需要通过索引访问作为硬件的一部分的集合(比如寄存器文件);
我们可以利用Vec
构建一个运行时可配置taps的FIR滤波器:
class MyManyDynamicElementVecFir(length: Int) extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
val out = Output(UInt(8.W))
val consts = Input(Vec(length, UInt(8.W)))
})
// Reference solution
val regs = RegInit(VecInit(Seq.fill(length - 1)(0.U(8.W))))
for(i <- 0 until length - 1) {
if(i == 0) regs(i) := io.in
else regs(i) := regs(i - 1)
}
val muls = Wire(Vec(length, UInt(8.W)))
for(i <- 0 until length) {
if(i == 0) muls(i) := io.in * io.consts(i)
else muls(i) := regs(i - 1) * io.consts(i)
}
val scan = Wire(Vec(length, UInt(8.W)))
for(i <- 0 until length) {
if(i == 0) scan(i) := muls(i)
else scan(i) := muls(i) + scan(i - 1)
}
io.out := scan(length - 1)
}
测试代码如下:
val goldenModel = new ScalaFirFilter(Seq(1, 1, 1, 1))
test(new MyManyDynamicElementVecFir(4)) {
c =>
c.io.consts(0).poke(1.U)
c.io.consts(1).poke(1.U)
c.io.consts(2).poke(1.U)
c.io.consts(3).poke(1.U)
for(i <- 0 until 100) {
val input = scala.util.Random.nextInt(8)
val goldenModelResult = goldenModel.poke(input)
c.io.in.poke(input.U)
c.io.out.expect(goldenModelResult.U, s"i $i, input $input, gm $goldenModelResult, ${
c.io.out.peek().litValue}")
c.clock.step(1)
}
}
print(getVerilogString(new MyManyDynamicElementVecFir(4)))
测试通过,生成的Verilog代码如下:
module MyManyDynamicElementVecFir(
input clock,
input reset,
input [7:0] io_in,
output [7:0] io_out,
input [7:0] io_consts_0,
input [7:0] io_consts_1,
input [7:0] io_consts_2,
input [7:0] io_consts_3
);
reg [7:0] regs_0; // @[MyModule.scala 71:23]
reg [7:0] regs_1; // @[MyModule.scala 71:23]
reg [7:0] regs_2; // @[MyModule.scala 71:23]
wire [15:0] _muls_0_T = io_in * io_consts_0; // @[MyModule.scala 79:37]
wire [15:0] _muls_1_T = regs_0 * io_consts_1; // @[MyModule.scala 80:43]
wire [15:0] _muls_2_T = regs_1 * io_consts_2; // @[MyModule.scala 80:43]
wire [15:0] _muls_3_T = regs_2 * io_consts_3; // @[MyModule.scala 80:43]
wire [7:0] muls_1 = _muls_1_T[7:0]; // @[MyModule.scala 77:20 80:28]
wire [7:0] muls_0 = _muls_0_T[7:0]; // @[MyModule.scala 77:20 79:28]
wire [7:0] scan_1 = muls_1 + muls_0; // @[MyModule.scala 86:33]
wire [7:0] muls_2 = _muls_2_T[7:0]; // @[MyModule.scala 77:20 80:28]
wire [7:0] scan_2 = muls_2 + scan_1; // @[MyModule.scala 86:33]
wire [7:0] muls_3 = _muls_3_T[7:0]; // @[MyModule.scala 77:20 80:28]
assign io_out = muls_3 + scan_2; // @[MyModule.scala 86:33]
always @(posedge clock) begin
if (reset) begin // @[MyModule.scala 71:23]
regs_0 <= 8'h0; // @[MyModule.scala 71:23]
end else begin
regs_0 <= io_in; // @[MyModule.scala 73:28]
end
if (reset) begin // @[MyModule.scala 71:23]
regs_1 <= 8'h0; // @[MyModule.scala 71:23]
end else begin
regs_1 <= regs_0; // @[MyModule.scala 74:28]
end
if (reset) begin // @[MyModule.scala 71:23]
regs_2 <= 8'h0; // @[MyModule.scala 71:23]
end else begin
regs_2 <= regs_1; // @[MyModule.scala 74:28]
end
end
// Register and memory initialization
...
endmodule
实现32位RISC-V处理器的寄存器文件
寄存器文件(Register File)是构建处理器的一个重要组件。寄存器文件指的是一个数组的寄存器,可以通过一组读/写端口来从寄存器中读取值或向寄存器中写入值。每个端口都由一个地址字段和数据字段构成。
RISCV指令集定义了多种ISA(Instruction Set Architecture,指令集架构)的变体,其中最简单的子集就是RV32I。RV32I有32个32位的寄存器,
现在要为RV32I实现一个寄存器文件,要求具有单个写端口和一个可配置数量的读端口,且写寄存器仅在wen
(write enable,写使能)信号设置上了才能执行。实现如下:
class RegisterFile(readPorts: Int) extends Module {
require(readPorts >= 0)
val io = IO(new Bundle{
val wen = Input(Bool())
val waddr = Input(UInt(5.W))
val wdata = Input(UInt(32.W))
val raddr = Input(Vec(readPorts, UInt(5.W)))
val rdata = Output(Vec(readPorts, UInt(325.W)))
})
// 32个 初始化为0的32位数 形成数组 构成寄存器
val reg = RegInit(VecInit(Seq.fill(32)(0.U(32.W))))
when (io.wen) {
reg(io.waddr) := io.wdata
}
for (i <- 0 until readPorts) {
when (io.raddr(i) === 0.U) {
io.rdata(i) := 0.U
} .otherwise {
io.rdata(i) := reg(io.raddr(i))
}
}
}
输出的Verilog代码太长就不放了,下面是测试代码:
test(new RegisterFile(2) ) { c => def readExpect(addr: Int, value: Int, port: Int = 0): Unit = { c.io.raddr(port).poke(addr.U) c.io.rdata(port).expect(value.U) } def write(addr: Int, value: Int): Unit = { c.io.wen.poke(true.B) c.io.wdata.poke(value.U) c.io.waddr.poke(addr.U) c.clock.step(1) c.io.wen.poke(