Skip to content
Torna al blog
Tutorial

Virgola fissa Q15: conversione, arrotondamento e overflow

0,1 in Q15 diventa 3277 e 1,0 salta a -1,0 senza saturazione. Leggi la notazione Q, converti a mano e verifica il risultato online gratis.

15 min di lettura

Virgola fissa Q15: conversione, arrotondamento e overflow

La virgola fissa Q15 memorizza una frazione dentro un normalissimo intero con segno a 16 bit, con una scala binaria implicita pari a 2^15 = 32768. Codificare significa fare una moltiplicazione e un arrotondamento:

raw = round(value × 32768)

Decodificare significa fare una divisione:

value = raw ÷ 32768

L’aritmetica finisce qui. 0.5 × 32768 = 16384, quindi 0.5 vive in memoria come l’intero 16384, e 16384 ÷ 32768 restituisce esattamente 0.5. Non c’è nessun campo esponente e nessun bit implicito. La virgola binaria è una convenzione concordata fra te e chi leggerà quella parola; l’hardware vede sempre e soltanto un int16.

Da quella formula discendono due conseguenze immediate, ed è su entrambe che si finisce per perdere un pomeriggio.

0.1 × 32768 = 3276.8, che non è un intero. Q15 tiene 3277, e il valore che rileggi è 0.100006103515625, non 0.1.

1.0 × 32768 = 32768, cioè uno in più del più grande intero con segno a 16 bit. Quindi 1.0 non ha alcuna codifica Q15, e come reagisce il tuo codice dipende da una politica che quasi nessuna codebase mette per iscritto. Se saturi ottieni 0.999969482421875. Se lasci girare il valore ottieni -1.0.

Il resto sta tutto in questi due fallimenti: leggere la notazione Q quando due datasheet si contraddicono, convertire a mano nelle due direzioni, capire quale regola di arrotondamento e di overflow ha scelto in silenzio la tua toolchain.

Q15, Q1.15, Qm.n: sono lo stesso layout?

Chiedi a tre fonti che cosa significhi Q15 e puoi ottenere tre risposte diverse. Non è colpa tua che le leggi male: la notazione non è mai stata davvero standardizzata, e il disaccordo riguarda un solo bit.

Come si legge Qm.n

La forma a due numeri è quella onesta. In Qm.n, n è il numero di bit frazionari e m è il numero di bit interi. Questo articolo conta la posizione del segno dentro m, ed è la lettura che rende Q16.16 una parola a 32 bit: 16 bit interi segno compreso, 16 bit frazionari, scala 2^16 = 65536. Codifica 1.5 in Q16.16 e ottieni 1.5 × 65536 = 98304, in esadecimale 0x00018000, senza il minimo errore di arrotondamento.

I guai iniziano con il bit di segno. Alcuni autori lo contano dentro m, altri lo aggiungono sopra. Con la prima convenzione Q1.15 è una parola a 16 bit: una posizione di segno/intero più 15 bit di frazione. Con la seconda, la stessa etichetta descrive 17 bit, che nessuna macchina ha.

Perché la stessa etichetta Q15 indica larghezze diverse in documenti diversi

La forma a numero singolo Q15 elimina m del tutto e lascia implicita la larghezza. Nella pratica DSP indica quasi sempre una parola a 16 bit con segno in complemento a due e 15 bit frazionari, ed è il significato su cui gli ecosistemi TI e ARM si sono assestati decenni fa.

Ma in giro trovi spiegazioni molto copiate che dicono più o meno: Q15 significa 15 bit frazionari, quindi se definiamo un numero a 32 bit abbiamo 1 bit di segno e 16 bit interi. Leggi con attenzione quella frase e vedrai che conta il bit di segno sopra m invece che dentro, cioè con la convenzione opposta a quella usata qui. Il layout che descrive è reale, e di solito si scrive Q16.15. Sbagliata è l’etichetta che gli viene appiccicata: una parola a 32 bit con 16 bit interi non è Q15 con nessuna delle due convenzioni. Lo stesso paragrafo è stato riprodotto su abbastanza blog da posizionarsi ormai più in alto della definizione corretta su alcune query.

Tratta un Q15 isolato in un documento che non conosci come un’ipotesi, non come un fatto. Verificalo su qualcosa di misurabile: la larghezza del registro nella mappa di memoria, il tipo C nell’header del driver oppure un valore di esempio noto.

