这一篇在干嘛?

上一章的除法 mostly 在玩定点数,但当计算需要覆盖数量级跨度极大的数值范围时,就需要浮点表示了。本章以一个符合 IEEE 754-1985 标准的浮点运算单元(FPU,Floating-Point Unit)硬件模型为例,重点讲加减法运算的全流水线架构及其实现分析。浮点加减法听起来简单,但严格遵守 IEEE 标准就要处理一堆边界情况——这些”麻烦的部分”恰恰占了芯片面积的大头。

9.1 浮点数格式:IEEE 754 速成

按 IEEE 标准,一个浮点数由三段拼成:符号位(sign)、指数(exponent)、尾数(mantissa)。32 位单精度格式的排布如下:

字段SignExponentMantissa
3130 ~ 2322 ~ 0
  • 符号位:0 正 1 负;指数:8 bit,带偏置(bias = 127),存”2 的多少次方”;尾数:23 bit,存有效数字的小数部分,规格化数隐含首位 1(即 1.m),相当于白赚一位精度。

当不考虑边界条件时,浮点加减法确实很简单:对齐指数、操作尾数即可。但要符合 IEEE 标准,必须处理一个特殊区域——亚正常区域(subnormal region)。它是标准为数值表示低端附近的数提供额外精度的条款。正常区与亚正常区的定义见表 9.1:

区域条件表示形式
Normal(正常)0 < exponent < 255(−1)ˢ × 2^(e−127) × 1.m
Subnormal(亚正常)exponent = 0(−1)ˢ × 2^(−126) × 0.m

理解一下差别:正常数的尾数隐含首位是 1(1.m),而亚正常数的首位是 0(0.m),且指数固定为 −126。这允许表示比最小正常数更小的数,实现”渐进下溢”。此外标准还有无穷(infinity)、NaN(not-a-number,非数)等条件,本章不展开讨论。

9.2 全流水线架构

这个 FPU 采用全流水线架构,目标是高速应用下的最大吞吐率——每个时钟沿都可以送入新的操作数。图 9.2 展示了各功能模块和流水线的流动顺序:

图 9.2 全流水线 FPU 的功能模块与数据流

流水线的核心步骤按顺序过一遍:

第一步:亚正常检测。检测操作数是否落在亚正常区域(指数是否为 0)。图 9.3 的电路:正常区给尾数 MSB 补一个逻辑 1(还原隐含的 1.m),亚正常区补逻辑 0(保持 0.m):

图 9.3 亚正常检测电路:根据指数是否为 0 决定尾数隐含位

第二步:对阶(规格化较小的数)。两个数指数不同不能直接加尾数——就像 1.5×10³ + 2.0×10¹ 必须先统一指数。图 9.4 把较小数的尾数按两个指数之差右移,使两个尾数的指数相等:

图 9.4 尾数规格化:小数按指数差移位对阶

第三步:尾数加/减。对齐后就可以对两个尾数做加减了。规则很简单:两个浮点数符号相同则尾数相加,符号相反则尾数相减,逻辑如图 9.5:

图 9.5 符号检测:决定尾数相加还是相减

第四步:后规格化(postnormalize)。加减后的结果尾数要移回标准浮点格式(正常数 1.xxx,亚正常数 0.xxx)。尾数移位的同时,指数要相应调整(尾数左移规格化多少位,指数就减多少),如图 9.6:

图 9.6 后规格化:尾数移位成标准格式,指数同步调整

所有的溢出和特殊条件操作都由比较器和多路选择器完成(在最后的 Verilog 实现里能看到,但上面几张逻辑图里没画出来)。使用流程:把操作数 OpA/OpB 与控制信号 AddSub(加还是减)、Rmode(舍入模式)同步送到输入端口,在下一个时钟上升沿被锁存、定义一次运算;运算完成后输出变为有效,所有条件标志(不精确、溢出等)同步输出。整个设计是流水线的,因此每个时钟沿都能开始一次新的浮点运算

9.2.1 Verilog 实现

先看常量定义——四种 IEEE 舍入模式和几个特殊值:

