FPGA 定点数、小数与负数的加减乘除原理

FPGA 定点数、小数与负数的加减乘除原理

面向 Verilog/SystemVerilog、Vivado 和 FPGA DSP 硬件实现的定点数入门与工程实践文档。

重点讨论:二进制小数、补码负数、加法、减法、乘法、除法、位宽、溢出、舍入、饱和以及 DSP48 推断。


1. 为什么 FPGA 中通常使用定点数

FPGA 中表示实数有两种主要方式:

  1. 浮点数
  2. 定点数

浮点数能够表示很大的动态范围,但硬件代价较高,通常需要浮点加法器、乘法器、规格化和舍入逻辑。

定点数把小数点的位置固定下来。例如:

text 复制代码
整数:        7
定点数:      7.25

在硬件中,二者本质上都是一组二进制位。区别只是:

如何解释这些位,以及小数点位于哪里。

因此,定点数的乘加、滤波、CORDIC、NCO 和 ADC 数据处理通常可以使用:

  • 加法器
  • 减法器
  • 移位器
  • DSP 乘法器
  • BRAM 查找表
  • 比较器

而不需要完整的浮点运算单元。


2. 定点数的基本表示方法

2.1 Q 格式

定点数常用 Qm.n 表示:

  • m:整数部分位数
  • n:小数部分位数
  • 总位宽通常为 m + n
  • 如果采用有符号补码,有时还需要单独考虑符号位

工程中不同资料对 Qm.n 的符号位定义可能略有不同。为了避免歧义,本文统一采用:

S(W,F) 表示一个总宽度为 W 位、其中 F 位为小数位的有符号补码定点数。

例如:

text 复制代码
S(16,12)

表示:

  • 总宽度:16 位
  • 小数位:12 位
  • 整数加符号区域:4 位
  • 缩放因子:2^12 = 4096

对于原始补码整数 raw,真实值为:

text 复制代码
real_value = raw / 2^F

反过来,真实值转换为定点原始码:

text 复制代码
raw = round(real_value × 2^F)

2.2 定点小数点位置

S(8,4) 为例:

text 复制代码
位号:       7   6   5   4 . 3   2   1   0
权重:     -8    4   2   1   . 1/2 1/4 1/8 1/16

这里:

  • 最高位 bit7 是补码符号位
  • bit6:4 表示整数部分
  • bit3:0 表示小数部分
  • 最小分辨率为 2^-4 = 1/16

其数值范围为:

text 复制代码
最小值 = -2^(8-4-1) = -8
最大值 =  2^(8-4-1) - 2^-4 = 7.9375

所以 S(8,4) 的范围是:

text 复制代码
[-8, 7.9375]

2.3 常见定点格式

格式 总位宽 小数位 分辨率 大致范围
S(8,4) 8 4 0.0625 -8 ~ 7.9375
S(16,8) 16 8 0.00390625 -128 ~ 127.996
S(16,12) 16 12 0.00024414 -8 ~ 7.9998
S(18,16) 18 16 0.00001526 -2 ~ 1.99998
S(24,20) 24 20 0.0000009537 -8 ~ 7.999999

格式选择的核心是平衡:

text 复制代码
动态范围 ↔ 精度 ↔ 资源 ↔ 时序

3. 补码负数原理

FPGA 中的有符号定点数通常使用二进制补码表示。

3.1 正数

正数直接使用二进制表示。

例如在 S(8,4) 中:

text 复制代码
3.25 × 16 = 52
52 = 0011_0100

因此:

text 复制代码
3.25 的定点码 = 0011_0100

3.2 负数

负数的补码计算步骤:

  1. 取正数的二进制
  2. 按位取反
  3. 加 1

例如 -2.5

text 复制代码
2.5 × 16 = 40
40 = 0010_1000

取反:

text 复制代码
1101_0111

加 1:

text 复制代码
1101_1000

因此:

text 复制代码
-2.5 的 S(8,4) 定点码 = 1101_1000

将其看作有符号整数:

text 复制代码
1101_1000 = -40

恢复真实值:

text 复制代码
-40 / 16 = -2.5

3.3 补码的取值范围

对于总宽度为 W 的有符号补码整数:

text 复制代码
[-2^(W-1), 2^(W-1)-1]

对于 S(W,F) 定点数:

text 复制代码
最小值 = -2^(W-F-1)
最大值 =  2^(W-F-1) - 2^-F

4. Verilog 中正确声明有符号数

4.1 推荐写法