L’unica descrizione che sopravvive al contatto con il codice altrui

Metti per iscritto tre cose e l’ambiguità sparisce: se la parola è con segno in complemento a due oppure senza segno, quanti bit ha in tutto (W) e quanti di quei bit sono frazionari (F).

signed, W=16, F=15 non si può fraintendere. E nemmeno unsigned, W=32, F=16. Tutto il resto è derivato: la scala è 2^F, la risoluzione è 2^-F e l’intervallo è l’intervallo intero della parola diviso per 2^F. Scrivili nel documento di protocollo e nel commento della struct e questa discussione non la rifarai più.

Il convertitore di formato Q online stampa presenza di segno, W, F e scala accanto a ogni risultato proprio per questo motivo. Quando un datasheet dice “Q15” e il parser di un collega dice altro, decodificare una parola nota chiude la questione in una decina di secondi.

La formula di conversione, svolta a mano

Entrambe le direzioni sono abbastanza brevi da fare su carta, il che conta parecchio quando stai fissando un dump esadecimale sullo schermo di un oscilloscopio.

Da float a fisso: round(x × 2^F)

Portiamo 0.5 in Q15.

  1. Scala: 0.5 × 32768 = 16384
  2. Arrotonda: è già un intero, quindi 16384 resta com’è
  3. Controllo di intervallo: un intero con segno a 16 bit va da -32768 a 32767, e 16384 ci sta
  4. Memorizza: 16384, in esadecimale 0x4000, in binario 0100000000000000

Solo il passo 2 può perdere informazione e solo il passo 3 può fallire. Tutto ciò che è interessante nella virgola fissa accade in uno di quei due punti.

Da fisso a float: raw ÷ 2^F

Ora nella direzione opposta, partendo da una cattura di registro che legge 0xC000 in un campo Q15 con segno.

  1. Interpreta il testo come codice a 16 bit senza segno: 0xC000 = 49152
  2. Il formato è con segno e il bit più alto è impostato, quindi sottrai 2^16: 49152 - 65536 = -16384
  3. Riporta in scala: -16384 ÷ 32768 = -0.5

Il passo 2 è quello che si salta. Senza la correzione in complemento a due, 0xC000 si legge come +1.5, che non è nemmeno nell’intervallo di Q15: un ottimo campanello d’allarme. Se un valore decodificato cade fuori dall’intervallo del formato, quasi sicuramente hai dimenticato il passaggio sul segno.

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

Qui Fraction sta facendo un lavoro concreto. Scalare passando prima da un float reintrodurrebbe l’arrotondamento binario proprio nell’istante in cui stai cercando di misurarlo, mentre Fraction(str(x)) legge il letterale decimale che hai scritto e non il double più vicino a esso.

Lettura e scrittura della parola esadecimale

L’esadecimale è il modo in cui questi valori compaiono davvero nelle viste dei registri, e la conversione è meccanica: 3277 in esadecimale è 0xCCD, riempito fino a W/4 cifre diventa 0x0CCD. Riempi sempre: una parola Q15 ha quattro cifre esadecimali e una parola Q31 ne ha otto, ed è proprio saltando lo zero iniziale che un valore finisce disallineato dentro un decodificatore batch.

Tieni anche i codici grezzi senza segno in esadecimale. 0x8000 è la parola Q15 più negativa, non +32768, e scriverla come -0x8000 non aiuta nessuno. L’ordine dei byte è una questione a parte, perché il formato Q specifica la scala numerica e non dice nulla sull’endianness: un dump little-endian di 0x0CCD arriva come i byte CD 0C. Per i semplici cambi di base mentre leggi un dump, il convertitore di basi numeriche gestisce binario, ottale ed esadecimale senza nessuna scala né larghezza con segno agganciate.

Q7, Q15, Q31 e Q16.16: intervallo e risoluzione

Ogni numero di questa tabella è -2^(W-1) diviso per 2^F a un estremo e (2^(W-1) - 1) diviso per 2^F all’altro. I valori sono esatti, non arrotondati per la visualizzazione.

