Ponto fixo Q15: conversão, arredondamento e overflow
O ponto fixo Q15 guarda uma fração como um inteiro com sinal de 16 bits comum, com uma escala binária implícita de 2^15 = 32768. Codificar é uma multiplicação mais um passo de arredondamento:
raw = round(value × 32768)
Decodificar é uma divisão:
value = raw ÷ 32768
É toda a aritmética envolvida. 0.5 × 32768 = 16384, então 0.5 mora na memória como o inteiro 16384, e 16384 ÷ 32768 devolve exatamente 0.5. Não existe campo de expoente nem bit implícito. O ponto binário é uma convenção combinada entre você e quem for ler a palavra; o hardware só enxerga um int16.
Duas consequências saem direto dessa fórmula, e é nelas que se perde uma tarde inteira.
0.1 × 32768 = 3276.8, que não é inteiro. O Q15 guarda 3277, e o valor que você lê de volta é 0.100006103515625, não 0.1.
1.0 × 32768 = 32768, uma unidade acima do maior inteiro com sinal de 16 bits. Ou seja, 1.0 simplesmente não tem codificação em Q15, e o que o seu código faz a respeito depende de uma política que a maioria dos projetos nunca coloca no papel. Se saturar, você fica com 0.999969482421875. Se der a volta (wraparound), fica com -1.0.
Essas duas falhas ocupam o resto do texto: ler a notação Q quando dois datasheets se contradizem, converter à mão nos dois sentidos e descobrir qual regra de arredondamento e de overflow o seu toolchain escolheu sem avisar.
Q15, Q1.15, Qm.n: são o mesmo layout?
Pergunte a três referências o que significa Q15 e você pode receber três respostas. O problema não é a sua leitura. A notação nunca foi de fato padronizada, e a divergência gira em torno de um único bit.
Como ler Qm.n
A forma com dois números é a honesta. Em Qm.n, n é a quantidade de bits fracionários e m é a quantidade de bits inteiros. Este artigo conta a posição de sinal dentro de m, que é a leitura pela qual Q16.16 é uma palavra de 32 bits: 16 bits inteiros incluindo o sinal, 16 bits fracionários, escala 2^16 = 65536. Codifique 1.5 em Q16.16 e você obtém 1.5 × 65536 = 98304, em hexadecimal 0x00018000, sem nenhum erro de arredondamento.
A confusão começa no bit de sinal. Alguns autores o contam dentro de m, outros o somam por fora. Q1.15 pela primeira convenção é uma palavra de 16 bits: uma posição de sinal/inteiro mais 15 bits de fração. Pela segunda convenção, o mesmo rótulo descreve 17 bits, largura que máquina nenhuma tem.
Por que o mesmo rótulo Q15 significa larguras diferentes em documentos diferentes
A forma com um número só, Q15, elimina o m e deixa a largura implícita. Na prática de DSP, quase sempre quer dizer uma palavra de 16 bits com sinal em complemento de dois e 15 bits fracionários, que é o sentido adotado pelos ecossistemas da TI e da ARM décadas atrás.
Mas você vai encontrar explicações muito copiadas dizendo algo como: Q15 significa 15 bits fracionários, logo, se definirmos um número de 32 bits, há 1 bit de sinal e 16 bits inteiros. Leia com atenção e você vai ver que ela conta o bit de sinal por fora de m, e não dentro — a outra convenção, diferente da que este texto usa. O layout que ela descreve é real, e normalmente se escreve Q16.15. Errado mesmo é o rótulo: uma palavra de 32 bits com 16 bits inteiros não é Q15 por convenção nenhuma. Esse parágrafo foi reproduzido em blogs suficientes para hoje aparecer acima da definição correta em algumas buscas.
Trate um Q15 solto num documento desconhecido como hipótese, não como fato. Confira contra algo mensurável: a largura do registrador no mapa de memória, o tipo C no header do driver ou um valor de amostra já conhecido.
A única descrição que sobrevive ao contato com o código dos outros
Anote três coisas e a ambiguidade desaparece:
- Sinal (signedness) — com sinal em complemento de dois, ou sem sinal
- W — total de bits da palavra
- F — bits fracionários
signed, W=16, F=15 não tem como ser lido errado. unsigned, W=32, F=16 também não. Todo o resto é derivado: a escala é 2^F, a resolução é 2^-F, e a faixa é a faixa inteira da palavra dividida por 2^F. Coloque esses três valores no documento do protocolo e no comentário da struct e você nunca mais vai ter essa discussão.
O conversor de formato Q online mostra o sinal, W, F e a escala ao lado de cada resultado justamente por isso. Quando o datasheet diz “Q15” e o parser de um colega diz outra coisa, decodificar uma palavra conhecida resolve a questão em uns dez segundos.
A fórmula de conversão, feita à mão
Os dois sentidos são curtos o bastante para fazer no papel, o que importa quando você está encarando um dump hexadecimal na tela do osciloscópio.
De float para ponto fixo: round(x × 2^F)
Leve 0.5 para Q15.
- Escala:
0.5 × 32768 = 16384 - Arredondamento: já é inteiro, então
16384fica como está - Verificação de faixa: 16 bits com sinal comportam de -32768 a 32767, e 16384 cabe
- Armazenamento:
16384, em hexadecimal0x4000, em binário0100000000000000
Só o passo 2 pode perder informação, e só o passo 3 pode falhar. Tudo que é interessante em ponto fixo acontece num desses dois lugares.
De ponto fixo para float: raw ÷ 2^F
Agora no sentido inverso, partindo de uma captura de registrador que mostra 0xC000 num campo Q15 com sinal.
- Interprete o texto como um código de 16 bits sem sinal:
0xC000 = 49152 - O formato tem sinal e o bit mais alto está ligado, então subtraia 2^16:
49152 - 65536 = -16384 - Reduza a escala:
-16384 ÷ 32768 = -0.5
O passo 2 é o que todo mundo pula. Sem a correção de complemento de dois, 0xC000 vira +1.5, que nem sequer está na faixa do Q15. Vale como alarme geral: se um valor decodificado cai fora da faixa do formato, você quase certamente esqueceu o passo do sinal.
from fractions import Fraction
def encode_q15(x):
"""Decimal value -> signed Q15 stored integer (ties to even)."""
return round(Fraction(str(x)) * 32768)
def decode_q15(word):
"""Unsigned 16-bit code -> exact Q15 value."""
if word & 0x8000:
word -= 0x10000
return Fraction(word, 32768)
print(encode_q15(0.5)) # 16384
print(encode_q15(0.1)) # 3277
print(float(decode_q15(0xC000))) # -0.5
print(float(decode_q15(0x0CCD))) # 0.100006103515625
Fraction está fazendo trabalho de verdade aqui. Escalar passando antes por um float reintroduziria o arredondamento binário exatamente no momento em que você tenta medi-lo, e Fraction(str(x)) lê o literal decimal que você digitou, não o double mais próximo dele.
Lendo e escrevendo a palavra hexadecimal
Hexadecimal é como esses valores realmente aparecem nas janelas de registradores, e a conversão é mecânica: 3277 em hexadecimal é 0xCCD, preenchido até W/4 dígitos como 0x0CCD. Sempre preencha. Uma palavra Q15 tem quatro dígitos hexadecimais e uma palavra Q31 tem oito; largar o zero à esquerda é o que faz um valor sair desalinhado num decodificador em lote.
Mantenha os códigos brutos sem sinal no hexadecimal também. 0x8000 é a palavra Q15 mais negativa, não +32768, e escrevê-la como -0x8000 não ajuda ninguém. Ordem de bytes é outra questão. O formato Q especifica escala numérica e não diz nada sobre endianness, então um dump little-endian de 0x0CCD chega como os bytes CD 0C. Para mudanças de base puras enquanto você lê um dump, o conversor de base numérica trata binário, octal e hexadecimal sem escala nem largura com sinal atreladas.
Q7, Q15, Q31 e Q16.16: faixa e resolução
Todo número desta tabela é -2^(W-1) dividido por 2^F numa ponta e (2^(W-1) - 1) dividido por 2^F na outra. Os valores são exatos, não arredondados para exibição.
| Formato | Largura | Bits fracionários F | Escala 2^F | Mínimo | Máximo (exato) | Resolução |
|---|---|---|---|---|---|---|
| Q7 | 8 | 7 | 128 | -1 | 127/128 = 0.9921875 | 1/128 = 0.0078125 |
| Q15 | 16 | 15 | 32768 | -1 | 32767/32768 = 0.999969482421875 | 1/32768 = 0.000030517578125 |
| Q31 | 32 | 31 | 2147483648 | -1 | (2^31−1)/2^31 = 0.9999999995343387126922607421875 | 1/2^31 ≈ 4.6566128730773926e-10 |
| Q16.16 | 32 | 16 | 65536 | -32768 | (2^31−1)/65536 = 32767.9999847412109375 | 1/65536 = 0.0000152587890625 |
Cole qualquer um desses valores de fronteira no conversor de formato Q e ele devolve o inteiro armazenado, a palavra hexadecimal e o decimal exato. Serve bem para conferir uma constante de firmware antes que ela vá para produção.
Por que o topo da faixa do formato Q15 é 0.999969482421875
A faixa do formato Q15 é assimétrica, e a assimetria vem do complemento de dois, não de algo específico do ponto fixo. Uma palavra de 16 bits com sinal cobre os inteiros de -32768 a 32767. Divida as duas pontas por 32768 e a faixa vira -32768/32768 a 32767/32768, ou seja, de -1 a 0.999969482421875.
Então falta exatamente um LSB para chegar a 1.0. O valor não tem codificação nenhuma em Q15, e descrever o máximo como “aproximadamente 1.0” ou “1.0 com arredondamento” só empurra o problema para frente. Um conversor que informa 1.0 para Q15 sem dizer que saturou está mentindo para você.
Por que -1 está incluído
Muitas referências escrevem a faixa do Q15 como -1 < X < 0.9999695, aberta dos dois lados. O limite inferior está errado. -32768 ÷ 32768 = -1 exatamente, portanto -1 é representável, e as duas pontas de [-1, 0.999969482421875] são valores alcançáveis. Você também vai ver a faixa escrita como [-1, 1), que diz a mesma coisa em relação à escala plena: 1.0 mesmo é inalcançável, mas o maior valor que se alcança é 0.999969482421875, não algo aquém disso.
Isso pesa mais do que uma implicância de notação. -1 é justamente o valor que quebra a multiplicação, porque -1 × -1 = 1 e 1 está fora da faixa. Quem parte do princípio de que -1 é inalcançável não escreve o ramo de saturação que pega esse caso.
O limite superior truncado 0.9999695 nessas mesmas referências é artefato de exibição. Não há nada de dízima no valor: ele é 32767/32768, e 32768 é potência de dois, então a expansão decimal termina depois de 15 dígitos em 0.999969482421875.
0.1 não cabe em Q15
Basta escalar e o problema aparece na hora: 0.1 × 32768 = 3276.8. Ponto fixo só guarda inteiros, então alguma coisa tem de ceder.
Com arredondamento para o mais próximo, a palavra armazenada é 3277, em hexadecimal 0x0CCD. O valor que essa palavra representa é:
3277 ÷ 32768 = 0.100006103515625
O erro de quantização é a diferença entre o que você pediu e o que a grade conseguiu entregar:
0.100006103515625 - 0.1 = +0.000006103515625
Isso dá cerca de seis milionésimos, ou um quinto de LSB. Inofensivo num controle de volume. Nada inofensivo num integrador que soma esse erro mil vezes por segundo, onde ele vira uma deriva constante de aproximadamente 0.006 por segundo no valor acumulado.
O erro do ponto fixo é uniforme; o do ponto flutuante não é
O Q15 estende uma grade de 65536 pontos separados por exatamente 1/32768 ao longo de [-1, 0.999969482421875]. O espaçamento perto de 0.9 é igual ao espaçamento perto de 0.0001, então o erro absoluto de pior caso é meio LSB em qualquer ponto. Isso deixa a análise de erro entediante no melhor sentido: dá para limitar o piso de ruído de um filtro com uma conta que um engenheiro júnior consegue conferir.
O IEEE 754 faz o contrário. Ele mantém um número fixo de bits significativos e move o expoente, então o intervalo absoluto entre doubles vizinhos cresce junto com a magnitude, enquanto o erro relativo fica quase constante. Perto de 1.0 esse intervalo é de cerca de 2.2e-16; perto de 1e12 já é 0.0001220703125.
Mesma falha, formato diferente. Um double também não guarda 0.1. Ele guarda 0.1000000000000000055511151231257827021181583404541015625, e é por isso que 0.1 + 0.2 devolve 0.30000000000000004. Esse caso está destrinchado no artigo companheiro sobre precisão de ponto flutuante. Ponto fixo não resolve frações decimais em binário; ele só torna o tamanho do erro previsível.
Três modos de arredondamento, um LSB de diferença
A regra de arredondamento faz parte do contrato de dados, não é detalhe de implementação. Duas implementações corretas que discordam no arredondamento produzem vetores de teste que diferem no último bit para sempre, e caçar esse tipo de coisa é um trabalho ingrato.
| Modo | Regra | 10911.744 | -3276.8 |
|---|---|---|---|
| Arredondar para o mais próximo, empate para o par | Ponto mais próximo da grade; metades exatas vão para o inteiro par | 10912 | -3277 |
| Truncar em direção a zero | Descarta a fração, a magnitude só encolhe | 10911 | -3276 |
| Piso em direção a menos infinito | Sempre para baixo na reta numérica | 10911 | -3277 |
As duas colunas mostram por que um exemplo só nunca basta. Para um valor positivo, truncamento e piso concordam. Para um valor negativo, eles se separam por um LSB inteiro, porque o truncamento puxa -3276.8 para cima, em direção a zero, e o piso empurra para baixo.
O exemplo resolvido que todo mundo erra
0.333 em Q15 é um bom caso de teste porque fica perto de uma fronteira. 0.333 × 32768 = 10911.744.
- Truncamento:
10911, que é lido de volta como0.332977294921875 - Arredondamento para o mais próximo:
10912, que é lido de volta como0.3330078125
Tutoriais antigos imprimem 10911 e seguem em frente sem dizer qual regra gerou aquilo. Leve esse número para um projeto cujo codificador arredonda e os seus vetores de referência (golden vectors) falham já na primeira execução, com uma diferença de uma unidade que parece erro de digitação e não divergência de política.
É com negativos que as três regras se separam. -0.1 × 32768 = -3276.8 dá -3276 com truncamento (valor -0.0999755859375) e -3277 com piso ou com o mais próximo (valor -0.100006103515625). O truncamento e o arredondamento para o mais próximo tratam os dois sinais do mesmo jeito — ±3276 e ±3277, respectivamente —, então um par simétrico de coeficientes continua simétrico. O piso não: ele manda +0.1 para 3276 e -0.1 para -3277, e o par sai desequilibrado por uma unidade da grade.
O que a sua linguagem faz por padrão
Nenhum desses padrões está errado. Eles só são diferentes, e nenhum se anuncia.
- C/C++: um cast de ponto flutuante para um tipo inteiro trunca em direção a zero.
(int16_t)(0.333f * 32768)dá10911. - Python: a função embutida
round()usa empate para o par, entãoround(3276.8)é3277eround(2.5)é2. - JavaScript:
Math.rounddesempata em direção a mais infinito, o que não é simétrico.Math.round(2.5)é3, masMath.round(-2.5)é-2. - Hardware: muitos caminhos de multiplicação e acumulação de DSP arredondam no deslocamento e oferecem o modo par-mais-próximo como bit de configuração, então o modelo C de referência e o silício discordam até alguém ler o registrador de modo.
Escolha uma regra, registre-a na especificação do formato ao lado de W e F, e faça os vetores de teste carregarem essa informação.
Overflow: saturação vs wraparound
O overflow em Q15 faz uma de três coisas: rejeitar o valor, prendê-lo em 0.999969482421875 ou dar a volta até -1.0. Qual delas você recebe é uma política que o seu toolchain escolheu, e a terceira opção inverte o sinal em silêncio.
Pegue 1.0. A escala dá 1.0 × 32768 = 32768, e o maior inteiro com sinal de 16 bits é 32767. O valor está fora da faixa por exatamente uma unidade.
| Política | Palavra armazenada | Valor lido de volta | Como aparece lá na frente |
|---|---|---|---|
| Erro | nenhuma | conversão rejeitada | Explícito e fácil de capturar, em geral o certo para ferramentas |
| Saturação | 0x7FFF = 32767 | 0.999969482421875 | Indistinguível de 1.0 a ouvido ou a olho nu |
| Wraparound | 0x8000 = -32768 | -1.0 | Inversão de sinal em escala plena |
A linha da saturação perde 0.000030517578125 e ninguém percebe. A linha do wraparound transforma uma amostra positiva de escala plena numa negativa de escala plena, e num caminho de áudio isso é um estalo que se ouve do outro lado da sala. Numa malha de controle, é um comando de escala plena na direção errada.
A propriedade perigosa do wraparound é que ele produz uma palavra de aparência perfeitamente válida. 0x8000 é uma codificação Q15 legítima de -1.0. Nada lá na frente consegue distingui-la de uma amostra -1.0 genuína, então não sobra assinatura nenhuma para procurar depois; você só encontra o problema instrumentando o ponto onde o overflow aconteceu.
Por que o silício de DSP traz instruções com saturação
Saturação é o comportamento que o processamento de sinais quer, então os processadores a implementam em vez de deixá-la para um desvio condicional. A ARM tem QADD/QSUB e as instruções de deslocamento com saturação SSAT/USAT, o NEON tem VQADD e companhia, e o SSE do x86 tem somas empacotadas com saturação como paddsw. As famílias C6000 e C55x da TI expõem a saturação como um bit de modo no caminho do acumulador.
Num filtro ou num mixer, uma amostra clipada de vez em quando é uma pequena distorção local. Uma amostra que deu a volta é uma descontinuidade com energia em todo o espectro. Por padrão, o hardware prefere o levemente errado ao catastroficamente errado, mas só se você tiver habilitado isso. A aritmética inteira comum em C, no mesmo chip, continua dando a volta.
Por que uma multiplicação em Q15 precisa de um deslocamento de 15 bits à direita
Multiplique duas palavras Q15 como inteiros e o resultado é correto, mas já não é Q15. Os bits fracionários se somam na multiplicação: Q15 × Q15 dá Q30.
Acompanhe 0.5 × 0.5, cuja resposta certa é obviamente 0.25:
16384 × 16384 = 268435456 ← this is Q30, not Q15
268435456 ÷ 2^30 = 0.25 ← read as Q30, correct
268435456 >> 15 = 8192 ← realign to Q15
8192 ÷ 32768 = 0.25 ← same answer, back in Q15
Interprete 268435456 como Q15 e você leria 8192.0, errado por um fator de 32768. Esse fator é o bug inteiro, e explica por que filtros de ponto fixo que “quase funcionam” erram por uma potência de dois.
O produto também precisa de espaço. Dois valores de 16 bits se multiplicam em até 32 bits, então o intermediário tem de ser int32_t. Acumular muitos produtos exige ainda mais folga, daí os acumuladores de DSP terem 40 bits em peças como o C55x.
Arredonde no deslocamento, não descarte os bits e pronto
Um >> 15 puro joga fora os 15 bits menos significativos, o que equivale a truncar em direção a menos infinito para valores com sinal. Somar antes meio LSB da precisão de saída transforma isso em arredondamento para o mais próximo:
#include <stdint.h>
#include <stdio.h>
static int16_t sat_q15(int32_t v) {
if (v > 32767) return 32767;
if (v < -32768) return -32768;
return (int16_t)v;
}
static int16_t mul_q15(int16_t a, int16_t b) {
int32_t prod = (int32_t)a * (int32_t)b; /* Q30 */
int32_t back = (prod + (1 << 14)) >> 15; /* round, then Q30 -> Q15 */
return sat_q15(back);
}
int main(void) {
printf("0.5*0.5 -> %d\n", mul_q15(16384, 16384));
printf("0.1*0.1 -> %d\n", mul_q15(3277, 3277));
printf("-1*-1 -> %d\n", mul_q15(-32768, -32768));
printf("no-round -> %d\n", (int)(((int32_t)3277 * 3277) >> 15));
return 0;
}
Compilado com cc -std=c11 -Wall -o q15 q15.c && ./q15, isto imprime:
0.5*0.5 -> 8192
0.1*0.1 -> 328
-1*-1 -> 32767
no-round -> 327
A terceira linha é o caso do -1 visto antes. -32768 × -32768 = 1073741824, que é 1.0 em Q30 e está fora da faixa do Q15, então sat_q15 prende o valor em 32767. Tire o limitador e o cast para int16_t dá a volta para -32768, transformando -1 × -1 em -1.
As duas últimas linhas mostram a diferença de arredondamento. 3277 × 3277 = 10738729, e o deslocamento puro dá 327 (0.009979248046875), enquanto o deslocamento arredondado dá 328 (0.010009765625). O produto verdadeiro é 0.01, então a versão arredondada chega mais de duas vezes mais perto, ao custo de uma única soma.
Uma ressalva sobre o deslocamento em si: deslocar um inteiro com sinal negativo para a direita é definido pela implementação em C antes do C23, embora todo compilador que você vai encontrar faça um deslocamento aritmético. Se isso te incomoda, divida por 32768 e deixe o compilador emitir o deslocamento, ou faça o deslocamento num tipo sem sinal depois de aplicar um viés. A mecânica geral de deslocamentos e máscaras está no guia de operações bit a bit.
A soma exige valores de Q iguais antes
A multiplicação muda o valor de Q de forma previsível. A soma não tolera divergência nenhuma: somar uma palavra Q7 a uma palavra Q15 produz besteira, porque os operandos não compartilham a mesma escala.
Alinhe os dois com um deslocamento antes. 0.5 em Q7 é 64, e 64 << 8 é 16384, que é 0.5 em Q15. A quantidade de deslocamento é a diferença de bits fracionários, 15 - 7 = 8.
Deslocar para cima é exato, mas custa folga, já que um valor Q7 promovido a Q15 precisa do contêiner mais largo. Deslocar para baixo perde informação e exige a mesma decisão de arredondamento da multiplicação. De um jeito ou de outro, escreva o valor de Q de cada intermediário num comentário. Código de ponto fixo em que as escalas moram só na cabeça do autor fica impossível de manter depois de um mês.
Ponto fixo Q15 ou ponto flutuante IEEE 754: como escolher
Os dois são sistemas binários de valor posicional, então nenhum leva vantagem de precisão em princípio. A escolha depende do que o hardware alvo cobra de você e das garantias de que você precisa.
| Pergunta | Aponta para ponto fixo | Aponta para ponto flutuante |
|---|---|---|
| Existe FPU em hardware? | Sem FPU, ou uma biblioteca de soft-float | FPU em hardware com operações de ciclo único |
| Qual é a faixa dinâmica? | Conhecida e limitada, como áudio normalizado | Abrange várias ordens de grandeza |
| O formato de transmissão define uma escala? | Protocolo ou registrador fixa a escala binária | O campo é um float de verdade |
| Os resultados precisam bater bit a bit entre builds? | Sim, inteiros se reproduzem em qualquer lugar | Tolerável variar com FMA e otimização |
| Memória ou banda estão apertadas? | Amostras de 16 bits cortam pela metade o espaço de floats de 32 bits | Não é uma restrição |
| Quem mantém o código? | Time que já domina a notação Q | Time misto, bugs de escala são o risco maior |
A última linha não é piada. O ponto fixo desloca o erro do tempo de execução para a fase de projeto, o que só é uma boa troca quando alguém está de fato fazendo o trabalho de projeto. Num Cortex-M4F com FPU em hardware, float de precisão simples costuma ser a escolha mais rápida e mais segura, e o reflexo tradicional de recorrer ao Q15 é um hábito herdado de peças que já não dominam o mercado.
Onde o IEEE 754 assume
Recorra ao ponto flutuante quando a própria palavra carrega sinal, expoente e significando em vez de uma escala fixa. É o momento em que o formato Q deixa de se aplicar: não existe um único 2^F para dividir, porque o expoente varia de valor para valor.
As duas representações se encontram o tempo todo na prática. Dados de sensor chegam como palavras Q15 de registrador, são promovidos a float para uma conta longa e voltam como Q15 para o DAC. Para inspecionar bit a bit a metade em ponto flutuante desse caminho, o conversor IEEE 754 separa um valor em sinal, expoente e mantissa e imprime o decimal exato armazenado, que é o mesmo serviço que o conversor de formato Q presta para uma escala fixa.
Os dois se apoiam na mesma base
Formato Q, IEEE 754 e inteiros comuns leem todos os mesmos bits, com regras diferentes sobre onde o ponto fica e se ele pode se mover. Se a parte de valor posicional ainda parece frágil, ou se você quer ganhar velocidade para ler 0x0CCD como 0000 1100 1100 1101 sem pegar a calculadora, a introdução à conversão entre binário, hexadecimal e octal cobre a base sobre a qual os dois formatos se apoiam.
FAQ sobre ponto fixo Q15
O que significa Q15?
Na convenção comum de DSP, Q15 é uma palavra de 16 bits com sinal em complemento de dois e 15 bits fracionários: uma posição de sinal e 15 posições de fração. A escala é 2^15 = 32768, a resolução é 1/32768 = 0.000030517578125, e a faixa vai de -1 até 32767/32768. Como os rótulos Q variam de documento para documento, confirme a largura total e o sinal; o rótulo sozinho não basta.
Q15 e Q1.15 são a mesma coisa?
Em geral, descrevem o mesmo layout de 16 bits com sinal, sendo que o 1 de Q1.15 conta a posição de sinal. Mas a notação não é universal, e alguns autores somam o bit de sinal por fora do m em vez de contá-lo dentro. A descrição confiável é sinal mais total de bits W mais bits fracionários F: para este layout, signed, W=16, F=15.
Quanto é 0.5 em Q15?
0.5 em Q15 é o inteiro armazenado 16384, 0x4000 em hexadecimal. A conta é 0.5 × 32768 = 16384, que já é inteiro, então não há arredondamento nem erro de quantização. A decodificação confirma: 16384 ÷ 32768 = 0.5 exatamente.
Quais são os valores máximo e mínimo do Q15?
O mínimo é -1, e ele está incluído, porque -32768 ÷ 32768 é exatamente -1. O máximo é 32767/32768 = 0.999969482421875. Os dois são alcançáveis, então a faixa é [-1, 0.999969482421875]; 1.0 é o valor que fica de fora. Referências que escrevem o limite inferior como intervalo aberto estão erradas, e esse erro esconde o caso de overflow -1 × -1.
O que acontece quando o Q15 estoura?
Depende da política em vigor. Uma política de erro rejeita a conversão. A saturação prende o valor no extremo mais próximo, então 1.0 vira 0.999969482421875, uma perda de um LSB que costuma ser inaudível. O wraparound aplica módulo 2^16, então 1.0 escala para 32768, que é lido de volta como -32768 e portanto -1.0, uma inversão completa de sinal. O hardware de DSP adota saturação por padrão exatamente por isso, mas a aritmética inteira comum em C dá a volta.
Por que dois números Q15 precisam de um deslocamento de 15 bits à direita depois da multiplicação?
Porque os bits fracionários se somam. Q15 × Q15 produz um produto Q30, então o resultado inteiro carrega 30 bits fracionários em vez de 15. Deslocar 15 posições à direita o realinha para Q15: 16384 × 16384 = 268435456, e 268435456 >> 15 = 8192, que decodifica para 0.25. Some 1 << 14 antes do deslocamento para arredondar para o mais próximo em vez de truncar, e mantenha o intermediário num int32_t para o produto de 32 bits não estourar.
Quando devo usar o formato Q em vez do IEEE 754?
Use o formato Q quando um protocolo, mapa de registradores, algoritmo de DSP ou codec já tiver fixado uma escala binária para uma palavra inteira, porque aí a escala faz parte da interface e não cabe a você escolher. Use IEEE 754 quando o valor precisa de faixa dinâmica ampla, quando o alvo tem FPU em hardware, ou quando o campo realmente armazena sinal, expoente e significando. Para uma mudança de base pura, sem escala atrelada, nenhum dos dois se aplica; isso é conversão de base comum.
A versão curta
Ponto fixo Q15 é uma multiplicação, uma decisão de arredondamento e uma verificação de faixa. raw = round(value × 32768) na entrada, value = raw ÷ 32768 na saída. A fórmula é trivial; as falhas moram todas nas partes que ninguém documenta.
Então documente. Escreva sinal, W e F ao lado de todo rótulo Q, em vez de supor que o rótulo se explica sozinho. Registre o modo de arredondamento no mesmo lugar, porque truncamento e piso se separam por um LSB inteiro nos negativos. Deixe explícito se o overflow gera erro, satura ou dá a volta, porque o caso do wraparound transforma 1.0 em -1.0 e não deixa evidência nenhuma para trás.
Quando uma palavra de registrador e uma planilha discordam, decodifique um valor conhecido no conversor de formato Q. Ele mostra o inteiro armazenado, o decimal exato, o erro de quantização e a faixa representável lado a lado, tudo calculado no navegador. Costuma ser o suficiente para descobrir de quem era a suposição errada sobre W e F.