![e79f606644c461a307afe83f5fda42c1.png](https://i-blog.csdnimg.cn/blog_migrate/7d23aadc8e6a199c50cd8080a3b0851b.png)
一、前文回顾
在之前的两篇文章(1,2)中,我们对如何使用Chisel快速搭建FFT流水线电路的基本方法进行了简要介绍,且在开源地址https://github.com/IA-C-Lab-Fudan/Chisel-FFT 对相应的代码和工程进行了开源。在实际的应用场景和FPGA实现中,我们发现了若干可以优化的地方,并在开源项目中对改进和优化进行了落实。
二、BRAM资源的使用
我们注意到,在之前的FPGA的流程中,旋转因子所占用的存储资源会随着FFT长度的增加而不断增加。具体的资源消耗如下表所示:
可以看到,虽然旋转因子所占用的存储资源会随着FFT长度的增加而不断增加,但其所消耗的存储资源为分布式的存储单元LUT或LUTRAM。在存储需求较大时,这种分布式的存储资源会显得捉襟见肘,这时候就需要用到FPGA中的BRAM(Block Memory)。所以,能够控制FPGA生成的存储器类型对于可定制化的设计是很有必要的。那么如何实现这一点呢?首先,我们需要审视一下Chisel生成旋转因子的代码,例如下面是一个生成一系列正弦函数值的ROM。
def sinTable(k: Int): Vec[FixedPoint] = {
val times = (0 until FFTLength / 2 by pow(2, k).toInt)
.map(i => -(i * 2 * Pi) / FFTLength.toDouble)
val inits = times.map(t => FixedPoint.fromDouble(sin(t), DataWidth.W, BinaryPoint.BP))
VecInit(inits)
}
生成的Verilog代码片段如下,可以看到是若干个常数值的线网变量。
assign _GEN_2 = 6'h1 == cnt[5:0] ? $signed(32'shffb1) : $signed(32'sh10000); // @[FFT.scala 51:18]
assign _GEN_3 = 6'h2 == cnt[5:0] ? $signed(32'shfec4) : $signed(_GEN_2); // @[FFT.scala 51:18]
assign _GEN_4 = 6'h3 == cnt[5:0] ? $signed(32'shfd3b) : $signed(_GEN_3); // @[FFT.scala 51:18]
assign _GEN_5 = 6'h4 == cnt[5:0] ? $signed(32'shfb15) : $signed(_GEN_4); // @[FFT.scala 51:18]
assign _GEN_6 = 6'h5 == cnt[5:0] ? $signed(32'shf854) : $signed(_GEN_5); // @[FFT.scala 51:18]
为了使生成的Verilog代码能够在FPGA中综合成BRAM,我们最好使用能被Vivado识别为BRAM的Verilog代码。如下所示,可以看到我们使用了对Vivado综合器友好的Verilog代码,使之能够被综合为BRAM。其中tw.txt文件是相应的旋转因子的值作为BRAM的初始化文件,一系列的内存初始化文件可以由/src/test/scala/FFT/twFileGen.scala文件自动生成。
module BRAM (clock, en, addr, dout);
parameter ADDR_WIDTH = 5;
parameter ADDR_DEPTH = 2**ADDR_WIDTH;
parameter DATA_WIDTH = 64;
input clock;
input en;
input [ADDR_WIDTH-1:0] addr;
output reg [DATA_WIDTH-1:0] dout;
(* rom_style = "block" *)
reg [DATA_WIDTH-1:0] rom [ADDR_DEPTH-1:0];
initial begin
$readmemb("tw.txt", rom);
end
always @(posedge clock) begin
if (en) begin
dout <= rom[addr];
end
end
endmodule : BRAM
经过多方权衡,我们最后决定采用Chisel的BlackBox特性来实现Verilog定制代码的嵌入。按照sbt的文件组织规则, 我们首先添加BRAM.v作为嵌入的Verilog文件到/src/main/resources/目录下,接着我们在Chisel源代码中实现对BlackBox文件的调用,如下所示:
class BRAM(dep: Int, dw: Int) extends BlackBox(
Map("ADDR_WIDTH" -> dep,
"DATA_WIDTH" -> dw))
with HasBlackBoxResource {
val io = IO(new Bundle {
val clock = Input(Bool())
val en = Input(Bool())
val addr = Input(UInt(dep.W))
val dout = Output(UInt(dw.W))
})
addResource("/BRAM.v")
}
如此这般,我们在顶层文件就可以例化上述的BRAM了,采用配置变量来决定是否嵌入BlackBox,我们就可以得到如下的代码片段。
if (useBRAM) {
val mem = Module(new BRAM(dep = log2Ceil(currentDep), dw = 2 * DataWidth))
mem.io.en := true.B
mem.io.addr := wnCtrl
mem.io.clock := clock.asUInt()
wn := mem.io.dout.asTypeOf(new MyComplex)
} else {
wn := wnTable(i)(wnCtrl)
}
采用了BlackBox的方法后,我们重新对FFT代码进行了FPGA的综合实现的评估,具体如下表所示(其中v1代表没有显式指定BRAM的方案,v2为显式指定BRAM的方案)。可以看到,当FFT的长度较小时,资源的节省并不明显,当FFT长度为512时,LUT的消耗有一定的下降,但FF和BRAM的资源消耗增加了。
有趣的是,当我们设置FFT的长度为2048时,即使我们按照之前的方案不显式指定BRAM,Vivado也自动使用了BRAM来存储代码中的线网常量,可见Vivado的综合实现工具确实具有相当的智能性。在我们的开源地址中,BRAM分支实现了对BRAM显式指定的功能。
三、对IFFT的支持
在诸多实际应用中,不仅需要支持FFT运算,对IFFT(Inverse FFT)运算的支持也是很有必要的。我们通过复用现有的大部分硬件,以很小的额外硬件资源消耗实现了对IFFT运算的支持。其中,额外消耗的硬件资源主要包括:IFFT所需旋转因子的的存储资源以及一些移位单元和MUX单元。
为了实现电路配置的可裁剪性,我们在代码中定义了名为supportIFFT的Boolean变量,当此变量为false时,电路不支持IFFT;当此变量为true时,电路支持FFT / IFFT运算,并可以通过mode输入信号来选择运算模式。此时,mode输入信号需要定义如下:
val mode = if(supportIFFT) Some(Input(Bool())) else None
接着定义IFFT所需的旋转因子如下:
def sinTable2(k: Int): Vec[FixedPoint] = {
val times = (0 until FFTLength / 2 by pow(2, k).toInt)
.map(i => (i * 2 * Pi) / FFTLength.toDouble)
val inits = times.map(t => FixedPoint.fromDouble(sin(t), DataWidth.W, BinaryPoint.BP))
VecInit(inits)
}
def cosTable2(k: Int): Vec[FixedPoint] = {
val times = (0 until FFTLength / 2 by pow(2, k).toInt)
.map(i => (i * 2 * Pi) / FFTLength.toDouble)
val inits = times.map(t => FixedPoint.fromDouble(cos(t), DataWidth.W, BinaryPoint.BP))
VecInit(inits)
}
def wnTable(k: Int)(idx: UInt): MyComplex = {
val res = Wire(new MyComplex)
res.re := Mux(mode, cosTable2(k)(idx), cosTable(k)(idx))
res.im := Mux(mode, sinTable2(k)(idx), sinTable(k)(idx))
res
}
IFFT需要对输出进行算术右移,我们定义了如下的函数。至此,IFFT的支持也完成了。
def timesInvn(a: MyComplex): MyComplex = {
val b = Wire(new MyComplex)
b.re := a.re >> stages
b.im := a.im >> stages
b
}
为了对支持IFFT所消耗的额外硬件资源进行评估,我们对同时支持FFT和IFFT运算的硬件进行了FPGA的综合实现评估,与仅支持FFT方案的比较结果如下(其中v1代表仅支持FFT的方案,v3代表同时支持FFT和IFFT的方案):
可以看到,通过对流水线硬件的复用,支持IFFT的额外资源消耗确实并不大,仅仅造成了LUT的消耗少许增加,这来源于新增旋转因子查找表的成本。这也说明,通过复用大部分硬件来实现FFT和IFFT这两个功能,可以有效地优化资源利用率。
四、结语
在本篇文章里,我们对FFT的硬件设计进行了优化和改进,主要包括了对FPGA中BRAM资源的显式调用和对IFFT运算的支持。这些改进运用到了Chisel中的BlackBox特性,这为定制代码的嵌入提供了方便;同时也运用到了Chisel中的配置可裁剪的思想(主要通过Option对象来实现),使用户能根据需要选择仅支持FFT或是同时支持FFT和IFFT的设计实现。