FormatoLarghezzaBit frazionari FScala 2^FMinimoMassimo (esatto)Risoluzione
Q787128-1127/128 = 0.99218751/128 = 0.0078125
Q15161532768-132767/32768 = 0.9999694824218751/32768 = 0.000030517578125
Q3132312147483648-1(2^31−1)/2^31 = 0.99999999953433871269226074218751/2^31 ≈ 4.6566128730773926e-10
Q16.16321665536-32768(2^31−1)/65536 = 32767.99998474121093751/65536 = 0.0000152587890625

Incolla uno qualsiasi di questi valori di confine nel convertitore di formato Q e ti restituirà l’intero memorizzato, la parola esadecimale e il decimale esatto: è il modo più rapido per confermare una costante del firmware prima che vada in produzione.

Perché il vertice dell’intervallo del formato Q15 è 0.999969482421875

L’intervallo del formato Q15 è asimmetrico, e l’asimmetria viene dal complemento a due, non da qualcosa di specifico della virgola fissa. Una parola con segno a 16 bit copre gli interi da -32768 a 32767. Dividi entrambi gli estremi per 32768 e l’intervallo diventa da -32768/32768 a 32767/32768, cioè da -1 a 0.999969482421875.

Quindi a 1.0 manca esattamente un LSB. Non è “il massimo è circa 1.0” né “1.0 con arrotondamento”: quel valore semplicemente non ha una codifica, e un convertitore che riporta 1.0 per Q15 senza dire di aver saturato ti sta mentendo.

Perché -1 è incluso

Parecchie fonti scrivono l’intervallo Q15 come -1 < X < 0.9999695, con parentesi aperta da entrambi i lati. L’estremo inferiore è sbagliato. -32768 ÷ 32768 = -1 esattamente, quindi -1 è rappresentabile ed entrambi gli estremi di [-1, 0.999969482421875] sono valori raggiungibili. Troverai anche l’intervallo scritto come [-1, 1), che dice la stessa cosa rispetto al fondo scala: 1.0 è irraggiungibile, ma il valore più grande raggiungibile è 0.999969482421875, non un valore appena al di sotto.

La cosa pesa più di un cavillo di notazione. -1 è il valore che rompe la moltiplicazione, perché -1 × -1 = 1 e 1 è fuori intervallo. Chi dà per scontato che -1 sia irraggiungibile non scriverà il ramo di saturazione che lo intercetta.

Anche l’estremo superiore troncato 0.9999695 di quelle stesse fonti è un artefatto di visualizzazione. Nel valore non c’è nulla di periodico: è 32767/32768, e 32768 è una potenza di due, quindi lo sviluppo decimale termina dopo 15 cifre a 0.999969482421875.

0.1 non ci sta in Q15

Basta scalarlo e il problema salta subito all’occhio: 0.1 × 32768 = 3276.8. La virgola fissa può memorizzare solo interi, quindi qualcosa deve cedere.

Con l’arrotondamento al più vicino, la parola memorizzata è 3277, in esadecimale 0x0CCD. Il valore che quella parola rappresenta è:

3277 ÷ 32768 = 0.100006103515625

L’errore di quantizzazione è la differenza fra quello che hai chiesto e quello che la griglia poteva darti:

0.100006103515625 - 0.1 = +0.000006103515625

Sono circa sei milionesimi, ovvero un quinto di LSB: in un controllo di volume non se ne accorge nessuno. In un integratore che lo riaccumula mille volte al secondo diventa invece una deriva costante di circa 0.006 al secondo sul valore accumulato.

L’errore in virgola fissa è uniforme, quello in virgola mobile no

Q15 stende una griglia di 65536 punti spaziati esattamente di 1/32768 lungo [-1, 0.999969482421875]. La spaziatura vicino a 0.9 è la stessa che c’è vicino a 0.0001, e l’errore assoluto nel caso peggiore è mezzo LSB ovunque. Questo rende l’analisi dell’errore noiosa nel migliore dei modi: puoi limitare il rumore di fondo di un filtro con un’aritmetica che un junior riesce a controllare.

IEEE 754 fa l’opposto. Mantiene un numero fisso di bit significativi e sposta l’esponente: la distanza assoluta fra due double vicini cresce con la magnitudine, mentre l’errore relativo resta quasi costante. Vicino a 1.0 quella distanza è circa 2.2e-16; vicino a 1e12 è circa 0.0001220703125.