// Floating point addition according to the IEEE 754 standard.
`define ROUNDEVEN  2'b00   // 就近舍入(默认,向偶数舍)
`define ROUNDZERO  2'b01   // 向零舍入(截断)
`define ROUNDUP    2'b10   // 向上(正无穷方向)舍入
`define ROUNDDOWN  2'b11   // 向下(负无穷方向)舍入
// other
`define INF        31'b1111111100000000000000000000000  // 无穷
`define INDVAL     31'b1111111110000000000000000000000  // 不定值
`define LARGESTVAL 31'b1111111011111111111111111111     // 最大有限值

模块端口和关键信号声明。注意大量信号带 S0~S5 后缀——这是流水线级号(stage),同一信号每过一级寄存器就换一个名字:

module FPAdd(
    output [31:0] oFPSum,      // floating point sum
    output    oIneAdd,         // inexact addition(不精确标志)
    output    oOverflowAdd,    // overflow from addition(溢出标志)
    input    iClk,
    input [31:0] iFP1, iFP2,   // floating point inputs
    input [1:0] iRMode,        // rounding mode
    input    iSNaNS5);         // "Not a number" was input
    wire [7:0] NormShiftedS2, ShiftSubNormalS2; // normalization shift counts
    wire    RoundAdjS2;         // value to be subtracted for rounding adjust
    wire [7:0] ShiftVal;        // amount to shift second mantissa
    wire [23:0] Man1, Man2;     // input mantissas in stage 0
    wire    IneAddS0;
    wire [22:0] ManS4p1;
    wire [7:0] ExpOutS4;        // adjusted exponent in stage 3
    wire    coS4;
    // staged pipeline registers(后缀数字 = 流水线级号)
    reg    Sign1S2, Sign2S2, SignS3, SignS4, SignS5;
    reg    [48:0] ManS3Norm, ManS3SubnormalNorm, ManSubNormAdjS3;
    reg    [7:0] ExpS2, ExpS3, ExpS4;
    reg    [48:0] ManS2, ManS3, ManS4, ManS5;
    reg    IneAddS1, IneAddS2, IneAddS3, IneAddS4, IneAddS5;
    reg    [1:0] RModeS0, RModeS1, RModeS2, RModeS3, RModeS4, RModeS5;
    reg    SubS2, SubS3, SubS4;        // flags if operation was subtract
    reg    ManZeroS3, ManZeroS2;       // flags if mantissa is zero
    reg    ExpAdjS4;                   // flags a round-down adjustment
    reg    s1, s2;
    reg    [7:0] e1, e2;
    reg    [22:0] f1, f2;
    reg    [7:0] NormShiftedS3;
    reg    [47:0] Man2ShiftedS1;
    reg    [23:0] Man1S1;
    reg    [31:0] FP1Adj, FP2Adj;      // adjusted inputs: swap so first is larger
    reg    [22:0] ManS5Out, ManS5p1;   // mantissa stage 3, and man stage 3 plus 1
    reg    coS5;                       // carry out from plus 1 operation
    reg    [7:0] ExpOutS5;
    reg    SwapS0;                     // flags a swap of input operators
    reg    [7:0] ShiftValSwapS0, ShiftValNoSwapS0;
    reg    RoundAdjS3;

关键的组合逻辑。首先还原尾数隐含位(对应图 9.3 的亚正常检测——指数为 0 就补 0,否则补 1):

assign Man1 = (FP1Adj[30:23] == 0) ? {1'b0, FP1Adj[22:0]} :
    // if e1=0, then it is subnormal
    {1'b1, FP1Adj[22:0]};
assign Man2 = (FP2Adj[30:23] == 0) ? {1'b0, FP2Adj[22:0]} : {1'b1, FP2Adj[22:0]};
// shift less 1 if the smaller value is subnormal and larger is not
assign ShiftVal = SwapS0 ? ShiftValSwapS0 : ShiftValNoSwapS0;
// stage 3 mantissa plus 1
assign {coS4, ManS4p1} = ManS4[47:25] + 1;
assign ExpOutS4 = ExpS4 - ExpAdjS4;
// number of times to shift mantissa if result is subnormal
assign ShiftSubNormalS2 = ExpS2 + (ExpS2 == 0); // shift at least once
// Rounding conditions to subtract 1 from result
assign RoundAdjS2 = ((RModeS2 == `ROUNDZERO) &
    IneAddS2 & SubS2 & ManZeroS2) |
    ((RModeS2 == `ROUNDDOWN) & IneAddS2 &
    !Sign1S2 & SubS2 & ManZeroS2) |
    ((RModeS2 == `ROUNDUP) & IneAddS2 &
    Sign1S2 & SubS2 & ManZeroS2);
  • Man1/Man2:亚正常检测的硬件化,一行三目运算符实现图 9.3;ShiftVal:对阶移位量 = 两指数之差,若”小数为亚正常、大数不是”还要减 1;ManS4p1:预计算”尾数 + 1”供舍入逻辑选择,避免在最后一级塞加法器影响时序。