systemverilog 复制代码
logic signed [15:0] a;
logic signed [15:0] b;
logic signed [16:0] sum;

或者:

systemverilog 复制代码
reg signed [15:0] a;
wire signed [15:0] b;

关键字 signed 非常重要。


4.2 常见错误

systemverilog 复制代码
logic [15:0] a;
logic [15:0] b;
logic signed [16:0] sum;

assign sum = a + b;

这里 ab 被声明为无符号数。即使 sum 是有符号数,表达式 a + b 也可能先按照无符号方式计算,导致负数运算错误。

应改为:

systemverilog 复制代码
logic signed [15:0] a;
logic signed [15:0] b;
logic signed [16:0] sum;

assign sum = $signed(a) + $signed(b);

4.3 拼接会丢失 signed 属性

下面的拼接结果通常被视为无符号:

systemverilog 复制代码
assign y = {a[15], a};

更稳妥的写法是:

systemverilog 复制代码
assign y = $signed({a[15], a});

或者直接使用符号扩展:

systemverilog 复制代码
assign y = {{1{a[15]}}, a};

并在表达式中显式使用:

systemverilog 复制代码
assign y = $signed({{1{a[15]}}, a});

5. 定点数加法原理

5.1 加法的前提:小数位必须相同

两个定点数只有在小数位数相同时,才能直接相加。

设:

text 复制代码
A = A_raw / 2^F
B = B_raw / 2^F

那么:

text 复制代码
A + B = (A_raw + B_raw) / 2^F

因此硬件中只需对原始码做整数加法:

text 复制代码
sum_raw = A_raw + B_raw

小数点位置保持不变。


5.2 示例:定点加法

设:

text 复制代码
A = 1.25
B = 2.50
格式:S(8,4)

转换为原始码:

text 复制代码
A_raw = 1.25 × 16 = 20
B_raw = 2.50 × 16 = 40

二进制:

text 复制代码
A_raw = 0001_0100
B_raw = 0010_1000

相加:

text 复制代码
  0001_0100
+ 0010_1000
------------
  0011_1100

十进制:

text 复制代码
60 / 16 = 3.75

所以:

text 复制代码
1.25 + 2.50 = 3.75

5.3 加法位宽

两个 W 位有符号数相加,建议使用 W+1 位保存结果:

systemverilog 复制代码
localparam int W = 16;
localparam int F = 12;

logic signed [W-1:0] a;
logic signed [W-1:0] b;
logic signed [W:0]   sum_ext;

assign sum_ext = $signed(a) + $signed(b);

为什么需要增加一位?

例如无符号数:

text 复制代码
7 + 1 = 8

如果只有 3 位:

text 复制代码
111 + 001 = 000

结果溢出。

有符号补码也存在同样问题,只是溢出的判断规则不同。


5.4 有符号加法溢出判断

对于两个有符号数:

text 复制代码
a + b = y

发生溢出的条件是:

text 复制代码
a 和 b 符号相同,但 y 的符号与它们不同

Verilog:

systemverilog 复制代码
logic signed [15:0] a;
logic signed [15:0] b;
logic signed [15:0] y;
logic              overflow_add;

assign overflow_add =
    (~(a[15] ^ b[15])) &&
    (y[15] ^ a[15]);

更安全的工程方法是:

  1. 先扩展到 W+1
  2. 再根据最高两位判断是否溢出
  3. 根据需要进行饱和处理

6. 不同小数位数的加法

如果两个操作数的小数位不同,需要先对齐小数点。

设:

text 复制代码
A 为 S(WA, FA)
B 为 S(WB, FB)

如果 FA > FB,则将 B_raw 左移:

text 复制代码
B_aligned = B_raw << (FA - FB)

如果 FB > FA,则将 A_raw 左移:

text 复制代码
A_aligned = A_raw << (FB - FA)

然后使用较大的小数位数作为结果的小数位。


6.1 示例

text 复制代码
A = 1.25,格式 S(8,4)
B = 2.5,格式 S(8,2)

原始码:

text 复制代码
A_raw = 1.25 × 16 = 20
B_raw = 2.5 × 4 = 10

统一到 4 个小数位:

text 复制代码
B_aligned = 10 << 2 = 40

相加:

text 复制代码
20 + 40 = 60
60 / 16 = 3.75

6.2 硬件实现

systemverilog 复制代码
logic signed [7:0] a_s4;
logic signed [7:0] b_s2;

logic signed [9:0] a_ext;
logic signed [9:0] b_ext;
logic signed [9:0] sum_ext;

