Q 表示法#

在开始讨论 q_multiply_shift() 之前,先介绍下 Q 格式计数法 表示定点数。

Q 表示法是一种指定二进制定点数格式参数的方法。例如,在 Q 表示法中,由 Q8.8 表示的数字格式意味着此格式中的定点数具有 8 位整数部分和 8 位小数部分。

Q 表示法的定义#

Texas Instruments 版本

Texas Instruments 定义的 Q 表示法,由字母 Q 后跟一对数字 m.n 组成,其中 m 表示用于值的整数部分的位数,n 表示小数位的位数。

默认情况下,该表示法描述了有符号二进制定点格式,未缩放的整数值以补码格式存储,用于大多数二进制处理器中。第一位总是给出值的符号(1 = 负数,0 = 非负数),并且不计入 m 参数。因此,使用的总位数为 1 + m + n

例如,Q3.12 表示总位数为 16 位的有符号二进制定点数,包括符号位、三位整数部分和 12 位小数部分。也就是说,隐式乘以缩放因子 \(2^{-12}\) 的 16 位有符号(补码)整数。

特别是,当 n 为零时,数字只是整数。如果 m 为零,除符号位外的所有位都是小数位;则存储数字的范围是从 \([-1.0, +1.0)\)

m 和小数点可以省略,在这种情况下,它们会根据存储值的变量或寄存器的大小的推断得出。因此,Q12 表示具有任意位数的有符号整数,隐式地乘以 \(2^{-12}\)

字母 U 可以添加到 Q 前面,表示无符号二进制定点格式。例如,UQ1.15 描述为隐含缩放因子为 \(2^{-15}\) 的无符号 16 位整数,其范围 \([0.0, \cfrac{2^{16}-1}{2^{15}})\)

ARM 版本

ARM 使用的 Q 表示法的变体中,m 数包括符号位。例如,16 位有符号整数在 TI 变体中表示为 Q15.0,但在 ARM 变体中表示为 Q16.0

Qm.n 或者 UQm.n 格式的分辨率(连续值之间的差)总是 \(2^{-n}\)。可表示值的范围取决于所使用的符号:

Notation

Texas Instruments Notation

ARM Notation

Signed Qm.n

\([-2^m, 2^m - 2^{-n}]\)

\([-2^{m-1}, 2^{m-1} - 2^{-n}]\)

Unsigned UQm.n

\([0, 2^m - 2^{-n}]\)

\([0, 2^m - 2^{-n}]\)

\[ \sum_{k=0}^{m-1} 2^k + \sum_{k=1}^{n} 2^{-k} = (2^m - 1) + (1 - 2^{-n}) = 2^m - 2^{-n} \]

例如,Q15.1 格式需要 15+1 = 16 位,分辨率为 \(2^{−1} = 0.5\),并且可表示的值范围从 \(−2^{14} = −16384.0\)\(+2^{14} − 2^{−1} = +16383.5\)。在十六进制中,负值范围从 0x80000xFFFF,后面是非负值从 0x00000x7FFF

Q 表示法的数学运算#

Q 表示法是两个整数的比率:分子保存在存储器中,分母 \(d\) 等于 \(2^n\)

比如,在 Q8 中,分母是 \(2^8 = 256\)。对于 \(1.5\) 等于 \(384/256\),这是将 \(1.5\) 转换为 Q8 数。在这种情况下,可以将 \(1.5\) 视为 \(15/10\),然后将其转换 为Q8 形式。首先,需要找到一个数,使得当将其除以 10 时,结果的分母是 \(2\)\(8\) 次方。这个数就是 \(15 * 2^8 = 15 * 256 = 3840\)。所以,\(15/10 = 3840/1000\)。然后,我们再将结果除以 \(2\),得到 \(384/1000\)。这就是 \(384/256\)Q8 表示形式。

如果要保持 Q 数的基数(\(n\)保持不变),则 Q 数数学运算必须保持分母 \(d\) 不变。以下公式显示了对一般 Q 数 \(N1\)\(N2\) 的数学运算。(如果我们考虑上述示例,则 \(N1\)\(384\)\(d\)\(256\)。)

\[\begin{split} \cfrac{N1}{d} + \cfrac{N2}{d} = \cfrac{N1+N2}{d} \\ \cfrac{N1}{d} - \cfrac{N2}{d} = \cfrac{N1-N2}{d} \\ (\cfrac{N1}{d} \times \cfrac{N2}{d}) \times d = \cfrac{N1 \times N2}{d} \\ (\cfrac{N1}{d} / \cfrac{N2}{d}) / d = \cfrac{N1/N2}{d} \end{split}\]

由于分母是 \(2\) 的幂,乘法可以作为算术左移实现,除法可以作为算术右移实现;在许多处理器上,移位比乘法和除法更快。

为了保持准确性,中间的乘法和除法结果必须是双精度的,并且在转换回所需的 Q 数之前必须小心处理四舍五入中间结果。

