Skip to content
Retour au blog
Tutoriels

Virgule fixe Q15 : conversion, arrondi et débordement

0,1 stocké en Q15 devient 3277, et 1,0 bascule à -1,0 sans saturation. Lisez n'importe quelle notation Q, convertissez à la main, vérifiez en ligne.

15 min de lecture

Virgule fixe Q15 : conversion, arrondi et débordement

La virgule fixe Q15 stocke une fraction sous la forme d’un entier signé 16 bits ordinaire, avec une échelle binaire implicite de 2^15 = 32768. L’encodage tient en une multiplication suivie d’une étape d’arrondi :

raw = round(value × 32768)

Le décodage tient en une division :

value = raw ÷ 32768

Voilà toute l’arithmétique. 0.5 × 32768 = 16384, donc 0.5 occupe la mémoire sous la forme de l’entier 16384, et 16384 ÷ 32768 redonne exactement 0.5. Il n’y a ni champ d’exposant ni bit implicite. La virgule binaire est une convention passée entre vous et quiconque lira le mot ; le matériel, lui, ne voit jamais qu’un int16.

Cette formule a deux conséquences immédiates, et elles coûtent des après-midi entiers.

0.1 × 32768 = 3276.8, qui n’est pas un entier. Q15 retient 3277, et la valeur que vous relisez est 0.100006103515625, pas 0.1.

1.0 × 32768 = 32768, soit un cran au-delà du plus grand entier signé 16 bits. 1.0 n’a donc aucun encodage Q15, et ce que votre code en fait dépend d’une politique que la plupart des bases de code n’écrivent nulle part. Avec saturation, vous obtenez 0.999969482421875. Avec repliement, vous obtenez -1.0.

Reste à savoir lire une notation Q quand deux fiches techniques se contredisent, convertir à la main dans les deux sens, et repérer la règle d’arrondi et de débordement que votre chaîne d’outils a choisie sans vous le dire.

Q15, Q1.15, Qm.n : est-ce la même disposition ?

Demandez à trois références ce que signifie Q15 et vous risquez d’obtenir trois réponses. Ce n’est pas vous qui lisez mal. La notation n’a réellement jamais été normalisée, et le désaccord porte sur un seul bit.

Comment lire Qm.n

La forme à deux nombres est la forme honnête. Dans Qm.n, n est le nombre de bits fractionnaires et m le nombre de bits entiers. Cet article compte la position du signe à l’intérieur de m : c’est la lecture qui fait de Q16.16 un mot de 32 bits, soit 16 bits entiers signe compris, 16 bits fractionnaires, et une échelle de 2^16 = 65536. Encodez 1.5 en Q16.16 et vous obtenez 1.5 × 65536 = 98304, soit 0x00018000 en hexadécimal, sans la moindre erreur d’arrondi.

Les ennuis commencent avec le bit de signe. Certains auteurs le comptent à l’intérieur de m, d’autres l’ajoutent par-dessus. Q1.15 selon la première convention est un mot de 16 bits : une position de signe/entier plus 15 bits de fraction. Selon la seconde convention, la même étiquette décrit 17 bits, ce qu’aucune machine ne possède.

Pourquoi la même étiquette Q15 désigne des largeurs différentes selon les documents

La forme à un seul nombre, Q15, abandonne m et laisse la largeur implicite. Dans la pratique du DSP, elle désigne presque toujours un mot signé 16 bits en complément à deux avec 15 bits fractionnaires, un sens sur lequel les écosystèmes TI et ARM se sont accordés il y a des décennies.

Mais vous trouverez des explications largement recopiées qui affirment à peu près ceci : Q15 signifie 15 bits fractionnaires, donc si l’on définit un nombre sur 32 bits, il y a 1 bit de signe et 16 bits entiers. Lisez-la attentivement : elle compte le bit de signe par-dessus m et non à l’intérieur, soit l’autre convention que celle retenue ici. La disposition qu’elle décrit existe bel et bien, et on l’écrit normalement Q16.15. Ce qui cloche, c’est l’étiquette posée dessus : un mot de 32 bits avec 16 bits entiers n’est pas Q15, quelle que soit la convention. Ce même paragraphe a été reproduit sur assez de blogs pour devancer aujourd’hui la définition correcte sur certaines requêtes.

