Kika's
Blog
图片简介 | CC BY 4.0 | 换一张

Chisel实现的双缓冲FIFO

2023-12-27

代码都在这里

刚学完AXI-Lite,在用valid/ready握手在一条链上传递信息的时候,发现每传递一个消息,每一级都需要切换idlewait两个状态,当时就在想能不能砍掉idle状态,毫无停顿地发下去.学到Double Buffer FIFO的时候,才发现原来可以这样简单地就解决了.

Bubble FIFO

先考虑最简单的Bubble FIFO,每个Buffer只有两种状态:空empty或满full. 空就拉高ready,表示可以接受数据;满就不能再接受数据,拉高valid,阻塞直至和下一级ready握手.

flowchart LR
    e["`**empty**
        enq.ready=1
        deq.valid=0
        `"]
        f["`**full**
        enq.ready=0
        deq.valid=1
        `"]
        e-- enq.valid -->f
        f-- deq.ready -->e

由于Bubble FIFO只有两种状态,直接用Bool表示即可.

class BubbleFifo[T <: Data](t: T, depth: Int) extends Fifo(t: T, depth: Int){
    private class Buffer() extends Module{
        val io = IO(new FifoIO(t))
        val full = RegInit(false.B)
        val dataReg = Reg(t)

        when((!full)&io.enq.valid){
            dataReg := io.enq.bits
            full := true.B
        }.elsewhen(full&io.deq.ready){
            full := false.B
        }

        io.enq.ready := !full
        io.deq.valid := full
        io.deq.bits := dataReg
    }
    private val buffers = Array.fill(depth) {Module(new Buffer())}
    for (i <- 0 until depth-1) {
        buffers(i+1).io.enq <> buffers(i).io.deq
    }
    io.enq <> buffers(0).io.enq
    io.deq <> buffers(depth-1).io.deq
}

其中Fifo是抽象出的基类,所有的FIFO都有一个入队端口enq和一个出队端口deq,而且有一个大小depth的属性.

class FifoIO[T <: Data](t: T) extends Bundle {
  val enq = Flipped(new DecoupledIO(t))
  val deq = new DecoupledIO(t)
}

abstract class Fifo[T <: Data](val t: T, val depth: Int) extends Module {
    val io = IO(new FifoIO(t))
    assert(depth > 0)
}

然后分析一下这个的性能.传输过程中可能的波形图大概是这样的(其中第4个时刻buf(2)没有收到下一级的ready信号,直到第5个时刻才收到):

可以看到,每个buffer传输一次数据要至少花费两个周期(经历emptyfull两个状态),所以带宽就是2 cycles/word. chiseltest的结果也是如此

Bubble FIFO就是每次都要在empty停顿一下,我当初的设计也便是如此,下面介绍不停顿传输的Double Buffer FIFO

Double Buffer FIFO

停顿的根本原因在于,我们无法保证收到数据时下一级一定是ready状态. 因为如果收到数据时下一级并不ready,那么下一个周期新收到的数据就会覆盖掉当前buffer里面的数据,导致数据丢失. 所以在Bubble FIFO中,我们选择当收到数据后便拉低ready,保证只有当数据成功传递给下一级后,我们再考虑接受新的信息.

然而我们其实可以在收到数据后下仍然拉高ready,假定下一级一定是ready状态,便可毫不停顿地一级一级传递下去.但如果下一级并不ready,而又有新的数据传入,第一个buffer的数据就会被覆盖,数据丢失了. 为了解决这个问题,我们可以增加一个buffer,将新的数据存到第二个buffer里,以防把第一个buffer的数据覆盖,同时还要拉低ready,阻止新的数据进入,以防第二个buffer的数据也被覆盖. 等到下一级ready再次拉高,即第一个buffer里面的数据已经传递到下一级后,再将第二个buffer里的数据传入到第一个buffer里(下一级始终读取的是第一个buffer里面的数据).

具体实现时,Double Buffer FIFO便有三个状态,全空empty,第一个buffer被填满one,两个buffer都被填满two

flowchart LR
    empty["`**empty**
        enq.ready=1
        deq.valid=0
        `"]
        one["`**one**
        enq.ready=1
        deq.valid=1
        `"]
        two["`**two**
        enq.ready=0
        dep.valid=1
        `"]
        empty-->|enq.valid|one
        one-->|!deq.ready&enq.valid|two
        one-->|deq.ready&!enq.valid|empty
        two-->|deq.ready|one

代码中,第一个buffer就是dataReg,第二个buffer就是shadowReg.

class DoubleBufFifo[T <: Data](t: T, depth: Int) extends Fifo(t: T, depth: Int){
    private class Buffer() extends Module{
        val io = IO(new FifoIO(t))
        val dataReg = Reg(t)
        val shadowReg = Reg(t)
        object State extends ChiselEnum{
            val empty, one, two = Value
        }
        val stateReg = RegInit(State.empty)

        switch(stateReg){
            is(State.empty){
                when(io.enq.valid) {
                    stateReg := State.one
                    dataReg := io.enq.bits
                }
            }
            is(State.one){
                when((!io.deq.ready)&(io.enq.valid)){
                    stateReg := State.two
                    shadowReg := io.enq.bits
                }
                when(io.deq.ready&io.enq.valid){
                    stateReg := State.one
                    dataReg := io.enq.bits
                }
                when(io.deq.ready&(!io.enq.valid)){
                    stateReg := State.empty
                }
            }
            is(State.two){
                when(io.deq.ready){
                    stateReg := State.one
                    dataReg := shadowReg
                }
            }
        }
        io.enq.ready := stateReg =/= State.two
        io.deq.valid := stateReg =/= State.empty
        io.deq.bits := dataReg
    }

    private val buffers = Array.fill(depth){Module(new Buffer())}
    for(i <- 0 until depth-1){
        buffers(i+1).io.enq <> buffers(i).io.deq
    }
    io.enq <> buffers(0).io.enq
    io.deq <> buffers(depth-1).io.deq
}

传输过程中可能的波形图大概是这样(其中第1个周期buf(2)没有收到下一级的ready信号,直到第2个周期才收到ready信号):

可以看到当所有buffer没有阻塞时,可以毫不停顿地传递下去,每个周期都可以传递一个数据,所以带宽就是1 cycles/word.

参考