verilog 初始化_用Chisel快速搭建FFT流水线电路(续篇二)——嵌入预先设计的Verilog模块和进行资源优化...

e79f606644c461a307afe83f5fda42c1.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的设计实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值