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

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

前文聊了队列管理的几种典型电路,硬件逻辑简单,代码实现时容易操作。链表也是队列管理的常用电路,相比前文的几种结构,会稍微复杂一些。

1 什么是链表

在非连续、非顺序的物理存储结构上,通过指针的方式记录元素的顺序关系。在硬件电路中,通常使用generate组建table,记录每个entry的内容,除了有效位和Payload之外,还有link_next,可能有head/tail信息。head/tail信息也可以基于队列进行单独记录,而不在table体现。

image.png

使用NEXT标注链表下一个元素的位置,也即table对应的标号。

image.png

2 链表操作

使用链表作为队列管理电路,涉及到的逻辑主要包括查找空闲entry、添加元素和释放元素,其中添加和释放需要先获取tail或head位置。除此之外,如有其余需求,则做相应处理。

2.1 查找entry

新请求输入至队列,可放置在table任意位置,需查找到一个空闲的entry添加该请求至相关队列的尾部。一般来说,从table的index 0处开始查找,获取第一个可用的entry,使用如下代码。

always @* begin : gen_idle_entry
  integer i;
  reg flag;
  flag = 1'b0;
  for ( i = 0; i < TABLE_DEPTH; i = i + 1) begin
    if (~flag & table_entry_idle[i]) begin
      table_entry_sel[i] = 1'b1;
      flag = 1'b0;
    end else begin
      table_entry_sel[i] = 1'b0;
    end
  end
end

在Spinal Lib提供了一种简洁的方式,可以参考一下。

def first[T <: Data](that : T) : T = new Composite(that, "ohFirst"){
  val input = that.asBits.asUInt
  val masked = input & ~(input - 1)
  val value = cloneOf(that)
  value.assignFromBits(masked.asBits)
}.value

除此之外,还可以用其余的查找方式,其实就是一种调度逻辑,如定义一个计数器,需要查找时则计数,检查对应entry是否空闲,但速度较慢。

2.2 添加元素

完成entry查找后,有两方面内容,一是将请求保存至entry内,二是更新上一元素的NEXT信号。

将请求保存至entry,较为简单,根据查找逻辑得到的sel信号选择对应entry,更新其内容及其head/tail信号。

更新上一元素的NEXT信号及其head/tail信号。若table内采用了tail信号,相关处理代码如下,其中table_next信号可使用不带复位的寄存器。

assign table_next_update[index] = table_valid[index] & table_tail[index] & (table_queue_id[index] == req_queue_id);
always @ (posedge clk) if (table_next_update[index]) begin
  table_next[index] <= req_table_entry;
end
always @ (posedge clk or negedge rst_n) begin
  if(~rst_n)
    table_tail[index] <= 1'b0;
  else if (table_next_update[index])
    table_tail[index] <= 1'b0;
  else if (req_table_sel[index])
    table_tail[index] <= 1'b1;
end

2.3 释放元素

链表通常用于记录操作的先后顺序,tail添加,head释放;但也有用于管理credit的场景,tail添加,也在tail释放。

在链表的head释放,主要需要完成两个操作,一是释放Entry,对valid进行清零,二是head信号传递。首先,通过调度逻辑选取需释放的链表Entry,获取其NEXT信号,并将其valid信号进行清零;然后,将NEXT所指向Entry的head信号进行置位。若存在时序紧张,可将NEXT信号进行打拍,但要注意valid清零与head置位是否存在功能时序要求。

在链表的tail释放,是类似的,区别是需置位tail信号,基于释放的Entry匹配获取链表的上一元素,代码如下。对于这一场景,也可以考虑使用逆向链表,释放逻辑就跟上面的head释放是类似的了,但添加元素会有所区别。插入一个问题,存在双向链表的数据结构,但从硬件来看,其实没有必要,或者说硬件链表就是双向链表,一侧使用NEXT标识,另一侧使用NEXT匹配。

always @ (posedge clk or negedge rst_n) begin
  if (~rst_n)
    table_tail[index] <= 1'b0;
  else if (table_next_update[index])  // new entry added to tail
    table_tail[index] <= 1'b0;
  else if (req_table_sel[index])  // added as new entry
    table_tail[index] <= 1'b1;
  else if (release_grant & (table_next[index] == release_index))
    table_tail[index] <= 1'b1;
end

3 链表应用

链表的特征是在无序的结构内记录先后顺序关系。理论来说,所有队列管理电路都可以使用链表实现,但其需要一些逻辑开销,并不适用于所有场景。个人理解,存在多个队列和乱序释放的场景,可以考虑使用链表。

只有单个队列,直接使用FIFO就足够了;顺序释放元素,其存储结构相当于是顺序释放的,使用链表的必要性也不大。在乱序释放的场景中,必定会存在离散的空闲Entry,若需要提高table的利用率,链表则是一个较优解。

举一个链表使用的典型例子。AXI接口的不同ID存在乱序返回的场景,发送至不同目的侧的延时存在差别,上游存在多个源头,且不同源头又期望顺序返回响应。需要使用数据结构记录ID的状态,可以使用链表维护ID的使用及其先后关系。新发出操作先从table获取一个空闲的Entry,也即添加元素,使用该Entry的Index作为ID,基于源头建立链表关系;操作返回后,则按顺序释放Entry,返回响应。当然,若仅存在一个源头,或上游也无需顺序返回响应,使用链表的意义不大。

4 类FIFO的伪链表

考虑一个场景,需要较大数量的Entry,如256,实现链表结构,可想而知,维护该链表所需的资源。另外,链表管理逻辑、Entry Payload 选取需要的MUX逻辑等等,在后端实现时,还可能出现Congestion风险。

简单的做法就是,将较大数量的Entry进行分组,组内按顺序使用,不同组之间则使用链表NEXT指针。如将256分为16组,每组16个Entry,不同组之间基于链表维护,组内Entry使用则按顺序使用,同一组的16个Entry空闲或其空闲数量满足要求后,可再分配使用。基于每组16个Entry进行逻辑处理,不同组之间可添加寄存器打拍等等,降低Congestion风险。

之前在做IP开发时,TLP转AXI操作涉及到的ID分配和维护,就采用了这种伪链表的形式。每组32个Entry,例化了很多组,实现了AXI接口较大的Outstanding数量。将TLP转化为AXI操作,需将较大的TLP报文切分为多个AXI操作,每个TLP报文作为一个队列,由于TLP报文最大为4KByte,一个队列最多只存在32个元素,多组之间就不再需要NEXT指针进行维护了。每组内进行空闲Entry的搜索,找出连续且可用的最大数量的Entry,再供TLP转换AXI时使用。

这一结构相对简单,存在的问题是会降低利用率。但是,从另一方面来看,在系统中,乱序是小概率的,顺序是常见的,使用这一结构可以达到期望的功能,在PPA方面也是较优解。

5 小结

以上两篇文章仅是简单罗列了在过往工作中涉及到的队列管理逻辑,未必全面,希望能对大家有一些启发。

如有相关的设计细节讨论,欢迎留言~~

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

推荐阅读

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