Q格式

Q格式是二进制的定点数格式，其中会标示小数位元（也可能包括整数位元）的长度。例如Q15数表示分数部分有15个位元，而Q1.14数表示1个整数位元以及14个小数位元。

针对有号的定点数，有二种Q格式的表示方式。其中一种是将符号位元算在整数位元里，但另外一种就不是。例如，16位元的有号整数（无小数位元）可以表示为Q16.0或Q15.0。因此，在用说明Q格式的有号定点数字时，可能也需要一并标示总位元数（在此处为16）。

Q格式是几种广为使用的定点数中的一种。定点数常用在不支援浮点运算器的硬件,或是用在需要固定分辨率的应用。

## 特点

Q格式用来标示定点数，储存以及运算都是以一般的二进制整数为基础来处理，因此允许标准的整数硬件／算术逻辑单元来进行有理数运算。程式设计者可以依应用需求，选定其整数位元数、小数位元数（以及资料总位元数），而之后定点数的范围以及分辨率就会依整数位元数、小数位元数而不同。

有些DSP架构原生支持一些常用的Q格式（例如Q1.15），这种情形下，可以在一个指令下就进行四则运算，若是加减法，可以支援饱和运算，若是乘除法，可以有正规化的机能。不过大部分的DSP无此功能，若DSP架构不支持选定的Q格式，程式设计者就要自行再针对加减法进行饱和运算，针对乘除法进行正规化。

有号号Q格式的表示方式有两种，都写成Qm.n：

- Q表示这个数字是以Q格式表示，在德州仪器则用Q表示是有号的Q格式（Q是数学里表示有理数的字母）
- m.（可省略，若省略的话，假设是0或1）是数字在用二补数表示下的整数位元数，可能包括符位元，也可能不包括（这也是若m省略的话，假设是0或1的原因）。
- n是数字中小数的位元数，也就是二进制表示时，在小数点右边的二进制个数（若n = 0，表示Q格式数字是整数）。

一种表示方式将符号位元放在整数位元 m中一起计算，另一种则不是。确认方式可以将m和n相加，看是否等于数字位元数，若相等，表示其符号位元放在整数位元 m，若比数字位元数少1，表示其其符号位元独立计算。

此外，字母U可以放在Q前面，说明是无号数，例如UQ1.15，数值范围是0.0 to +1.999969482421875 （也就是$1 + \frac{2^{15}-1}{2^{15}}$）。

有号的Q格式数值会用二补数储存，正如大部分处理器处理整数的情形一样。在二补数中，会用符号位元填满没有用到数字位元。

针对给定的Qm.n格式（假设符号位元也算在m个位元内），用m+n位元的有号整数来处理，再考虑其中有n位元是表示小数：

- 其范围为$[ - (2^{m-1}) , 2^{m-1} -2^{-n}]$
- 其分辨率为$2^{-n}$

针对给定的UQm.n格式，用m+n位元的有号整数来处理，再考虑其中有n位元是表示小数：

- 其范围为$[ 0 , 2^m -2^{-n}]$
- 其分辨率为$2^{-n}$

例如Q15.1格式的数字：

- 需要位元数为15+1 = 16位元
- 其范围为-214, 214 - 2−1 = -16384.0, +16383.5 = 0x8000, 0x8001 … 0xFFFF, 0x0000, 0x0001 … 0x7FFE, 0x7FFF
- 其分辨率为2−1 = 0.5

Q格式和浮点数不同，Q格式数字的分辨率不随数字大小而不冋。

## 转换

### 浮点数到Q格式

若要将浮点数（例如IEEE 754）转换为Qm.n的格式：

- 将浮点数乘以2n
- 四舍五入到最接近的整数

### Q格式到浮点数

若要将Qm.nQ格式的数数转换为浮点数：

- 将整数转换为浮点数
- 乘以2−n

## 数学运算

Q格式的数字是二个整数的比例：会储存分子，而分母固定是2n。

考虑以下的例子。

- Q8格式的分母是28 = 256
- 1.5等于384/256
- 会储存384，256不储存（因为是Q8格式，分母固定是256）

若Q格式的基底固定（n都相同），则Q格式的数学运算需维持分母不变。以下是二个相同Q格式数字$N_1$和$N_2$的运算。

$$\begin{align} \frac{N_1}{d} + \frac{N_2}{d} &= \frac{N_1+N_2}{d}\\ \frac{N_1}{d} - \frac{N_2}{d} &= \frac{N_1-N_2}{d}\\ \left(\frac{N_1}{d} \times \frac{N_2}{d}\right) \times d &= \frac{N_1\times N_2}{d}\\ \left(\frac{N_1}{d} / \frac{N_2}{d}\right)/d &= \frac{N_1/N_2}{d} \end{align}$$

因为分母是二的次幂，因此和二的次幂相乘可以表示为二进制下的左移，和二的次幂相除可以表示为二进制下的右移，很多处理器的左移和右移会比乘除法要快。

为了要维持乘法和除法的精度，中间值需要用双精度来储存，在转换回需要的Q格式数字之间，也需要先注意数值修约的处理。

两个不同Q格式基底的数字也可以相乘除，相乘除的数字也可以用另一个基底表示。以下是二个Q格式数字$N_1$（分母$d_1$）和$N_2$（分母$d_2$）的运算，运算结果的分母是$d_3$。

$$\begin{align} \left(\frac{N_1}{d_1} \times \frac{N_2}{d_2}\right) \times \frac{d_1 d_2}{d_3} &= \frac{N_1\times N_2}{d_3}\\ \left(\frac{N_1}{d_1} / \frac{N_2}{d_2}\right) \times \frac{d_1}{d_2 d_3} &= \frac{N_1/N_2}{d_3} \end{align}$$

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

### 加法

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

有饱和

```c
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语言实现时需要的型态转换。

### 减法

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

### 乘法

```c
// precomputed value:
#define Q   16
#define 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;
}
```

### 除法

```c
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);
}
```

## 延伸阅读

- (Note: the accuracy of the article is in dispute; see discussion.)