assign a_ext = {{2{a_s4[7]}}, a_s4};
assign b_ext = $signed({{2{b_s2[7]}}, b_s2}) <<< 2;

assign sum_ext = a_ext + b_ext;

注意:

  • 扩展前必须保留符号
  • 对有符号数移位时要使用算术左移/右移语义
  • 实际工程中应根据最大范围进一步增加保护位

7. 定点数减法原理

减法本质上是加上被减数的补码:

text 复制代码
A - B = A + (-B)

因此硬件通常使用加法器完成减法。


7.1 示例:正数减正数

text 复制代码
A = 3.5
B = 1.25
格式:S(8,4)

原始码:

text 复制代码
A_raw = 3.5 × 16  = 56
B_raw = 1.25 × 16 = 20

相减:

text 复制代码
56 - 20 = 36
36 / 16 = 2.25

结果:

text 复制代码
3.5 - 1.25 = 2.25

7.2 示例:正数减负数

text 复制代码
A = 1.5
B = -2.25

则:

text 复制代码
A - B = 1.5 - (-2.25)
      = 1.5 + 2.25
      = 3.75

S(8,4) 表示:

text 复制代码
A_raw = 1.5 × 16  = 24
B_raw = -2.25 × 16 = -36

硬件计算:

text 复制代码
24 - (-36) = 60
60 / 16 = 3.75

7.3 减法位宽和溢出

减法也建议增加一位:

systemverilog 复制代码
logic signed [15:0] a;
logic signed [15:0] b;
logic signed [16:0] diff_ext;

assign diff_ext = $signed(a) - $signed(b);

例如:

text 复制代码
最大值 - 最小值

可能超出原始位宽,因此必须进行:

  • 截断
  • 饱和
  • 或扩大输出位宽

8. 定点数乘法原理

乘法是定点计算中最容易出现小数位错误的地方。

设:

text 复制代码
A = A_raw / 2^FA
B = B_raw / 2^FB

则:

text 复制代码
A × B
= (A_raw × B_raw) / 2^(FA+FB)

因此:

两个定点数相乘后,小数位数等于两个输入小数位数之和。


8.1 相同格式相乘

两个 S(W,F) 数相乘:

text 复制代码
A × B

乘积原始位宽为:

text 复制代码
2W

乘积小数位为:

text 复制代码
2F

结果格式:

text 复制代码
S(2W, 2F)

8.2 示例:乘法

text 复制代码
A = 1.5
B = -0.75
格式:S(8,4)

原始码:

text 复制代码
A_raw = 1.5 × 16 = 24
B_raw = -0.75 × 16 = -12

整数乘法:

text 复制代码
P_raw = 24 × (-12) = -288

乘积有 8 个小数位:

text 复制代码
P = -288 / 256 = -1.125

因此:

text 复制代码
1.5 × (-0.75) = -1.125

8.3 SystemVerilog 乘法示例

systemverilog 复制代码
localparam int W = 16;
localparam int F = 12;

logic signed [W-1:0] a;
logic signed [W-1:0] b;
logic signed [(2*W)-1:0] product_full;

assign product_full = $signed(a) * $signed(b);

这里:

text 复制代码
product_full
格式:S(32,24)

如果希望恢复成与输入相同的小数位 F=12,需要右移 F 位:

systemverilog 复制代码
logic signed [W-1:0] product_scaled;

assign product_scaled = product_full >>> F;

原因是:

text 复制代码
S(16,12) × S(16,12) = S(32,24)

右移 12 位后:

text 复制代码
S(32,12)

再根据需要截断或饱和到 S(16,12)


8.4 乘法后的位宽处理

完整乘积不应直接粗暴截断。

推荐流程:

text 复制代码
输入扩展
   ↓
完整乘法
   ↓
舍入
   ↓
按小数位右移
   ↓
溢出判断
   ↓
饱和或截断
   ↓
输出

例如:

systemverilog 复制代码
logic signed [31:0] product_full;
logic signed [31:0] product_round;
logic signed [31:0] product_shifted;