Stesso fallimento, forma diversa. Nemmeno un double riesce a contenere 0.1. Memorizza 0.1000000000000000055511151231257827021181583404541015625, ed è per questo che 0.1 + 0.2 restituisce 0.30000000000000004. Quel caso è svolto nell’articolo di accompagnamento sulla precisione in virgola mobile. La virgola fissa non risolve le frazioni decimali in binario; rende soltanto prevedibile la dimensione dell’errore.

Tre modalità di arrotondamento, distanti un LSB

La regola di arrotondamento fa parte del contratto sui dati, non è un dettaglio implementativo. Due implementazioni entrambe corrette che non concordano sull’arrotondamento producono per sempre vettori di test diversi nell’ultimo bit, e rintracciare la causa è un lavoro ingrato.

ModalitàRegola10911.744-3276.8
Al più vicino, parità all’intero pariPunto della griglia più vicino; le metà esatte vanno all’intero pari10912-3277
Troncamento verso zeroScarta la parte frazionaria, la magnitudine può solo calare10911-3276
Floor verso meno infinitoSempre verso il basso lungo la retta dei numeri10911-3277

Le due colonne mostrano perché un solo esempio non basta mai. Su un valore positivo, troncamento e floor concordano. Su un valore negativo si separano di un LSB pieno, perché il troncamento tira -3276.8 verso lo zero e il floor lo spinge verso il basso.

L’esempio svolto che sbagliano tutti

0.333 in Q15 è un buon caso di prova perché sta vicino a un confine. 0.333 × 32768 = 10911.744.

  • Troncamento: 10911, che si rilegge come 0.332977294921875
  • Al più vicino: 10912, che si rilegge come 0.3330078125

I tutorial più vecchi stampano 10911 e tirano dritto senza dire quale regola l’abbia prodotto. Porta quel numero dentro una codebase il cui encoder arrotonda e i tuoi vettori di riferimento falliranno al primo giro, con uno scarto di una singola unità che sembra un errore di battitura invece che un disaccordo di policy.

Con i negativi le tre regole si separano. -0.1 × 32768 = -3276.8-3276 con il troncamento (valore -0.0999755859375) e -3277 con floor o al più vicino (valore -0.100006103515625). Troncamento e al più vicino trattano i due segni allo stesso modo, rispettivamente ±3276 e ±3277, quindi una coppia di coefficienti simmetrici resta simmetrica. Il floor no: manda +0.1 a 3276 ma -0.1 a -3277, e la coppia esce sbilanciata di un’unità.

Che cosa fa il tuo linguaggio per impostazione predefinita

  • In C e C++ un cast da virgola mobile a un tipo intero tronca verso lo zero: (int16_t)(0.333f * 32768)10911.
  • La round() integrata di Python usa la parità all’intero pari, quindi round(3276.8) è 3277 e round(2.5) è 2.
  • Math.round in JavaScript risolve le parità verso più infinito e non è simmetrico: Math.round(2.5) è 3 ma Math.round(-2.5) è -2.
  • Molti percorsi DSP di moltiplicazione e accumulo arrotondano sullo shift e offrono il pari-più-vicino come bit di modalità, per cui il modello C di riferimento e il silicio possono discordare finché qualcuno non legge il registro di modalità.

Nessuno di questi comportamenti è sbagliato. Sono semplicemente diversi, e nessuno di loro si annuncia.

Scegli una regola, scrivila nella specifica del formato accanto a W e F e fai in modo che i vettori di test se la portino dietro.

Overflow: saturazione o wraparound

L’overflow in Q15 fa una di tre cose: rifiuta il valore, lo blocca a 0.999969482421875 oppure lo fa girare fino a -1.0. Quale delle tre ottieni è una politica scelta dalla tua toolchain, e la terza opzione inverte il segno in silenzio.

Prendi 1.0. La scalatura dà 1.0 × 32768 = 32768, e il più grande intero con segno a 16 bit è 32767. Il valore è fuori intervallo esattamente di uno.

PoliticaParola memorizzataValore rilettoChe aspetto ha a valle
Errorenessunaconversione rifiutataRumoroso e intercettabile, di solito la scelta giusta per i tool
Saturazione0x7FFF = 327670.999969482421875Indistinguibile da 1.0 all’orecchio o all’occhio
Wraparound0x8000 = -32768-1.0Inversione di segno a fondo scala

