代码都在这里
刚学完AXI-Lite,在用valid/ready握手在一条链上传递信息的时候,发现每传递一个消息,每一级都需要切换idle和wait两个状态,当时就在想能不能砍掉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传输一次数据要至少花费两个周期(经历empty和full两个状态),所以带宽就是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.