棋子 · 2022年08月24日 · 北京市

[IC设计]队列管理电路-上

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

1 最简单的队列-FIFO

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

image.png

使用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的利用率。

image.png

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

3 重力FIFO

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

image.png

该结构的问题,存在大量的移位,设想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仅作为状态信号的示例,并非实际场景。

image.png

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

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

5 小结

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

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

==== END ====

作者:芯工阿文
文章来源:芯工阿文

推荐阅读
SystemVerilog | UVM | 如果你要搞很多Sequence,请看过来
CCIX(九)
CCIX(八)

更多IC设计技术干货请关注IC设计技术专栏。
迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
20180
内容数
1307
主要交流IC以及SoC设计流程相关的技术和知识
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息