计算机如何实现浮点运算

2025-07-29

本文基于 IEEE754 标准,介绍计算机实现 FP32(32 位浮点数)加法、乘法运算的过程,并提供一份 Verilog 实现,因为我并没有仔细推敲,所以我提供的代码可能效率不够高、简洁,仅作参考。

IEEE754标准

在 IEEE754 标准下,32 个比特位被拆分为 符号位(1bit)、指数位(8bit)、小数位(23bit) 三个部分。比如:

0 1 0 0 1 0 0 1 1 0 0 1 1 1 0 0 0 0 1 0 1 0 0 1 1 1 0 0 0 0 1 0
符号位 指数位 指数位 指数位 指数位 指数位 指数位 指数位 指数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位 小数位

上例中,

  • 符号位sign 为 0
  • 指数位exponent 为 10010011,指数位的范围为(00000001 ~ 11111110),其中 00000000 代表非规格化数、11111111 代表无穷大。
  • 尾数位fraction 为 00111000010100111000010
    表示的浮点数为 \((-1)^{sign} * 2 ^{exponent - 127} * (1 + \frac{fraction}{2^{23}})\), 值得一提的是,尾数为有个隐含的 1,比如尾数为’100000’ 实际上代表的是 ‘1.100000’。这也是为什么表示的浮点数中是 $(1 + \frac{fraction}{2^{23}})$,而不是 $\frac{fraction}{2^{23}}$
module adder(
    input[31:0] a,
    input[31:0] b,
    output reg[31:0] o
    );
    reg sign_a, sign_b;
    reg[7:0] exp_a, exp_b, exp_diff, exp_o;
    reg[22:0] frac_a, frac_b;
    reg [25:0] with_double_sign_a, with_double_sign_b,with_double_sign_sum;
    reg[1:0] double_sign;
    reg[4:0] cnt;

    
    always @(*) begin
         {sign_a, exp_a, frac_a} = a;
         {sign_b, exp_b, frac_b} = b;
         ......
endmodule

上述代码实现了符号位、指数位、尾数位的提取,还定义了一些我们后面要用到的变量。

双符号位判决法

在进行加法的过程中,可能会产生溢出,比如两个极大的正数相加,结果可能为负数。为了方便地判断是否产生溢出现象,我们采用双符号判决法。
比如计算 +111111 和 +111111 的加法结果时,我们添加两个 “0” 作为符号位(双符号位名字的由来)。也即计算 “00;111111” 和 “00;111111” 的加法结果。 结果为 “01;000000” 符号位 “01” 说明发生正溢出(两个极大的正数相加,结果为负数)。

除此之外,还可能负溢出(两个极大的负数相加,结果为正数)。同样举一个例子,计算 -111111 和 -111111 的加法结果,首先转换为补码(上面其实也需要转换为补码,只不过正数补码不变)、再添加符号位 “11” 后,我们需要计算的是 “11;000001” 和 “11;000001” 的加法结果。 结果为 “10;000010” 符号位 “10” 说明发生负溢出

如果双符号位为 “00” 或者 “11”,说明没有发生溢出。

特殊情况处理

首先我们需要处理一些特殊情况,比如 “+0” “1” “0” 等,这些计算是可以直接得到结果的。

对齐