两个不同 Q 格式基底的数字也可以相乘除,相乘除的数字也可以用另一个基底表示。以下是二个 Q 格式数字 \(N1\)(分母\(d_1\)) 和 \(N2\)(分母\(d_2\)) 的运算,运算结果的分母是 \(d_{3}\)

\[\begin{split} (\cfrac{N1}{d_1} \times \cfrac{N2}{d_2}) \times \cfrac{d_1 d_2}{d_3} = \cfrac{N1 \times N2}{d_3} \\ (\cfrac{N1}{d_1} / \cfrac{N2}{d_2}) / \cfrac{d_1 d_2}{d_3} = \cfrac{N1 / N2}{d_3} \end{split}\]

若用 C 语言,相同 Q 格式基底数字四则运算对应的程式如下(以下的 Q 是表示小数部分的位元数):

Q 数加法#

int16_t q_add(int16_t a, int16_t b)
{
    return a + b;
}

有饱和:

int16_t q_add_sat(int16_t a, int16_t b)
{
    int16_t result;
    int32_t tmp;

    tmp = (int32_t)a + (int32_t)b;
    if (tmp > 0x7FFF)
        tmp = 0x7FFF;
    if (tmp < -1 * 0x8000)
        tmp = -1 * 0x8000;
    result = (int16_t)tmp;

    return result;
}

浮点数有 ±Inf,但 Q 格式没有,若不进行饱和处理,二个很大的正数相加,可能会变成很大的负数。若是用组合语言,可以用 Signed Overflow 旗标来避免 C 语言实现时需要的型态转换。

Q 数减法#

int16_t q_sub(int16_t a, int16_t b)
{
    return a - b;
}

Q 数乘法#

// precomputed value:
int Q = 8;
int K = 1 << (Q - 1);
// saturate to range of int16_t
int16_t sat16(int32_t x)
{
	if (x > 0x7FFF) return 0x7FFF;
	else if (x < -0x8000) return -0x8000;
	else return (int16_t)x;
}
int16_t q_mul(int16_t a, int16_t b)
{
    int16_t result;
    int32_t temp;

    temp = (int32_t)a * (int32_t)b; // result type is operand's type
    // Rounding; mid values are rounded up
    temp += K;
    // Correct by dividing by base and saturate result
    result = sat16(temp >> Q);

    return result;
}

Q 数除法#

int16_t q_div(int16_t a, int16_t b)
{
    /* pre-multiply by the base (Upscale to Q16 so that the result will be in Q8 format) */
    int32_t temp = (int32_t)a << Q;
    /* Rounding: mid values are rounded up (down for negative values). */
    /* OR compare most significant bits i.e. if (((temp >> 31) & 1) == ((b >> 15) & 1)) */
    if ((temp >= 0 && b >= 0) || (temp < 0 && b < 0)) {
        temp += b / 2;    /* OR shift 1 bit i.e. temp += (b >> 1); */
    } else {
        temp -= b / 2;    /* OR shift 1 bit i.e. temp -= (b >> 1); */
    }
    return (int16_t)(temp / b);
}
int16_t q_max = 0x7fff;
int16_t q_min = 0x8000;

float f_max = 0;
float f_min = 0;
for (int8_t i=15; i>=0; i--) {
    f_max = (float)q_max /pow(2, i);
    f_min = (float)q_min /pow(2, i);
    printf("\t| Q%d | Q%d.%d %f | %f |\r\n",
           i, (15-i), i, f_max, f_min);
}
| Q15 | Q0.15 0.999969 | -1.000000 |
	| Q14 | Q1.14 1.999939 | -2.000000 |
	| Q13 | Q2.13 3.999878 | -4.000000 |
	| Q12 | Q3.12 7.999756 | -8.000000 |
	| Q11 | Q4.11 15.999512 | -16.000000 |
	| Q10 | Q5.10 31.999023 | -32.000000 |
	| Q9 | Q6.9 63.998047 | -64.000000 |
	| Q8 | Q7.8 127.996094 | -128.000000 |
	| Q7 | Q8.7 255.992188 | -256.000000 |
	| Q6 | Q9.6 511.984375 | -512.000000 |
	| Q5 | Q10.5 1023.968750 | -1024.000000 |
	| Q4 | Q11.4 2047.937500 | -2048.000000 |
	| Q3 | Q12.3 4095.875000 | -4096.000000 |
	| Q2 | Q13.2 8191.750000 | -8192.000000 |
	| Q1 | Q14.1 16383.500000 | -16384.000000 |
	| Q0 | Q15.0 32767.000000 | -32768.000000 |
// 《Fast Inverse Square Root》
float Q_rsqrt( float number )
{
 long i;
 float x2, y;
 const float threehalfs = 1.5F;

 x2 = number * 0.5F;
 y   = number;
 i   = * ( long * ) &y;   // evil floating point bit level hacking
 i   = 0x5f3759df - ( i >> 1 ); // what the fuck?
 y   = * ( float * ) &i;
 y   = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
 // y   = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed

 #ifndef Q3_VM
 #ifdef __linux__
   assert( !isnan(y) ); // bk010122 - FPE?
 #endif
 #endif
 return y;
}