输出逻辑(溢出与特殊值处理)。这一大段三目运算符链就是一个多路选择器:当结果指数达到 8’hff(最大),根据舍入模式和符号决定输出无穷还是最大有限值:

// largest value
assign oFPSum = ((ExpOutS5 == 8'hff) & !iSNaNS5 &
(RModeS5 == `ROUNDEVEN ) ) ?
{SignS5, `INF } :
((ExpOutS5 == 8'hff) & !iSNaNS5 &
(RModeS5 == `ROUNDZERO )) ?
{SignS5, `LARGESTVAL } :
((ExpOutS5 == 8'hff) & !iSNaNS5 &
(RModeS5 == `ROUNDUP ) & !SignS5) ?
{1'b0,`INF } :
((ExpOutS5 == 8'hff) & !iSNaNS5 &
(RModeS5 == `ROUNDUP ) & SignS5) ?
{1'b1, `LARGESTVAL } :
((ExpOutS5 == 8'hff) & !iSNaNS5 &
(RModeS5 == `ROUNDDOWN ) & !SignS5) ?
{1'b0, `LARGESTVAL } :
((ExpOutS5 == 8'hff) & !iSNaNS5 &
(RModeS5 == `ROUNDDOWN ) & SignS5) ?
{1'b1,`INF } :
((ExpOutS5 == 8'hff) && iSNaNS5) ?
{SignS5, ExpOutS5, 1'b1, ManS5Out[21:0]} :
{SignS5, ExpOutS5 + (coS5 & (ManS5Out == ManS5p1)), ManS5Out};
// adjust exponent if there is a carry over
 
// overflow if we reached our maximum value
assign oOverflowAdd = ((oFPSum[30:0] == `LARGESTVAL) &
({ExpOutS5, ManS5Out} != `LARGESTVAL) );
 
// inexact also if there is overflow or if there are truncated bits
assign oIneAdd = IneAddS5 | oOverflowAdd | (|ManS5[24:0]);
  • 溢出时的舍入规则:向偶数/向零模式溢出输出 INF/LARGESTVAL,向上模式正数溢出输出 +INF、负数输出最大值,向下模式相反——体会”向无穷方向舍入”的含义。oOverflowAdd:结果顶着最大值但内部数据还没到最大值,说明溢出;oIneAdd:不精确标志,溢出或尾数低位有被截断的位(|ManS5[24:0] 归约或)都会置位。

三个例化的移位模块,分别对应流水线的三个移位操作(对应图 9.4 和图 9.6):

// pre-normalize second operator so that decimals are aligned
PreNormAdd PreNormAdd (.iPreShift(Man2),
    .iShiftVal(ShiftVal),
    .oPostShift(Man2Shifted),
    .oIneAdd(IneAddS0));
// normalize result by shifting mantissa and adjusting
// exponent by NormShifted
PostNormAdd PostNormAdd (.iMantissa(ManS2),
    .oMantissaNorm(ManS2Norm),
    .oNormShifted(NormShiftedS2));
// normalization if result is sub normal
NormSubNormalAdd NSNA (.iPreShift(ManS2),
    .oPostShift(ManS2SubnormalNorm),
    .iShiftVal(ShiftSubNormalS2));

然后是流水线时序主体。Stage 0:先给两个操作数排大小——保证 FP1 是较大的那个(否则交换),因为对阶永远移小数:

always @(posedge iClk) begin
// Stage 0: First FP must be bigger than the second, if not swap
if(iFP1[30:0] > iFP2[30:0]) begin
    FP1Adj <= iFP1;
    FP2Adj <= iFP2;
    SwapS0 <= 0;
end
else begin
    FP1Adj <= iFP2;
    FP2Adj <= iFP1;
    SwapS0 <= 1;
end
ShiftValNoSwapS0 <= iFP1[30:23]-iFP2[30:23] - ((iFP2[30:23] == 0) &
    (iFP1[30:23] != 0));
ShiftValSwapS0 <= iFP2[30:23]-iFP1[30:23] - ((iFP1[30:23] == 0) & (iFP2[30:23] != 0));
RModeS0 <= iRMode;
// Stage 1
{s1, e1, f1} <= FP1Adj; // pick out fields from raw FP values
{s2, e2, f2} <= FP2Adj;
RModeS1 <= RModeS0;
IneAddS1 <= IneAddS0;
Man2ShiftedS1 <= Man2Shifted;
Man1S1 <= Man1;
// Stage 2
Sign1S2 <= s1;
Sign2S2 <= s2;
ExpS2 <= e1;
RModeS2 <= RModeS1;
IneAddS2 <= IneAddS1;
ManZeroS2 <= (Man2ShiftedS1 == 0); // flags addition to zero
// add or sub mantissa values
if(s1 == s2) begin // add mans if signs equal
    ManS2 <= {Man1S1, 24'b0} + Man2ShiftedS1;
    SubS2 <= 0;
end
else begin // subtract mans if signs opposite
    ManS2 <= {Man1S1, 24'b0} - Man2ShiftedS1;
    SubS2 <= 1;
end
// Stage 3
SignS3 <= Sign1S2;
ExpS3 <= ExpS2;
IneAddS3 <= IneAddS2;
RModeS3 <= RModeS2;
ManZeroS3 <= ManZeroS2;
SubS3 <= SubS2;
ManS3 <= ManS2;
ManS3Norm <= ManS2Norm;
ManS3SubnormalNorm <= ManS2SubnormalNorm;
NormShiftedS3 <= NormShiftedS2;
ManSubNormAdjS3 <= ManS2SubnormalNorm - RoundAdjS2;
RoundAdjS3 <= RoundAdjS2;
// Stage 4
RModeS4 <= RModeS3;
// zeroth bit shifted out of mantissa - if 1 then inexact
IneAddS4 <= IneAddS3;
SubS4 <= SubS3;
if(ManS3 == 0) begin
    // sign depends on rounding mode
    SignS4 <= ((RModeS3 == `ROUNDDOWN) & (SubS3 | SignS3)) |
    ((RModeS3 == `ROUNDEVEN) & (!SubS3 & SignS3)) |
    ((RModeS3 == `ROUNDZERO) & (!SubS3 & SignS3)) |
    ((RModeS3 == `ROUNDUP) & (!SubS3 & SignS3));
    // if the total mantissa is zero, then result is zero
    // and therefore exponent is zero
    ExpS4 <= 0;
    ExpAdjS4 <= 0;
    ManS4 <= ManS3Norm;    // normalized result
end
else if((ExpS3 < NormShiftedS3) & (NormShiftedS3 != 1)) begin
    // the result is a subnormal number
    SignS4 <= SignS3;
    ExpS4 <= 0;
    ExpAdjS4 <= 0;
    ManS4 <= ManSubNormAdjS3;   // adjust for rounding mode
end
else begin
    // otherwise, the final exponent is reduced by the
    // number of shifts to normalize the mantissa
    SignS4 <= SignS3;
    ExpS4 <= ExpS3 - NormShiftedS3 + 1 + (ExpS3 == 0);
    ExpAdjS4 <= (RoundAdjS3 & (ManS3Norm[47:24] == 0));
    ManS4 <= ManS3Norm - {24'b0, RoundAdjS3, 24'b0};
end
// Stage 5
SignS5 <= SignS4;
RModeS5 <= RModeS4;
ManS5p1 <= ManS4p1;
IneAddS5 <= IneAddS4;
ManS5 <= ManS4;
coS5 <= coS4;
  • iFP1[30:0] > iFP2[30:0]:直接比较低 31 位就能定大小——指数在高位,天然形成字典序比较;ShiftValNoSwapS0 里的减 1 项:小数为亚正常、大数为正常时,隐含位不同导致移位量少 1;
  • Stage 2 的加减选择:符号相同加、相反减(图 9.5 的逻辑),{Man1S1, 24'b0} 是给移位进来的对阶尾数留出低位空间;
  • Stage 4 是结果分类的三路分支:结果为 0 时指数也必须为 0(IEEE 规定 0 是特殊编码),且结果符号由舍入模式决定——比如向零舍入时 (−a) + a 结果是 +0 还是 −0 有讲究;规格化移位量超过剩余指数(ExpS3 < NormShiftedS3)时结果小到落入亚正常区,指数置 0 并按亚正常方式移位;否则最终指数 = 原指数减去规格化移位量——这就是”后规格化”的减法调整(图 9.6)。

Stage 5:最后一行——按舍入模式决定结果是否 +1(进位):

// adjust for rounding mode - various conditions to round up
ManS5Out <= (RModeS4 == `ROUNDEVEN &((ManS4[24]& |ManS4[23:0])|(ManS4[24]& ManS4[25]& ((SubS4& !IneAddS4)|!SubS4)))?
    ManS4p1:
    (RModeS4 == `ROUNDUP)&((|ManS4[24:0]& !SignS4)|(IneAddS4& !SignS4& !SubS4))?
    ManS4p1:(RModeS4 == `ROUNDDOWN) & ((|ManS4[24:0]& SignS4)|(IneAddS4& SignS4& !SubS4))? ManS4p1:
    ManS4[47:25];
ExpOutS5 <= ExpOutS4;
end
endmodule
  • ROUNDEVEN(就近舍偶):被舍弃部分正好一半(第 24 位为 1 且后面有非零位)时进位,或恰好一半且上一位是 1(凑成偶数)时进位;ROUNDUP/ROUNDDOWN:根据结果符号决定朝哪个方向”向上”——正数向上进位、负数”向上”其实是绝对值变大。最终用预计算好的 ManS4p1(尾数+1)或截断值 ManS4[47:25] 二选一,最后一级没有任何加法运算。

9.2.2 资源与性能

本节报告 Add-Sub 模块在不同架构下的资源利用和性能。这个流水线架构是为最大吞吐率设计的,因此流水线寄存器多、并行逻辑多。在 Xilinx Spartan-3 上的实现结果见表 9.2:

频率65 MHz110 MHz
FFs(触发器)5381087
LUTs(查找表)23702363
  • 最高频率 110 MHz 下吞吐率 3.52 Gbps(110 MHz × 32 bit);如果目标频率放宽到 65 MHz,可以通过避免寄存器复制省下一半触发器(538 vs 1087),吞吐率仍有 2.08 Gbps。LUT 数几乎不变——省的都是流水线寄存器;
  • 真正的 IEEE 兼容代价高昂:很大一部分面积花在亚正常区处理、溢出条件和各种输入/输出条件(无穷、NaN 等)的检测上。如果你的应用不需要 IEEE 兼容、数值范围可以约束在正常区内,资源消耗可以降到表 9.2 的一半左右。

常见坑:项目不需要 IEEE 全兼容却照单全收

很多团队直接例化完整的 IEEE 浮点 IP 或照抄本章实现,结果亚正常区处理和特殊值检测吃掉了一半资源。动手前先问三个问题:数值范围会不会进入亚正常区?需不需要处理 NaN/无穷?舍入模式用得到几种?范围可保证在正常区内的话,砍掉亚正常逻辑能省约一半面积;FPGA 设计里”够用的精度 + 够用的范围”远比”完整标准”划算。

通关标准:

学完本篇你应该能做到:

  • 默画 IEEE 754 单精度 32 位格式(1 位符号 / 8 位指数 / 23 位尾数),写出正常区与亚正常区的判定条件和表示公式;
  • 复述浮点加法的四步流程:亚正常检测补隐含位 → 小数对阶右移 → 尾数同号加/异号减 → 后规格化并调整指数;
  • 看懂 FPAdd 代码中 S0~S5 的流水线级命名,说明为什么”尾数+1”要在前一级预计算;
  • 解释结果为 0 时符号为何依赖舍入模式;
  • 说出 IEEE 全兼容与”正常区限定”实现在资源上的差距(约 2 倍),并据此为项目做取舍。