Traitez un Q15 nu, dans un document que vous ne connaissez pas, comme une hypothèse et non comme un fait. Vérifiez-le contre quelque chose de mesurable : la largeur du registre dans la table d’adressage, le type C dans l’en-tête du pilote, ou une valeur d’échantillon connue.

La description qui survit au code de quelqu’un d’autre

Notez trois choses et l’ambiguïté disparaît : le signe, c’est-à-dire complément à deux ou non signé ; W, le nombre total de bits du mot ; F, le nombre de bits fractionnaires.

signed, W=16, F=15 ne peut pas être mal lu. unsigned, W=32, F=16 non plus. Tout le reste s’en déduit : l’échelle vaut 2^F, la résolution 2^-F, et la plage est celle des entiers du mot divisée par 2^F. Mettez ces trois valeurs dans le document de protocole et dans le commentaire de la structure ; la discussion s’arrête là.

Le convertisseur Q à virgule fixe en ligne affiche le signe, W, F et l’échelle à côté de chaque résultat, pour la même raison. Quand une fiche technique dit « Q15 » et que l’analyseur d’un collègue dit autre chose, décoder un mot connu tranche la question en une dizaine de secondes.

La formule de conversion, faite à la main

Les deux sens tiennent sur un coin de feuille, ce qui aide devant un dump hexadécimal figé sur l’écran d’un oscilloscope.

Du flottant vers le fixe : round(x × 2^F)

Prenons 0.5 vers Q15.

  1. Mise à l’échelle : 0.5 × 32768 = 16384
  2. Arrondi : c’est déjà un entier, donc 16384 reste tel quel
  3. Contrôle de plage : un entier signé 16 bits couvre -32768 à 32767, et 16384 y tient
  4. Stockage : 16384, 0x4000 en hexadécimal, 0100000000000000 en binaire

Seule l’étape 2 peut perdre de l’information, et seule l’étape 3 peut échouer. Les surprises en virgule fixe se produisent presque toutes à l’un de ces deux endroits.

Du fixe vers le flottant : raw ÷ 2^F

Faisons maintenant le trajet inverse, en partant d’une capture de registre qui affiche 0xC000 dans un champ Q15 signé.

  1. Lire le texte comme un code 16 bits non signé : 0xC000 = 49152
  2. Le format est signé et le bit de poids fort est actif, on soustrait donc 2^16 : 49152 - 65536 = -16384
  3. Ramener à l’échelle : -16384 ÷ 32768 = -0.5

L’étape 2 est celle que l’on saute. Sans la correction en complément à deux, 0xC000 se lit +1.5, une valeur qui n’est même pas dans la plage de Q15. Ce genre d’incohérence fait un bon signal d’alarme : si une valeur décodée sort de la plage du format, vous avez presque à coup sûr oublié l’étape du signe.

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 fait ici un vrai travail. Passer d’abord par un float réintroduirait l’arrondi binaire au moment précis où vous cherchez à le mesurer, et Fraction(str(x)) lit le littéral décimal que vous avez tapé plutôt que le double le plus proche.

Lire et écrire le mot hexadécimal

L’hexadécimal est la forme sous laquelle ces valeurs apparaissent réellement dans les vues de registres, et la conversion est mécanique : 3277 s’écrit 0xCCD, complété à W/4 chiffres, soit 0x0CCD. Complétez toujours. Un mot Q15 fait quatre chiffres hexadécimaux et un mot Q31 en fait huit ; laisser tomber le zéro de tête est exactement la façon dont une valeur se retrouve désalignée dans un décodeur par lots.

Gardez aussi les codes bruts non signés en hexadécimal. 0x8000 est le mot Q15 le plus négatif, pas +32768, et l’écrire -0x8000 n’aide personne. L’ordre des octets est une question distincte, puisque le format Q spécifie une mise à l’échelle numérique et ne dit rien du boutisme. Un dump petit-boutiste de 0x0CCD arrive sous la forme des octets CD 0C. Pour de simples changements de base pendant la lecture d’un dump, le convertisseur de bases traite le binaire, l’octal et l’hexadécimal sans échelle ni largeur signée attachées.

Q7, Q15, Q31 et Q16.16 : plage et résolution