La riga della saturazione perde 0.000030517578125 e non se ne accorge nessuno. La riga del wraparound trasforma un campione positivo a fondo scala in uno negativo a fondo scala, e in una catena audio è un clic che senti dall’altra parte della stanza. In un anello di controllo è un comando a fondo scala nella direzione sbagliata.

La proprietà pericolosa del wraparound è che produce una parola perfettamente plausibile. 0x8000 è una codifica Q15 legittima di -1.0. Niente a valle riesce a distinguerla da un campione -1.0 autentico, quindi non resta nessuna firma da cercare a posteriori con un grep; lo scopri solo strumentando il punto in cui l’overflow è avvenuto.

Perché il silicio DSP offre istruzioni saturanti

La saturazione è il comportamento che il signal processing vuole, quindi i processori la implementano invece di lasciarla a un salto condizionato. ARM ha QADD/QSUB e le istruzioni di shift saturante SSAT/USAT, NEON ha VQADD e compagnia, e SSE su x86 ha somme saturanti impacchettate come paddsw. Le famiglie C6000 e C55x di TI espongono la saturazione come bit di modalità sul percorso dell’accumulatore.

In un filtro o in un mixer, un campione clippato ogni tanto è una piccola distorsione locale. Un campione andato in wraparound è una discontinuità con energia su tutto lo spettro. Per impostazione predefinita l’hardware sceglie il leggermente sbagliato al posto del catastroficamente sbagliato, ma solo se la saturazione l’hai abilitata: l’aritmetica intera del C puro sullo stesso chip continua a girare comunque.

Perché una moltiplicazione Q15 richiede uno shift a destra di 15 bit

Moltiplica due parole Q15 come interi e il risultato è corretto, ma non è più Q15. I bit frazionari si sommano durante la moltiplicazione: Q15 × Q15 dà Q30.

Svolgiamo 0.5 × 0.5, dove la risposta giusta è ovviamente 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

Interpreta 268435456 come Q15 e leggeresti 8192.0, sbagliato di un fattore 32768. Quel fattore è tutto il bug, e spiega perché i filtri in virgola fissa che “quasi funzionano” sono spesso fuori di una potenza di due.

Anche il prodotto ha bisogno di spazio. Due valori a 16 bit si moltiplicano fino a 32 bit, quindi l’intermedio deve essere un int32_t. Accumulare molti prodotti richiede ancora più margine, e infatti su parti come il C55x gli accumulatori sono a 40 bit.

Arrotonda lo shift, non limitarti a buttare via i bit

Un >> 15 secco butta via i 15 bit bassi, che per i valori con segno equivale a un troncamento verso meno infinito. Aggiungere prima mezzo LSB della precisione in uscita lo trasforma in un arrotondamento al più vicino:

#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;
}

Compilato con cc -std=c11 -Wall -o q15 q15.c && ./q15, stampa:

0.5*0.5   -> 8192
0.1*0.1   -> 328
-1*-1     -> 32767
no-round  -> 327

La terza riga è il caso -1 di prima. -32768 × -32768 = 1073741824, che in Q30 vale 1.0 ed è fuori intervallo per Q15, quindi sat_q15 lo blocca a 32767. Togli il clamp e il cast a int16_t lo fa girare a -32768, trasformando -1 × -1 in -1.

Le ultime due righe sono la differenza di arrotondamento. 3277 × 3277 = 10738729, e uno shift secco dà 327 (0.009979248046875) mentre lo shift arrotondato dà 328 (0.010009765625). Il prodotto vero è 0.01: l’errore della versione arrotondata è meno della metà. Il prezzo è una sola addizione in più.

Un’avvertenza sullo shift stesso: lo scorrimento a destra di un intero con segno negativo è definito dall’implementazione in C prima di C23, anche se ogni compilatore che incontrerai esegue uno shift aritmetico. Se la cosa ti mette a disagio, dividi per 32768 e lascia che sia il compilatore a emettere lo shift, oppure fai lo shift su un tipo senza segno dopo aver applicato un bias. La meccanica più generale di shift e maschere è trattata nella guida alle operazioni bit a bit.

