FPGA 定点数缩位:截断、舍入与饱和,避免溢出回绕

FPGA 定点数缩位:截断、舍入与饱和,避免溢出回绕

乘加结果接到较窄的输出端口时,截掉几位看起来很直接。真正需要先确定的是:丢弃小数位时怎样取近似值,整数范围不足时怎样处理溢出。这两件事分别影响量化误差和大幅度信号行为,不能用一句"做个截断"代替。本篇先建立整数参考模型,用穷举把负数、中点和进位边界检查清楚,再用于后续 RTL 对照。

1. 把格式写完整

只写 Qm.n 容易产生歧义:不同资料对 m 是否包含符号位的约定可能不同。这里统一用三项定义:总位宽 W、小数位数 F、有符号补码。存储整数为 R,真实值为 R×2^(-F),范围是 -2^(W-1-F) 到 2(W-1-F)-2(-F)。步进为 2^(-F)。补码负端比正端多一个量化台阶。精度与范围说明

例如 W=8、F=4 表示 -8 到 7.9375。改为 W=4、F=2 后,范围变成 -2 到 1.75,步进由 1/16 增至 1/4。这里既少了小数精度,也缩小了可表示范围;仅仅右移不能解决第二个问题。

下面限定 F_in≥F_out,令 d=F_in-F_out,目标存储整数的理想值为 R/2^d。输入 R 必须是已解码的有符号整数,不是尚未解释符号的十六进制码字。

2. 负数的截断不等于向零

令 D=2^d,做欧几里得除法 R=qD+r,其中 q 向负无穷取整,0≤r<D。Python 的 divmod 对正除数正好符合此约定。

以 R=-7、d=2 为例,理想输出整数是 -1.75。直接丢弃补码低位得到 -2,即 floor;向零取整得到 -1。两者在正数上可能一致,在负数上却差一个输出 LSB。MathWorks 也明确区分补码低位截断与向零取整。舍入模式说明

因此 floor 直接取 q;向零模式仅在 R<0 且 r≠0 时取 q+1。别把"truncation"这个词当作跨语言、跨库完全一致的接口约定,应写明数学方向。

3. 最近偶数舍入怎样处理中点

最近舍入还需要规定正好半格时选哪边。本例采用 ties-to-even:距离相等时选偶数输出整数。例如 -2.5→-2、-1.5→-2、-0.5→0,正数 0.5→0、1.5→2。

基于 q、r,只需判断:2r>D 时加一;2r=D 且 q 为奇数时也加一,其余保留 q。这对负数同样成立,不需要给负数另加一个"负半格偏置"。当 d=0 时余数为零,算法自然保持输入,不应构造 2^(d-1) 这样的特殊常量。

偶数舍入避免把所有中点固定推向同一方向,但不能保证任意信号序列的平均误差都是零。量化误差还依赖数据分布;若后续发生饱和,误差也不再受半个 LSB 限制。

4. 饱和必须赶在窄化之前

输出 W 位有符号整数的边界为 L=-2(W-1)、H=2(W-1)-1。先在足够宽的域中得到舍入结果 Q,再计算 min(H,max(L,Q)),最后编码为 W 位。

取 W=4、d=2、R=30,理想目标整数是 7.5,偶数舍入产生 8。虽然 floor 结果 7 能放进去,舍入进位却越过正端。饱和输出应为 7;若先只保留四位,8 会被解释成 -8,此后再检查范围已经无法发现原来的正溢出。

负端也要独立覆盖:R=-36 对应 -9,饱和为 -8,回绕则为 7。饱和是夹到端点,回绕是模运算;两种策略都可以被明确选择,但不能混用。厂商工具同样将舍入模式与溢出动作分开配置。转换与溢出设置

整数模型利用 Python 的任意精度整数保留进位。移植到硬件时,中间位宽必须容纳舍入前后的值;不能让加法器先溢出,再期待比较器补救。

5. 怎样选规则并定位误差

不发生饱和时,若误差定义为"输出真实值减输入真实值",floor 的误差位于负一个输出 LSB 到零之间,且不包含负一个 LSB;向零取整的误差方向随输入符号改变;最近偶数舍入的绝对误差不超过半个输出 LSB。调试时可以先按这些界限检查正常幅度样本,再单独观察越界样本,避免把削顶误认为普通量化噪声。

不要在乘法输出、加法输出和最终接口上随意重复缩位。多次量化可能与宽域运算后的一次量化不同:例如目标整数值 1.375,先舍入到半整数网格得到 1.5,再舍入到整数得到 2;直接舍入到整数则得到 1。两步都用最近偶数,结果仍然不同。哪些节点需要保留保护位,应由数据范围与误差预算决定。

如果系统规定模运算,例如某些相位累加路径,回绕可以是期望行为;如果输出代表受限幅度,饱和更容易保持端点方向。最终选择应服从算法接口,不宜仅因某种写法少几行代码就更改数值规则。软件模型保留两种溢出动作,正是为了让这项选择能被单独测试。

6. 可直接运行的整数模型

保存以下代码为 demo.py,用 Python 3 执行。模型没有浮点除法,也不借助 Python 的 round,避免参考值自身引入另一套舍入规则。oracle 使用符号与绝对值独立计算,再与商余数算法对照。

python 复制代码
"""Integer-only fixed-point narrowing model; Python 3, no dependencies."""

def rounded(raw, drop, mode):
    if drop < 0:
        raise ValueError("drop must be nonnegative")
    den = 1 << drop
    q, r = divmod(raw, den)  # floor quotient; 0 <= r < den
    if mode == "floor":
        return q
    if mode == "zero":
        return q + int(raw < 0 and r != 0)
    if mode == "even":
        return q + int(2*r > den or (2*r == den and q % 2 != 0))
    raise ValueError("unknown rounding mode")