assign product_full = $signed(a) * $signed(b);
assign product_round = product_full + (32'sd1 <<< (F-1));
assign product_shifted = product_round >>> F;

上面的简单舍入方式只适用于正数。对于负数,工程中应明确选择:

  • 向零舍入
  • 向下舍入
  • 四舍五入
  • 收敛舍入
  • 对称舍入

不能默认所有舍入方式都对正负数完全对称。


9. 定点数除法原理

除法的核心是先扩大被除数,使结果保留所需的小数位。

设:

text 复制代码
A = A_raw / 2^FA
B = B_raw / 2^FB

希望结果采用 FR 个小数位:

text 复制代码
Y = A / B

推导:

text 复制代码
Y = (A_raw / 2^FA) / (B_raw / 2^FB)
  = A_raw × 2^FB / (B_raw × 2^FA)

如果直接得到结果原始码 Y_raw,要求:

text 复制代码
Y = Y_raw / 2^FR

因此:

text 复制代码
Y_raw = (A_raw << (FB + FR - FA)) / B_raw

这就是通用定点除法公式。


9.1 同格式除法

当:

text 复制代码
FA = FB = F

并且希望结果仍然有 F 个小数位时:

text 复制代码
Y_raw = (A_raw << F) / B_raw

9.2 示例:除法

text 复制代码
A = 3.0
B = 1.5
输入格式:S(16,8)
输出小数位:8

原始码:

text 复制代码
A_raw = 3.0 × 256 = 768
B_raw = 1.5 × 256 = 384

计算:

text 复制代码
Y_raw = (768 << 8) / 384
      = 196608 / 384
      = 512

恢复真实值:

text 复制代码
512 / 256 = 2.0

所以:

text 复制代码
3.0 / 1.5 = 2.0

9.3 除法的硬件代价

FPGA 中直接使用 / 可能推断出较大的组合除法器,导致:

  • LUT 使用量大
  • 组合路径很长
  • Fmax 降低
  • 功耗增加
  • 时序难以收敛

常见替代方案:

  1. 除以 2 的幂:使用算术右移
  2. 常数除法:乘以倒数,再右移
  3. 变量除法:使用迭代除法器
  4. 高吞吐场景:使用流水线除法器
  5. 非线性计算:使用 LUT、CORDIC 或 Newton-Raphson
  6. 低速控制路径:允许多周期计算

9.4 除以 2 的幂

对于有符号数:

text 复制代码
Y = X / 2^N

可以使用算术右移:

systemverilog 复制代码
assign y = x >>> N;

注意:

systemverilog 复制代码
>>> 

是算术右移,左侧需要是 signed。

如果 x 未声明为 signed,可能发生逻辑右移,负数高位补零,结果错误。


9.5 除法中的截断问题

整数除法会丢弃余数。

例如:

text 复制代码
7 / 3 = 2 余 1

对于定点数,直接截断会产生量化误差。

可以通过增加舍入项改善:

text 复制代码
quotient = (numerator + denominator/2) / denominator

但是对于负数,需要明确舍入规则,不能简单地无条件加上 denominator/2


9.6 除零处理

硬件必须明确处理除数为零的情况。

常见策略:

text 复制代码
除数为 0:
    输出最大正数
    或输出最大负数
    或输出 0
    或拉高 divide_by_zero 标志

示例:

systemverilog 复制代码
always_comb begin
    if (b == 0) begin
        y = '0;
        divide_by_zero = 1'b1;
    end
    else begin
        y = numerator / b;
        divide_by_zero = 1'b0;
    end
end

在控制系统和信号处理系统中,建议同时输出:

text 复制代码
结果值 + 异常标志

而不是静默地产生一个看似正常的数。


10. 定点数的符号扩展与零扩展

10.1 有符号数扩展

有符号补码扩展时,复制符号位:

text 复制代码
正数:高位补 0
负数:高位补 1

例如:

text 复制代码
8'b0001_1000  →  12'b0000_0001_1000
8'b1110_1000  →  12'b1111_1110_1000

SystemVerilog:

systemverilog 复制代码
assign y = {{4{x[7]}}, x};

10.2 无符号数扩展

无符号数扩展时,高位补 0:

systemverilog 复制代码
assign y = {4'b0, x};

10.3 定点小数扩展

如果只是增加小数位,应该在右侧补零,相当于左移:

text 复制代码
S(W,F) → S(W+N,F+N)

例如:

text 复制代码
1.5 的 S(8,4) 原始码 = 24

转换到 6 个小数位:

text 复制代码
24 << 2 = 96
96 / 64 = 1.5

11. 截断、舍入和饱和

11.1 截断

截断是直接丢弃低位:

systemverilog 复制代码
assign y = x[MSB -: OUT_W];

优点:

  • 逻辑简单
  • 资源少
  • 时序容易

缺点:

  • 有量化误差
  • 可能产生直流偏置
  • 信号处理中的噪声性能较差

11.2 舍入

常见的正数四舍五入:

text 复制代码
保留高位 + 被丢弃部分最高位

例如保留 N 位:

text 复制代码
rounded = (x + 2^(N-1)) >>> N

SystemVerilog 示例:

systemverilog 复制代码
logic signed [31:0] x;
logic signed [31:0] x_round;

assign x_round = (x + (32'sd1 <<< (SHIFT-1))) >>> SHIFT;

但对于有符号数,应根据项目要求采用明确的对称舍入策略,尤其是音频、通信和高精度滤波场景。


11.3 饱和

如果结果超出可表示范围,饱和处理会将其限制在最大值或最小值:

text 复制代码
超过最大值 → 最大值
低于最小值 → 最小值
范围内 → 原值

S(8,4) 为例:

text 复制代码
最大值 = 7.9375
最小值 = -8.0

如果结果为:

text 复制代码
9.2 → 7.9375
-10.0 → -8.0

饱和比直接截断更适合:

  • 音频
  • ADC/DAC
  • 控制算法
  • 图像像素
  • 通信基带
  • FIR 输出

11.4 饱和逻辑示例

下面示例将较宽的 S(17,12) 结果压缩为 S(16,12)

systemverilog 复制代码
module sat_s16_f12 (
    input  logic signed [16:0] din,
    output logic signed [15:0] dout,
    output logic               overflow
);

    localparam logic signed [15:0] MAX_VAL = 16'sh7fff;
    localparam logic signed [15:0] MIN_VAL = 16'sh8000;

    always_comb begin
        if (din > 17'sh07fff) begin
            dout = MAX_VAL;
            overflow = 1'b1;
        end
        else if (din < -17'sh08000) begin
            dout = MIN_VAL;
            overflow = 1'b1;
        end
        else begin
            dout = din[15:0];
            overflow = 1'b0;
        end
    end

endmodule

实际工程中建议使用参数化模块,避免手写常量造成位宽错误。


12. 定点运算总公式

设:

text 复制代码
A:总宽度 WA,小数位 FA
B:总宽度 WB,小数位 FB

12.1 加法

前提:

text 复制代码
FA = FB

结果:

text 复制代码
Fresult = FA

原始码:

text 复制代码
Y_raw = A_raw + B_raw

建议结果位宽:

text 复制代码
Wresult = max(WA, WB) + 1

12.2 减法

前提:

text 复制代码
FA = FB

结果:

text 复制代码
Fresult = FA

原始码:

text 复制代码
Y_raw = A_raw - B_raw

建议结果位宽:

text 复制代码
Wresult = max(WA, WB) + 1

12.3 乘法

结果:

text 复制代码
Fresult = FA + FB

原始码:

text 复制代码
Y_raw = A_raw × B_raw

完整结果位宽:

text 复制代码
Wresult = WA + WB

12.4 除法

希望输出有 FR 个小数位:

text 复制代码
Y_raw = (A_raw << (FB + FR - FA)) / B_raw

同格式时:

text 复制代码
Y_raw = (A_raw << F) / B_raw

13. FPGA 中的乘法与 DSP48 推断

对于 Xilinx 7 Series,乘法和乘加通常应尽量映射到 DSP48E1,而不是全部使用 LUT。

推荐代码结构:

systemverilog 复制代码
module fixed_mult #(
    parameter int A_W = 16,
    parameter int B_W = 16,
    parameter int F   = 12
) (
    input  logic                         clk,
    input  logic signed [A_W-1:0]        a,
    input  logic signed [B_W-1:0]        b,
    output logic signed [A_W+B_W-1:0]    p
);

    logic signed [A_W-1:0] a_r;
    logic signed [B_W-1:0] b_r;
    logic signed [A_W+B_W-1:0] p_r;

    always_ff @(posedge clk) begin
        a_r <= a;
        b_r <= b;
        p_r <= a_r * b_r;
    end

    assign p = p_r;

endmodule

工程上需要关注:

  • 输入是否为 signed
  • 乘法两侧位宽是否明确
  • 是否使用了异步复位
  • 是否存在复杂 enable
  • 是否把乘法、加法和输出压在一个时钟周期
  • 是否使用 DSP48 内部寄存器
  • 是否需要在乘法后增加流水线

典型的高性能结构是:

text 复制代码
输入寄存器
    ↓
DSP 乘法器
    ↓
乘积寄存器
    ↓
加法/累加器
    ↓
输出寄存器

DSP48 外挂寄存器未被吸收或乘加路径未分级时,可能出现明显时序违例。应结合综合报告检查 AREG、BREG、MREG 和 PREG 的使用情况,并用固定点模型验证延迟和数值误差。


14. FPGA 中的除法实现方式

14.1 常数除法

如果除数是常数,可以将除法改写为乘法:

text 复制代码
x / C ≈ x × (1/C)

例如:

text 复制代码
x / 10 ≈ x × 0.1

0.1 转换为定点常数后,使用 DSP 乘法器,再进行右移。


14.2 除以 2 的幂

text 复制代码
x / 2^N = x >>> N

这是最简单、最快的除法形式。


14.3 迭代除法器

变量除数可使用:

  • restoring division
  • non-restoring division
  • radix-2 division
  • radix-4 division
  • SRT division

特点:

  • 资源较少
  • 延迟较长
  • 适合低吞吐率场景

14.4 流水线除法器

适用于每拍都需要处理一个数据的场景:

text 复制代码
输入一组数据 → 经过多个时钟周期 → 每拍输出一组结果

特点:

  • 吞吐率高
  • 延迟固定
  • 资源较多
  • 需要严格对齐 valid、tag 和数据

14.5 CORDIC 或倒数迭代

对于:

text 复制代码
1 / x

可以先求倒数,再乘法:

text 复制代码
a / b = a × (1/b)

倒数可以使用:

  • LUT
  • CORDIC
  • Newton-Raphson
  • Goldschmidt

适用于高吞吐率和高精度场景,但需要额外的范围归一化和误差分析。


15. 定点乘加中的小数位管理

FIR 和 MAC 中经常出现:

text 复制代码
y = x0*h0 + x1*h1 + x2*h2 + ...

如果输入和系数分别为:

text 复制代码
x:S(Wx,Fx)
h:S(Wh,Fh)

每个乘积的格式为:

text 复制代码
S(Wx+Wh, Fx+Fh)

所有乘积的小数位相同,可以直接相加。

但是累加时必须增加保护位。

如果有 N 项相加,理论上至少需要增加:

text 复制代码
ceil(log2(N))

个保护位。

例如:

text 复制代码
16 项乘积累加

至少建议增加:

text 复制代码
ceil(log2(16)) = 4

位保护位。

实际设计还应考虑:

  • 输入最大幅度
  • 系数绝对值之和
  • 信号峰值
  • 是否允许过载
  • 是否使用缩放系数
  • 是否在每一级进行饱和

16. 一个完整的 Q 格式计算例子

设:

text 复制代码
x = 1.25
h = -0.5
格式:S(16,12)

16.1 转换为原始码

text 复制代码
x_raw = 1.25 × 4096 = 5120
h_raw = -0.5 × 4096 = -2048

16.2 原始整数相乘

text 复制代码
p_raw = 5120 × (-2048)
      = -10485760

16.3 乘积格式

输入都是 S(16,12)

text 复制代码
乘积格式 = S(32,24)

16.4 恢复到 12 个小数位

text 复制代码
y_raw = p_raw >>> 12
      = -2560

16.5 还原真实值

text 复制代码
y = -2560 / 4096
  = -0.625

验证:

text 复制代码
1.25 × (-0.5) = -0.625

17. 位宽设计建议

17.1 加法器

text 复制代码
W_out = max(W_a, W_b) + 1

如果最终必须回到原位宽,应增加:

  • 溢出检测
  • 饱和逻辑
  • 或明确的截断规则

17.2 乘法器

完整乘法:

text 复制代码
W_product = W_a + W_b

不要只保留输出所需的低位,除非已经明确分析过:

  • 符号位
  • 小数位
  • 量化噪声
  • 溢出范围

17.3 累加器

对于 N 项相加:

text 复制代码
W_acc ≈ W_product + ceil(log2(N))

如果数据可能同向叠加,建议再增加额外保护位。


17.4 除法器

除法前应增加分子位宽:

text 复制代码
numerator = A_raw <<< scaling_bits

其中:

text 复制代码
scaling_bits = FB + FR - FA

否则结果的小数精度会不足。


18. 负数右移的注意事项

对有符号补码数:

text 复制代码
-3 >>> 1

通常得到:

text 复制代码
-2

这是算术右移,负数高位补 1。

但数学上的:

text 复制代码
-3 / 2 = -1.5

如果输出仍为整数,硬件必须定义舍入方向:

  • 向负无穷取整:-2
  • 向零取整:-1
  • 四舍五入:可能为 -2
  • 对称舍入:根据余数决定

因此,>>> 并不自动等价于你想要的数学除法规则。


19. Verilog 定点运算模板

19.1 定点加法

systemverilog 复制代码
module fixed_add #(
    parameter int W = 16
) (
    input  logic signed [W-1:0] a,
    input  logic signed [W-1:0] b,
    output logic signed [W:0]   y
);

    always_comb begin
        y = $signed(a) + $signed(b);
    end

endmodule

19.2 定点乘法并恢复小数位

systemverilog 复制代码
module fixed_mult_scaled #(
    parameter int W = 16,
    parameter int F = 12
) (
    input  logic signed [W-1:0] a,
    input  logic signed [W-1:0] b,
    output logic signed [W-1:0] y
);

    logic signed [(2*W)-1:0] product_full;
    logic signed [(2*W)-1:0] product_scaled;

    always_comb begin
        product_full   = $signed(a) * $signed(b);
        product_scaled = product_full >>> F;
        y              = product_scaled[W-1:0];
    end

endmodule

注意:此版本没有饱和处理,仅适合作为基础演示。工程版本应增加舍入、溢出检测和饱和。


19.3 同格式定点除法

systemverilog 复制代码
module fixed_div #(
    parameter int W = 16,
    parameter int F = 12
) (
    input  logic signed [W-1:0] a,
    input  logic signed [W-1:0] b,
    output logic signed [W-1:0] y,
    output logic                div_zero
);

    logic signed [(2*W)-1:0] numerator;
    logic signed [(2*W)-1:0] quotient;

    always_comb begin
        if (b == 0) begin
            numerator = '0;
            quotient  = '0;
            y         = '0;
            div_zero  = 1'b1;
        end
        else begin
            numerator = $signed(a) <<< F;
            quotient  = numerator / $signed(b);
            y         = quotient[W-1:0];
            div_zero  = 1'b0;
        end
    end

endmodule

该写法适合说明原理。对于高速数据通路,建议使用专用流水线除法器或将除法转换为乘以倒数。


20. Python 定点模型

硬件设计前,建议先建立 Python 参考模型。

python 复制代码
from dataclasses import dataclass


@dataclass
class FixedFormat:
    width: int
    frac: int

    @property
    def scale(self):
        return 1 << self.frac

    @property
    def raw_min(self):
        return -(1 << (self.width - 1))

    @property
    def raw_max(self):
        return (1 << (self.width - 1)) - 1

    def encode(self, value: float) -> int:
        raw = round(value * self.scale)
        return max(self.raw_min, min(self.raw_max, raw))

    def decode(self, raw: int) -> float:
        return raw / self.scale


def fixed_add(a_raw: int, b_raw: int) -> int:
    return a_raw + b_raw


def fixed_sub(a_raw: int, b_raw: int) -> int:
    return a_raw - b_raw


def fixed_mul(a_raw: int, b_raw: int, frac: int) -> int:
    product = a_raw * b_raw
    return product >> frac


def fixed_div(a_raw: int, b_raw: int, frac: int) -> int:
    if b_raw == 0:
        raise ZeroDivisionError("fixed-point division by zero")

    numerator = a_raw << frac
    return int(numerator / b_raw)


fmt = FixedFormat(width=16, frac=12)

a = fmt.encode(1.25)
b = fmt.encode(-0.5)

sum_raw = fixed_add(a, b)
mul_raw = fixed_mul(a, b, fmt.frac)

print("a =", fmt.decode(a))
print("b =", fmt.decode(b))
print("a+b =", fmt.decode(sum_raw))
print("a*b =", fmt.decode(mul_raw))

Python 模型可用于:

  • 生成 testbench 激励
  • 计算 golden reference
  • 对比 RTL 输出
  • 统计最大误差
  • 验证溢出和饱和
  • 评估不同位宽的资源/精度折中

21. 定点误差的主要来源

定点误差通常来自以下几个方面:

21.1 量化误差

真实值无法精确表示为有限小数位:

text 复制代码
0.1 × 256 = 25.6

只能选择:

text 复制代码
25 或 26

因此会产生量化误差。


21.2 截断误差

乘法、除法和移位后丢弃低位,会产生误差。


21.3 溢出误差

结果超过表示范围后:

  • 直接截断可能回绕
  • 饱和会限制到边界
  • 过载可能产生严重非线性失真

21.4 系数误差

滤波器系数、CORDIC 常数、NCO 增量等也需要量化。


21.5 累加误差

多级累加会放大:

  • 量化噪声
  • 舍入误差
  • 过载风险

22. 推荐的定点设计流程

text 复制代码
1. 确定输入信号的最大幅度
2. 确定允许的最小分辨率
3. 选择整数位和小数位
4. 推导每个运算节点的位宽
5. 计算乘法后的总小数位
6. 为累加器增加保护位
7. 明确截断、舍入和饱和规则
8. 建立 Python/Matlab 参考模型
9. 编写 RTL 和 testbench
10. 对比 RTL 与参考模型
11. 检查综合后的 DSP/LUT 映射
12. 检查时序、资源和数值误差
13. 使用边界值、随机值和负数回归测试

23. 常见错误清单

错误 1:忘记声明 signed

systemverilog 复制代码
logic [15:0] a;

如果 a 表示负数,这是错误或高风险写法。


错误 2:乘法后忘记右移

text 复制代码
S(16,12) × S(16,12)

乘积有 24 个小数位。如果要恢复为 12 个小数位,必须右移 12 位。


错误 3:加法前没有对齐小数点

不同 F 值的数据不能直接相加。


错误 4:截断前没有分析符号位

随意选择位段可能破坏符号和数值比例。


错误 5:累加器没有保护位

多项累加很容易溢出。


错误 6:除法没有扩大分子

直接做:

systemverilog 复制代码
y = a / b;

通常不能得到期望的小数精度。


错误 7:负数使用逻辑右移

systemverilog 复制代码
x >> N

如果 x 未被正确声明为 signed,负数高位可能补零。


错误 8:没有定义除零行为

硬件必须明确:

text 复制代码
除数为 0 时输出什么?
是否产生异常标志?

错误 9:只看仿真数值,不看综合结构

定点运算正确不代表硬件实现合理,还必须检查:

  • DSP 是否被推断
  • LUT 是否过多
  • 乘加路径是否过长
  • 内部寄存器是否使用
  • 是否满足 Fmax

24. 总结

FPGA 定点数的本质是:

text 复制代码
有符号整数 + 固定的小数点解释规则

最重要的四条规则是:

加法

text 复制代码
小数位相同,原始码直接相加

减法

text 复制代码
小数位相同,原始码直接相减

乘法

text 复制代码
原始码相乘,小数位数相加

若要恢复到原来的小数位:

text 复制代码
乘积算术右移输入小数位数

除法

若结果需要 FR 个小数位:

text 复制代码
Y_raw = (A_raw << (FB + FR - FA)) / B_raw

在实际 FPGA 设计中,还必须同时考虑:

text 复制代码
signed
位宽
符号扩展
小数点对齐
保护位
截断
舍入
饱和
除零
DSP 推断
流水线
时序收敛

定点数不是简单地"把浮点数乘一个常数",而是一套贯穿整个数据通路的数值格式设计方法。每一个模块都应明确记录:

text 复制代码
输入格式
输出格式
小数位位置
最大值
最小值
溢出行为
舍入方式
延迟周期

只有这样,定点算法才能从仿真模型可靠地落地到 FPGA 硬件。

相关推荐
zlinear数据采集卡3 小时前
D223上位机C#开发实战:从帧解析到波形显示的完整实现
开发语言·arm开发·嵌入式硬件·fpga开发·开源·c#
zlinear数据采集卡7 小时前
D223 ADC数据处理流水线:从原始采样值到工程单位的完整转换链
开发语言·arm开发·嵌入式硬件·fpga开发·开源·c#
FPGA小迷弟7 小时前
Vivado中高速收发器GTX/GTH的配置与调试实战
fpga开发·fpga·硬件开发
minglie17 小时前
简易寄存器接口SMMR---iic控制器
fpga开发
FPGA小迷弟7 小时前
Vivado ILA与VIO调试工具实战指南:从入门到精通
fpga开发·fpga·硬件开发
FPGA小迷弟13 小时前
FPGA状态机设计详解:从Moore到Mealy的完整指南
fpga开发·fpga
zlinear数据采集卡1 天前
D223的PWM电机控制:6路独立脉冲+加减速算法深度解析
arm开发·stm32·嵌入式硬件·算法·fpga开发·架构
zlinear数据采集卡1 天前
ZLinear产品线全景对比:D223 vs DABL7606 vs DABL-G511选型指南
arm开发·嵌入式硬件·算法·fpga开发·开源
何事误红尘1 天前
ZYNQ学习FPGA笔记(一):vivado安装、XC7Z045
fpga开发