• 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的数量,可以在被除数和除数绝对值都较小的情况下节省很多运算周期。

  • 相关阅读:
    Linux系统编程 —互斥量mutex
    Linux系统编程 —读写锁rwlock
    Linux系统编程—条件变量
    Linux系统编程—信号量
    SkyWalking 源码的整体结构
    带拼音插件的索引和映射创建
    磁盘扩容后文件目录迁移步骤
    应用注册Eureka配置
    StringUtils.hasText()
    Java后台进行分页参数类封装
  • 原文地址:https://www.cnblogs.com/mcdeggy/p/7560536.html
Copyright © 2020-2023  润新知