仲裁器设计(一) -- Fixed Priority Arbiter
扫描二维码
随时随地手机看文章
仲裁器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_comb 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里的按顺序evaluate语法规则来实现了优先级。这里多说一句给verilog的初学者,尽管verilog和C看起来很像,很多关键字都是一样的,比如case,但是verilog的case和C的case是不一样的,verilog的case自带"break",即当一个condition满足之后,就只会执行这一条冒号之后的,只有input 发生变化之后才会再次evaluate,因为这是描述硬件电路。而C语言的case里如果一条满足之后会按照顺序继续往下执行,如果下一个条件依然满足,那么就会跳到下一个条件里,所以C的case语句我们通常要加break。
好,如果这一篇老李到此为止那就太水了,下面才是真正的干货内容。面试的时候如果你能够往下发挥才能让面试官眼前一亮。
还是那个老李一直强调的,如何设计一个参数化的模块。对于上面的仲裁器来说,我们希望可以参数化产生请求的个数,即设计下面的模块
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 logic [REQ_WIDTH-1:0] pre_req;
9
10 always_comb 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,很明显,按位与之后gnt依然都是全0,没有任何问题。
聪明的同学可能已经联想到,减1再取反,这不是计算2的补码的算法吗?只不过我们书本上学到的给一个数求2的补码的方法是取反再加1,这里倒过来,减1再取反,本质上是一样的。这其实是2的补码的一个特性,即一个数和它的补码相与,得到的结果是一个独热码,独热码为1的那一位是这个数最低的1。所以这个仲裁器的设计方法用一句话概括:request和它的2的补码按位与。如果同学们在面试的时候被问到设计优先级固定仲裁器,只要记住这句话,那么你就可以惊艳面试官。