📘 第01课:浮点数概述与IEEE 754标准

🎯 本课目标

📖 浮点数的历史与动机

在计算机诞生之初,浮点数的表示方式五花八门——每个硬件厂商都有自己的格式。IBM System/360使用16进制浮点,DEC VAX用自己的一套,CDC 6600又是一套。这导致:

1985年,IEEE发布了754-1985标准(由William Kahan主导设计),统一了浮点数格式。2008年修订为IEEE 754-2008,2019年再次更新为IEEE 754-2019。这一标准是现代所有FPU设计的基石。

📖 定点数 vs 浮点数

定点数(Fixed-Point)

小数点位置固定,例如Q15格式:1位符号 + 15位小数

值 = (-1)^sign × 2^(-15) × (整数表示)

优点:运算简单,硬件代价小

缺点:动态范围有限,精度固定

浮点数(Floating-Point)

小数点位置由指数决定,可以"浮动"

值 = (-1)^sign × mantissa × 2^exponent

优点:动态范围极大,能同时表示天文数字和微观量

缺点:硬件复杂,精度不均匀(大数附近稀疏,小数附近密集)

📖 IEEE 754的核心设计

三大要素

IEEE 754浮点数由三个字段组成:

┌─────┬──────────────┬────────────────────────────────────────┐ │ S │ E │ M (Mantissa/尾数) │ │ 符号 │ 指数 │ 有效数字 │ └─────┴──────────────┴────────────────────────────────────────┘ 单精度(32bit): S=1bit, E=8bit, M=23bit 双精度(64bit): S=1bit, E=11bit, M=52bit 半精度(16bit): S=1bit, E=5bit, M=10bit

指数偏移(Bias)

指数E不使用补码,而是使用偏移码(Excess/Bias)

实际指数 = E - Bias,其中 Bias = 2^(k-1) - 1(k为指数位宽)
精度指数位宽kBias值指数范围
半精度515-14 ~ +15
单精度8127-126 ~ +127
双精度111023-1022 ~ +1023

隐含的1(Hidden Bit)

对于规格化数,尾数的最高位总是1,因此不需要显式存储:

实际尾数 = 1.M(1后面接23/52位小数)

这相当于多了一位精度!单精度实际有24位有效数字。

📖 浮点数分类

五类浮点数

类别指数E尾数M含义
零(Zero)全0全0+0 或 -0
非规格化数(Denormal)全0非0极小数的渐进下溢
规格化数(Normal)1~254任意常规浮点数
无穷(Infinity)全1全0+∞ 或 -∞
NaN(Not a Number)全1非0无效运算结果
💡 关键洞察:非规格化数的引入是为了解决"突然下溢"问题。没有非规格化数时,最小的规格化数到零之间有一个突变间隙;非规格化数平滑地填充了这个间隙,实现了渐进下溢(gradual underflow)

单精度浮点数值域

-∞ -MaxNormal -MinNormal -MaxDenorm -0 +0 +MaxDenorm +MinNormal +MaxNormal +∞ │ │ │ │ │ │ │ │ │ │ NaN◄─────────────────────────────────────────────────────────────────────────────────►NaN ≈-3.4e38 ≈-1.2e-38 ≈-1.4e-45 0 0 ≈1.4e-45 ≈1.2e-38 ≈3.4e38

📖 IEEE 754的舍入模式

IEEE 754定义了4种舍入模式(第05课将详细讲解):

  1. 向最近偶数舍入(Round to Nearest Even)——默认模式
  2. 向零舍入(Round toward Zero)——截断
  3. 向正无穷舍入(Round toward +∞)——向上取整
  4. 向负无穷舍入(Round toward -∞)——向下取整

🔧 Verilog实现:浮点数分类器

下面实现一个单精度浮点数分类器,判断输入属于哪一类:

//=============================================================
// float_classifier.sv - IEEE 754 单精度浮点数分类器
// 功能:判断浮点数属于零/非规格化/规格化/无穷/NaN
// 验证:Verilator --lint-only
//=============================================================
module float_classifier (
    input  wire [31:0] fp_in,     // IEEE 754 单精度输入

    // 分类输出(one-hot)
    output wire        is_zero,     // ±0
    output wire        is_denormal,  // 非规格化数
    output wire        is_normal,    // 规格化数
    output wire        is_infinity,  // ±∞
    output wire        is_nan,       // NaN
    output wire        is_signaling, // SNaN
    output wire        is_quiet,     // QNaN
    output wire        sign,         // 符号位
    output wire [7:0]  exponent,    // 原始指数字段
    output wire [22:0] mantissa    // 原始尾数字段
);

    // 拆解字段
    assign sign     = fp_in[31];
    assign exponent = fp_in[30:23];
    assign mantissa = fp_in[22:0];

    // 中间信号
    wire exp_all_zero = (exponent == 8'b0);
    wire exp_all_one  = (exponent == 8'b1111_1111);
    wire mant_zero   = (mantissa == 23'b0);
    wire mant_msb    = mantissa[22];  // QNaN的最高尾数位为1

    // 分类逻辑
    assign is_zero     = exp_all_zero & mant_zero;
    assign is_denormal  = exp_all_zero & ~mant_zero;
    assign is_normal    = ~exp_all_zero & ~exp_all_one;
    assign is_infinity  = exp_all_one  & mant_zero;
    assign is_nan       = exp_all_one  & ~mant_zero;
    assign is_signaling = is_nan & ~mant_msb;  // SNaN: 尾数最高位为0但尾数非零
    assign is_quiet     = is_nan & mant_msb;   // QNaN: 尾数最高位为1

endmodule

测试平台

//=============================================================
// tb_float_classifier.sv - 测试平台
//=============================================================
module tb_float_classifier;

    reg  [31:0] fp_in;
    wire       is_zero, is_denormal, is_normal;
    wire       is_infinity, is_nan;
    wire       is_signaling, is_quiet;
    wire       sign;
    wire [7:0]  exponent;
    wire [22:0] mantissa;

    float_classifier uut (
        .fp_in(fp_in),
        .is_zero(is_zero), .is_denormal(is_denormal),
        .is_normal(is_normal), .is_infinity(is_infinity),
        .is_nan(is_nan), .is_signaling(is_signaling),
        .is_quiet(is_quiet), .sign(sign),
        .exponent(exponent), .mantissa(mantissa)
    );

    integer pass_count = 0;
    integer fail_count = 0;

    task check;
        input [31:0] val;
        input [4:0] expected; // {zero,denorm,normal,inf,nan}
        input [255:0] name;
        begin
            fp_in = val;
            #10;
            if ({is_zero,is_denormal,is_normal,is_infinity,is_nan} !== expected) begin
                $display("FAIL %0s: got %b%b%b%b%b expected %b",
                    name, is_zero, is_denormal, is_normal, is_infinity, is_nan, expected);
                fail_count = fail_count + 1;
            end else begin
                $display("PASS %0s", name);
                pass_count = pass_count + 1;
            end
        end
    endtask

    initial begin
        // +0.0: sign=0, exp=0, mant=0
        check(32'h0000_0000, 5'b10000, "+0.0");
        // -0.0
        check(32'h8000_0000, 5'b10000, "-0.0");
        // 最小非规格化数: exp=0, mant=1
        check(32'h0000_0001, 5'b01000, "MinDenorm");
        // 最大非规格化数: exp=0, mant=all1
        check(32'h007F_FFFF, 5'b01000, "MaxDenorm");
        // 最小规格化数: exp=1, mant=0
        check(32'h0080_0000, 5'b00100, "MinNormal");
        // 1.0: exp=127(0x7F), mant=0
        check(32'h3F80_0000, 5'b00100, "1.0");
        // +∞: exp=all1, mant=0
        check(32'h7F80_0000, 5'b00010, "+Infinity");
        // -∞
        check(32'hFF80_0000, 5'b00010, "-Infinity");
        // QNaN: exp=all1, mant MSB=1
        check(32'h7FC0_0000, 5'b00001, "QNaN");
        // SNaN: exp=all1, mant MSB=0, mant!=0
        check(32'h7F80_0001, 5'b00001, "SNaN");

        $display("\n=== Results: %0d passed, %0d failed ===", pass_count, fail_count);
        $finish;
    end

endmodule

📊 仿真验证结果

使用Verilator进行仿真,验证结果如下:

PASS +0.0
PASS -0.0
PASS MinDenorm
PASS MaxDenorm
PASS MinNormal
PASS 1.0
PASS +Infinity
PASS -Infinity
PASS QNaN
PASS SNaN

=== Results: 10 passed, 0 failed ===

✅Verilator验证通过 ✅10/10测试通过

📖 深入理解:为什么指数用偏移码?

为什么不直接用补码表示指数?偏移码有几个关键优势:

  1. 简化比较:两个正数比较比补码比较简单,无符号比较即可判断大小
  2. 简化零检测:全零就是零,不需要特殊处理
  3. 简化溢出检测:全1就是特殊值,硬件判断更高效
  4. 整数排序=浮点排序:对于同号浮点数,当作无符号整数排序后大小关系不变
💡 实用技巧:正浮点数可以直接用无符号整数比较来判断大小!这在FPU比较器设计中非常有用。

📖 IEEE 754对硬件设计的影响

IEEE 754标准对FPU设计施加了严格的约束:

  1. 正确舍入:运算结果必须如同无限精度计算后再舍入
  2. 异常处理:5种异常必须可检测(无效操作、除零、上溢、下溢、不精确)
  3. 原子的比较-交换:不支持FPU的上下文切换可能导致不一致
  4. 融合乘加(FMA):只做一次舍入,比分开乘加精度更高

📝 练习

练习1:将十进制数 -6.625 转换为IEEE 754单精度浮点数的十六进制表示。

提示:6.625 = 110.101₂ = 1.10101 × 2²,指数 = 2 + 127 = 129 = 10000001₂

练习2:0x41200000表示的浮点数是多少?

提示:0100 0001 0010 0... → exp=130, 实际指数=3, mantissa=010...→1.01×2³=10.1₂

练习3:修改分类器,增加对半精度浮点数(16bit)的支持。

练习4:实现一个模块,将单精度浮点数的各字段提取并转换为实际值(用定点数近似表示)。

🏆 成就解锁

🏅 浮点初学者

✅ 理解了IEEE 754标准的设计动机

✅ 掌握了浮点数的五大分类

✅ 实现了浮点数分类器

✅ 理解了偏移码的优势

下一成就:单精度大师 — 掌握单精度格式的每一个细节