L’addizione pretende prima valori di Q coincidenti

La moltiplicazione cambia il valore di Q in modo prevedibile. L’addizione non tollera affatto un disallineamento: sommare una parola Q7 a una parola Q15 produce un risultato senza senso, perché gli operandi non condividono la scala.

Allineali prima con uno shift. 0.5 in Q7 è 64, e 64 << 8 è 16384, cioè 0.5 in Q15. L’entità dello shift è la differenza fra i bit frazionari, 15 - 7 = 8.

Spostare verso l’alto è esatto ma costa margine, perché un valore Q7 promosso a Q15 ha bisogno del contenitore più largo. Spostare verso il basso perde informazione e richiede la stessa decisione sull’arrotondamento della moltiplicazione. In entrambi i casi, scrivi in un commento il valore di Q di ogni intermedio. Un codice in virgola fissa dove le scale vivono solo nella testa di chi l’ha scritto diventa immantenibile nel giro di un mese.

Virgola fissa Q15 o virgola mobile IEEE 754: come scegliere

Sono entrambi sistemi binari posizionali, quindi in linea di principio nessuno dei due ha un vantaggio di accuratezza. La scelta dipende da quanto ti fa pagare l’hardware di destinazione e da quali garanzie ti servono.

DomandaPunta alla virgola fissaPunta alla virgola mobile
C’è una FPU hardware?Nessuna FPU, oppure una libreria soft-floatFPU hardware con operazioni a ciclo singolo
Quanto è ampia la gamma dinamica?Nota e limitata, come l’audio normalizzatoCopre molti ordini di grandezza
Il formato sul filo definisce una scala?Il protocollo o il registro fissa la scala binariaIl campo è un float vero
I risultati devono essere identici bit a bit fra build diverse?Sì, gli interi si riproducono ovunqueTollerabile che varino con FMA e ottimizzazioni
Memoria o banda sono risicate?I campioni a 16 bit dimezzano l’ingombro dei float a 32 bitNon è un vincolo
Chi mantiene il codice?Team già a suo agio con la notazione QTeam misto, i bug di scala sono il rischio maggiore

L’ultima riga non è una battuta. La virgola fissa sposta l’errore dal runtime alla fase di progettazione, ed è un buon scambio solo se qualcuno quel lavoro di progettazione lo sta davvero facendo. Su un Cortex-M4F con FPU hardware, il float a precisione singola è spesso la scelta più veloce e più sicura, e il riflesso tradizionale di buttarsi su Q15 è un’abitudine ereditata da componenti che non dominano più il mercato.

Dove subentra IEEE 754

Passa alla virgola mobile quando è la parola stessa a portare un segno, un esponente e un significando invece di una scala fissa. È il momento in cui il formato Q smette di applicarsi: non esiste un singolo 2^F per cui dividere, perché l’esponente cambia da valore a valore.

Nella pratica le due rappresentazioni si incrociano di continuo. I dati dei sensori arrivano come parole di registro Q15, vengono promossi a float per un calcolo lungo e tornano in Q15 per il DAC. Per ispezionare bit per bit la metà in virgola mobile di quel percorso, il convertitore IEEE 754 scompone un valore in segno, esponente e mantissa e stampa il decimale esatto memorizzato: lo stesso lavoro che il convertitore di formato Q fa per una scala fissa.

Poggiano entrambi sulle stesse fondamenta

Formato Q, IEEE 754 e interi normali leggono tutti gli stessi bit con regole diverse su dove sta la virgola e se può muoversi. Se la parte posizionale ti sembra traballante, o se vuoi diventare più rapido a leggere 0x0CCD come 0000 1100 1100 1101 senza allungare la mano verso la calcolatrice, l’introduzione alla conversione tra binario, esadecimale e ottale copre le basi comuni a entrambi i formati.

FAQ sulla virgola fissa Q15

Che cosa significa Q15?

Nella convenzione DSP più diffusa, Q15 è una parola a 16 bit con segno in complemento a due con 15 bit frazionari: una posizione di segno e 15 posizioni di frazione. La scala è 2^15 = 32768, la risoluzione è 1/32768 = 0.000030517578125 e l’intervallo va da -1 a 32767/32768. Poiché le etichette Q variano da documento a documento, conferma la larghezza totale e la presenza di segno invece di fidarti della sola etichetta.