def narrow(raw, drop, width, mode="even", overflow="sat"):
    if width < 2:
        raise ValueError("width must be at least 2")
    q = rounded(raw, drop, mode)
    lo, hi = -(1 << (width-1)), (1 << (width-1))-1
    if overflow == "sat":
        return min(hi, max(lo, q))  # clamp BEFORE packing into width bits
    if overflow == "wrap":
        return (q-lo) % (1 << width) + lo
    raise ValueError("unknown overflow mode")


def oracle_round(raw, drop, mode):
    # Independent sign/magnitude formulation, never floating point.
    den = 1 << drop
    a, rem = divmod(abs(raw), den)
    if mode == "floor":
        return -a-int(rem != 0) if raw < 0 else a
    if mode == "zero":
        return -a if raw < 0 else a
    if rem*2 > den or (rem*2 == den and a % 2):
        a += 1
    return -a if raw < 0 else a


def self_test():
    checks = 0
    for win in range(3, 9):
        for wout in range(2, win):
            lo, hi = -(1 << (wout-1)), (1 << (wout-1))-1
            for fin in range(win):
                for fout in range(fin+1):
                    drop = fin-fout
                    for raw in range(-(1 << (win-1)), 1 << (win-1)):
                        for mode in ("floor", "zero", "even"):
                            q = oracle_round(raw, drop, mode)
                            assert rounded(raw, drop, mode) == q
                            sat = lo if q < lo else hi if q > hi else q
                            # Obtain wrapping independently from low-bit pattern.
                            bits = q & ((1 << wout)-1)
                            wrap = bits-(1 << wout) if bits & (1 << (wout-1)) else bits
                            assert narrow(raw, drop, wout, mode, "sat") == sat
                            assert narrow(raw, drop, wout, mode, "wrap") == wrap
                            assert raw >> drop == rounded(raw, drop, "floor")
                            checks += 1
    assert [rounded(x, 1, "even") for x in (-5, -3, -1, 1, 3, 5)] == [-2, -2, 0, 0, 2, 2]
    assert narrow(31, 2, 4) == 7 and narrow(31, 2, 4, overflow="wrap") == -8
    assert narrow(-36, 2, 4) == -8 and narrow(-36, 2, 4, overflow="wrap") == 7
    print(f"PASS: {checks} exhaustive rounding cases; saturation and wrap both checked")
    print("PASS: positive/negative ties, input extrema, zero-drop, rounding carry")
    print("raw floor zero even sat4 wrap4 (drop=2)")
    for raw in (-36, -32, -7, -6, -2, 2, 6, 30, 31):
        row = [raw] + [rounded(raw, 2, m) for m in ("floor", "zero", "even")]
        row += [narrow(raw, 2, 4, "even", o) for o in ("sat", "wrap")]
        print(*row)


if __name__ == "__main__":
    self_test()

7. 实际测试结果与使用边界

本次运行穷举输入位宽 3~8 位、输出位宽 2~W_in-1 位,F_in 从 0 到 W_in-1,F_out 从 0 到 F_in;每组遍历全部输入码值及三种舍入模式。每个计数项均检查舍入结果、饱和结果和回绕结果,并覆盖 d=0、正负端点及中点。不同格式可能对应相同 d,计数保留这些格式组合。

运行输出如下,表中全部是存储整数,sat4、wrap4 使用偶数舍入;若 F_out=2,还需除以 4 才是真实值。

text 复制代码
PASS: 241200 exhaustive rounding cases; saturation and wrap both checked
PASS: positive/negative ties, input extrema, zero-drop, rounding carry
raw floor zero even sat4 wrap4 (drop=2)
-36 -9 -9 -9 -8 7
-32 -8 -8 -8 -8 -8
-7 -2 -1 -2 -2 -2
-6 -2 -1 -2 -2 -2
-2 -1 0 0 0 0
2 0 0 0 0 0
6 1 1 2 2 2
30 7 7 8 7 -8
31 7 7 8 7 -8

这 241200 组是软件模型的穷举检查,不是 RTL 仿真或板级验证,也不说明资源、频率或吞吐率。落到 RTL 后,应将同一批向量送入设计,按实际流水延迟比对输出,并额外检查复位和有效信号。验收数值接口时,把 W、F、补码符号、舍入规则、溢出动作一起写入规格,才能让算法模型和硬件实现讨论同一个结果。

相关推荐
星夜离尘564 小时前
I²C总线卡死:先分清SCL拉低、SDA拉低,再谈9个时钟恢复
fpga开发·verilog·i2c
鸥梨菌Honevid7 小时前
STA 与 CDC
fpga开发
科恒盛远7 小时前
ZU902 与 FMC200 高速采集示波器实测
fpga开发
NamoAmitabha1288 小时前
日常问答1---deepseek
fpga开发
鸥梨菌Honevid9 小时前
异步FIFO为什么用打两拍的格雷码?
fpga开发
努力搬砖的小H10 小时前
FPGA应用技巧之AXI4-FULL-MASTER IP FDMA(Fast Direct Memory)详细介绍
经验分享·fpga开发·fdma·axi总线·米联客
szxinmai主板定制专家1 天前
电力边缘 AI 落地难点剖析:RK 系列 + FPGA 异构平台在储能监控终端的 EMC 与算力优化
人工智能·fpga开发·arm+fpga·rk3588+fpga
szxinmai主板定制专家1 天前
基于RK3588+FPGA+AI的储能电站边缘网关,实现电力数据采集与智能预警
arm开发·人工智能·fpga开发·rk3576·半导体设备