这一篇在干嘛?
上一章的除法 mostly 在玩定点数,但当计算需要覆盖数量级跨度极大的数值范围时,就需要浮点表示了。本章以一个符合 IEEE 754-1985 标准的浮点运算单元(FPU,Floating-Point Unit)硬件模型为例,重点讲加减法运算的全流水线架构及其实现分析。浮点加减法听起来简单,但严格遵守 IEEE 标准就要处理一堆边界情况——这些”麻烦的部分”恰恰占了芯片面积的大头。
9.1 浮点数格式:IEEE 754 速成
按 IEEE 标准,一个浮点数由三段拼成:符号位(sign)、指数(exponent)、尾数(mantissa)。32 位单精度格式的排布如下:
| 字段 | Sign | Exponent | Mantissa |
|---|---|---|---|
| 位 | 31 | 30 ~ 23 | 22 ~ 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 展示了各功能模块和流水线的流动顺序:

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

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

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

第四步:后规格化(postnormalize)。加减后的结果尾数要移回标准浮点格式(正常数 1.xxx,亚正常数 0.xxx)。尾数移位的同时,指数要相应调整(尾数左移规格化多少位,指数就减多少),如图 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 MHz | 110 MHz |
|---|---|---|
| FFs(触发器) | 538 | 1087 |
| LUTs(查找表) | 2370 | 2363 |
- 最高频率 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 倍),并据此为项目做取舍。
自测:两个浮点数相加前为什么必须"对阶"?对阶时移大数还是小数?
因为浮点数 = 尾数 × 2^指数,指数不同的两个数其尾数代表的位权不同,直接相加没有意义(类似十进制科学计数法相加要先统一指数)。对阶必须右移较小数的尾数——右移丢掉的是小数的低位(影响小),若左移大数会丢高位(影响大)。这也是 Stage 0 要先交换保证 FP1 为大数的原因。
自测:亚正常数的尾数为什么要补 0,而正常数补 1?
正常数表示为 1.m,隐含首位是 1,硬件运算前要把它还原;亚正常数表示为 0.m,隐含首位本来就是 0(指数固定为 −126)。
assign Man1 = (FP1Adj[30:23] == 0) ? {1'b0, FP1Adj[22:0]} : {1'b1, FP1Adj[22:0]}正是这个规则的硬件化。
自测:为什么两个操作数符号相同做尾数加法、相反做减法?那"减法"去哪了?
浮点加减法共用一条通路:A + (−B) 的减法在送入 FPU 前就由符号位翻转统一成了加法框架。模块内部只需根据两数符号是否一致来决定尾数做加还是做减,这就是图 9.5 符号检测逻辑的作用。
自测:后规格化时,指数调整的方向和数量由什么决定?
由规格化尾数所需的左移位数决定:尾数每左移 1 位(消掉一个前导 0 或吸收进位),指数就要减 1(
ExpS4 <= ExpS3 - NormShiftedS3 + ...)。如果剩余指数不够减(结果太小),就落入亚正常区,指数固定为 0 改用亚正常格式。
自测:表 9.2 中 65 MHz 与 110 MHz 两列 LUT 数几乎一样而 FF 数差一倍,说明什么?
说明综合器为了达到 110 MHz 做了寄存器复制(用两份触发器驱动高扇出网络以缩短路径),逻辑本身没变。速度压力主要在布线/扇出而非逻辑量,若性能目标可以放宽,仅寄存器一项就能省近一半资源。