一、百度百科:关于优先编码器
优先编码器是一种能将多个二进制输入压缩成更少数目输出的电路或算法。其输出是序数0到输入最高有效位的二进制表示。优先编码器常用于在处理最高优先级请求时控制中断请求。
如果同时有两个或以上的输入作用于优先编码器,优先级最高的输入将会被优先输出。下图是一位4线-2线编码器的例子,其中最高优先级的输入在功能表的左侧,而“x”代表无关项,即可是1也可是0,也就是说不论无关项的值是什么,都不影响输出,只有最高优先级的输入有变化时,输出才会改变。
I3
I2
I1
I0
O1
O0
0
0
0
x
0
0
0
0
1
x
0
1
0
1
x
x
1
0
1
x
x
x
1
1
4线-2线优先编码器
优先编码器可以排列连接在一起,组成更大规模的编码器,如6个4线-2线优先编码器可以组成1个16线-4线编码器,其中信号源作为4个编码器的输入,前4个编码器的输入作为2个编码器的输入。
优先编码器相比简单编码器电路有更强的处理能力,因为其能处理所有的输入组合情况。
二、提出问题
近期正在用Verilog HDL编写一个功能较为完善的MIPS处理器MangoMIPS32(已开源,github链接见文末),在处理不同优先级的例外、CLO/CLZ指令(对操作数最高位连续的0/1进行计数)等问题上遇到了需要优先编码器的情况。在这些情景下,最容易想到的解决方案有以下两种:
【方案1:嵌套if-else】
// 以例外处理为例
always @(*) begin
if (exc[0] == 1'b1) begin
...
end
else if (exc[1] == 1'b1) begin
..
end
else if ...
...
end
end
该方案的优点是浅显易懂,但是仔细分析会发现,假设第一个if的路径是1单位的门延迟,那么最长的路径(也就是最后一个else)将会对前面31种情况都进行判断,也就是32个单位的门延迟了,这显然是令人难以接受的,更何况代码也十分冗长。
【方案2:嵌套的“expression ? do_if_true : do_if_false" 句式】
该方案来自于雷思磊的OpenMIPS处理器,当初写自己的第一个处理器的时候几乎是照搬的,现摘录如下:
assign clores = ~opr1[31] ? 0 : ~opr1[30] ? 1 :
~opr1[29] ? 2 : ~opr1[28] ? 3 :
~opr1[27] ? 4 : ~opr1[26] ? 5 :
~opr1[25] ? 6 : ~opr1[24] ? 7 :
~opr1[23] ? 8 : ~opr1[22] ? 9 :
~opr1[21] ? 10 : ~opr1[20] ? 11 :
~opr1[19] ? 12 : ~opr1[18] ? 13 :
~opr1[17] ? 14 : ~opr1[16] ? 15 :
~opr1[15] ? 16 : ~opr1[14] ? 17 :
~opr1[13] ? 18 : ~opr1[12] ? 19 :
~opr1[11] ? 20 : ~opr1[10] ? 21 :
~opr1[ 9] ? 22 : ~opr1[ 8] ? 23 :
~opr1[ 7] ? 24 : ~opr1[ 6] ? 25 :
~opr1[ 5] ? 26 : ~opr1[ 4] ? 27 :
~opr1[ 3] ? 28 : ~opr1[ 2] ? 29 :
~opr1[ 1] ? 30 : ~opr1[ 0] ? 31 : 32 ;
可以看出,该方案在代码长度上虽然比方案1精简了许多,但是本质上并无差别,最大的门延迟依旧是32级。
三、解决问题
1. 方案3:二分法
在思考解决方案的时候,我们不妨类比一下软件的算法。以上的解法时间复杂度相当于O(n),只是在软件的算法中,O(n)已经是比较出色的效率,而对于硬件而言则远远达不到最优,因为硬件设计事实上存在很多潜在的并行性。该问题如果从软件的角度看,类似于一个O(n)的查找,而对于查找算法的优化,很容易想到O(logn)的二分查找。
由此,我们能想到这样的解决方案:先将输入信号列表二分,然后将相对优先级高的一半信号相与,判断是否为0:如果为0,则优先级最高的有效信号一定在另一半,否则就在这一半;选出这个半边之后继续进行二分,直到。这时候由于二进制的特性,可以设置一个下标变量,描述优先级最高的输入信号在整个列表中的下标;而在上面这个二分的过程中,每次都能决定这个二进制数的一位。当然,使用这样的特性有一个很重要的限制条件,就是输入信号的数量n为2的幂;在n不为2的幂的时候,可能需要0(或者可能是1)来补足位数。
当二分至情况的数量可控的时候,为了稍微优化效率,我使用了casez语句。之所以是casez而不是case,是因为casez会把条件中高阻态z和问号?视为不关心的位,而case对于z和?都是必须按照高阻态来匹配。
作为例子,以下是我在最新的MangoMIPS32 v1.0.1中使用的CLO/CLZ指令实现的代码:
//CLO/CLZ
reg [`Word] clzopr;
reg [`Word] clzres;
wire [15:0] part1 = clzres[4] ? clzopr[15: 0] : clzopr[31:16];
wire [ 7:0] part2 = clzres[3] ? part1 [ 7: 0] : part1 [15: 8];
wire [ 3:0] part3 = clzres[2] ? part2 [ 3: 0] : part2 [ 7: 4];
always @(*) begin
case (aluop)
`ALU_CLO: clzopr <= ~opr1;
`ALU_CLZ: clzopr <= opr1;
default: clzopr <= `ZeroWord;
endcase
if(clzopr == `ZeroWord) clzres <= 32'd32;
else begin
clzres[31:5] <= 27'b0;
clzres[4] <= (clzopr[31:16] == 16'b0);
clzres[3] <= (part1 [15: 8] == 8'b0);
clzres[2] <= (part2 [ 7: 4] == 4'b0);
casez (part3)
4'b0001: clzres[1:0] <= 2'b11;
4'b001?: clzres[1:0] <= 2'b10;
4'b01??: clzres[1:0] <= 2'b01;
default: clzres[1:0] <= 2'b00;
endcase
end
end
在这个例子中,优先级高的是高位;而产生的目的数据事实上是第一个1左侧(高位)的0的数量,因此与一般的优先编码器稍有区别,但是殊途同归。在这个例子中,可能不是那么容易看清楚互相之间的依赖关系,我将语句按照类似软件的时间先后顺序稍微排一下:
clzres[4] <= (clzopr[31:16] == 16'b0);
wire [15:0] part1 = clzres[4] ? clzopr[15: 0] : clzopr[31:16];
clzres[3] <= (part1 [15: 8] == 8'b0);
wire [ 7:0] part2 = clzres[3] ? part1 [ 7: 0] : part1 [15: 8];
clzres[2] <= (part2 [ 7: 4] == 4'b0);
wire [ 3:0] part3 = clzres[2] ? part2 [ 3: 0] : part2 [ 7: 4];
casez (part3)
4'b0001: clzres[1:0] <= 2'b11;
4'b001?: clzres[1:0] <= 2'b10;
4'b01??: clzres[1:0] <= 2'b01;
default: clzres[1:0] <= 2'b00;
endcase
这样也许能清晰很多,在此不再赘述。
为了测试代码的效率,在Vivado中开了一个工程(目标器件选择了XC7A200T-FBG676-2),将输入和输出进行时钟同步后,进行仿真和综合;测试结果全部正确,/总延迟3.044ns,逻辑延迟0.945ns,线网延迟2.099ns。后面的测试都与此环境相同,延时仅作为一个相对参考值。
2:方案4:从二分法想到的另一个方法
二分法将所谓的时间效率提高到了O(logn),但仍然不是最优的。对于n=32的特定问题,为了继续挖掘潜在的并行性,决定试着先将32位划分为8组4位,先找出第一个不为全0的4位组,再计算4位组内下标的方式。至于找出第一个不为全0的组的方式,用casez来实现。样例代码是我原计划用于MangoMIPS32的TLB寻找首个匹配表项的部分,代码如下:
wire [7:0] flag;
genvar i;
generate
for(i = 0; i < 8; i = i + 1) begin
assign flag[i] = TLBhit[4*i+3:4*i] != 0;
end
endgenerate
reg [3:0] temp;
reg [4:0] base;
always @(*) begin
casez (flag)
8'b???????1: temp <= TLBhit[ 3: 0];
8'b??????10: temp <= TLBhit[ 7: 4];
8'b?????100: temp <= TLBhit[11: 8];
8'b????1000: temp <= TLBhit[15:12];
8'b???10000: temp <= TLBhit[19:16];
8'b??100000: temp <= TLBhit[23:20];
8'b?1000000: temp <= TLBhit[27:24];
default : temp <= TLBhit[31:28];
endcase
casez (flag)
8'b???????1: base <= 0;
8'b??????10: base <= 4;
8'b?????100: base <= 8;
8'b????1000: base <= 12;
8'b???10000: base <= 16;
8'b??100000: base <= 20;
8'b?1000000: base <= 24;
default: base <= 28;
endcase
casez (temp)
4'b???1: hitidx <= base;
4'b??10: hitidx <= base + 1;
4'b?100: hitidx <= base + 2;
default: hitidx <= base + 3;
endcase
end
令人沮丧的是,在测试中,该代码表现出的性能有些不如人意:总延迟3.250ns,逻辑延迟0.926ns,线网延迟2.324ns,较之方案3的逻辑延迟有细微优化,而线网延迟增加了,导致总延迟也增加了。这显然还不是我想要的结果。
3. 方案5:暴力casez
这原本是最容易想到的一个方案,但是这个代码实在敲起来太麻烦,思前想后决定写了个C语言代码来生成。不多说,看代码:
always @(*) begin
casez (TLBhit)
32'b???????????????????????????????1: hitidx <= 0;
32'b??????????????????????????????10: hitidx <= 1;
32'b?????????????????????????????100: hitidx <= 2;
32'b????????????????????????????1000: hitidx <= 3;
32'b???????????????????????????10000: hitidx <= 4;
32'b??????????????????????????100000: hitidx <= 5;
32'b?????????????????????????1000000: hitidx <= 6;
32'b????????????????????????10000000: hitidx <= 7;
32'b???????????????????????100000000: hitidx <= 8;
32'b??????????????????????1000000000: hitidx <= 9;
32'b?????????????????????10000000000: hitidx <= 10;
32'b????????????????????100000000000: hitidx <= 11;
32'b???????????????????1000000000000: hitidx <= 12;
32'b??????????????????10000000000000: hitidx <= 13;
32'b?????????????????100000000000000: hitidx <= 14;
32'b????????????????1000000000000000: hitidx <= 15;
32'b???????????????10000000000000000: hitidx <= 16;
32'b??????????????100000000000000000: hitidx <= 17;
32'b?????????????1000000000000000000: hitidx <= 18;
32'b????????????10000000000000000000: hitidx <= 19;
32'b???????????100000000000000000000: hitidx <= 20;
32'b??????????1000000000000000000000: hitidx <= 21;
32'b?????????10000000000000000000000: hitidx <= 22;
32'b????????100000000000000000000000: hitidx <= 23;
32'b???????1000000000000000000000000: hitidx <= 24;
32'b??????10000000000000000000000000: hitidx <= 25;
32'b?????100000000000000000000000000: hitidx <= 26;
32'b????1000000000000000000000000000: hitidx <= 27;
32'b???10000000000000000000000000000: hitidx <= 28;
32'b??100000000000000000000000000000: hitidx <= 29;
32'b?1000000000000000000000000000000: hitidx <= 30;
32'b10000000000000000000000000000000: hitidx <= 31;
default: hitidx <= 0;
endcase
end
是不是非常暴力?
令人惊喜的是,这样的暴力方法,表现出了令人惊讶的性能:总延迟2.433ns,逻辑延迟0.716ns,线网延迟1.717ns,远远优于前面两种方案。
三、效率对比
这里归总一下方案3~5的效率:
方案3:总延迟3.044ns,逻辑延迟0.945ns,线网延迟2.099ns
方案4:总延迟3.250ns,逻辑延迟0.926ns,线网延迟2.324ns
方案5:总延迟2.433ns,逻辑延迟0.716ns,线网延迟1.717ns
最终我们可以得到结论,方案5,也就是直接对输入条件进行casez,时间效率是最高的。
空间效率的对比上,由于输入输出使用的是相同数量的FF(输入32,输出5,总共37),这里列举一下综合后LUT的数量:
方案3:LUT*31
方案4:LUT*21
方案5:LUT*30
可以看到相对来说方案4尽管时间效率较低,但是在片上资源紧张的情况下更省空间。
四、结论
本文提出了5种实现优先编码器的方式,并对较新的3种进行了相对时间效率和空间效率的比对。在时间效率上,方案5直接使用casez对所有情况分类的算法相对占优,但是对于更高位数的实现对于代码的美观性还是有所影响,并且生成代码较为麻烦;空间效率上,方案4分为两层casez解决问题更节省器件。
五、参考文献
1. 《自己动手写CPU》,雷思磊,电子工业出版社
2. 优先编码器,百度百科:https://baike.baidu.com/item/%E4%BC%98%E5%85%88%E7%BC%96%E7%A0%81%E5%99%A8/969949?fr=aladdin
3. MangoMIPS32@github: https://github.com/RickyTino/MangoMIPS32
--------------------------------------------------------------------------------------------
2019.3.2更新
今日突然发现方案一、方案二和方案五在Vivado中综合出了几乎完全相同的电路。
在这里向大家道个歉,其实本文真的没有什么太大价值。实现优先编码器,你能想到的办法(方案1和方案2),就是最简单最有效的办法。
为我的不严谨再次致歉,本文不久后将删除。