Verilog|权重轮询仲裁器(Weight_Round_Robin_Arbiter)
语言: Verilog
工具:Vcs和Verdi
🚩本文内容为博主当前做的交换机开源项目所用IP,对交换芯片设计感兴趣的朋友可以关注博主git
😀博主git: Atom(100M以太网交换机)
🔑未经作者允许,禁止转载
🔈 文中部分代码思想借鉴了"IC加油站"的内容分享,侵权必删
文章目录
一、仲裁器
在数字电路当中,经常会出现多个源或用户共享同一资源(总线)的情况,这时就需要某种仲裁形式,使得所有用户基于一定的规则或算法得到获取或访问共享资源的机会。例如,共享总线上可以连接多个总线用户。还有交换芯片中的端口仲裁,当多个入口希望通过某一个出口输出数据时,需要使用一定的端口仲裁机制来选择某一时刻允许哪一个入口发送数据。
工业上常用的经典仲裁方案有如下三种:固定优先级仲裁器、轮询仲裁器和权重轮询仲裁器。其中固定优先级仲裁器和轮询仲裁器比较常见,并且相关资料介绍也很多,本文不做过多叙述,重点讨论权重轮询仲裁器。
(1)固定优先级仲裁器(Fixed_Priority_Arbiter)
顾名思义,发送请求的用户(agent)有着固定的优先级,优先级高的用户只要保持请求,就会持续得到授权,而低优先级的用户只能等待。随着优先级的降低,用户得到授权的机会也会随之下降。
例如,有4个用户需要共享数据总线,req[0
]优先级最高,req[3
]优先级最低。当req=1001时,仲裁结果为0001;当req为1100时,仲裁结果为0100。
(2)轮询仲裁器(Round_Robin_Arbiter)
固定优先级仲裁策略能够根据用户的重要性提供相应服务,但低优先级的用户也可能因此长期得不到服务而被“饿死”。比如在交换业务当中,如果端口采用了固定优先级的仲裁策略,那么就很容易出现网络延迟,甚至业务被挂死的可能。因此,我们希望每个用户都能公平地获得授权,而轮询仲裁器就可以满足这个需求。
轮询仲裁器在初始阶段可以任意选择用户顺序,在一个轮询周期内,所有发出请求的用户都能公平得到授权机会。仲裁器每次仲裁时,会记住上一次被授权的用户,当该用户的操作完成后,将其优先级置于最低。接着依序轮询其他用户的请求是否有效,如果一个用户的请求无效,那么将按顺序查看下一个用户。
并且,当某个用户得到授权后,可以长期使用总线或者占用资源,直到当前业务处理完成,仲裁器才会授权其他用户进行操作。这种方案适用于基于数据包的协议,如以太网交换或PCIe交换机。另外还有一种机制,每个用户获得授权后,占用资源的时间长度受到约束,不能超过规定的长度,否则即便当前用户没有完成操作,仲裁器也会收回授权并轮询后续用户。该方案适用于突发操作,如AMBA、AHB总线。
以交换业务为例,假设有4个端口,即req[3:0]。初始阶段优先授权低位,req=1001,因此grant=0001;接着req变成1101后,等到req[0
]完成操作,释放授权信号(switch_to_next)后,仲裁结果变为0100。再次释放授权,req已变为1100,则此时仲裁结果为1000。由此可体现出公平的轮询机制。
(3)权重轮询仲裁器(Weight_Round_Robin_Arbiter)
当用户之间仍然存在着优先级高低之分时,优先级高的用户希望能够获得更多的授权机会,但同时也需要考虑其他用户的情况下,权重轮询仲裁器就诞生了。这也是本文所要讨论的主要内容,将在下文进行分析阐述,并附上rtl代码。
二、分析
1.仲裁原理
权重轮询仲裁器,从名称上来看,首先是基于轮询仲裁机制的,但同时又不是公平的轮询机制,每个用户之间都有对应的权重关系。
举个例子说明:假设班级里有个A,B,C三位同学,其中A的成绩最好,B次之,C的成绩最差。在课堂上老师想要提问,希望三位同学都能够主动举手参与进来,因此不能总是让一个人回答。当提出第一个问题并且有同学举手之后,老师先按成绩选择一位同学回答,假设为A。A同学回答完之后,再次提出问题时,若B和C举手了,那么老师则优先选择让B来回答问题,若B和C只有一个人举手,则谁举手谁回答,此时不考虑A的情况;如果B和C都没有人举手,并且A举手了,这时老师才考虑继续让A同学回答。这是公平轮询的机制。
同样的情况,老师上课提问有意想要鼓励成绩较差的同学多回答一些问题,给A、B、C同学的回答次数分别为1:2:3次。第一次提问时,A举手的话,则选择A作答,此时已经A已经用完了回答次数。第二次提问,若B和C有人举手,则不允许A回答,假设第二次是B回答了问题,那么B还剩一次机会。第三次提问,B和C又同时举手,按照公平轮询的策略,此时应该是C回答,但是由于B同学还有一次回答机会,因此这时仍然由B来回答问题。同理,C同学也是如此。
需要注意的是,如果第三次提问,B没有举手,A和C同学举手了,意味着B还有一次机会,A同学已经用完了所有回答机会,因此不能回答,同时根据轮询原则,此时应该轮到C回答。C回答完之后,还剩2次机会。再下一个轮询周期,B和C用完剩下的机会后即便举手,老师也要提问下一个同学。这就表示权重轮询并不是仲裁一次之后,就轮询下一个用户,而是要综合权重来处理。
Cycles | A | B | C | grant(ABC) |
---|---|---|---|---|
cycle0 | 1 | 1 | 1 | 100 |
cycle1 | 1 | 1 | 1 | 010 |
cycle2 | 0 | 1 | 1 | 010 |
cycle3 | 0 | 1 | 1 | 001 |
cycle4 | 1 | 1 | 0 | 100 |
cycle5 | 1 | 0 | 1 | 001 |
cycle6 | 1 | 0 | 1 | 001 |
cycle7 | 0 | 1 | 1 | 010 |
2.硬件实现
从上述例子中可以很好地理解权重轮询的机制,那么如何用HDL去描述该机制,或者说如何用硬件电路去实现该机制是接下来需要考虑的问题。硬件实现权重仲轮询裁机制一般来说有两种思路:一种是基于grant进行轮询,另外一种则是基于request的方法。
(1)基于grant
轮询的思想简单来说就是屏蔽已经授权的用户,授权给还未仲裁的较高优先级用户,因此核心问题在于如何找到那个还未仲裁的较高优先级用户。这让我们很容易联想到如何找到序列里第一个1位置(由低到高)的问题,它是这样处理的。
将序列减1,这样序列中从低位开始,为0的bit位就会因为不够减向高位借位,当前位就变为1,直到遇到序列中第一个为1的bit位,其因为低位的借位变为0,更高位则保持不变。再将得到的新seq进行按位~
,此时原seq中第一个1仍然为1,再和原seq进行按位&
操作,即可得到由低到高的第一个1的位置。
result[N-1:0] = seq[N-1:0] & ~(seq[N-1:0] -1'b1);
而轮询仲裁需要找到上一次授权后的最高优先级用户,由于grant本身就是独热码,所以只需要将上述中的1‘b1换成上一次仲裁结果即可,这里就需要使用时序逻辑。考虑到grant的二进制数值可能比req大,因此将位宽扩大一倍,如下操作。将double_req减去上一次仲裁结果后,再取反和原double_req按位&
,此时就可找到当前可授权的用户,再把grant位宽恢复成N位。最后需要把当前仲裁结果进行循环左移处理,这是为了在下次仲裁时屏蔽当前已完成仲裁的用户。
reg [N-1:0] grant_base;
wire [N-1:0] grant;
wire [2*N-1:0] double_req;
wire [2*N-1:0] double_grant;
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
grant_base[N-1:0] <= {{N-1}{1'b0},1'b1};
else if(|req[N-1:0])
grant_base[N-1:0] <= {grant[N-2:0],grant[N-1]};
assign double_req[2*N-1:0] = {req,req};
assign double_grant[N-1:0] = double_req & ~(req-grant_base);
assign grant[N-1:0] = double_grant[2*N-1:N] | double_grant[N-1:0];
上文说过,权重轮询需要考虑权重的因素,常规做法是给每个用户固定的仲裁次数,然后各用户每仲裁一次计数一次。因此完成一次仲裁后,并不能马上对grant进行循环移位,而是判断当前用户可仲裁次数是否已用完:若已用完,则grant_base进行跳转;反之,如果当前用户停止请求仲裁,并且有其他用户请求了,仍然跳转;否则,保持不变。
需要注意的是,当仲裁器用于数据包的传输时,一次仲裁可能会占用很长时间,需要一个仲裁释放信号(switch_to_next)来开始下一次仲裁。因此仲裁的触发信号可以由两个信号或操作形成:一种是当前没有仲裁,且有新的req输入,表示第一次仲裁;另外一种则是由switch_to_next触发的。这是当前许多博客中没有提及的部分,也更加符合实际使用的需求。
为了增加IP的普适性,满足不同设计的需求,因此rtl采取参数化设计,具体的代码会在分享在下文。由于该设计比较简单,大家可以根据自己的需求和想法自行设计test bench进行验证。
(2)基于request
前面的方法很好地利用了grant为独热码的特性,达到了屏蔽的效果。而本方法则是通过给request主动加上mask的方式,屏蔽掉已经仲裁过的用户。
起初,允许所有的用户接受仲裁授权,即有一个为全1的指示信号,设为mask[N-1:0] = {N{1'b1}}
。接着按照req由低到高的顺序,每仲裁一次,将mask对应的bit位信号拉低,只保留未仲裁的用户,以便下一次仲裁时进行屏蔽。当有某个用户申请仲裁时,将mask和req进行按位&
操作得到req_mask,这时req中为1的bit位在req_mask中仍然为1。接下来如何找到req_mask中的第一个1仍是关键,这里我们可以从格雷码转化成二进制码的思想中得到启发,按照如下方式操作。由于默认req仲裁顺序由低到高,如果我们首先将mask_hi_pri_req的最低位设为0,然后将req_mask[N-2:0]和mask_hi_pri_req[N-2:0]按位|
作为mask_hi_pri_req[N-1:1]。这样的话,假设req_mask中由低往高的第一个请求仲裁的用户为第i位,则在mask_hi_pri_req中0-i位都为0,第i+1位到最高位都为1,这同时也就得到了下次仲裁时的mask。然后再将其按位~
后跟req_mask进行按位&
操作,即可得到grant,也就找到了req_mask中的第一个1。
此时可能有人会问,如果当前一轮都仲裁完了,那么按照上述方法,mask将会变成全0 ,这样就没法进行仲裁选择。因此我们需要另外开一路并行的unmask的仲裁选择,跟前面mask方法唯一的区别就是req没有经过mask。当mask为全0时,一轮已经仲裁完毕,下一次mask应该为全1,也就是req本身。因此可以通过一个选择器的形式来选择输出最终的grant,这里的选择器亦可用或门代替。
//gray to bin
assign bin[N-1] = gray[N];
assign bin[N-2:0] = bin[N-1:1] ^ gray[N-2:0];
//arbitration for mask
assign req_mask[N-1:0] = req[N-1:0] & mask[N-1:0];
assign mask_hi_pri_req[0] = 1'b0;
assign mask_hi_pri_req[N-1:1] = req_mask[N-2:0] | mask_hi_pri_req[N-2:0];
assign mask_grant[N-1:0] = req_mask[N-1:0] & ~mask_hi_pri_req[N-1:0];
//arbitration for unmask
assign unmask_hi_pri_req[0] = 1'b0;
assign unmask_hi_pri_req[N-1:1] = req[N-2:0] | unmask_hi_pri_req[N-2:0];
assign mask_grant[N-1:0] = req[N-1:0] & ~unmask_hi_pri_req[N-1:0];
//Use grant_masked if there is any there, otherwise use grant_unmasked
assign no_req_masked = ~(|req_masked);
assign grant = ({REQ_CNT{no_req_masked}} & grant_unmasked) | grant_masked;
到这里已经完成了一大步,最后还需要将权重因素考虑进来。权重不会影响仲裁选择的方法,仅仅只会改变mask跳转的判断条件。和上文中提到的一样,需要给每个用户设计一个计数器cnt,用来记录授权次数,当授权次数用完时则把cnt_over信号拉高,将所有用户的cnt_over按位|
起来得到round_en,以此作为mask的跳转使能。跳转时,如果当前mask不为0,则把mask_hi_pri_req作为下一次的mask;反之,若有用户请求仲裁,则将unmask_hi_pri_req作为下一次的mask,若没有请求则保持不变。
写到这里,博主发现了一个问题。假设有四名用户,初始mask=4‘b1111,且权重由高到低位为3:2:3:4,如果第一次req=4’b0110,则grant=4‘b0010,此时mask仍然为4‘b1111。如果在释放授权前req变为4’b0111,则下次grant将变为4’b0001,这就违反了权重轮询的规则。因此需要对第一次仲裁后的mask进行特别处理:如果req_mask不全为0,则将req_mask跟mask_hi_pri_req按位|
起来之后作为mask。对于刚才这个例子来说,req第一次仲裁后,mask变为4’b1110,req变为4‘b0111后,req_mask=4’b0110,mask_hi_pri_req=4‘b1100,grant=4‘b0010,符合仲裁规则。如果req_mask全为0,则对req和unmask_hi_pri_req作上述处理。
至此,基于两种方法的权重轮询仲裁都已经介绍完毕。两种方法都支持参数化设计,rtl代码放在下面。相较之下,基于grant的方法虽然更好理解,但其在双倍位宽下使用了减法器,对于用户较多的场景下,组合逻辑时延和面积会较大;而基于request的方法仅仅使用了与或非的逻辑,在时序上和面积上都有更好的表现。因此,博主更加推荐基于request的仲裁方法。
三、RTL
1.基于grant设计
module weight_rr_arbiter(
clk,
rst_n,
req,
weight,
grant,
grant_vld,
grant_ff,
grant_ff_vld,
switch_to_next
);
parameter REQ_CNT = 4;
parameter GRANT_WIDTH = 5;
parameter INIT_GRANT = {{REQ_CNT-1{1'b0}}, 1'b1};
parameter WEIGHT_WIDTH = GRANT_WIDTH * REQ_CNT;
input clk;
input rst_n;
input [REQ_CNT-1:0] req;
input [WEIGHT_WIDTH-1:0] weight;
input switch_to_next;
output [REQ_CNT-1:0] grant;
output grant_vld;
output [REQ_CNT-1:0] grant_ff;
output grant_ff_vld;
reg [REQ_CNT-1:0] grant_base;
wire [REQ_CNT*2-1:0] double_req;
wire [REQ_CNT*2-1:0] double_grant;
wire [REQ_CNT-1:0] grant;
wire grant_vld;
reg [REQ_CNT-1:0] grant_ff;
reg grant_ff_vld;
reg [GRANT_WIDTH-1:0] priority_cnt [REQ_CNT-1:0];
wire [REQ_CNT-1:0] cnt_over;
wire [REQ_CNT-1:0] round_en;
wire req_change;
wire no_req;
wire no_grant;
wire first_grant;
wire arb_trig;
assign no_req = ~(|req);
assign no_grant = ~(|grant_ff);
assign first_grant = no_grant && ~no_req;
assign arb_trig = first_grant || switch_to_next;
assign req_change = ~(|(grant_ff & req)) && |req;
generate
genvar i;
for(i=0;i<REQ_CNT;i=i+1)begin
assign cnt_over[i] = priority_cnt[i] == weight[GRANT_WIDTH*(i+1)-1-:GRANT_WIDTH];
assign round_en[i] = cnt_over[i] || (priority_cnt[i]!=0 && req_change);
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
priority_cnt[i] <= {GRANT_WIDTH{1'b0}};
else if(cnt_over[i] && arb_trig)
priority_cnt[i] <= {GRANT_WIDTH{1'b0}};
else if(grant[i])
priority_cnt[i] <= priority_cnt[i] + 1'b1;
end
end
endgenerate
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
grant_base[REQ_CNT-1:0] <= INIT_GRANT;
else if(|round_en && ~no_grant)
grant_base[REQ_CNT-1:0] <= {grant_ff[REQ_CNT-2:0],grant_ff[REQ_CNT-1]};
end
assign double_req[REQ_CNT*2-1:0] = {req,req};
assign double_grant[REQ_CNT*2-1:0] = double_req & (~(double_req - grant_base));
assign grant = arb_trig? (double_grant[REQ_CNT*2-1:REQ_CNT] | double_grant[REQ_CNT-1:0]) : {REQ_CNT{1'b0}};
assign grant_vld = (arb_trig && ~no_req)? 1'b1 : 1'b0;
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
grant_ff <= {REQ_CNT{1'b0}};
else if(arb_trig)
grant_ff <= grant;
else
grant_ff <= grant_ff;
end
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
grant_ff_vld <= 1'b0;
else if(arb_trig)
grant_ff_vld <= no_req? 1'b0 : 1'b1;
end
endmodule
2.基于request设计
module weight_rr_arbiter(
clk,
rst_n,
req,
weight,
grant,
grant_vld,
grant_ff,
grant_ff_vld,
switch_to_next
);
parameter REQ_CNT = 4;
parameter GRANT_WIDTH = 5;
parameter INIT_GRANT = {{REQ_CNT-1{1'b0}}, 1'b1};
parameter WEIGHT_WIDTH = GRANT_WIDTH * REQ_CNT;
input clk;
input rst_n;
input [REQ_CNT-1:0] req;
input [WEIGHT_WIDTH-1:0] weight;
input switch_to_next;
output [REQ_CNT-1:0] grant;
output grant_vld;
output [REQ_CNT-1:0] grant_ff;
output grant_ff_vld;
wire [REQ_CNT-1:0] grant;
wire grant_vld;
reg [REQ_CNT-1:0] grant_ff;
reg grant_ff_vld;
wire no_req;
wire no_grant;
wire first_grant;
wire arb_trig;
reg [GRANT_WIDTH-1:0] priority_cnt [REQ_CNT-1:0];
wire [REQ_CNT-1:0] cnt_over;
wire round_en;
assign no_req = ~(|req);
assign no_grant = ~(|grant_ff);
assign first_grant = ~no_req && no_grant;
assign arb_trig = first_grant || switch_to_next;
assign round_en = |cnt_over[REQ_CNT-1:0];
wire [REQ_CNT-1:0] req_masked;
wire [REQ_CNT-1:0] mask_higher_pri_reqs;
wire [REQ_CNT-1:0] grant_masked;
wire [REQ_CNT-1:0] unmask_higher_pri_reqs;
wire [REQ_CNT-1:0] grant_unmasked;
wire no_req_masked;
reg [REQ_CNT-1:0] mask_next;
//Simple priority arbitration for masked portion
assign req_masked[REQ_CNT-1:0] = req & mask_next;
assign mask_higher_pri_reqs[0] = 1'b0;
assign mask_higher_pri_reqs[REQ_CNT-1:1] = req_masked[REQ_CNT-2:0] | mask_higher_pri_reqs[REQ_CNT-2:0];
assign grant_masked[REQ_CNT-1:0] = req_masked[REQ_CNT-1:0] & ~mask_higher_pri_reqs[REQ_CNT-1:0];
//Simple priority arbitration for unmasked portion
assign unmask_higher_pri_reqs[0] = 1'b0;
assign unmask_higher_pri_reqs[REQ_CNT-1:1] = req[REQ_CNT-2:0] | unmask_higher_pri_reqs[REQ_CNT-2:0];
assign grant_unmasked[REQ_CNT-1:0] = req[REQ_CNT-1:0] & ~unmask_higher_pri_reqs[REQ_CNT-1:0];
//Use grant_masked if there is any there, otherwise use grant_unmasked
assign no_req_masked = ~(|req_masked);
assign grant = ({REQ_CNT{no_req_masked}} & grant_unmasked) | grant_masked;
assign grant_vld = (arb_trig && |req)? 1'b1 : 1'b0;
//round cnt
generate
genvar i;
for(i=0;i<REQ_CNT;i=i+1)begin
assign cnt_over[i] = (priority_cnt[i] == weight[GRANT_WIDTH*(i+1)-1-:GRANT_WIDTH]-1'b1);
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
priority_cnt[i] <= {GRANT_WIDTH{1'b0}};
else if(cnt_over[i])
priority_cnt[i] <= {GRANT_WIDTH{1'b0}};
else if(grant[i] && grant_vld)
priority_cnt[i] <= priority_cnt[i] + 1'b1;
end
end
endgenerate
//pointer update
always @(posedge clk or negedge rst_n)begin
if(!rst_n)
mask_next <= {REQ_CNT{1'b1}};
else begin
case({first_grant,round_en})
2'b10:begin
if(|req_masked)
mask_next <= req_masked | mask_higher_pri_reqs;
else
mask_next <= req | unmask_higher_pri_reqs;
end
2'b01,2'b11:begin
if(|req_masked)
mask_next <= mask_higher_pri_reqs;
else begin
if(|req)
mask_next <= unmask_higher_pri_reqs;
else
mask_next <= mask_next;
end
end
default:mask_next <= mask_next;
endcase
end
end
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
grant_ff <= {REQ_CNT{1'b0}};
grant_ff_vld <= 1'b0;
end
else if(arb_trig)begin
grant_ff <= grant;
grant_ff_vld <= no_req? 1'b0 : 1'b1;
end
else begin
grant_ff <= grant_ff;
grant_ff_vld <= grant_ff_vld;
end
end
endmodule
由于博主水平有限,内容如有不当之处,欢迎指教