仲裁器Arbiter是数字设计中非常常见的模块,应用也非常的广泛,定义就是当有两个或者两个以上的模块需要占用同一个资源的时候,我们需要由仲裁器arbiter来决定哪一个模块来占有这个资源,类比一下,老师上课问了一个问题,底下同学不止一个人举手了,老师这个时候就要扮演仲裁者的角色,来指定由哪位同学站起来回答问题。一般来说,提出占有资源的模块要产生一个请求(request),类比于学生要举手表示自己要回答问题。所有的请求送给仲裁器之后,仲裁器要返回一个许可(grant),也就是老师要选择一名同学,通过点这个同学的名字的方式,告诉这个同学可以站起来回答问题。
仲裁器很重要的一点是只能让一个模块得到许可,因为这个资源某一时刻只能由一个模块占用,类比老师提问,老师不能让两个学生同时站起来回答,因为两个人同时站起来说话就谁也听不清了,在数字电路中,总线仲裁是一个常见的例子,比如多个master要占用总线来写数据,那么需要仲裁器来许可哪个master来占用总线。
那么一个显而易见的问题就是,仲裁器应该基于什么原则来分配许可呢?这篇我们先讨论最常见的方式:固定优先级——fixed priority
固定优先级,顾名思义,就是说每个模块的优先级是固定的,是提前分配好的,如果有两个模块同时产生request,那么优先级高的模块可以获得grant,还是类比老师叫同学起来回答问题,比如,在老师心目中,老师可以这样安排一个优先级:学号,学号是1的同学优先级最高,2号其次,一直到最后一位同学N,当大家举手的时候,总是叫学号最小的哪一个,比如说如果1号同学举手了,那就叫1号;如果1号,2号都没有举手,3号举手了,那就叫3号,很好理解吧?当然,如果N个同学里只有1位举手,那不管这个同学是几号,都让他回答问题,如果大家都不举手,那么老师就不叫了(现实中可能不是这样,有的老师会点名,但是咱们不讨论)
回到仲裁器本身,如果我们要设计一个fixed priority arbiter,输入时一个multibit request,每一个bit代表一个模块的request,输出一个multibit grant,每个bit代表给对应的模块的grant信号,我们可以把优先级这样安排,最低位的优先级最高,最高位的优先级最低。
我们先以3个模块产生request为例,大家一般在面试的时候都会碰到给定模块数目,比如3,让你设计,咱们就直接上code来标识一种写法;
1 module fixed_prio_arb
2 (
3 input [2:0] req,
4
5 output logic [2:0] grant
6 );
7
8 always@(*) begin
9 case (1'b1)
10 req[0]: grant = 3'b001;
11 req[1]: grant = 3'b010;
12 req[2]: grant = 3'b100;
13 default:grant = 3'b000;
14 endcase
15 end
16
17 endmodule: fixed_prio_arb
这里的技巧时利用verilog中case语句,可以比用if else简洁,而且利用了case里的按顺序执行的语法规则来实现了优先级。
好,如果这一篇到此为止那就有点太水了,下面才是真正的干货内容,面试的时候如果你能够往下发挥才能让面试官眼前一亮。
我们一直强调的,如何设计一个参数化的模块,对于上面的仲裁器来说,我们希望可以参数化产生请求的个数,即设计下面的模块。
1 module priority_arbiter #(
2 parameter REQ_WIDTH = 16
3 )
(
4 input [REQ_WIDTH-1:0] req,
5 output [REQ_WIDTH-1:0] gnt
6 );
这样我们可以根据不同场合产生request的模块的个数来例化同一个仲裁器,只需要改变参数即可,很明显,上面利用case的写法不能scalable,我们需要更加巧妙的办法;
首先可以想到的办法是利用for循环,思路其实非常直接,从低位到高位依次去判断,借助一个pre_req来记录低位是否已经有了request,如果第i位有了request,那么第i+1位一直到最高位的pre_req都是1;
1 module prior_arb #(
2 parameter REQ_WIDTH = 16
3 )(
4 input logic [REQ_WIDTH-1:0] req,
5 output logic [REQ_WIDTH-1:0] grant
6 );
7
8 reg [REQ_WIDTH-1:0] pre_req;
9
10 always@(*) begin
11 pre_req[0] = req[0];
12 grant[0] = req[0];
13 for (int i = 1; i < REQ_WIDTH; i = i + 1) begin
14 grant[i] = req[i] & !pre_req[i-1]; // current req & no higher priority request
15 pre_req[i] = req[i] | pre_req[i-1]; // or all higher priority requests
16 end
17 end
18
19 endmodule
有没有更简介的办法呢?下面介绍两种实现方式,code非常简洁,先来上面的设计的变体,但是不用for循环,本质是一样的,只有3行code;
1 module prior_arb #(
2 parameter REQ_WIDTH = 16
3 )
(
4 input [REQ_WIDTH-1:0] req,
5 output [REQ_WIDTH-1:0] gnt
6 );
7
8 logic [REQ_WIDTH-1:0] pre_req;
9
10 assign pre_req[0] = 1'b0;
11
12 assign preq_req[REQ_WIDTH-1:1] = req[REQ_WIDTH-2:0] | pre_req[REQ_WIDTH-2:0];
13
14 assign gnt = req & ~pre_req;
15
16 endmodule
下面的这种实现方式就更夸张了,就一行实现,读到这里的读者可以说是赚到了,因为在各种介绍仲裁器的公众号文章里作者从来没有见到过这种方法,作者第一次学到的时候也是惊为天人,但是它背后的思想却非常朴素,简直不超过小学一年级的知识。
1 module prior_arb #(
2 parameter REQ_WIDTH = 16
3 ) (
4 input [REQ_WIDTH-1:0] req,
5 output [REQ_WIDTH-1:0] gnt
6 );
7
8 assign gnt = req & (~(req-1));
9 endmodule
来解释一下这个思路到底是啥:
本质上,我们要做的是找req这个信号里从低到高第一个出现的1,那么我们给req减去1会得到什么?假设req的第i位是1,第0到第i-1位都是0,那么减去1之后我们知道低位不够减,得要向高位借位,直到哪一位可以借到呢?就是第一次出现1的位,即从第i位借位,第0到i-1位都变成了1,而第i位变为了0,更高位不变,然后我们再给减1之后的结果取反,然后把结果再和req本身按位与,可以得出,只有第i位在取反之后又变成了1,而其余位都是和req本身相反的,按位与之后是0,这样就提取出来了第一个为1的那一位,也就是我们需要的grant,再考虑一下特殊情况req全0,很明显,按位与之后grant依然全都是0,没有任何问题。
相信已经有同学联想到了,减1再取反,这不是计算2进制的补码的算法吗?只不过我们书本上学到的给一个数求2的补码的方法是取反再加1,这里倒过来,减1再取反,本质上是一样的,这其实是2进制补码的一个特性,即一个数和它的补码相与,得到的结果是一个独热码,独热码为1的那一位是这个数最低的1位,所以这个仲裁器的设计方法用一句话概括:Request和它的2进制补码按位与,如果同学们在面试的时候被问到设计优先级固定仲裁器,只要记住这句话,那么你就可以惊艳面试官!