如果说,计算式不属于某种特殊情况,我们首先需要对齐。也就是将两个浮点数对应的指数位化为同一个,这样我们就可以直接对尾数位(小数位)进行加减。
我们采取的是 小阶对大阶 ,也就是把 指数位小的那个 调整为 大的指数位。这个过程还伴随尾数的移动。

 // fraction addition
        with_double_sign_a = {2'b001, frac_a};
        with_double_sign_b = {2'b001, frac_b};
            
        // Align
        if(exp_a > exp_b) begin
            exp_diff = exp_a - exp_b;
            with_double_sign_b = (with_double_sign_b >> exp_diff);
            exp_o = exp_a;
        end
        else if (exp_b > exp_a)begin
            exp_diff = exp_b - exp_a;
            with_double_sign_a = (with_double_sign_a >> exp_diff);
            exp_o = exp_b;
        end
        else begin
            exp_o = exp_b;
        end

上述代码展示了对齐过程,你可能会发现,我们先在23-bit尾数位前补充了 ‘2’b001’ 三位数。前两位代表双符号位,后面我们将根据真正的符号位进行变化,’1’ 代表隐含的 那个 ‘1’。

尾数相加减

        if(sign_a) begin
            with_double_sign_a[25:24] = 2'b11;
            with_double_sign_a[23:0] = ~with_double_sign_a[23:0] + 1;
        end
        if(sign_b) begin
            with_double_sign_b[25:24] = 2'b11;
            with_double_sign_b[23:0] = ~with_double_sign_b[23:0] + 1;
        end
        with_double_sign_sum = with_double_sign_a + with_double_sign_b; 

首先我们先根据真正的符号填充双符号位,再转换为补码,之后直接进行尾数位加减。

规格化

规格化的目的是为了使得 尾数为依然可以表示 ‘1.??????’。举个例子,’$100.1010 * (10)^{100}$’ 如果我们想要隐含的 ‘1’,那么这个形式是不行的(小数位为’1010’,如果我们令尾数为’1010’,再加上隐含的 ‘1’,实际上我们表示的是 ‘1.1010’)因此我们需要将形式转换为 ‘$1.001010 * (10)^{110}$’。

实际上,可以类比科学计数法,我们需要 “$a10^{b}$” 中的 $a$ 位于 [1,10) 之间。

        double_sign = with_double_sign_sum[25:24];
        if (double_sign == 2'b00) begin
            if (with_double_sign_sum[23] == 1'b1)begin
                o = {1'b0, exp_o, with_double_sign_sum[22:0]};
            end
            else if (with_double_sign_sum[22:0] == 0)
                o = 0;
            else begin
                cnt = 0;
                while (with_double_sign_sum[23] != 1 && (cnt < 23)) begin
                    with_double_sign_sum = (with_double_sign_sum << 1);
                    cnt = cnt + 1;
                end
                o = {1'b0, exp_o - cnt, with_double_sign_sum[22:0]};
            end
        end
        else if (double_sign == 2'b11) begin
            with_double_sign_sum[23:0] = ~(with_double_sign_sum[23:0] - 1);
            if (with_double_sign_sum[23] == 1'b1)begin
                o = {1'b1, exp_o, with_double_sign_sum[22:0]};
            end
            else if (with_double_sign_sum[22:0] == 0)
                o = 0;
            else begin
                cnt = 0;
                while (with_double_sign_sum[23] != 1 && (cnt < 23)) begin
                    with_double_sign_sum = (with_double_sign_sum << 1);
                    cnt = cnt + 1;
                end
                o = {1'b1, exp_o - cnt, with_double_sign_sum[22:0]};
            end
        end
        else if (double_sign == 2'b01) begin
            o = {1'b0, exp_o + 1, with_double_sign_sum[23:1]};
        end
        else if (double_sign == 2'b10) begin
            o = {1'b1, exp_o + 1, ~(with_double_sign_sum[23:1] - 1)};
        end

上面是根据双符号位进行规格化处理的代码。

浮点数乘法

相比较来说,浮点数乘法更加简单。我们分五个步骤:

  • 特殊情况处理
  • 确定符号位
  • 指数位加减
  • 小数位相乘
  • 规格化处理
module multiplier#(parameter int DataWidth=32,
parameter int exp_len=8,
parameter int mid=46)(
    input[DataWidth-1:0] a,
    input[DataWidth-1:0] b,
    output reg[DataWidth-1:0] o
    );
    reg sign_a, sign_b, sign_o;
    reg[exp_len-1:0] exp_a, exp_b, exp_o;
    reg[DataWidth-2-exp_len:0] frac_a, frac_b;
    reg[DataWidth-1-exp_len:0] with_1_frac_a, with_1_frac_b;
    reg[2 * (DataWidth-exp_len) - 1:0] frac_o;
    int cnt;
    always@(*)begin
        {sign_a, exp_a, frac_a} = a;
        {sign_b, exp_b, frac_b} = b;
        with_1_frac_a = {1'b1, frac_a};
        with_1_frac_b = {1'b1, frac_b};
        ......

首先同样进行拆分,补上隐含的 ‘1’。

特殊情况处理

这个无需多言

确定符号位

两个符号相同的数相乘,结果为正数;反之为负数——实际上只需要一个异或操作即可。

sign_o = sign_a ^ sign_b;

指数位加减

exp_o = exp_a + exp_b - (2 ** (exp_len - 1) - 1);

如果你细心的话,可以发现,指数位加减过程中还减去了一个偏移量。你可以思考一下为什么?原因也很简单,我这里便不再赘述了。

小数位相乘

frac_o = with_1_frac_a * with_1_frac_b;

规格化处理

        cnt = 2 * (DataWidth-exp_len) - 1;
        while(cnt >= 0)begin
            if (frac_o[cnt] == 1)begin
                if(cnt > mid)begin
                    frac_o = (frac_o >>> (cnt - mid));
                    exp_o = exp_o + (cnt - mid);
                end
                else if (cnt < mid)begin
                    frac_o = (frac_o << (mid - cnt));
                    exp_o = exp_o + (cnt - mid);
                end
                break;
            end
            cnt -= 1;
        end
        o = {sign_o, exp_o, frac_o[mid-1:mid-23]};