Chaque nombre de ce tableau vaut -2^(W-1) divisé par 2^F d’un côté et (2^(W-1) - 1) divisé par 2^F de l’autre. Les valeurs sont exactes, pas arrondies pour l’affichage.

FormatLargeurBits fractionnaires FÉchelle 2^FMinimumMaximum (exact)Résolution
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

Collez n’importe laquelle de ces valeurs limites dans le convertisseur Q à virgule fixe : il renvoie l’entier stocké, le mot hexadécimal et le décimal exact. C’est plus rapide que de refaire le calcul à la main avant de livrer une constante de firmware.

Pourquoi le haut de la plage du format Q15 vaut 0.999969482421875

La plage du format Q15 est asymétrique, et l’asymétrie vient du complément à deux plutôt que de la virgule fixe elle-même. Un mot signé 16 bits couvre les entiers de -32768 à 32767. Divisez les deux extrémités par 32768 et la plage devient -32768/32768 à 32767/32768, c’est-à-dire -1 à 0.999969482421875.

1.0 manque donc d’exactement un LSB. La tournure courante « le maximum vaut environ 1.0 » induit en erreur : la valeur n’a tout bonnement pas d’encodage, et un convertisseur qui annonce 1.0 pour Q15 sans préciser qu’il a saturé vous ment.

Pourquoi -1 est inclus

Quantité de références écrivent la plage Q15 sous la forme -1 < X < 0.9999695, avec un crochet ouvert des deux côtés. La borne inférieure est fausse. -32768 ÷ 32768 = -1 exactement : -1 est donc représentable, et les deux extrémités de [-1, 0.999969482421875] sont des valeurs atteignables. Vous verrez aussi la plage écrite [-1, 1), qui dit la même chose rapportée à la pleine échelle : 1.0 lui-même est hors d’atteinte, mais la plus grande valeur atteignable est bien 0.999969482421875, et non quelque chose en deçà.

Le détail a des conséquences. -1 est la valeur qui casse la multiplication, car -1 × -1 = 1 et 1 est hors plage. Qui suppose -1 inatteignable n’écrira pas la branche de saturation qui l’intercepte.

La borne supérieure tronquée 0.9999695 de ces mêmes références est un artefact d’affichage. La valeur n’a rien de périodique : c’est 32767/32768, et 32768 est une puissance de deux, donc le développement décimal se termine après 15 chiffres, à 0.999969482421875.

0.1 ne tient pas dans Q15

Mettez-le à l’échelle et le problème saute aux yeux : 0.1 × 32768 = 3276.8. La virgule fixe ne sait stocker que des entiers, il faut donc bien céder quelque chose.

Avec l’arrondi au plus proche, le mot stocké est 3277, soit 0x0CCD en hexadécimal. La valeur que ce mot représente est :

3277 ÷ 32768 = 0.100006103515625

L’erreur de quantification est l’écart entre ce que vous avez demandé et ce que la grille pouvait vous donner :

0.100006103515625 - 0.1 = +0.000006103515625

Cela fait environ six millionièmes, soit un cinquième de LSB : inoffensif dans un réglage de volume, beaucoup moins dans un intégrateur qui le rajoute mille fois par seconde, où la dérive atteint environ 0.006 par seconde sur la valeur accumulée.

L’erreur en virgule fixe est uniforme, celle en virgule flottante est proportionnelle

Q15 pose une grille de 65536 points espacés d’exactement 1/32768 sur [-1, 0.999969482421875]. L’espacement près de 0.9 est le même que près de 0.0001 : l’erreur absolue au pire cas vaut donc un demi-LSB partout. Cela rend l’analyse d’erreur ennuyeuse, au meilleur sens du terme, puisque vous pouvez borner le plancher de bruit d’un filtre avec une arithmétique qu’un ingénieur junior peut vérifier.

L’IEEE 754 fait l’inverse. Il conserve un nombre fixe de bits significatifs et déplace l’exposant : l’écart absolu entre deux doubles voisins croît donc avec la magnitude, tandis que l’erreur relative reste à peu près constante. Près de 1.0, cet écart vaut environ 2.2e-16 ; près de 1e12, environ 0.0001220703125.

Le même échec prend une autre forme : un double ne sait pas non plus contenir 0.1. Il stocke 0.1000000000000000055511151231257827021181583404541015625, ce qui explique que 0.1 + 0.2 renvoie 0.30000000000000004. Ce cas est déroulé en détail dans l’article compagnon sur la précision en virgule flottante. La virgule fixe ne résout pas les fractions décimales en binaire ; elle rend seulement la taille de l’erreur prévisible.

