😊Forward
发布日期

Q15 定点数格式完全指南(附 DIFR 波束系数 0x7fff0000 实例解析)

作者

M先生

Q15 定点数格式完全指南(附 0x7fff0000 实例解析)

起因:调试雷达 DIFR 波束系数初始化代码时,遇到 m_DifrBeamData[j][k] = 0x7fff0000; 这样的赋值,注释写着"零衰减零相位"。为什么是这个值?它和 Q15 有什么关系?本文从零开始把 Q15 讲透,最后逐位拆解这个例子。

一、为什么需要定点数?

1.1 浮点数的痛点

计算机里表示小数有两种方式:

  • 浮点数(float/double):像科学计数法,动态范围大、精度高,但运算慢、硬件贵
  • 定点数(fixed-point):用整数"假装"小数,运算快、硬件简单

雷达、通信、音频等实时信号处理系统,每秒要处理几百万次乘加运算。如果每个数都用浮点,芯片面积和功耗都扛不住。所以大量 DSP 芯片、FPGA 里的信号处理都用定点数——这就是 Q15 存在的根本原因。

1.2 一个直觉类比

想象一个"小数点位置固定"的刻度尺:

  • 浮点数:小数点可以前后移动(3.14、0.0314、314)
  • 定点数:小数点位置焊死在某个地方(比如 3.14 永远是 3.14,分辨率永远是 0.01)

Q15 就是:把小数点固定在"16 位有符号整数的最低位之前",也就是整数部分只有符号位(1 位),小数部分 15 位

二、Q 格式表示法

Q 格式的通用写法是 Qm.n

  • m:整数位数(含符号位)
  • n:小数位数
  • 总位数 = m + n

常见格式:

格式总位数整数位小数位范围
Q1.15(Q15)161(符号)15-1 ~ +0.99997
Q1.31(Q31)321(符号)31-1 ~ +0.99999...
Q8.81688-128 ~ +127.996
Q16.16321616-32768 ~ +32767.99

Q15 的含义:16 位有符号定点数,1 位符号 + 15 位小数,能表示的范围是 [1,1)[-1, 1)

注意:有些资料把 Q15 写作 Q.15 或 Q15,含义相同。还有无符号版本(如 UQ15),但 DSP 里绝大多数是带符号的 Q15。

三、Q15 的编码与解码

3.1 核心公式

编码(实数 → 整数):

xint=round(x×215)=round(x×32768)x_{int} = \text{round}(x \times 2^{15}) = \text{round}(x \times 32768)

解码(整数 → 实数):

x=xint215=xint32768x = \frac{x_{int}}{2^{15}} = \frac{x_{int}}{32768}

3.2 最小分辨率(LSB)

Q15 能分辨的最小量:

LSB=215=1327683.052×105\text{LSB} = 2^{-15} = \frac{1}{32768} \approx 3.052 \times 10^{-5}

也就是说,Q15 的精度约为小数点后 4~5 位,动态范围约 96dB(与 16 位 ADC 的理论动态范围一致)。

3.3 常见值对照表

十六进制十进制整数对应实数含义
0x7FFF327670.99996948...最大正数 ≈ +1.0
0x7FFF0000(高16位)327670.99996948...增益 = 1.0(0dB)
0x4000163840.50.5
0x200081920.250.25
0x000110.0000305最小正数(1 LSB)
0x0000000
0xFFFF-1-0.0000305最小负数
0x8000-32768-1.0最小负数 = -1.0

关键记忆:0x7FFF ≈ +1.0(最大正),0x8000 = -1.0(最小负),0x0000 = 0。这就是"0x7fff 表示零衰减"的由来——增益系数取满刻度 1.0,不做衰减。

四、逐位拆解 0x7fff0000(本文核心实例)

回到您的代码:

// 设置DIFR波束系数和校正系数为零衰减零相位
memset(m_DifrCalibData, 0, sizeof(m_DifrCalibData));
for (j=0; j<MAXFREQDIVBEAM; j++)
{
    for(k=0; k<g_ValidChannelNum; k++)
    {
        m_DifrBeamData[j][k] = 0x7fff0000;
        m_DifrCalibData[j][k] = 0x7fff0000;
    }
}

