MIPS32的DIV和DIVU实现(除法指令)

《自己动手写CPU》一书的7.11节到7.14节实现了DIV和DIVU指令。

书中通过“试商法”实现除法,并在原有的流水线结构之外另加了状态机进行计算。

照抄书上的实现方法需要另外添加个.v,我实在有点懒,不想在运算指令实现上再另外加个文件。

而且按照书上的实现,DIV和DIVU指令跟其他的运算指令差别很大,不符合我对结构的审美。

那么就必须想想办法自己写一个别的实现方法。

 

于是找到了一个结构化比较好的实现方法:

http://www.cnblogs.com/AndyJee/p/4575152.html

 

参照这个方法,使用verilog实现需要解决2个问题:

 

(1)文章是无符号数的除法,对应DIVU,而DIV是有符号数除法。

这里又要分两种情况讨论。

a、DIV指令的被除数和除数是有符号数

有符号数运算时,输入的被除数和除数都是补码,做除法时需要先将补码转成原码。

正数的补码就是原码,那么只需要在负数时转成原码即可。

b、DIV指令的商和余数正负符号判断

按照初等数学的定义,商的正负取决于被除数和除数的正负,而余数取决于除数的符号。

而计算机语言中,商的正负没有变化,余数符号取决于被除数。

该问题的讨论:

http://blog.sina.com.cn/s/blog_956c84bb0101j31f.html

DIV指令下,被除数/除数为负数,则转换成原码:

  assign div_dividend = ((aluop_i == `EXE_DIV_OP) && (reg1_i[31] == 1'b1)) ? (~reg1_i + 1) : reg1_i;
  assign div_divisor = ((aluop_i == `EXE_DIV_OP) && (reg2_i[31] == 1'b1)) ? (~reg2_i + 1) : reg2_i;

DIV指令下,将商和余数转回为补码:

  assign div_quotient = ((aluop_i == `EXE_DIV_OP) && (reg1_i[31] ^ reg2_i[31] == 1'b1)) ? {1'b1, (~div_quo_o[30:0] + 1)} : div_quo_o;
  assign div_remainder = ((aluop_i == `EXE_DIV_OP) && (reg1_i[31] == 1'b1)) ? {1'b1, (~div_rem_o[30:0] + 1)} : div_rem_o;

 

 

 

(2)参考的方法通过高级语言描述,需要转化成适合verilog实现的方式。

该方法的核心是通过减法来实现除法,使用中文通俗描述的步骤如下:

基本方法

a、比较被除数和除数的大小;

b、如果被除数比较大,将被除数减去除数,商加1;

c、循环步骤a和b,直到被除数比除数小,这时候被除数剩下的就是余数,商就是结果(初值为0)。

基本方法比较“笨”,效率是最低的。

加速方法

a、比较被除数和n*除数的大小;

b、如果被除数比较大,将被除数减去n*除数,商加n;

c、循环步骤a和b,直到被除数比除数小,这时候被除数剩下的就是余数,商就是结果(初值为0)。

 

加速方法适合使用移位来实现(verilog中,左移1位数值乘于2)。

但是上面文章中描述的方法不适合直接用verilog实现(2层while循环,verilog实现很麻烦)。

可以通过以下几个小技巧避免在verilog中出现类似2层while循环的结构:

a、考虑DIV和DIVU指令的除数位数有限,那么可以先判断最大需要移位几次进行加速方法步骤a(数一数除数第一个1之前有几个0);

b、在最大移位范围内,按照移位次数从大到小的顺序进行操作(使n*除数中的n尽量大),以尽快收敛结果。

最后实现时,在流水线的寄存器之间进行的运算如下(核心代码):

                //core method
                if (div_rem_i < (div_divisor << div_shift_cnt_i))
                begin
                    div_quo_o = div_quo_i;
                    div_rem_o = div_rem_i;
                end
                else
                begin
                    div_quo_o = div_quo_i + (1 << div_shift_cnt_i);
                    div_rem_o = div_rem_i + (~(div_divisor << div_shift_cnt_i)) + 1;
                end
            end

这样DIV和DIVU运算所需的周期受除数影响,除数越大则运算周期越短,除数越小则运算周期越长(移位操作次数多)。

例如,当除数是1时,需要31次移位操作(31个时钟);除数是10'b10_0000_0000时,需要22次移位(22个时钟)。

这种实现方法最大运算周期为32*时钟周期,而《自己动手写CPU》中给的例子对于32位的除法,至少需要32个时钟周期。

另外,这种实现方法与累乘加和累乘减(MADD/MADDU/MSUB/MSUBU)的结构类似,不需要另外再添加.v文件,符合懒人“初始结构以外,最好不做任何结构变动”的习惯。

 

最后,这种方法可以简单实现加速,即通过比较被除数和除数从bit31开始0的数量,可以在被除数和除数绝对值都较小的情况下节省很多运算周期。

 

posted on 2017-09-20 11:27  再见,列宁  阅读(5489)  评论(0编辑  收藏  举报

导航