大数跨境

从零实现流水线 FP32 乘法器

从零实现流水线 FP32 乘法器 ai算法芯片与系统
2026-10-19
10
导读:FP32流水线乘法器通常分三级:S1解包分类并计算指数,S2完成24×24尾数乘法,S3做规格化、RNE舍入和打包。各级间插入寄存器,由同一时钟沿驱动,valid随数据逐级传递。组合逻辑并行求值,寄存

 

浮点乘法是处理器中最基础也最常用的运算之一。无论是神经网络的矩阵乘、图形渲染的顶点变换,还是科学计算里的向量点积,背后都离不开它。这篇文章从解包开始,一步步搭出一个 3 级流水的 FP32 乘法器,最后扩展到数组接口。代码贯穿始终,每段代码配上解释与图表。

📌 本文所有代码均为可综合的 SystemVerilog,舍入模式采用 IEEE 754 默认的 RNE(最近偶数)。


一、先想清楚:浮点乘法到底在算什么

浮点数不是普通的二进制数。一个 IEEE 754 单精度浮点数由三部分组成:1 位符号、8 位指数、23 位尾数。它表示的值是:

两个浮点数相乘,就是把它们的符号、指数、尾数分别处理:

部分
运算
原因
符号
同号为正,异号为负
指数
指数相加,但要减掉两次偏置
尾数
两个小数相乘

这就是浮点乘法比浮点加法简单的地方。浮点加法要先对阶,两个数的指数差多少,尾数就要移多少位,还要处理大数吃小数、前导零检测等。浮点乘法不需要对阶,尾数直接乘,乘积范围固定在   之间,规格化最多右移一位。


二、解包:把 32 位拆开

拿到一个 32 位输入,第一件事是把它拆成符号、指数、尾数三段。这段代码是整个设计的起点:


   
   
   
   
    
   
   
   
   assign sa = a[31];
assign ea = a[30:23];
assign fa = a[22:0];

assign sb = b[31];
assign eb = b[30:23];
assign fb = b[22:0];
位段
宽度
含义
[31]
1
符号位
,0 为正,1 为负
[30:23]
8
偏置指数
,实际指数要减 127
[22:0]
23
尾数
小数部分,隐含整数位 1

这里没有做任何运算,只是把线网切开。综合工具看到这几行,只会接几根线,几乎不消耗逻辑资源。


三、识别特殊值