Trois modes d’arrondi, séparés d’un LSB

La règle d’arrondi fait partie du contrat de données, au même titre que W et F. Deux implémentations par ailleurs correctes qui divergent sur l’arrondi produisent des vecteurs de test qui diffèrent au dernier bit, et remonter à la cause prend des heures.

ModeRègle10911.744-3276.8
Arrondi au plus proche, moitiés vers le pairPoint de grille le plus proche ; les moitiés exactes vont vers l’entier pair10912-3277
Troncature vers zéroOn laisse tomber la fraction, la magnitude ne fait que diminuer10911-3276
Plancher vers moins l’infiniToujours vers le bas de la droite numérique10911-3277

Les deux colonnes montrent pourquoi un seul exemple ne suffit jamais. Pour une valeur positive, troncature et plancher coïncident. Pour une valeur négative, elles se séparent d’un LSB entier, parce que la troncature tire -3276.8 vers zéro tandis que le plancher le pousse vers le bas.

Un exemple déroulé : 0.333 en Q15

0.333 en Q15 est un bon cas de test parce qu’il se situe près d’une frontière. 0.333 × 32768 = 10911.744.

  • Troncature : 10911, qui se relit 0.332977294921875
  • Arrondi au plus proche : 10912, qui se relit 0.3330078125

Les tutoriels plus anciens impriment 10911 et passent à la suite sans dire quelle règle a produit ce nombre. Reprenez-le dans une base de code dont l’encodeur arrondit et vos vecteurs de référence échouent dès la première exécution, avec un écart d’une unité qui ressemble à une coquille plutôt qu’à un désaccord de politique.

C’est sur les négatifs que les trois règles se séparent. -0.1 × 32768 = -3276.8 donne -3276 par troncature (valeur -0.0999755859375) et -3277 par plancher ou au plus proche (valeur -0.100006103515625). La troncature et l’arrondi au plus proche traitent les deux signes de la même façon, ±3276 pour l’une et ±3277 pour l’autre : une paire de coefficients symétrique reste donc symétrique. Le plancher, lui, non : il envoie +0.1 sur 3276 mais -0.1 sur -3277, et la paire ressort déséquilibrée d’une unité.

Ce que fait votre langage par défaut

Aucun de ces comportements par défaut n’est faux ; ils sont simplement différents, et rien ne les signale au moment de la conversion.

  • C/C++ : une conversion d’un flottant vers un type entier tronque vers zéro. (int16_t)(0.333f * 32768) donne 10911.
  • Python : la fonction native round() arrondit les moitiés vers le pair, donc round(3276.8) vaut 3277 et round(2.5) vaut 2.
  • JavaScript : Math.round départage les moitiés vers plus l’infini, ce qui n’est pas symétrique. Math.round(2.5) vaut 3 mais Math.round(-2.5) vaut -2.
  • Matériel : de nombreux chemins de multiplication-accumulation DSP arrondissent lors du décalage et proposent l’arrondi au pair sous forme de bit de mode, d’où les désaccords entre le modèle C de référence et le silicium tant que personne n’a lu le registre de mode.

Choisissez une règle, nommez-la dans la spécification du format à côté de W et F, et faites-la porter par les vecteurs de test.

Débordement : saturation ou repliement

Un débordement Q15 fait l’une de ces trois choses : rejeter la valeur, l’écrêter à 0.999969482421875, ou la replier à -1.0. Laquelle vous obtenez est une politique choisie par votre chaîne d’outils, et la troisième inverse le signe en silence.

Prenons 1.0. La mise à l’échelle donne 1.0 × 32768 = 32768, et le plus grand entier signé 16 bits vaut 32767. La valeur dépasse la plage d’exactement un.

PolitiqueMot stockéValeur relueCe que cela donne en aval
Erreuraucunconversion rejetéeBruyant, interceptable, en général le bon choix pour l’outillage
Saturation0x7FFF = 327670.999969482421875Impossible à distinguer de 1.0 à l’oreille ou à l’œil
Repliement0x8000 = -32768-1.0Inversion de signe pleine échelle