4.1 32 位拆成两个 16 位字段

0x7fff0000 是 32 位整数,按高 16 位 / 低 16 位拆开:

二进制:0111 1111 1111 1111  0000 0000 0000 0000
        └──── 高16位 ────┘  └──── 低16位 ────┘
            0x7FFF               0x0000
            幅度/增益字段          相位字段
字段解码含义
高 16 位(增益)0x7FFF = 32767Q15 格式 → 32767/32768 ≈ 1.0,即增益 = 1,零衰减(0dB)
低 16 位(相位)0x0000 = 0零相位偏移(0°)

所以 0x7fff0000 = "幅度系数 1.0(不衰减)+ 相位 0(不移相)",正好对应注释"零衰减零相位"。

4.2 为什么这样打包?

在相控阵/波束形成系统中,每个通道的复加权系数(幅度 + 相位)需要存成一个数。常见的打包方式:

  1. 高 16 位存 Q15 幅度:0x7FFF = 1.0 满增益,0x4000 = 0.5(-6dB),0x0000 = 0(关断)
  2. 低 16 位存相位:通常相位也用 16 位无符号表示,0x0000=0°0x0000 = 0°0xFFFF0xFFFF 对应接近 360°360°(或 180°+180°-180° \sim +180°

这种"幅度高字、相位低字"的 32 位打包格式,在雷达波控、数字波束形成(DBF)、DIFR(数字中频接收)通道校正中是常见约定。

DIFR 说明:DIFR 通常是"Digital IF Receiver"(数字中频接收机)的缩写,负责多通道信号的数字下变频、滤波、幅相校正。波束系数(BeamData)和校正系数(CalibData)就是各通道的复加权系数。

4.3 为什么是 0x7fff 而不是 0x7fff + 符号扩展

注意 0x7fff0000 作为有符号 32 位整数看是正数(最高位 0),但如果把高 16 位 0x7fff 单独当作有符号 16 位整数看,也是最大正数 32767,语义一致。若想表示"增益 1.0"用 0x7fff 而不用 0x8000,是因为:

  • 0x7FFF = +32767 ≈ +1.0(Q15 满刻度正)
  • 0x8000 = -32768 = -1.0(符号位为 1,是负数)

增益不能为负,所以"零衰减"必须用正满刻度 0x7FFF,不能用 0x8000

4.4 memset 与循环赋值的语义差异(工程细节)

代码里先 memset 清零整个数组,再用循环把有效槽位赋成 0x7fff0000。这意味着:

  • 有效通道([j][k]MAXFREQDIVBEAM × g_ValidChannelNum 范围内):幅度 1.0、相位 0(直通)
  • 超出有效范围的槽位:保持 0(幅度 0 = 通道关闭)

两者语义不同:0 表示通道禁用/无效,0x7fff0000 表示有效通道零衰减直通。如果实际需求是"全部通道都直通",要确认循环范围覆盖了全部数组;如果数组更大而只初始化有效部分,超出部分会因 memset 而被禁用——这可能是有意设计(无效通道关闭),也可能是个隐患,值得确认。

五、Q15 的四则运算

5.1 加法/减法

直接整数加减,注意溢出

int16_t a = 0x7FFF;   // 0.99997
int16_t b = 0x0001;   // 0.00003
int32_t sum = a + b;  // 0x8000 → 溢出成了 -1.0!

0x7FFF + 0x0001 = 0x8000 = -1.0,正溢出变成最大负数。所以定点加法必须饱和(saturate):结果钳制到 [1,1)[-1, 1)

int16_t sat_add(int16_t a, int16_t b) {
    int32_t s = (int32_t)a + b;
    if (s > 32767)  return 32767;   // 饱和到 +1.0
    if (s < -32768) return -32768;  // 饱和到 -1.0
    return (int16_t)s;
}

5.2 乘法(重点!)

两个 Q15 数相乘,乘积是 Q30(2×15 位小数),需要 32 位变量承接,然后右移 15 位回到 Q15:

x×y=xintyint215215×215=xintyint215x \times y = \frac{x_{int} \cdot y_{int}}{2^{15} \cdot 2^{15}} \times 2^{15} = \frac{x_{int} \cdot y_{int}}{2^{15}}
int16_t q15_mul(int16_t a, int16_t b) {
    int32_t prod = ((int32_t)a * b) >> 15;  // Q30 → Q15
    return (int16_t)prod;
}

特殊情形(1.0)×(1.0)(-1.0) \times (-1.0)0x8000 * 0x8000 = 0x40000000,右移 15 位得 0x8000(-1.0)——结果是正确的(-1×-1=1,但 Q15 里 +1.0 是 0x7FFF,0x8000 是 -1.0,所以这个特例严格说结果是 -1.0,标准做法是钳制到 0x7FFF)。

5.3 除法

Q15 除法先移位再除:

int16_t q15_div(int16_t a, int16_t b) {
    int32_t q = ((int32_t)a << 15) / b;  // 分子先扩到 Q30
    return (int16_t)q;
}

5.4 乘加(MAC)—— DSP 的核心

滤波、FFT、波束形成都是大量 MAC:

y=i=1Naixiy = \sum_{i=1}^{N} a_i \cdot x_i

每步乘加用 32 位(甚至 40 位)累加器,最后再一次性缩回 Q15,避免中间溢出:

int32_t acc = 0;
for (int i = 0; i < N; i++) {
    acc += (int32_t)a[i] * x[i];   // 32位累加
}
int16_t y = (int16_t)(acc >> 15);  // 最后缩放

六、为什么波束系数用 Q15 而不是浮点?

结合您的 DIFR 例子,雷达波束形成/通道校正系数用 Q15 有实际原因:

  1. 乘法效率:FPGA/DSP 的硬件乘法器对 16 位×16 位最友好(一个周期),32 位浮点乘法代价高
  2. 存储带宽:系数表可能很大(频率×通道×波束),16 位比 32 位浮点省一半存储和带宽
  3. 与 ADC/DAC 对齐:16 位 ADC 输出、16 位 DAC 输入都是 Q15 格式,全链路定点避免反复转换
  4. 精度足够:Q15 分辨率 3×1053\times10^{-5}(约 96dB 动态范围),对增益 0.01dB 量级的通道校正足够

代价:动态范围只有 96dB,且每级运算都会累积量化误差——所以 DSP 里常用"块浮点"(block floating point)或 Q31/Q40 累加来缓解。

七、工程注意事项清单

注意点说明
溢出饱和加减法结果超出 [-1,1) 必须饱和,否则符号翻转
乘法右移Q15×Q15 用 32 位积再 >>15,别丢符号位
舍入方式右移前可加 0x4000(0.5 LSB)做四舍五入,减少截断误差
初始化约定0x7FFF=直通(0dB)、0x8000=-1、0x0000=关断,需在代码注释中明确
有效范围数组初始化要确认 memset 清零与循环赋值的覆盖范围一致
系数量化浮点系数转 Q15 用 round(x*32768) 并检查 |x|<1
级联误差多级滤波/多级校正的量化误差会累积,必要时用更高精度中间变量

八、Q15 速查卡

Q15 = 16位有符号定点数 = 1位符号 + 15位小数
范围:      -1.0 ~ +0.99996948
LSB:       2^-153.05e-5
编码:      x_int = round(x × 32768)
解码:      x = x_int / 32768
0x7FFF:    +0.99997  (+1.0, 满增益/零衰减)
0x4000:    +0.5      (-6dB)
0x0000:    0         (关断)
0x8000:    -1.0      (负满刻度)
加法:      饱和
乘法:      32位积 >> 15
MAC:       32位累加, 最后缩放

九、一句话总结

Q15 就是把"小数"用 16 位整数表示:乘以 32768 存进去,除以 32768 取出来。0x7fff0000 不过是一个 32 位"快递包裹"——高 16 位装着 Q15 格式的幅度 1.0(0x7FFF = 零衰减),低 16 位装着相位 0(0x0000 = 零相移),合起来就是雷达通道的"零衰减零相位直通系数"。


本文为技术学习笔记,实例代码来自实际工程中的 DIFR 波束系数初始化片段。

Q15 定点数格式完全指南(附 DIFR 波束系数 0x7fff0000 实例解析)

评论加载中…