Q15 e Q1.15 sono la stessa cosa?

Di solito descrivono lo stesso layout con segno a 16 bit, con l’1 di Q1.15 che conta la posizione del segno. Ma la notazione non è universale, e alcuni autori aggiungono il bit di segno sopra m invece di contarlo dentro. La descrizione affidabile mette insieme presenza di segno, bit totali W e bit frazionari F: per questo layout, signed, W=16, F=15.

Quanto vale 0.5 in Q15?

0.5 in Q15 è l’intero memorizzato 16384, 0x4000 in esadecimale. Il conto è 0.5 × 32768 = 16384, che è già un intero, quindi non c’è arrotondamento né errore di quantizzazione. La decodifica lo conferma: 16384 ÷ 32768 = 0.5 esatto.

Quali sono i valori massimo e minimo di Q15?

Il minimo è -1, ed è incluso, perché -32768 ÷ 32768 fa esattamente -1. Il massimo è 32767/32768 = 0.999969482421875. Entrambi sono raggiungibili, quindi l’intervallo è [-1, 0.999969482421875]; 1.0 è il valore che resta fuori. Le fonti che scrivono l’estremo inferiore come intervallo aperto sbagliano, e quell’errore nasconde il caso di overflow -1 × -1.

Che cosa succede quando Q15 va in overflow?

Dipende dalla politica in vigore. Una politica di errore rifiuta la conversione. La saturazione blocca il valore all’estremo più vicino, quindi 1.0 diventa 0.999969482421875, una perdita di un LSB di solito impercettibile. Il wraparound applica il modulo 2^16: 1.0 scala a 32768, che si rilegge come -32768, cioè -1.0, un’inversione di segno completa. L’hardware DSP usa la saturazione come impostazione predefinita esattamente per questo motivo, ma l’aritmetica intera del C puro continua a fare wraparound.

Perché due numeri Q15 hanno bisogno di uno shift a destra di 15 dopo la moltiplicazione?

Perché i bit frazionari si sommano. Q15 × Q15 produce un prodotto Q30, quindi il risultato intero porta 30 bit frazionari invece di 15. Uno shift a destra di 15 lo riallinea a Q15: 16384 × 16384 = 268435456, e 268435456 >> 15 = 8192, che si decodifica come 0.25. Aggiungi 1 << 14 prima dello shift per arrotondare al più vicino invece di troncare, e tieni l’intermedio in un int32_t così il prodotto a 32 bit non va in overflow.

Quando conviene usare il formato Q invece di IEEE 754?

Usa il formato Q quando un protocollo, una mappa dei registri, un algoritmo DSP o un codec hanno già fissato una scala binaria per una parola intera: la scala fa parte dell’interfaccia e non sei tu a sceglierla. Usa IEEE 754 quando il valore richiede un’ampia gamma dinamica, quando il target ha una FPU hardware o quando il campo memorizza davvero un segno, un esponente e un significando. Per un semplice cambio di base senza nessuna scala agganciata non serve né l’uno né l’altro: quella è una normale conversione di base.

In breve

La virgola fissa Q15 è una moltiplicazione, una decisione sull’arrotondamento e un controllo di intervallo. raw = round(value × 32768) all’andata, value = raw ÷ 32768 al ritorno. La formula è banale; i fallimenti stanno tutti nelle parti che nessuno documenta.

Quindi documentale. Scrivi presenza di segno, W e F accanto a ogni etichetta Q invece di dare per scontato che l’etichetta basti. Nomina la modalità di arrotondamento nello stesso posto, perché troncamento e floor si separano di un LSB pieno sui negativi. Di’ esplicitamente se l’overflow dà errore, satura o gira, perché il caso del wraparound trasforma 1.0 in -1.0 e non lascia alcuna traccia.

Quando una parola di registro e un foglio di calcolo si contraddicono, decodifica un valore noto nel convertitore di formato Q: mostra affiancati l’intero memorizzato, il decimale esatto, l’errore di quantizzazione e l’intervallo rappresentabile, tutto calcolato nel browser. Di solito basta a capire quale ipotesi su W e F fosse sbagliata.

Tag: fixed-point dsp embedded q-format number-representation

Articoli correlati

Vedi tutti gli articoli