LoginSignup
22
11

More than 5 years have passed since last update.

FPGA用にシンプルな浮動小数点数演算器を作る

Last updated at Posted at 2018-12-22

浮動小数点数は広いダイナミックレンジを扱う上で非常に便利です。自身の都合上FPGAで浮動小数点数を使用する機会が多いのですが、演算にはFloating Point Unit (FPU)が必要になります。Xilinx LogiCORE IPコアやAltera Megafunctionでモジュールを生成すれば良いのですが、設定が微妙に違うために他社製品への移行が面倒だったりします。また、タイミングバイオレーションの原因になります。モジュールの設定管理も面倒なので、用途に合った構成の32bitのFPUを作成しました。

目的

概念はFloPoCoに通ずるものがあると思います。(http://flopoco.gforge.inria.fr/)
用途に合った最小構成の回路を用意できるのがFPGAです。今回作成するFPUは
1. 非正規化数を0として扱う。
2. Guard, Round, Sticky (GSR) Bitsを無視する。
という条件の下で設計を行います。XilinxもIntelも非正規化数は無視しているとの記述が仕様書に記載されています。流石にSticky bitは無視していませんが、自身の用途では無視しても特段問題がないので、設定しません。

浮動小数点数のルール

32bitの浮動小数点数はIEEE 754で規定されているルールに則れば、符号部1ビット、指数部8ビット、仮数部23ビットで構成されます。このルール自体を変更して自身の用途に合った回路を作成することも可能です。指数部(Sign)のバイナリをs、指数部(Exponent)のバイナリをe、仮数部(Mantissa)のバイナリをmとすると、浮動小数点数は以下の値Fを示します。

F=(-1)^{s} \times 2^{e-127} \times (1+2^{-23}m)

仮数部の1はケチ表現と言われるものです。

特別なルールとして、0、非正規仮数、無限大、NaNにコードが割り当てられています。
非正規仮数は指数部をe=0として仮数部のケチ1を取り払ったものであり、扱いづらいものです。
0は32'b0、無限大は指数部が8'hffかつ仮数部が0の値、NaNは無限大は指数部が8'hffかつ仮数部が0でない値が割り当てられています。

乗算器

浮動小数点数の演算においては、掛け算は足し算より簡単です。二つの数A、Bを入力と仮定します。

A=(-1)^{s_a} \times 2^{e_a-127} \times (1+2^{-23}m_a)\\
B=(-1)^{s_b} \times 2^{e_b-127} \times (1+2^{-23}m_b)

二つの値の積は

A \times B = (-1)^{s_a+s_b} \times 2^{e_a+e_b-254} \times (1+2^{-23}m_a) \times (1+2^{-23}m_b)\\
= (-1)^{s_a+s_b} \times 2^{(e_a+e_b-127)-127} \times (1+2^{-23}m_a) \times (1+2^{-23}m_b)

となります。したがって、各部の計算を行って、最後に仮数部と指数部のシフトを行えば良いです。
この乗算器において、例外処理を二つ必要とします。一つは入力値が0、無限大、NaN、非正規仮数で会った際の処理、一つは演算結果が浮動小数点数の表現領域を超えた際です。この例外処理を加えた回路記述を以下に示します。

multplier.vhd

module sf_mlt(input wire clk, input wire ena, input wire [31:0] in1,input wire [31:0] in2,input wire [31:0] out);

localparam integer LATENCY=2;
localparam signed [9:0] exp_bias=10'd127;

reg [1:0] exc_inf;
reg [1:0] exc_zero;
reg [LATENCY-1:0] exc_inf_fifo;     initial exc_inf_fifo=1'b0;
reg [LATENCY-1:0] exc_zero_fifo;    initial exc_zero_fifo=1'b0;

reg sign;
reg [LATENCY-1:0] sign_fifo;

reg signed [9:0] exp;
reg [7:0] exp_exc;
reg [7:0] exp_shift;

reg [47:0] man;
reg [24:0] man_exc;
reg [22:0] man_shift;

reg [31:0] sf;
localparam sf_zero=32'h0;
localparam sf_inf=32'h7f800000;

assign out=sf;

always@(posedge clk)
begin

    if(!ena)
    begin
        exc_inf<= 1'b0;
        exc_inf_fifo <= 1'b0;

        exc_zero <= 1'b0;
        exc_zero_fifo <= 1'b0;

        sign <= 1'b0;
        sign_fifo <= 1'b0;

        exp <= 1'b0;
        exp_exc <= 1'b0;
        exp_shift <= 1'b0;

        man <= 1'b0;
        man_exc <= 1'b0;
        man_shift <= 1'b0;

        sf <= 1'b0;
    end
    else
    begin
        //Abstracting Infinity and NaN
        exc_inf[0] <= (in1[30:23]==8'hff)? 1'b1:1'b0;
        exc_inf[1] <= (in2[30:23]==8'hff)? 1'b1:1'b0;
        exc_inf_fifo <= {exc_inf_fifo[LATENCY-2:0],exc_inf[0]|exc_inf[1]};

        //Rounding Denormalized Number to Zero
        exc_zero[0] <= (in1[30:23]==8'b0)?  1'b1:1'b0;
        exc_zero[1] <= (in2[30:23]==8'b0)?  1'b1:1'b0;
        exc_zero_fifo <= {exc_zero_fifo[LATENCY-2:0],exc_zero[0]|exc_zero[1]};

        //XOR signs
        sign <= in1[31]^in2[31];
        sign_fifo <= {sign_fifo[LATENCY-2:0],sign};

        //Exponent
        exp <= $signed({2'b0,in1[30:23]})+$signed({2'b0,in2[30:23]})-$signed(exp_bias);
        exp_exc <= (exp[9]!=1'b1|exp!=10'h0)? ((exp[8]!=1'b1)? exp:8'hff):8'h00;
        exp_shift <= (man_exc[24]==1'b1&exp_exc!=8'hff&exp_exc!=8'h0)? exp_exc+1'b1:exp_exc;

        //Mantissa
        man <= (in1!=32'b0&in2!=32'b0)? {1'b1,in1[22:0]}*{1'b1,in2[22:0]}:48'b0;
        man_exc <= (exp[9]!=1'b1&exp!=10'h0&exp[8]!=1'b1)? man[47:23]:25'b0;
        man_shift <= (man_exc[24]==1'b1)? man_exc[23:1]:man_exc[22:0];

        //Output
        sf <= (exc_zero_fifo[LATENCY-1]==1'b0&exp_shift!=8'b0)? ((exc_inf_fifo[LATENCY-1]==1'b0)? {sign_fifo[LATENCY-1],exp_shift,man_shift}:sf_inf):sf_zero;
    end
end

endmodule

仮数部の演算において、幅23(IEEE 754単精度浮動小数点数の規定仮数部ビット)+2=25のバッファを演算結果格納用に使用することで、シフト処理が短縮されています。この2ビットは、ケチビットと繰り上がり判定用ビットです。ケチビットを含めた仮数部の表現範囲は[1.0,2.0)なので、二つの数の仮数部の積の範囲は[1.0,4.0)となります(入力が0および非正規仮数でなければ)。つまり、ケチビット込みの仮数部24ビット同士の乗算において得られる48ビットのバイナリにおいて、小数点位置は左から二個目であり、最上位ビットは桁上がりを示しています。桁上がりをしていたら指数部をシフトすれば良いです。この演算では、仮数部の積の範囲の制約から「桁上がりを一つする」か「桁上がりしない」かのどちらかしか起こりません。そのためGSRビットの影響を受けることは特にありません。

指数部の演算では、幅8(IEEE 754単精度浮動小数点数の規定指数部ビット)+2=10のバッファを演算結果格納用に使用することで、例外処理が短縮されています。この2ビットは符号ビットと繰り上がり判定用ビットです。これにより、次のような判定ができます。

e_a+e_b-127<{\rm 8'h0} \rightarrow {\rm Exp}[9]=1\\
e_a+e_b-127>{\rm 8'hff} \rightarrow {\rm Exp}[8]=1

以上により、小規模な回路でレイテンシ3のパイプライン乗算器が作成できました。

加算器

加算器は複雑なので、回路記述の紹介のみに留めます。
入力値の内、大きいほうの値に指数部を合わせてから演算する処理が必要です。
また、仮数部の計算結果のシフトに処理が必要なため、乗算器と比較して回路規模が大きくなってしまいました。シフトに際してonehot信号を使用しているため乗算器を使って回路を小さくすることもできますが、簡単な回路だけで構成したかったためcase文を使用しています。

adder.vhd

module sf_add(input wire clk, input wire ena, input wire [31:0] in1,input wire [31:0] in2,input wire [31:0] out);

localparam integer LATENCY=6;

reg [1:0] exc_inf;
reg [1:0] exc_zero;
reg [LATENCY-1:0] exc_inf_fifo;     initial exc_inf_fifo=1'b0;
reg [LATENCY-1:0] exc_zero_fifo;    initial exc_zero_fifo=1'b0;

reg [7:0] expD;
reg [9:0] exp_shifter;
reg signed [9:0] exp_sh;
reg [7:0] exp_fifo [5:0];
reg exp_clear;

reg [1:0] sign;
reg sign_shiftS;
reg sign_sh;
reg sign_out_buf1;
reg sign_out_buf2;
reg sign_out;

reg [25:0] manL;
reg [25:0] manS;
reg [25:0] manL_shift;
reg [25:0] manS_shift;
reg signed [25:0] man_res;
reg signed [24:0] man_abs_buf1;
reg signed [24:0] man_abs_buf2;
reg signed [22:0] man_out;
reg signed [24:0] man_abs_reverse;
reg signed [22:0] man_sh;

reg [31:0] sf;
localparam sf_zero=32'h0;
localparam sf_inf=32'h7f800000;

assign out=sf;

always@(posedge clk)
begin

    if(!ena)
    begin

        exc_inf <= 1'b0;
        exc_inf_fifo <= 1'b0;

        exc_zero <= 1'b0;
        exc_zero_fifo <= 1'b0;

        exp_fifo[0] <= 1'b0;
        expD <= 1'b0;
        sign <= 1'b0;
        manL <= 1'b0;
        manS <= 1'b0;

        exp_fifo[1] <= 1'b0;
        exp_fifo[2] <= 1'b0;
        exp_fifo[3] <= 1'b0;
        exp_fifo[4] <= 1'b0;
        exp_fifo[5] <= 1'b0;

        sign_shiftS <= 1'b0;
        manL_shift <= 1'b0;     
        manS_shift <= 1'b0;

        man_res <= 1'b0;

        sign_out_buf1 <= 1'b0;
        sign_out_buf2 <= 1'b0;
        sign_out <= 1'b0;

        man_abs_reverse <= 1'b0;        
        exp_clear <= 1'b0;

        man_abs_buf1 <= 1'b0;
        man_abs_buf2 <= 1'b0;

        man_out <= 1'b0;
        exp_shifter <= 1'd0;

        exp_sh  <= 1'b0;
        man_sh  <= 1'b0;
        sign_sh <= 1'b0;

        sf <= 1'b0;

    end
    else
    begin
        //Abstracting Infinity and NaN
        exc_inf[0] <= (in1[30:23]==8'hff)? 1'b1:1'b0;
        exc_inf[1] <= (in2[30:23]==8'hff)? 1'b1:1'b0;
        exc_inf_fifo <= {exc_inf_fifo[LATENCY-2:0],exc_inf[0]|exc_inf[1]};

        //Rounding Denormalized Number to Zero
        exc_zero[0] <= (in1[30:23]==8'b0)?  1'b1:1'b0;
        exc_zero[1] <= (in2[30:23]==8'b0)?  1'b1:1'b0;
        exc_zero_fifo <= {exc_zero_fifo[LATENCY-2:0],exc_zero[0]&exc_zero[1]};

        //Exponent and Sign
        if((in1[30:23]>in2[30:23]))
        begin
            exp_fifo[0] <= in1[30:23];
            expD <= in1[30:23]-in2[30:23];
            sign <= {in1[31],in2[31]};
            manL <= (in1[30:23]==8'b0)? 26'b0:{3'b001,in1[22:0]};   //Trancating denormalized one, +Sign, Null and Hidden 1
            manS <= (in2[30:23]==8'b0)? 26'b0:{3'b001,in2[22:0]};
        end
        else
        begin
            exp_fifo[0] <= in2[30:23];
            expD <= in2[30:23]-in1[30:23];
            sign <= {in2[31],in1[31]};
            manL <= (in2[30:23]==8'b0)? 26'b0:{3'b001,in2[22:0]};
            manS <= (in1[30:23]==8'b0)? 26'b0:{3'b001,in1[22:0]};
        end

        exp_fifo[1] <= exp_fifo[0];
        exp_fifo[2] <= exp_fifo[1];
        exp_fifo[3] <= exp_fifo[2];
        exp_fifo[4] <= exp_fifo[3];
        exp_fifo[5] <= exp_fifo[4];

        sign_shiftS <= sign[0];
        manL_shift <= (sign[1]==1'b0)? manL:~manL+1'b1;
        case (expD)     //Ignoring Stikky bit.
            8'd0:   manS_shift <= manS;
            8'd1:   manS_shift <= {1'b0, manS[25:1]};
            8'd2:   manS_shift <= {2'b0, manS[25:2]};
            8'd3:   manS_shift <= {3'b0, manS[25:3]};
            8'd4:   manS_shift <= {4'b0, manS[25:4]};
            8'd5:   manS_shift <= {5'b0, manS[25:5]};
            8'd6:   manS_shift <= {6'b0, manS[25:6]};
            8'd7:   manS_shift <= {7'b0, manS[25:7]};
            8'd8:   manS_shift <= {8'b0, manS[25:8]};
            8'd9:   manS_shift <= {9'b0, manS[25:9]};
            8'd10:  manS_shift <= {10'b0,manS[25:10]};
            8'd11:  manS_shift <= {11'b0,manS[25:11]};
            8'd12:  manS_shift <= {12'b0,manS[25:12]};
            8'd13:  manS_shift <= {13'b0,manS[25:13]};
            8'd14:  manS_shift <= {14'b0,manS[25:14]};
            8'd15:  manS_shift <= {15'b0,manS[25:15]};
            8'd16:  manS_shift <= {16'b0,manS[25:16]};
            8'd17:  manS_shift <= {17'b0,manS[25:17]};
            8'd18:  manS_shift <= {18'b0,manS[25:18]};
            8'd19:  manS_shift <= {19'b0,manS[25:19]};
            8'd20:  manS_shift <= {20'b0,manS[25:20]};
            8'd21:  manS_shift <= {21'b0,manS[25:21]};
            8'd22:  manS_shift <= {22'b0,manS[25:22]};
            8'd23:  manS_shift <= {23'b0,manS[25:23]};
            default:manS_shift <= 26'b0;
        endcase

        man_res <= (sign_shiftS==1'b0)? $signed(manL_shift)+$signed(manS_shift) : $signed(manL_shift)-$signed(manS_shift);

        sign_out_buf1 <= man_res[25];
        sign_out_buf2 <= sign_out_buf1;
        sign_out <= sign_out_buf2;

        man_abs_reverse <= {<<{man_abs_buf1}};
        exp_clear <= (man_abs_reverse==1'b0)? 1'b1:1'b0;

        man_abs_buf1 <= (man_res[25]==1'b0)? man_res:(~man_res)+1'b1;
        man_abs_buf2 <= man_abs_buf1;
        case(man_abs_reverse&(~man_abs_reverse+1'b1))       //Ignoring Stikky bit.
            25'b0000000000000000000000000:  begin man_out<=23'b0;                       exp_shifter<=10'd0;     end
            25'b0000000000000000000000001:  begin man_out<=man_abs_buf2[23:1];          exp_shifter<=10'd1;     end
            25'b0000000000000000000000010:  begin man_out<=man_abs_buf2[22:0];          exp_shifter<=10'd0;     end
            25'b0000000000000000000000100:  begin man_out<={man_abs_buf2[21:0],1'b0};   exp_shifter<=-10'd1;    end
            25'b0000000000000000000001000:  begin man_out<={man_abs_buf2[20:0],2'b0};   exp_shifter<=-10'd2;    end
            25'b0000000000000000000010000:  begin man_out<={man_abs_buf2[19:0],3'b0};   exp_shifter<=-10'd3;    end
            25'b0000000000000000000100000:  begin man_out<={man_abs_buf2[18:0],4'b0};   exp_shifter<=-10'd4;    end
            25'b0000000000000000001000000:  begin man_out<={man_abs_buf2[17:0],5'b0};   exp_shifter<=-10'd5;    end
            25'b0000000000000000010000000:  begin man_out<={man_abs_buf2[16:0],6'b0};   exp_shifter<=-10'd6;    end
            25'b0000000000000000100000000:  begin man_out<={man_abs_buf2[15:0],7'b0};   exp_shifter<=-10'd7;    end
            25'b0000000000000001000000000:  begin man_out<={man_abs_buf2[14:0],8'b0};   exp_shifter<=-10'd8;    end
            25'b0000000000000010000000000:  begin man_out<={man_abs_buf2[13:0],9'b0};   exp_shifter<=-10'd9;    end
            25'b0000000000000100000000000:  begin man_out<={man_abs_buf2[12:0],10'b0};  exp_shifter<=-10'd10;   end
            25'b0000000000001000000000000:  begin man_out<={man_abs_buf2[11:0],11'b0};  exp_shifter<=-10'd11;   end
            25'b0000000000010000000000000:  begin man_out<={man_abs_buf2[10:0],12'b0};  exp_shifter<=-10'd12;   end
            25'b0000000000100000000000000:  begin man_out<={man_abs_buf2[9:0],13'b0};   exp_shifter<=-10'd13;   end
            25'b0000000001000000000000000:  begin man_out<={man_abs_buf2[8:0],14'b0};   exp_shifter<=-10'd14;   end
            25'b0000000010000000000000000:  begin man_out<={man_abs_buf2[7:0],15'b0};   exp_shifter<=-10'd15;   end
            25'b0000000100000000000000000:  begin man_out<={man_abs_buf2[6:0],16'b0};   exp_shifter<=-10'd16;   end
            25'b0000001000000000000000000:  begin man_out<={man_abs_buf2[5:0],17'b0};   exp_shifter<=-10'd17;   end
            25'b0000010000000000000000000:  begin man_out<={man_abs_buf2[4:0],18'b0};   exp_shifter<=-10'd18;   end
            25'b0000100000000000000000000:  begin man_out<={man_abs_buf2[3:0],19'b0};   exp_shifter<=-10'd19;   end
            25'b0001000000000000000000000:  begin man_out<={man_abs_buf2[2:0],20'b0};   exp_shifter<=-10'd20;   end
            25'b0010000000000000000000000:  begin man_out<={man_abs_buf2[1:0],21'b0};   exp_shifter<=-10'd21;   end
            25'b0100000000000000000000000:  begin man_out<={man_abs_buf2[0],22'b0};     exp_shifter<=-10'd22;   end
            25'b1000000000000000000000000:  begin man_out<=23'b0;                       exp_shifter<=-10'd23;   end
            default:                        begin man_out<=23'b0;                       exp_shifter<=10'd0;     end
        endcase

        exp_sh  <= (exp_clear!=1'b1)? $signed({2'b00,exp_fifo[5]})+$signed(exp_shifter) : 1'b0;
        man_sh  <= man_out;
        sign_sh <= sign_out;

        sf <= (exc_zero_fifo[LATENCY-1]==1'b0&exp_sh[9]!=1'b1&exp_sh!=10'h0)?((exc_inf_fifo[LATENCY-1]==1'b0&exp_sh[8]!=1'b1)? {sign_sh,exp_sh[7:0],man_sh}:sf_inf):sf_zero;
    end
end

endmodule

レイテンシ8のパイプライン加算器です。onehot信号処理を使わなければレイテンシ6まで短縮できますが、if文を使う必要があり、好きではないため使用していません。減算は入力値の符号ビットを反転により実現できます。

加算器、乗算器共に小規模な回路ができたと思います。

22
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
11