FPGA 定点数、小数与负数的加减乘除原理
面向 Verilog/SystemVerilog、Vivado 和 FPGA DSP 硬件实现的定点数入门与工程实践文档。
重点讨论:二进制小数、补码负数、加法、减法、乘法、除法、位宽、溢出、舍入、饱和以及 DSP48 推断。
1. 为什么 FPGA 中通常使用定点数
FPGA 中表示实数有两种主要方式:
- 浮点数
- 定点数
浮点数能够表示很大的动态范围,但硬件代价较高,通常需要浮点加法器、乘法器、规格化和舍入逻辑。
定点数把小数点的位置固定下来。例如:
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.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;
这里 a 和 b 被声明为无符号数。即使 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]);
更安全的工程方法是:
- 先扩展到
W+1位 - 再根据最高两位判断是否溢出
- 根据需要进行饱和处理
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 降低
- 功耗增加
- 时序难以收敛
常见替代方案:
- 除以 2 的幂:使用算术右移
- 常数除法:乘以倒数,再右移
- 变量除法:使用迭代除法器
- 高吞吐场景:使用流水线除法器
- 非线性计算:使用 LUT、CORDIC 或 Newton-Raphson
- 低速控制路径:允许多周期计算
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 硬件。