La ligne « saturation » perd 0.000030517578125 et personne ne le remarque. La ligne « repliement » transforme un échantillon positif pleine échelle en un échantillon négatif pleine échelle, et dans une chaîne audio c’est un claquement que l’on entend d’un bout à l’autre de la pièce. Dans une boucle d’asservissement, c’est une commande pleine échelle dans le mauvais sens.

La propriété dangereuse du repliement est qu’il produit un mot parfaitement valide en apparence. 0x8000 est un encodage Q15 légal de -1.0. Rien en aval ne peut le distinguer d’un véritable échantillon à -1.0 : il n’y a donc aucune signature à rechercher après coup, et on ne le trouve qu’en instrumentant l’endroit où le débordement s’est produit.

Pourquoi le silicium DSP embarque des instructions saturantes

La saturation est le comportement que veut le traitement du signal ; les processeurs l’implémentent donc plutôt que de le laisser à un branchement. ARM propose QADD/QSUB et les instructions de décalage saturant SSAT/USAT, NEON a VQADD et ses semblables, et le SSE x86 offre des additions saturantes empaquetées comme paddsw. Chez TI, les familles C6000 et C55x exposent la saturation sous forme de bit de mode sur le chemin de l’accumulateur.

Dans un filtre ou un mélangeur, un échantillon écrêté de temps en temps est une petite distorsion locale. Un échantillon replié est une discontinuité dont l’énergie s’étale sur tout le spectre. Par défaut, le matériel préfère l’erreur légère à l’erreur catastrophique, mais seulement si vous avez activé le mode ; l’arithmétique entière en C ordinaire sur la même puce se replie toujours.

Pourquoi une multiplication Q15 exige un décalage à droite de 15 bits

Multipliez deux mots Q15 comme des entiers et le résultat est correct, mais ce n’est plus du Q15. Les bits fractionnaires s’additionnent lors de la multiplication : Q15 × Q15 donne du Q30.

Déroulons 0.5 × 0.5, dont la bonne réponse est manifestement 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

Interpréter 268435456 comme du Q15 vous ferait lire 8192.0, soit un facteur 32768 d’écart. Ce facteur est le bug tout entier, et il explique pourquoi les filtres en virgule fixe qui « marchent presque » sont souvent faux d’une puissance de deux.

Le produit a aussi besoin de place. Multiplier deux valeurs 16 bits donne jusqu’à 32 bits de résultat : l’intermédiaire doit donc être un int32_t. Accumuler beaucoup de produits demande encore plus de marge, et c’est pourquoi les accumulateurs DSP font 40 bits sur des composants comme le C55x.

Arrondissez le décalage, ne vous contentez pas de jeter les bits

Un simple >> 15 jette les 15 bits de poids faible, ce qui revient à une troncature vers moins l’infini pour les valeurs signées. Ajouter d’abord un demi-LSB de la précision sortante en fait un arrondi au plus proche :

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

Compilé avec cc -std=c11 -Wall -o q15 q15.c && ./q15, cela affiche :

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

La troisième ligne est le cas -1 vu plus haut. -32768 × -32768 = 1073741824, ce qui vaut 1.0 en Q30 et sort de la plage de Q15 : sat_q15 l’écrête donc à 32767. Retirez l’écrêtage et la conversion vers int16_t le replie à -32768, transformant -1 × -1 en -1.

Les deux dernières lignes montrent la différence d’arrondi. 3277 × 3277 = 10738729, et un décalage nu donne 327 (0.009979248046875) tandis que le décalage arrondi donne 328 (0.010009765625). Le vrai produit vaut 0.01 : la version arrondie tombe donc plus de deux fois plus près, pour le prix d’une addition.

Une réserve sur le décalage lui-même : décaler à droite un entier signé négatif relève d’un comportement défini par l’implémentation en C avant C23, même si tous les compilateurs que vous croiserez effectuent un décalage arithmétique. Si cela vous met mal à l’aise, divisez par 32768 et laissez le compilateur émettre le décalage, ou faites le décalage sur un type non signé après avoir appliqué un biais. Les mécanismes plus généraux des décalages et des masques sont traités dans le guide des opérations bit à bit.

L’addition exige d’abord des valeurs Q identiques

