队列管理电路-上篇

电子说

1.3w人已加入

描述

在数字芯片设计中,几乎所有模块都会涉及到队列管理。输入输出的管理、不同数据流的调度、乱序数据的重排序、不同模块的同步处理、资源管理,等等,均会涉及到队列管理逻辑。如何选择合适的硬件逻辑,对模块的微架构有较大的影响,需要基于具体需求做综合权衡后再做选择。本文简单罗列几种队列管理逻辑,均是个人曾经实现过的。

1 最简单的队列-FIFO

First In First Out,用于输入输出之间的缓冲,吸收输入侧的突发流量。实现也比较简单,深度固定的环形buffer,使用读写指针进行管理。需要注意的是,读写指针的管理,FIFO为空和FIFO为满,读写指针均是相等的,需使用另外的标号进行处理。也有其余的实现方式,比如移位寄存器。

数字芯片

使用SpinalHDL实现FIFO的代码如下。输入输出的push/pop,使用了valid/ready握手的Stream接口;使用Mem定义环形buffer,pushPtr/popPtr分别对应读写指针;特别关注risingOccupancy信号,push和pop没有同时发生时,更新为push,该信号可用于标记FIFO的空满状态。读写指针相等且该信号为低,表示FIFO为空;读写指针相等且该信号为高,表示FIFO为满。

// spinal/lib/Stream.scala
val io = new Bundle {
val push = slave Stream (dataType)
val pop = master Stream (dataType)
val flush= in Bool() default(False)
val occupancy = out UInt (log2Up(depth + 1) bits)
val availability = out UInt (log2Up(depth + 1) bits)
}
val ram = Mem(dataType, depth)
val pushPtr = Counter(depth)
val popPtr = Counter(depth)
val ptrMatch = pushPtr === popPtr
val risingOccupancy = RegInit(False)
val pushing = io.push.fire
val popping = io.pop.fire
val empty = ptrMatch & !risingOccupancy
val full = ptrMatch & risingOccupancy

io.push.ready := !full
io.pop.valid := !empty & !(RegNext(popPtr.valueNext === pushPtr, False) & !full) //mem write to read propagation
io.pop.payload := ram.readSync(popPtr.valueNext)

when(pushing =/= popping) {
risingOccupancy := pushing
}
when(pushing) {
ram(pushPtr.value) := io.push.payload
pushPtr.increment()
}
when(popping) {
popPtr.increment()
}

2 共享Buffer的多队列FIFO

考虑一个场景,输入的请求需要分发至不同的输出侧,下游存在反压。简单实现,基于不同的输出分别设置FIFO,但可能存在资源浪费,某些数据流场景FIFO的利用率不高,尤其是在数据位宽较大的场景。

共享Buffer的多队列FIFO,每个队列的FIFO还是按照简单队列进行管理,基于每个队列管理读写指针。但是,不再使用环形Buffer,每个buffer entry记录其队列号、队列指针和Payload,如下图所示。对于Payload位宽较小的场景,收益不大,若存在大位宽时,可有效提升Buffer的利用率。

数字芯片

将数据写入Buffer时,先找一个Free Entry(Vliad为低),将该数据所属的队列号及其对应的写指针、Payload写入到对应的Entry内。读取Buffer时,则使用队列号和读指针进行匹配,将命中的Entry内容读取出来。若读写指针所能描述的范围比buffer深度大,则不需要额外的标号记录空满状态。存在的问题,若buffer深度较大或队列数量较多,队列号和指针匹配逻辑会占用较多的资源。

3 重力FIFO

类似于排队,从队头开始寻找可输出的Entry,调度输出并留下空位,后面的Entry再往前排,新输入的请求则放置在队列尾。如图所示,存在有效数据的Entry,其前面的Entry被调度后留下空位,该Entry就像受到重力作用往下掉,因此我也称之为重力FIFO。

数字芯片

该结构的问题,存在大量的移位,设想Payload位宽为32bit,深度为32,将近1kbit的寄存器在做移位处理,其功耗可想而知。但是对于一些具体场景,还是能够带来一些收益的,如队列数量较大,甚至大于buffer深度;至于Payload位宽较大的场景,可考虑二次索引处理,Payload保存至另外的buffer,该结构内的Payload Entry则缓存其索引信息。

4 Bitmap排序

先来看一个结构,深度为8的队列,每个Entry使用8bit缓存8个Entry的状态,若该状态信号满足触发条件,如全为0,则调度该Entry内容。

Bitmap排序就是使用了这一结构,在输入请求进入队列后,检查当前队列状态,存在关联请求的Entry位置置位为1,否则为0。若存在请求输出之后,所有Entry状态的对应位置均设为0。若某个Entry的状态信号全为0,则请求调度输出。其数据结构如下图所示,其中0/1仅作为状态信号的示例,并非实际场景。

数字芯片

该结构可以实现较为灵活的排序,队列的数量几乎不会受到限制,进入队列的请求,也可修改其Mask Bitmap,动态刷新其先后关系。与重力FIFO类似,无需额外的数据结构保存其队列关系,而是直接体现在原有结构内。存在的问题,队列深度会受到面积限制,面积与深度的平方成正比;另外,在动态更新Mask Bitmap之后,某些实现可能无法保证先后关系。

面积问题可以考虑用分级处理。如需实现256深度的队列,其Mask Bitmap需要65536个寄存器实现Mask Bitmap。分解为8个32深度的队列,需要的寄存器数量为8192;分解为16个16深度的队列,寄存器数量为4096。

5 小结

队列管理电路还有一个比较常见的实现,链表。在乱序数据的重排序、资源管理等等方面,通常会用链表实现,与上几个结构相比,链表会复杂一些。该部分将在下篇描述。

除最简单的FIFO之外,其余几个都没有代码,如各位要有兴趣,请留言,我可以再尝试写一些Spinal代码实现。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分