本文基于 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]};