La multiplication change la valeur Q de façon prévisible. L’addition, elle, ne tolère aucun écart : ajouter un mot Q7 à un mot Q15 produit n’importe quoi, parce que les opérandes ne partagent pas la même échelle.

Alignez-les d’abord avec un décalage. 0.5 en Q7 vaut 64, et 64 << 8 vaut 16384, soit 0.5 en Q15. L’amplitude du décalage est la différence de bits fractionnaires, 15 - 7 = 8.

Décaler vers le haut est exact mais coûte de la marge, puisqu’une valeur Q7 promue en Q15 réclame le conteneur plus large. Décaler vers le bas perd de l’information et demande la même décision d’arrondi que la multiplication. Dans les deux cas, écrivez la valeur Q de chaque intermédiaire en commentaire. Un code en virgule fixe dont les échelles ne vivent que dans la tête de son auteur devient impossible à maintenir en moins d’un mois.

Virgule fixe Q15 ou virgule flottante IEEE 754 : comment choisir

Les deux sont des systèmes binaires de numération positionnelle : aucun n’a donc d’avantage de précision par principe. Le choix se joue sur ce que le matériel cible vous facture et sur les garanties dont vous avez besoin.

QuestionPlaide pour la virgule fixePlaide pour la virgule flottante
Y a-t-il une FPU matérielle ?Pas de FPU, ou une bibliothèque de flottants logicielsFPU matérielle avec opérations en un cycle
Quelle est l’étendue de la plage dynamique ?Connue et bornée, comme de l’audio normaliséS’étale sur de nombreux ordres de grandeur
Le format de transport définit-il une échelle ?Un protocole ou un registre fixe l’échelle binaireLe champ est un véritable flottant
Les résultats doivent-ils être identiques au bit près d’une compilation à l’autre ?Oui, les entiers se reproduisent partoutTolérable qu’ils varient avec le FMA et les optimisations
La mémoire ou la bande passante sont-elles serrées ?Des échantillons 16 bits divisent par deux l’empreinte des flottants 32 bitsCe n’est pas une contrainte
Qui maintient le code ?Une équipe déjà à l’aise avec la notation QÉquipe mixte, les bugs d’échelle sont le risque principal

La dernière ligne compte autant que les autres. La virgule fixe déplace l’erreur de l’exécution vers la phase de conception, ce qui n’est un bon échange que si quelqu’un fait effectivement le travail de conception. Sur un Cortex-M4F doté d’une FPU matérielle, le flottant simple précision est souvent le choix à la fois plus rapide et plus sûr, et le réflexe d’aller chercher Q15 est une habitude héritée de puces qui ne dominent plus.

Là où l’IEEE 754 prend le relais

Sortez la virgule flottante quand le mot lui-même porte un signe, un exposant et une mantisse plutôt qu’une échelle fixe. C’est le moment où le format Q cesse de s’appliquer : il n’y a plus un unique 2^F par lequel diviser, puisque l’exposant varie d’une valeur à l’autre.

Les deux représentations se croisent en permanence dans la pratique. Les données d’un capteur arrivent en mots de registre Q15, sont promues en flottant pour un long calcul, puis reviennent en Q15 pour le DAC. Pour inspecter bit à bit la moitié flottante de ce chemin, le convertisseur IEEE 754 décompose une valeur en signe, exposant et mantisse et affiche le décimal exact stocké, exactement le travail que le convertisseur Q accomplit pour une échelle fixe.

Les deux reposent sur les mêmes fondations

Le format Q, l’IEEE 754 et les entiers ordinaires lisent tous les mêmes bits, avec des règles différentes sur l’endroit où se place la virgule et sur sa capacité à bouger. Si la partie numération positionnelle vous semble fragile, ou si vous voulez lire plus vite 0x0CCD comme 0000 1100 1100 1101 sans sortir une calculatrice, l’introduction à la conversion binaire, hexadécimale et octale couvre les bases sur lesquelles les deux formats s’appuient.

FAQ sur la virgule fixe Q15

Que signifie Q15 ?

Dans la convention DSP courante, Q15 désigne un mot signé 16 bits en complément à deux avec 15 bits fractionnaires : une position de signe et 15 positions de fraction. L’échelle vaut 2^15 = 32768, la résolution 1/32768 = 0.000030517578125, et la plage va de -1 à 32767/32768. Comme les étiquettes Q varient d’un document à l’autre, confirmez la largeur totale et le signe plutôt que de vous fier à l’étiquette seule.