浮点数里有一批特殊值,不能按正常流程算。比如 NaN 乘任何数都是 NaN,Inf 乘 0 是 NaN,Inf 乘非零是 Inf,0 乘非零是 0。这些情况必须在主数据通路之外单独判断:


   
   
   
   
    
   
   
   
   assign a_nan  = (ea == 8'hFF) && (fa != 0);
assign b_nan  = (eb == 8'hFF) && (fb != 0);
assign a_inf  = (ea == 8'hFF) && (fa == 0);
assign b_inf  = (eb == 8'hFF) && (fb == 0);
assign a_zero = (ea == 0)      && (fa == 0);
assign b_zero = (eb == 0)      && (fb == 0);

这几个表达式的逻辑很直白:

条件
判断式
含义
NaN
exp 全 1 且 frac 非零
非数
Inf
exp 全 1 且 frac 为零
无穷
Zero
exp 为零且 frac 为零
零
非规格化
exp 为零且 frac 非零
极小值
规格化
0 < exp < 255
正常数

四、非规格化数的修正

非规格化数的指数编码是 0,但它的实际指数按 1 处理。同时它的尾数没有隐含的整数位 1,只有一个 0。所以要把这两个修正统一做掉:


   
   
   
   
    
   
   
   
   assign ea_adj = (ea == 0) ? 8'd1 : ea;
assign eb_adj = (eb == 0) ? 8'd1 : eb;
assign ma     = (ea == 0) ? {1'b0, fa} : {1'b1, fa};
assign mb     = (eb == 0) ? {1'b0, fb} : {1'b1, fb};
输入类型
指数调整
尾数范围
规格化
保持原值
[1.0, 2.0),隐含 1
非规格化 改为 1 (0.0, 1.0),无隐含 1

这段代码的含义是:对规格化数,尾数前面补一个 1,变成 24 位;对非规格化数,尾数前面补一个 0,也变成 24 位。这样后面做乘法时,输入永远是 24 位,不用再分支。


五、符号与指数

符号最简单,一个异或门就够:


   
   
   
   
    
   
   
   
   s1_sign <= sa ^ sb;

指数相加时要注意位宽。8 位加 8 位可能产生 9 位结果,所以要先扩展一位再相加:


   
   
   
   
    
   
   
   
   s1_exp_sum <= {1'b0, ea_adj} + {1'b0, eb_adj};
运算
代码
位宽
说明
符号
sa ^ sb
1
异或
指数
ea_adj + eb_adj
9
扩展一位防溢出

六、尾数乘法

两个 24 位尾数相乘,得到一个 48 位乘积:


   
   
   
   
    
   
   
   
   assign prod = s1_ma * s1_mb;

这一步是整个设计里最耗资源的部分。综合工具会把它映射到 DSP 硬核,或者用 Booth 编码 + Wallace 树 实现。乘积的范围是:

输入范围
乘积范围
最高位位置
[1.0, 2.0) × [1.0, 2.0)
[1.0, 4.0)
bit 46 或 bit 47

为什么是 46 或 47?因为 1.0 乘 1.0 等于 1.0,最高位在 bit 46;1.5 乘 1.5 等于 2.25,最高位在 bit 47。乘积的隐含位要么在 46 要么在 47,不会更靠前,也不会更靠后。


七、找最高位

乘积是 48 位,最高位不一定在 bit 47。为了做规格化,要先找到最高位的位置:


   
   
   
   
    
   
   
   
   always_comb begin
    msb_idx = 6'd0;
    for (int i = 0; i < 48; i++)
        if (prod[i]) msb_idx = i[5:0];
end

这个循环从低位扫到高位,遇到 1 就更新 msb_idx,最后留下的就是最高位的位置。综合后它是一个 48 位优先编码器,本质是一串多级 MUX。

输入最高位位置
msb_idx
需要左移量
bit 47
47
0
bit 46
46
1
bit 45
45
2
bit 0
0
47

八、规格化

规格化的目的是把最高位对齐到 bit 47,这样尾数的隐含 1 就统一在 bit 47 上:


   
   
   
   
    
   
   
   
   assign shamt = 6'd47 - msb_idx;
assign norm  = s2_prod << shamt;

左移量等于 47 减去最高位位置:

原最高位
shamt
移位后最高位
47
0
47
46
1
47
45
2
47

这个移位器综合后是一个 6 级桶形移位器,每一级都是一个 2:1 MUX,根据 shamt 的某一位决定移不移。


九、指数计算

指数不能直接用 ea_adj + eb_adj,因为还要补偿尾数乘法的隐含位偏移。完整的公式是:


   
   
   
   
    
   
   
   
   assign exp_biased = $signed({3'b0, s2_exp_sum})
                  + $signed({6'b0, msb_idx})
                  - 12'sd173;

173 这个数字怎么来的?

组成
数值
说明
偏置
127
IEEE 754 单精度
尾数隐含位基准
46
24×24 乘积的隐含位位置
合计 173 127 + 46

✅ 验证:1.0 × 1.0,ea_adj = 127,eb_adj = 127,msb_idx = 46,代入得  ,即真实值 1.0。

✅ 验证:1.5 × 1.5,msb_idx = 47,代入得  ,即真实值 2.25。


十、取尾数

规格化之后,尾数的隐含 1 在 bit 47,接下来的 23 位是小数部分:


   
   
   
   
    
   
   
   
   assign mant24 = norm[47:24];

mant24 是 24 位,其中最高位是隐含 1,低 23 位是小数。但这时还不能直接输出,因为低位可能被丢弃,需要舍入。


十一、舍入

IEEE 754 默认的舍入模式是 RNE(Round to Nearest Even,最近偶数)。它需要三个位来判断:


   
   
   
   
    
   
   
   
   assign guard  = norm[23];
assign sticky = |norm[22:0];
assign lsb    = mant24[0];
位
来源
作用
guard
norm[23]
舍入位
sticky
norm[22:0] 的或
低位是否有 1
lsb
mant24[0]
最低位,用于判偶

舍入规则是:如果 guard 为 1 且(sticky 为 1 或 lsb 为 1),就进位;否则不进位。写成代码:


   
   
   
   
    
   
   
   
   always_comb begin
    if (guard && (sticky || lsb))
        mant_rnd = {1'b0, mant24} + 1'b1;
    else
        mant_rnd = {1'b0, mant24};
end
guard
sticky
lsb
操作
原因
0
x
x
不进位
距离下边更近
1
1
x
进位
距离上边更近
1
0
1
进位
恰好中间,取偶
1
0
0
不进位
恰好中间,取偶

十二、舍入溢出

舍入时可能发生进位溢出。比如尾数全是 1,加 1 之后就会变成 25 位:


   
   
   
   
    
   
   
   
   always_comb begin
    if (mant_rnd[24]) begin
        mant_final = mant_rnd[24:1];
        exp_final  = exp_biased + 12'sd1;
    end else begin
        mant_final = mant_rnd[23:0];
        exp_final  = exp_biased;
    end
end
条件
尾数
指数
mant_rnd[24] = 0
低 24 位
保持不变
mant_rnd[24] = 1 右移一位 加一

这和十进制里 9.99 进位成 10.0 类似,尾数右移一位,指数加一。


十三、打包与特殊值

正常结果的打包分几种情况:


   
   
   
   
    
   
   
   
   always_comb begin
    if (s3_zero_prod)
        y_norm = {s3_sign, 31'd0};
    else if (s3_exp >= 12'sd255)
        y_norm = {s3_sign, 8'hFF, 23'd0};
    else if (s3_exp <= 12'sd0)
        y_norm = {s3_sign, 31'd0};
    else
        y_norm = {s3_sign, s3_exp[7:0], s3_mant[22:0]};
end
条件
输出
含义
乘积为零
符号 + 全零
零
指数 >= 255
符号 + FF + 全零
上溢到 Inf
指数 <= 0
符号 + 全零
下溢到 0(简化)
其他
符号 + 指数 + 尾数
正常

特殊值的优先级选择:


   
   
   
   
    
   
   
   
   always_comb begin
    if (s3_a_nan || s3_b_nan)
        y_result = {s3_sign, 8'hFF, 23'h400000};
    else if (s3_a_inf || s3_b_inf) begin
        if ((s3_a_inf && s3_b_zero) || (s3_b_inf && s3_a_zero))
            y_result = {s3_sign, 8'hFF, 23'h400000};
        else
            y_result = {s3_sign, 8'hFF, 23'd0};
    end
    else if (s3_a_zero || s3_b_zero)
        y_result = {s3_sign, 31'd0};
    else
        y_result = y_norm;
end
优先级
条件
结果
1
任一为 NaN
NaN
2
Inf × 0
NaN
3
任一为 Inf
Inf
4
任一为 0
0
5
正常
y_norm

十四、纯组合版本的问题

如果把上面所有逻辑放在一个 always_comb 里,中间不插寄存器,那么综合工具看到的是一条从输入到输出的串联路径:


   
   
   
   
    
   
   
   
   always_comb begin
    // 解包
    // 特殊值判断
    // 指数相加
    // 尾数乘法
    // 规格化
    // 舍入
    // 打包
end
always_ff @(posedge clk) y <= result;

⚠️ 关键路径是所有这些延迟之和。乘法器本身就要几纳秒,再加上优先编码器、移位器、舍入 MUX,整条路径可能十几纳秒,频率只能跑到几十兆赫兹。这就是纯组合版本的瓶颈。


十五、插入寄存器:3 级流水线

解决方案是在关键位置插入寄存器,把长路径切成几段。每一段单独算延迟,周期只取最慢的那一段:


   
   
   
   
    
   
   
   
   always_ff @(posedge clk) s1_ma   <= ma;
always_ff @(posedge clk) s2_prod <= prod;
always_ff @(posedge clk) s3_mant <= mant_final;
always_ff @(posedge clk) y       <= y_packed;

三级的分工:

级
主要逻辑
延迟量级
S1
比较器 + MUX + 9 位加法
小
S2
24×24 乘法
大(瓶颈)
S3
优先编码 + 移位 + 舍入 + MUX
中

十六、第一级寄存器

第一级寄存器的写法:


   
   
   
   
    
   
   
   
   always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        s1_valid   <= 1'b0;
        s1_sign    <= 1'b0;
        s1_ma      <= 24'd0;
        s1_mb      <= 24'd0;
        s1_exp_sum <= 9'd0;
    end else begin
        s1_valid   <= in_valid;
        s1_sign    <= sa ^ sb;
        s1_ma      <= ma;
        s1_mb      <= mb;
        s1_exp_sum <= {1'b0, ea_adj} + {1'b0, eb_adj};
    end
end
信号
来源
位宽
含义
s1_valid
in_valid
1
数据是否有效
s1_sign
sa ^ sb
1
结果符号
s1_ma
ma
24
a 的尾数
s1_mb
mb
24
b 的尾数
s1_exp_sum
ea_adj + eb_adj
9
指数和

📝 注意 s1_valid 直接来自 in_valid,因为这是第一级。后面的级就不一样了。


十七、第二级寄存器


   
   
   
   
    
   
   
   
   always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        s2_valid   <= 1'b0;
        s2_sign    <= 1'b0;
        s2_prod    <= 48'd0;
        s2_exp_sum <= 9'd0;
    end else begin
        s2_valid   <= s1_valid;
        s2_sign    <= s1_sign;
        s2_prod    <= prod;
        s2_exp_sum <= s1_exp_sum;
    end
end

📝 注意 s2_valid 来自 s1_valid,不是来自 in_valid。这是流水线里 valid 传递的关键:valid 和数据一起走,每经过一级寄存器就存一次。


十八、第三级寄存器


   
   
   
   
    
   
   
   
   always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        s3_valid <= 1'b0;
        s3_sign  <= 1'b0;
        s3_mant  <= 24'd0;
        s3_exp   <= 12'd0;
    end else begin
        s3_valid <= s2_valid;
        s3_sign  <= s2_sign;
        s3_mant  <= mant_final;
        s3_exp   <= exp_final;
    end
end

第三级存的是规格化和舍入后的结果。


十九、输出寄存器


   
   
   
   
    
   
   
   
   always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        out_valid <= 1'b0;
        y         <= 32'd0;
    end else begin
        out_valid <= s3_valid;
        y         <= y_result;
    end
end

输出寄存器是最后一级,把打包后的结果和 valid 一起送出。


二十、valid 传递链

数据和 valid 是两条并排的路径:

各级 valid 的含义:

信号
描述的是哪个数据
来源
s1_valid
当前 S1 中的数据
in_valid
s2_valid
当前 S2 中的数据
s1_valid
s3_valid
当前 S3 中的数据
s2_valid
out_valid
输出端口上的数据
s3_valid

⚠️ 同一拍里,s1_valid、s2_valid、s3_valid 描述的是三个不同数据,所以不能用它们全部相与来判断输出是否有效。正确的做法是只取最后一级:


   
   
   
   
    
   
   
   
   always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) out_valid <= 1'b0;
    else        out_valid <= s3_valid;

二十一、三级并行的时间关系

三级流水的意义在于,同一时刻不同 stage 在处理不同数据:

第 3 拍时,S1 处理 i+2,S2 处理 i+1,S3 处理 i。三个数据在三条不同的路径上,互不干扰。


二十二、同一时钟沿的作用

所有边界寄存器由同一个时钟沿驱动,这是流水线能工作的根本:


   
   
   
   
    
   
   
   
   always_ff @(posedge clk or negedge rst_n) s1_ma   <= ma;
always_ff @(posedge clk or negedge rst_n) s2_prod <= prod;
always_ff @(posedge clk or negedge rst_n) s3_mant <= mant_final;
always_ff @(posedge clk or negedge rst_n) y       <= y_result;

时钟沿到来时,所有寄存器同时采样、同时切换:

💡 因为所有寄存器在同一个瞬间切换,每一级看到的上一拍数据是对齐的。如果 R1 和 R2 用不同时钟,就会出现 R1 已更新、R2 还没更新 的错位,数据就乱了。


二十三、周期由最慢级决定

既然所有寄存器在同一个时钟沿采样,那么时钟周期必须大于最慢那一级的延迟:

一拍内的时间分配:

💡 S2 占满整拍,S1 和 S3 有空闲。空闲时间无法被利用,因为寄存器只在时钟沿统一更新。如果 S2 太慢,就把它拆成两级:


   
   
   
   
    
   
   
   
   always_ff @(posedge clk) partial <= booth_encode(s1_ma, s1_mb);
always_ff @(posedge clk) prod    <= wallace_tree(partial);

这样最慢一级变小,周期可以缩短,代价是多一级寄存器和一拍延迟。


二十四、子模块划分

把组合逻辑拆成子模块,顶层只放寄存器,这样结构清晰,也方便替换:

模块
类型
功能
fp32_unpack
组合
解包、分类、非规格化修正
fp32_mul_core
组合
24×24 乘法
fp32_norm_round
组合
优先编码、规格化、RNE
fp32_pack
组合
打包、特殊值

二十五、子模块代码

解包模块:


   
   
   
   
    
   
   
   
   module fp32_unpack (
    input  logic [31:0] x,
    output logic        sign,
    output logic [7:0]  exp_raw,
    output logic [22:0] frac,
    output logic        is_nan,
    output logic        is_inf,
    output logic        is_zero,
    output logic [7:0]  exp_adj,
    output logic [23:0] mant
);
    assign sign    = x[31];
    assign exp_raw = x[30:23];
    assign frac    = x[22:0];

    assign is_nan  = (exp_raw == 8'hFF) && (frac != 0);
    assign is_inf  = (exp_raw == 8'hFF) && (frac == 0);
    assign is_zero = (exp_raw == 0)      && (frac == 0);

    assign exp_adj = (exp_raw == 0) ? 8'd1 : exp_raw;
    assign mant    = (exp_raw == 0) ? {1'b0, frac} : {1'b1, frac};
endmodule

乘法模块:


   
   
   
   
    
   
   
   
   module fp32_mul_core (
    input  logic [23:0] ma,
    input  logic [23:0] mb,
    output logic [47:0] prod
);
    assign prod = ma * mb;
endmodule

规格化舍入模块:


   
   
   
   
    
   
   
   
   module fp32_norm_round (
    input  logic [47:0] prod,
    input  logic [8:0]  exp_sum,
    output logic [23:0] mant_final,
    output logic [11:0] exp_final,
    output logic        is_zero_prod
);
    // 找最高位
    logic [5:0] msb_idx;
    always_comb begin
        msb_idx = 6'd0;
        for (int i = 0; i < 48; i++)
            if (prod[i]) msb_idx = i[5:0];
    end

    // 规格化
    logic [5:0]  shamt;
    logic [47:0] norm;
    assign shamt = 6'd47 - msb_idx;
    assign norm  = prod << shamt;

    // 指数
    logic signed [11:0] exp_biased;
    assign exp_biased = $signed({3'b0, exp_sum})
                      + $signed({6'b0, msb_idx})
                      - 12'sd173;

    // 舍入
    logic [23:0] mant24;
    assign mant24 = norm[47:24];

    logic guard, sticky, lsb;
    assign guard  = norm[23];
    assign sticky = |norm[22:0];
    assign lsb    = mant24[0];

    logic [24:0] mant_rnd;
    always_comb begin
        if (guard && (sticky || lsb))
            mant_rnd = {1'b0, mant24} + 1'b1;
        else
            mant_rnd = {1'b0, mant24};
    end

    always_comb begin
        if (mant_rnd[24]) begin
            mant_final = mant_rnd[24:1];
            exp_final  = exp_biased + 12'sd1;
        end else begin
            mant_final = mant_rnd[23:0];
            exp_final  = exp_biased;
        end
    end

    assign is_zero_prod = (prod == 48'd0);
endmodule

打包模块:


   
   
   
   
    
   
   
   
   module fp32_pack (
    input  logic        sign,
    input  logic [11:0] exp_final,
    input  logic [23:0] mant_final,
    input  logic        is_zero_prod,
    input  logic        a_nan, b_nan,
    input  logic        a_inf, b_inf,
    input  logic        a_zero, b_zero,
    output logic [31:0] y
);
    logic [31:0] y_norm;
    always_comb begin
        if (is_zero_prod)
            y_norm = {sign, 31'd0};
        else if (exp_final >= 12'sd255)
            y_norm = {sign, 8'hFF, 23'd0};
        else if (exp_final <= 12'sd0)
            y_norm = {sign, 31'd0};
        else
            y_norm = {sign, exp_final[7:0], mant_final[22:0]};
    end

    always_comb begin
        if (a_nan || b_nan)
            y = {sign, 8'hFF, 23'h400000};
        else if (a_inf || b_inf) begin
            if ((a_inf && b_zero) || (b_inf && a_zero))
                y = {sign, 8'hFF, 23'h400000};
            else
                y = {sign, 8'hFF, 23'd0};
        end
        else if (a_zero || b_zero)
            y = {sign, 31'd0};
        else
            y = y_norm;
    end
endmodule

二十六、数组接口:N 路并行

如果一次要算很多组乘法,最简单的方式是例化 N 份独立的流水线:


   
   
   
   
    
   
   
   
   generate
    for (genvar k = 0; k < N; k++) begin : g_pipe
        // 每路独立的 unpack / mul / norm / pack
    end
endgenerate
项目
值
硬件份数
N
吞吐
N/周期
面积
N 倍
延迟
3 周期

这种方式吞吐最高,但面积也最大。适合面积不敏感、吞吐要求高的场景。


二十七、数组接口:单份硬件分时复用

如果面积敏感,可以只用一份硬件,按顺序把数组元素喂进去:


   
   
   
   
    
   
   
   
   logic [CW-1:0] feed_cnt;
logic          feeding;

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        feed_cnt <= '0;
        feeding  <= 1'b0;
    end else if (in_start && !feeding) begin
        feed_cnt <= '0;
        feeding  <= 1'b1;
    end else if (feeding) begin
        if (feed_cnt == N-1) begin
            feed_cnt <= '0;
            feeding  <= 1'b0;
        end else begin
            feed_cnt <= feed_cnt + 1'b1;
        end
    end
end

assign a_sel = a[feed_cnt];
assign b_sel = b[feed_cnt];
assign feed_valid = feeding;

输出侧按计数写回:


   
   
   
   
    
   
   
   
   logic [CW-1:0] out_cnt;
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) out_cnt <= '0;
    else if (s3_valid) out_cnt <= (out_cnt == N-1) ? '0 : out_cnt + 1'b1;
end

generate
    for (genvar k = 0; k < N; k++) begin : g_y
        always_ff @(posedge clk or negedge rst_n) begin
            if (!rst_n) y[k] <= 32'd0;
            else if (s3_valid && (out_cnt == k[CW-1:0]))
                y[k] <= y_packed;
        end
    end
endgenerate
项目
值
硬件份数
1
吞吐
1/周期
面积
1 倍
总延迟
约 N+3 拍

N=4 时的分时复用时间表:

💡 每个周期 S1、S2、S3 都在工作,但处理的是不同元素。S1 的 unpack 模块被 4 个元素轮流使用,S2 的乘法器被 4 个元素轮流使用,S3 的 norm/round/pack 也被轮流使用。这就是分时复用。


二十八、两种策略对比

项目
N 路并行
单份复用
硬件
N 份
1 份
吞吐
N/周期 1/周期
面积
大 小
延迟
3 周期
N+3 周期
适用
高吞吐场景
面积敏感场景

折中方案是 M 份硬件(M < N),吞吐 M/周期,面积 M 倍,适合既要一定吞吐又要控制面积的场景。


二十九、如何保证 i、i+1、i+2 不冲突

流水线能正常工作的前提是各级数据不打架。这靠的是几条结构性约束,不需要任何额外的逻辑:

五条规则:

规则
说明
1
每级组合逻辑只读上一级 Q 端,不穿透
2
每级组合逻辑只写下一级 D 端,不写回输入
3
所有级共用同一时钟、同一边沿
4 valid 逐级寄存
,和数据一起流动
5 无后级到前级的组合反馈

违反规则的几种写法:

错误写法
问题
assign prod = ma * mb
绕过 R1,S2 和 S1 算同一数据
assign ma = s2_prod[23:0]
后级反馈到前级,组合环
assign out_valid = in_valid
valid 没打拍,和数据不对齐
assign out_valid = s1_valid & s2_valid & s3_valid
同一拍描述不同数据,会误判
两个 always_ff 用不同 clk
数据错位

三十、完整顶层代码


   
   
   
   
    
   
   
   
   module fp32_mul_pipe (
    input  logic        clk, rst_n, in_valid,
    input  logic [31:0] a, b,
    output logic        out_valid,
    output logic [31:0] y
);
    // ========== Stage 1: 解包 ==========
    logic sa, sb;
    logic [7:0] ea, eb;
    logic [22:0] fa, fb;
    logic a_nan, b_nan, a_inf, b_inf, a_zero, b_zero;
    logic [7:0] ea_adj, eb_adj;
    logic [23:0] ma, mb;

    assign sa = a[31]; assign ea = a[30:23]; assign fa = a[22:0];
    assign sb = b[31]; assign eb = b[30:23]; assign fb = b[22:0];

    assign a_nan  = (ea == 8'hFF) && (fa != 0);
    assign b_nan  = (eb == 8'hFF) && (fb != 0);
    assign a_inf  = (ea == 8'hFF) && (fa == 0);
    assign b_inf  = (eb == 8'hFF) && (fb == 0);
    assign a_zero = (ea == 0) && (fa == 0);
    assign b_zero = (eb == 0) && (fb == 0);

    assign ea_adj = (ea == 0) ? 8'd1 : ea;
    assign eb_adj = (eb == 0) ? 8'd1 : eb;
    assign ma     = (ea == 0) ? {1'b0, fa} : {1'b1, fa};
    assign mb     = (eb == 0) ? {1'b0, fb} : {1'b1, fb};

    // S1 寄存器
    logic s1_valid, s1_sign;
    logic [23:0] s1_ma, s1_mb;
    logic [8:0]  s1_exp_sum;
    logic s1_a_nan, s1_b_nan, s1_a_inf, s1_b_inf, s1_a_zero, s1_b_zero;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            s1_valid <= 1'b0; s1_sign <= 1'b0;
            s1_ma <= 24'd0; s1_mb <= 24'd0; s1_exp_sum <= 9'd0;
            s1_a_nan <= 1'b0; s1_b_nan <= 1'b0;
            s1_a_inf <= 1'b0; s1_b_inf <= 1'b0;
            s1_a_zero <= 1'b0; s1_b_zero <= 1'b0;
        end else begin
            s1_valid   <= in_valid;
            s1_sign    <= sa ^ sb;
            s1_ma      <= ma;
            s1_mb      <= mb;
            s1_exp_sum <= {1'b0, ea_adj} + {1'b0, eb_adj};
            s1_a_nan <= a_nan; s1_b_nan <= b_nan;
            s1_a_inf <= a_inf; s1_b_inf <= b_inf;
            s1_a_zero <= a_zero; s1_b_zero <= b_zero;
        end
    end

    // ========== Stage 2: 乘法 ==========
    logic [47:0] prod;
    assign prod = s1_ma * s1_mb;

    logic s2_valid, s2_sign;
    logic [47:0] s2_prod;
    logic [8:0]  s2_exp_sum;
    logic s2_a_nan, s2_b_nan, s2_a_inf, s2_b_inf, s2_a_zero, s2_b_zero;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            s2_valid <= 1'b0; s2_sign <= 1'b0;
            s2_prod <= 48'd0; s2_exp_sum <= 9'd0;
            s2_a_nan <= 1'b0; s2_b_nan <= 1'b0;
            s2_a_inf <= 1'b0; s2_b_inf <= 1'b0;
            s2_a_zero <= 1'b0; s2_b_zero <= 1'b0;
        end else begin
            s2_valid   <= s1_valid;
            s2_sign    <= s1_sign;
            s2_prod    <= prod;
            s2_exp_sum <= s1_exp_sum;
            s2_a_nan <= s1_a_nan; s2_b_nan <= s1_b_nan;
            s2_a_inf <= s1_a_inf; s2_b_inf <= s1_b_inf;
            s2_a_zero <= s1_a_zero; s2_b_zero <= s1_b_zero;
        end
    end

    // ========== Stage 3: 规格化 + 舍入 ==========
    logic [5:0] msb_idx;
    always_comb begin
        msb_idx = 6'd0;
        for (int i = 0; i < 48; i++)
            if (s2_prod[i]) msb_idx = i[5:0];
    end

    logic [5:0]  shamt;
    logic [47:0] norm;
    assign shamt = 6'd47 - msb_idx;
    assign norm  = s2_prod << shamt;

    logic signed [11:0] exp_biased;
    assign exp_biased = $signed({3'b0, s2_exp_sum})
                      + $signed({6'b0, msb_idx}) - 12'sd173;

    logic [23:0] mant24;
    assign mant24 = norm[47:24];

    logic guard, sticky, lsb;
    assign guard  = norm[23];
    assign sticky = |norm[22:0];
    assign lsb    = mant24[0];

    logic [24:0] mant_rnd;
    always_comb begin
        if (guard && (sticky || lsb))
            mant_rnd = {1'b0, mant24} + 1'b1;
        else
            mant_rnd = {1'b0, mant24};
    end

    logic [23:0] mant_final;
    logic signed [11:0] exp_final;
    always_comb begin
        if (mant_rnd[24]) begin
            mant_final = mant_rnd[24:1];
            exp_final  = exp_biased + 12'sd1;
        end else begin
            mant_final = mant_rnd[23:0];
            exp_final  = exp_biased;
        end
    end

    logic is_zero_prod;
    assign is_zero_prod = (s2_prod == 48'd0);

    // S3 寄存器
    logic s3_valid, s3_sign;
    logic [23:0] s3_mant;
    logic [11:0] s3_exp;
    logic s3_zero_prod;
    logic s3_a_nan, s3_b_nan, s3_a_inf, s3_b_inf, s3_a_zero, s3_b_zero;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            s3_valid <= 1'b0; s3_sign <= 1'b0;
            s3_mant <= 24'd0; s3_exp <= 12'd0;
            s3_zero_prod <= 1'b0;
            s3_a_nan <= 1'b0; s3_b_nan <= 1'b0;
            s3_a_inf <= 1'b0; s3_b_inf <= 1'b0;
            s3_a_zero <= 1'b0; s3_b_zero <= 1'b0;
        end else begin
            s3_valid   <= s2_valid;
            s3_sign    <= s2_sign;
            s3_mant    <= mant_final;
            s3_exp     <= exp_final;
            s3_zero_prod <= is_zero_prod;
            s3_a_nan <= s2_a_nan; s3_b_nan <= s2_b_nan;
            s3_a_inf <= s2_a_inf; s3_b_inf <= s2_b_inf;
            s3_a_zero <= s2_a_zero; s3_b_zero <= s2_b_zero;
        end
    end

    // ========== Stage 3 后半: 打包 ==========
    logic [31:0] y_norm;
    always_comb begin
        if (s3_zero_prod)
            y_norm = {s3_sign, 31'd0};
        else if (s3_exp >= 12'sd255)
            y_norm = {s3_sign, 8'hFF, 23'd0};
        else if (s3_exp <= 12'sd0)
            y_norm = {s3_sign, 31'd0};
        else
            y_norm = {s3_sign, s3_exp[7:0], s3_mant[22:0]};
    end

    logic [31:0] y_result;
    always_comb begin
        if (s3_a_nan || s3_b_nan)
            y_result = {s3_sign, 8'hFF, 23'h400000};
        else if (s3_a_inf || s3_b_inf) begin
            if ((s3_a_inf && s3_b_zero) || (s3_b_inf && s3_a_zero))
                y_result = {s3_sign, 8'hFF, 23'h400000};
            else
                y_result = {s3_sign, 8'hFF, 23'd0};
        end
        else if (s3_a_zero || s3_b_zero)
            y_result = {s3_sign, 31'd0};
        else
            y_result = y_norm;
    end

    // 输出寄存器
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            out_valid <= 1'b0;
            y         <= 32'd0;
        end else begin
            out_valid <= s3_valid;
            y         <= y_result;
        end
    end
endmodule

三十一、总结

从一段解包代码开始,到三级流水线,再到数组接口,整个设计可以归纳成下面这张图:

核心要点:

要点
说明
浮点乘法比加法简单 不需要对阶
三级流水是平衡点
S1 预处理、S2 乘法、S3 后处理
同步靠同一时钟沿
所有寄存器同时采样
周期取最慢级
由综合工具分析每级延迟得出
valid 必须逐级寄存 不能全部相与
不冲突靠结构性约束
只读上级 Q,只写下级 D,无反馈
数组可并行可复用
按面积和吞吐需求选择

🎯 到这里,一个从零开始的 FP32 流水线乘法器就完整了。每一段代码都对应一个具体的计算步骤,每一个寄存器都切出一条清晰的流水线边界。理解了这个设计,再去看更复杂的 FMA、双精度乘法、SIMD 乘法单元,思路都是一样的。

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 235
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读6.6k
粉丝0
内容235