Q15 et Q1.15 sont-ils identiques ?

Ils décrivent en général la même disposition signée sur 16 bits, le 1 de Q1.15 comptant la position de signe. Mais la notation n’est pas universelle, et certains auteurs ajoutent le bit de signe par-dessus m au lieu de le compter à l’intérieur. La description fiable, c’est le signe plus le nombre total de bits W plus les bits fractionnaires F : pour cette disposition, signed, W=16, F=15.

Combien vaut 0.5 en Q15 ?

0.5 en Q15, c’est l’entier stocké 16384, soit 0x4000 en hexadécimal. Le calcul est 0.5 × 32768 = 16384, qui est déjà un entier : il n’y a donc ni arrondi ni erreur de quantification. Le décodage le confirme : 16384 ÷ 32768 = 0.5 exactement.

Quelles sont les valeurs maximale et minimale de Q15 ?

Le minimum vaut -1, et il est inclus, parce que -32768 ÷ 32768 fait exactement -1. Le maximum vaut 32767/32768 = 0.999969482421875. Ces deux valeurs sont atteignables, la plage est donc [-1, 0.999969482421875] ; c’est 1.0 qui tombe en dehors. Les références qui écrivent la borne inférieure comme un intervalle ouvert ont tort, et cette erreur masque le cas de débordement -1 × -1.

Que se passe-t-il quand Q15 déborde ?

Cela dépend de la politique en vigueur. Une politique d’erreur rejette la conversion. La saturation écrête à la borne la plus proche, donc 1.0 devient 0.999969482421875, une perte d’un LSB en général inaudible. Le repliement applique un modulo 2^16, donc 1.0 se met à l’échelle en 32768, qui se relit -32768 et donc -1.0, soit une inversion de signe complète. Le matériel DSP sature par défaut exactement pour cette raison, mais l’arithmétique entière en C ordinaire se replie.

Pourquoi deux nombres Q15 exigent-ils un décalage à droite de 15 après multiplication ?

Parce que les bits fractionnaires s’additionnent. Q15 × Q15 produit un produit en Q30, donc le résultat entier porte 30 bits fractionnaires au lieu de 15. Un décalage à droite de 15 le réaligne sur Q15 : 16384 × 16384 = 268435456, et 268435456 >> 15 = 8192, qui se décode en 0.25. Ajoutez 1 << 14 avant le décalage pour arrondir au plus proche au lieu de tronquer, et gardez l’intermédiaire dans un int32_t pour que le produit 32 bits ne déborde pas.

Quand faut-il utiliser le format Q plutôt que l’IEEE 754 ?

Utilisez le format Q quand un protocole, une table de registres, un algorithme DSP ou un codec a déjà fixé une échelle binaire pour un mot entier : l’échelle fait partie de l’interface et vous n’avez pas à la choisir. Utilisez l’IEEE 754 quand la valeur exige une large plage dynamique, quand la cible dispose d’une FPU matérielle, ou quand le champ stocke réellement un signe, un exposant et une mantisse. Pour un simple changement de base sans échelle attachée, ni l’un ni l’autre ne s’applique ; c’est de la conversion de base ordinaire.

La version courte

La virgule fixe Q15, c’est une multiplication, une décision d’arrondi et un contrôle de plage. raw = round(value × 32768) à l’entrée, value = raw ÷ 32768 à la sortie. La formule est triviale ; les échecs se logent tous dans ce que personne ne documente.

Alors documentez-le. Écrivez le signe, W et F à côté de chaque étiquette Q au lieu de supposer que l’étiquette suffit. Nommez le mode d’arrondi au même endroit : troncature et plancher se séparent d’un LSB entier sur les négatifs. Dites explicitement si le débordement lève une erreur, sature ou se replie, parce que le cas du repliement transforme 1.0 en -1.0 sans laisser la moindre trace.

Quand un mot de registre et un tableur se contredisent, décodez une valeur connue dans le convertisseur Q à virgule fixe : il affiche côte à côte l’entier stocké, le décimal exact, l’erreur de quantification et la plage représentable, le tout calculé dans le navigateur. C’est en général suffisant pour dire qui s’était trompé sur W et F.

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

Articles connexes

Voir tous les articles