Skip to content
Terug naar blog
Tutorials

Q15 vastekommagetallen: omrekenen, afronden en overflow

0,1 wordt in Q15 opgeslagen als 3277 en 1,0 klapt zonder verzadiging om naar -1,0. Lees Q-notatie, reken het uit en controleer het gratis online.

15 min leestijd

Q15 vastekommagetallen: omrekenen, afronden en overflow

Q15 slaat een breuk op als een gewoon signed 16-bits geheel getal, met een impliciete binaire schaal van 2^15 = 32768. Encoderen is één vermenigvuldiging plus één afrondstap:

raw = round(value × 32768)

Decoderen is één deling:

value = raw ÷ 32768

Meer rekenwerk is er niet. 0.5 × 32768 = 16384, dus 0.5 staat in het geheugen als het gehele getal 16384, en 16384 ÷ 32768 levert exact 0.5 op. Er is geen exponentveld en geen verborgen bit. De plaats van de binaire komma is een afspraak tussen jou en degene die het woord uitleest; de hardware ziet alleen maar een int16.

Twee dingen volgen daar meteen uit, en aan allebei ben je zo een middag kwijt.

0.1 × 32768 = 3276.8, en dat is geen geheel getal. Q15 bewaart 3277, en wat je terugleest is 0.100006103515625, niet 0.1.

1.0 × 32768 = 32768, precies één hoger dan het grootste signed 16-bits gehele getal. 1.0 heeft in Q15 dus helemaal geen codering, en wat je code daarmee doet hangt af van een keuze die in de meeste codebases nergens is vastgelegd. Verzadig je, dan krijg je 0.999969482421875. Klap je om, dan krijg je -1.0.

Om die twee gevallen draait de rest van dit artikel: Q-notatie lezen als twee datasheets elkaar tegenspreken, in beide richtingen met de hand omrekenen, en achterhalen welke afrondregel en overflowregel je toolchain stilletjes voor je koos.

Q15, Q1.15, Qm.n: is dat dezelfde indeling?

Vraag aan drie bronnen wat Q15 betekent en je krijgt drie antwoorden. De notatie is nooit echt gestandaardiseerd, en het meningsverschil gaat over één bit.

Zo lees je Qm.n

De vorm met twee getallen is de eerlijke. In Qm.n is n het aantal fractiebits en m het aantal integerbits. Dit artikel telt de tekenpositie mee in m, en dat is de lezing die van Q16.16 een 32-bits woord maakt: 16 integerbits inclusief het teken, 16 fractiebits, schaal 2^16 = 65536. Encodeer 1.5 in Q16.16 en je krijgt 1.5 × 65536 = 98304, hex 0x00018000, zonder enige afrondfout.

Het gedoe begint bij de tekenbit. Sommige auteurs tellen die mee in m, andere zetten hem er bovenop. Onder de eerste afspraak is Q1.15 een 16-bits woord: één positie voor teken en geheel deel plus 15 fractiebits. Onder de tweede afspraak beschrijft hetzelfde label 17 bits, en die breedte heeft geen enkele machine.

Waarom hetzelfde label Q15 in verschillende documenten een andere breedte betekent

De vorm met één getal, Q15, laat m helemaal weg en houdt de breedte impliciet. In de DSP-praktijk betekent het bijna altijd een signed 16-bits woord in twee-complement (two’s complement) met 15 fractiebits, en op die betekenis zijn de ecosystemen van TI en ARM decennia geleden uitgekomen.

Maar je komt breed overgenomen uitleg tegen in de trant van: Q15 betekent 15 fractiebits, dus als we een 32-bits getal definiëren, zijn er 1 tekenbit en 16 integerbits. Lees die zin nauwkeurig en je ziet dat hij de tekenbit boven op m telt in plaats van erin — de andere afspraak dan die dit artikel gebruikt. De indeling die hij beschrijft bestaat echt, en die schrijf je normaal gesproken als Q16.15. Wat niet klopt is het etiket erop: een 32-bits woord met 16 integerbits is onder geen van beide afspraken Q15. Dezelfde alinea is op zo veel blogs overgenomen dat hij inmiddels bij sommige zoekopdrachten hoger staat dan de juiste definitie.

Behandel een kale Q15 in een onbekend document als een vermoeden, niet als een feit. Toets het aan iets meetbaars: de registerbreedte in de memory map, het C-type in de driverheader of een bekende voorbeeldwaarde.

De enige omschrijving die andermans code overleeft

Noteer drie dingen en de dubbelzinnigheid is weg:

  • Teken — signed in twee-complement, of unsigned
  • W — het totale aantal bits in het woord
  • F — het aantal fractiebits

signed, W=16, F=15 valt niet verkeerd te lezen. unsigned, W=32, F=16 evenmin. De rest volgt daaruit: de schaal is 2^F, de resolutie is 2^-F, en het bereik is het bereik van het gehele getal in het woord, gedeeld door 2^F. Zet die drie waarden in het protocoldocument en in het commentaar bij de struct, en je hoeft deze discussie nooit meer te voeren.

De online converter voor Q-formaat zet om dezelfde reden signed of unsigned, W, F en de schaal naast elk resultaat. Als een datasheet “Q15” zegt en de decoder van een collega iets anders beweert, is één bekend woord decoderen genoeg om het in een seconde of tien te beslechten.

De omrekenformule, met de hand uitgewerkt

Beide richtingen zijn kort genoeg om op papier te doen, en dat telt als je naar een hexdump op het scherm van een oscilloscoop zit te turen.

Van float naar vaste komma: round(x × 2^F)

Reken 0.5 om naar Q15.

  1. Schalen: 0.5 × 32768 = 16384
  2. Afronden: al een geheel getal, dus 16384 blijft staan
  3. Bereikcontrole: een signed 16-bits woord loopt van -32768 tot en met 32767, en 16384 past
  4. Opslaan: 16384, hex 0x4000, binair 0100000000000000

Alleen stap 2 kan informatie kwijtraken en alleen stap 3 kan mislukken. Alles wat interessant is aan vaste komma gebeurt op een van die twee plekken.

Van vaste komma naar float: raw ÷ 2^F

Nu de andere kant op, met als startpunt een registeruitlezing waarin een signed Q15-veld 0xC000 bevat.

  1. Lees de tekst als een unsigned 16-bits code: 0xC000 = 49152
  2. Het formaat is signed en de hoogste bit staat aan, dus trek 2^16 af: 49152 - 65536 = -16384
  3. Terugschalen: -16384 ÷ 32768 = -0.5

Stap 2 slaat iedereen over. Zonder de correctie voor twee-complement leest 0xC000 als +1.5, en dat valt niet eens binnen het bereik van Q15. Handig alarmbelletje: ligt een gedecodeerde waarde buiten het bereik van het formaat, dan ben je vrijwel zeker de tekenstap vergeten.

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 doet hier echt werk. Eerst via een float schalen zou binaire afronding terugbrengen op precies het moment dat je die wilt meten, en Fraction(str(x)) leest het decimale literal dat je hebt ingetypt in plaats van de double die daar het dichtst bij ligt.

Het hexwoord lezen en schrijven

In registerweergaven zie je deze waarden in hex, en de omzetting is mechanisch: 3277 is in hex 0xCCD, aangevuld tot W/4 cijfers 0x0CCD. Vul altijd aan tot de volle breedte, want een Q15-woord is vier hexcijfers en een Q31-woord acht, en die voorloopnul weglaten is precies hoe een waarde in een batchgewijze decoder verkeerd komt te liggen.

Houd ruwe codes in hex ook unsigned. 0x8000 is het meest negatieve Q15-woord, niet +32768, en er -0x8000 van maken helpt niemand. De bytevolgorde is een aparte kwestie: Q-formaat legt alleen de numerieke schaal vast en zegt niets over endianness. Een little-endian dump van 0x0CCD komt binnen als de bytes CD 0C. Wil je tijdens het lezen van zo’n dump alleen even van talstelsel wisselen, dan kun je talstelsels omrekenen tussen binair, octaal en hex, zonder dat er een schaal of signed breedte aan vastzit.

Q7, Q15, Q31 en Q16.16: bereik en resolutie

Elk getal in deze tabel is aan de ene kant -2^(W-1) gedeeld door 2^F en aan de andere kant (2^(W-1) - 1) gedeeld door 2^F. De waarden zijn exact, niet afgerond voor de weergave.

FormaatBreedteFractiebits FSchaal 2^FMinimumMaximum (exact)Resolutie
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

Plak een van deze grenswaarden in de converter voor Q-formaat en je krijgt het opgeslagen gehele getal, het hexwoord en het exacte decimale getal terug. Handig om een firmwareconstante na te lopen voordat die de deur uit gaat.

Waarom de bovenkant van het Q15-bereik 0.999969482421875 is

Het bereik van Q15 is asymmetrisch, en die asymmetrie komt uit twee-complement, niet uit iets wat eigen is aan vaste komma. Een signed 16-bits woord dekt de gehele getallen -32768 tot en met 32767. Deel beide uiteinden door 32768 en het bereik wordt -32768/32768 tot en met 32767/32768, oftewel -1 tot en met 0.999969482421875.

1.0 ontbreekt dus met precies één LSB. Het is niet “ongeveer 1.0 is het maximum” of “1.0 met afronding”: de waarde heeft simpelweg geen codering, en een converter die voor Q15 1.0 meldt zonder erbij te zeggen dat hij verzadigd heeft, liegt tegen je.

Waarom -1 er wel bij hoort

Genoeg bronnen schrijven het Q15-bereik als -1 < X < 0.9999695, aan beide kanten open. De ondergrens klopt niet. -32768 ÷ 32768 = -1 precies, dus -1 is wel degelijk representeerbaar, en beide uiteinden van [-1, 0.999969482421875] zijn waarden die je echt kunt bereiken. Je komt het bereik ook tegen als [-1, 1), en dat zegt hetzelfde, alleen afgezet tegen de volle schaal: 1.0 zelf ligt buiten bereik, maar de grootste waarde die wél binnen bereik ligt is 0.999969482421875, en niet iets wat daar net onder zit.

Dat is meer dan een muggenzifterij over notatie. -1 is juist de waarde die de vermenigvuldiging sloopt, want -1 × -1 = 1 en 1 valt buiten het bereik. Wie aanneemt dat -1 onbereikbaar is, schrijft de verzadigingstak niet die dat geval opvangt.

De afgekapte bovengrens 0.9999695 in diezelfde bronnen is een weergaveartefact. Er is niets repeterends aan die waarde: het is 32767/32768, en 32768 is een macht van twee, dus de decimale ontwikkeling stopt na 15 cijfers bij 0.999969482421875.

0.1 past niet in Q15

Schaal het en je ziet het probleem meteen: 0.1 × 32768 = 3276.8. Vaste komma kan alleen gehele getallen opslaan, dus er moet iets wijken.

Met afronden naar dichtstbij is het opgeslagen woord 3277, hex 0x0CCD. De waarde die dat woord voorstelt is:

3277 ÷ 32768 = 0.100006103515625

De kwantisatiefout is het verschil tussen wat je vroeg en wat het raster kon leveren:

0.100006103515625 - 0.1 = +0.000006103515625

Dat is ongeveer zes miljoenste, oftewel een vijfde van een LSB. In een volumeregeling merkt niemand daar iets van. In een integrator die het duizend keer per seconde optelt wel: daar wordt het een gestage drift van ruwweg 0.006 per seconde in de opgetelde waarde.

De fout bij vaste komma is uniform, bij drijvende komma niet

Q15 legt een raster van 65536 punten neer die precies 1/32768 uit elkaar liggen over [-1, 0.999969482421875]. De afstand rond 0.9 is dezelfde als rond 0.0001, dus de absolute fout is overal maximaal een halve LSB. Daarmee wordt foutanalyse saai op de prettigste manier: je begrenst de ruisvloer van een filter met rekenwerk dat een junior kan narekenen.

IEEE 754 doet het omgekeerde. Het houdt een vast aantal significante bits aan en schuift met de exponent, dus het absolute gat tussen twee opeenvolgende doubles groeit mee met de grootte, terwijl de relatieve fout ongeveer constant blijft. Rond 1.0 is dat gat zo’n 2.2e-16; rond 1e12 ongeveer 0.0001220703125.

Zelfde probleem, andere vorm. Een double kan 0.1 net zomin bevatten. Er staat 0.1000000000000000055511151231257827021181583404541015625 in, en daarom levert 0.1 + 0.2 de uitkomst 0.30000000000000004 op. Het artikel over precisie bij drijvende komma werkt dat geval stap voor stap uit. Vaste komma lost decimale breuken in binair niet op; het maakt alleen de grootte van de fout voorspelbaar.

Drie afrondmodi, één LSB uit elkaar

De afrondregel hoort in het datacontract thuis. Twee correcte implementaties die het over afronden oneens zijn, leveren testvectoren op die tot in de eeuwigheid in de laatste bit verschillen, en dat uitzoeken is beroerd werk.

ModusRegel10911.744-3276.8
Afronden naar dichtstbij, halven naar evenHet dichtstbijzijnde rasterpunt; exacte halven gaan naar het even gehele getal10912-3277
Afkappen richting nulLaat het deel achter de komma vallen, de grootte kan alleen kleiner worden10911-3276
Floor richting min oneindigAltijd omlaag langs de getallenlijn10911-3277

De twee kolommen laten zien waarom één voorbeeld nooit genoeg is. Bij een positieve waarde geven afkappen en floor hetzelfde. Bij een negatieve waarde lopen ze een volle LSB uiteen, want afkappen trekt -3276.8 omhoog richting nul en floor duwt hem omlaag.

Het rekenvoorbeeld dat iedereen fout doet

0.333 in Q15 is een goede testwaarde, omdat die dicht bij een grens ligt. 0.333 × 32768 = 10911.744.

  • Afkappen: 10911, wat terugleest als 0.332977294921875
  • Afronden naar dichtstbij: 10912, wat terugleest als 0.3330078125

Oudere handleidingen drukken 10911 af en gaan verder zonder te vermelden welke regel dat getal opleverde. Neem dat getal mee naar een codebase waarvan de encoder afrondt, en je referentievectoren sneuvelen meteen bij de eerste uitvoering, met een verschil van precies één dat eruitziet als een typefout in plaats van als botsende afspraken.

Bij negatieve waarden gaan de drie regels uit elkaar. -0.1 × 32768 = -3276.8 geeft -3276 bij afkappen (waarde -0.0999755859375) en -3277 bij floor of afronden naar dichtstbij (waarde -0.100006103515625). Afkappen en afronden naar dichtstbij behandelen beide tekens gelijk — ±3276 respectievelijk ±3277 — dus een symmetrisch paar coëfficiënten blijft symmetrisch. Floor doet dat niet: die stuurt +0.1 naar 3276 maar -0.1 naar -3277, waardoor het paar er precies één LSB scheef uit komt.

Wat je taal standaard doet

Geen van deze standaardkeuzes is fout. Ze zijn alleen verschillend, en geen enkele meldt zichzelf.

  • C/C++: een cast van drijvende komma naar een integertype kapt af richting nul. (int16_t)(0.333f * 32768) geeft 10911.
  • Python: de ingebouwde round() stuurt halven naar even, dus round(3276.8) is 3277 en round(2.5) is 2.
  • JavaScript: Math.round stuurt halven naar plus oneindig, en dat is niet symmetrisch. Math.round(2.5) is 3, maar Math.round(-2.5) is -2.
  • Hardware: veel MAC-paden (multiply-accumulate) in DSP’s ronden af bij de verschuiving en bieden dichtstbij-even aan als modusbit, en daarom kunnen het C-referentiemodel en het silicium van elkaar afwijken totdat iemand het modusregister uitleest.

Kies één regel, noem hem in de formaatspecificatie naast W en F, en laat de testvectoren hem meedragen.

Overflow: verzadigen of omklappen

Overflow in Q15 doet een van drie dingen: de waarde weigeren, hem begrenzen tot 0.999969482421875, of hem laten omklappen naar -1.0. Welke je krijgt is een keuze die je toolchain heeft gemaakt, en de derde optie draait stilletjes het teken om.

Neem 1.0. Schalen geeft 1.0 × 32768 = 32768, en het grootste signed 16-bits gehele getal is 32767. De waarde valt er precies één buiten.

BeleidOpgeslagen woordTeruggelezen waardeHoe het er verderop uitziet
Foutgeenomzetting geweigerdLuid, af te vangen, meestal het juiste voor tooling
Verzadigen0x7FFF = 327670.999969482421875Met oog of oor niet van 1.0 te onderscheiden
Omklappen0x8000 = -32768-1.0Tekenomkering op volle schaal

De verzadigingsrij verliest 0.000030517578125 en niemand merkt het. De omklaprij maakt van een positieve sample op volle schaal een negatieve op volle schaal, en in een audiopad is dat een klik die je aan de andere kant van de kamer hoort. In een regelkring is het een commando op volle schaal in de verkeerde richting.

Het gevaarlijke aan omklappen is dat het een woord oplevert dat er volstrekt geldig uitziet. 0x8000 is een geldige Q15-codering van -1.0. Verderop in de keten valt het niet te onderscheiden van een echte sample van -1.0, dus er is achteraf geen kenmerk waarop je kunt zoeken; je vindt het alleen door meetpunten te zetten op de plek waar de overflow ontstaat.

Waarom DSP-silicium verzadigende instructies meelevert

Verzadiging is het gedrag dat signaalverwerking wil, dus bouwen processors het in plaats van het aan een sprong in de code over te laten. ARM heeft QADD/QSUB en de verzadigende schuifinstructies SSAT/USAT, NEON heeft VQADD en verwanten, en x86 SSE heeft gepakte verzadigende optellingen zoals paddsw. De C6000- en C55x-families van TI bieden verzadiging aan als modusbit op het accumulatorpad.

In een filter of een mixer is een enkele geknipte sample een kleine, lokale vervorming. Een omgeklapte sample is een sprong met energie over het hele spectrum. De hardware kiest standaard voor een beetje fout boven catastrofaal fout, maar alleen als je het hebt aangezet. Gewone integerberekeningen in C op dezelfde chip klappen nog steeds om.

Waarom een Q15-vermenigvuldiging 15 bits naar rechts moet schuiven

Vermenigvuldig twee Q15-woorden als gehele getallen en het resultaat klopt, maar het is geen Q15 meer. Bij vermenigvuldigen tellen de fractiebits op: Q15 × Q15 geeft Q30.

Werk 0.5 × 0.5 uit, waar het goede antwoord uiteraard 0.25 is:

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

Lees je 268435456 als Q15, dan krijg je 8192.0, en dat zit er een factor 32768 naast. Die factor is precies de bug, en hij verklaart waarom filters met vaste komma die “bijna werken” er vaak een macht van twee naast zitten.

Het product heeft ook ruimte nodig. Twee 16-bits waarden leveren samen maximaal 32 bits op, dus het tussenresultaat moet int32_t zijn. Veel producten optellen vraagt nog meer marge, en daarom zijn de accumulatoren op onderdelen als de C55x 40 bits breed.

Rond de verschuiving af, gooi de bits niet zomaar weg

Een kale >> 15 gooit de onderste 15 bits weg, en dat is bij signed waarden afkappen richting min oneindig. Tel er eerst een halve LSB van de uitgaande precisie bij op en het wordt afronden naar dichtstbij:

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

Gecompileerd met cc -std=c11 -Wall -o q15 q15.c && ./q15 levert dit op:

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

De derde regel is het geval -1 van eerder. -32768 × -32768 = 1073741824, wat in Q30 gelijk is aan 1.0 en buiten het bereik van Q15 valt, dus begrenst sat_q15 het tot 32767. Haal die begrenzing weg en de cast naar int16_t klapt het om naar -32768, waarmee -1 × -1 verandert in -1.

De laatste twee regels laten het verschil in afronding zien. 3277 × 3277 = 10738729, en een kale verschuiving geeft 327 (0.009979248046875), terwijl de afgeronde verschuiving 328 geeft (0.010009765625). Het echte product is 0.01, dus de afgeronde versie komt er ruim twee keer zo dicht bij. Dat kost je één extra optelling.

Eén kanttekening bij de verschuiving zelf: een negatief signed geheel getal naar rechts schuiven is in C vóór C23 implementatieafhankelijk, al voert elke compiler die je tegenkomt een rekenkundige verschuiving uit. Voelt dat ongemakkelijk, deel dan door 32768 en laat de compiler de verschuiving genereren, of doe de verschuiving op een unsigned type nadat je er een bias bij hebt opgeteld. De bredere werking van shifts en maskers staat in de handleiding over bitsgewijze bewerkingen.

Optellen vraagt eerst om dezelfde Q-waarde

Vermenigvuldigen verandert de Q-waarde op een voorspelbare manier. Optellen verdraagt helemaal geen verschil: een Q7-woord bij een Q15-woord optellen levert onzin op, want de operanden delen geen schaal.

Zet ze eerst gelijk met een verschuiving. 0.5 is in Q7 gelijk aan 64, en 64 << 8 is 16384, oftewel 0.5 in Q15. Het aantal posities is het verschil in fractiebits, 15 - 7 = 8.

Omhoog schuiven is exact maar kost marge, want een Q7-waarde die naar Q15 gaat heeft de bredere container nodig. Omlaag schuiven verliest informatie en vraagt om dezelfde afrondkeuze als de vermenigvuldiging. Hoe dan ook: schrijf de Q-waarde van elk tussenresultaat in het commentaar. Code met vaste komma waarin de schalen alleen in het hoofd van de auteur bestaan, is binnen een maand niet meer te onderhouden.

Q15 vaste komma of IEEE 754 drijvende komma: zo kies je

Allebei zijn het binaire positiestelsels, dus in principe heeft geen van beide een voordeel in nauwkeurigheid. De keuze hangt af van wat de doelhardware je kost en welke garanties je nodig hebt.

VraagPleit voor vaste kommaPleit voor drijvende komma
Is er een FPU in hardware?Geen FPU, of alleen een bibliotheek voor soft-floatFPU in hardware met bewerkingen in één klokcyclus
Hoe breed is het dynamisch bereik?Bekend en begrensd, zoals genormaliseerde audioBeslaat vele ordes van grootte
Legt het transportformaat een schaal vast?Protocol of register legt de binaire schaal vastHet veld is een echte float
Moeten resultaten bit-exact zijn over builds heen?Ja, gehele getallen komen overal hetzelfde uitVariatie door FMA en optimalisatie is acceptabel
Is geheugen of bandbreedte krap?16-bits samples halveren de ruimte van 32-bits floatsGeen beperking
Wie onderhoudt de code?Team dat Q-notatie al vloeiend leestGemengd team, schaalfouten zijn dan het grotere risico

Die laatste rij is geen grap. Vaste komma verplaatst de fout van de uitvoering naar de ontwerpfase, en dat is alleen een goede ruil als iemand dat ontwerpwerk ook echt doet. Op een Cortex-M4F met FPU in hardware is een float met enkele precisie vaak de snellere én veiligere keuze, en de oude reflex om naar Q15 te grijpen stamt uit een tijd van chips die het veld niet meer domineren.

Waar IEEE 754 het overneemt

Grijp naar drijvende komma als het woord zelf een teken, een exponent en een significand draagt in plaats van een vaste schaal. Op dat moment houdt Q-formaat op te gelden: er is geen enkele 2^F om door te delen, want de exponent verschilt per waarde.

In de praktijk komen de twee representaties voortdurend samen. Sensordata komt binnen als registerwoorden in Q15, gaat over naar float voor een lange berekening en keert als Q15 terug richting de DAC. Wil je de helft met drijvende komma bit voor bit bekijken, dan splitst de IEEE 754-converter een waarde in teken, exponent en mantisse en toont het exacte opgeslagen decimale getal — hetzelfde werk dat de converter voor Q-formaat doet bij een vaste schaal.

Beide rusten op dezelfde basis

Q-formaat, IEEE 754 en gewone gehele getallen lezen allemaal dezelfde bits, met andere regels over waar de komma staat en of die kan bewegen. Zit het positiestelsel nog niet vast in je hoofd, of wil je sneller 0x0CCD als 0000 1100 1100 1101 kunnen lezen zonder naar een rekenmachine te grijpen, dan behandelt de introductie over binair, hex en octaal omrekenen de basis waarop beide formaten voortbouwen.

Veelgestelde vragen over Q15 vastekommagetallen

Wat betekent Q15?

In de gangbare DSP-afspraak is Q15 een signed 16-bits woord in twee-complement met 15 fractiebits: één positie voor het teken en 15 posities achter de komma. De schaal is 2^15 = 32768, de resolutie is 1/32768 = 0.000030517578125 en het bereik loopt van -1 tot en met 32767/32768. Omdat Q-labels per document verschillen, kun je beter controleren hoe breed het woord is en of het signed is dan blind op het label af te gaan.

Zijn Q15 en Q1.15 hetzelfde?

Meestal beschrijven ze dezelfde signed 16-bits indeling, waarbij de 1 in Q1.15 de tekenpositie meetelt. Maar de notatie is niet universeel: sommige auteurs zetten de tekenbit boven op m in plaats van hem erin mee te tellen. De betrouwbare omschrijving is signed of unsigned, plus het totale aantal bits W, plus het aantal fractiebits F. Voor deze indeling: signed, W=16, F=15.

Wat is 0.5 in Q15?

0.5 is in Q15 het opgeslagen gehele getal 16384, hex 0x4000. Het rekenwerk is 0.5 × 32768 = 16384, en dat is al een geheel getal, dus er is geen afronding en geen kwantisatiefout. Decoderen bevestigt het: 16384 ÷ 32768 = 0.5, precies.

Wat zijn de maximale en minimale waarde van Q15?

Het minimum is -1, en dat hoort erbij, want -32768 ÷ 32768 is precies -1. Het maximum is 32767/32768 = 0.999969482421875. Allebei zijn ze bereikbaar, dus het bereik is [-1, 0.999969482421875]; 1.0 is de waarde die erbuiten valt. Bronnen die de ondergrens als open interval schrijven, hebben het mis, en die fout verbergt juist het overflowgeval -1 × -1.

Wat gebeurt er bij overflow in Q15?

Dat hangt af van het geldende beleid. Een foutbeleid weigert de omzetting. Verzadiging begrenst tot het dichtstbijzijnde eindpunt, dus 1.0 wordt 0.999969482421875, een verlies van één LSB dat je meestal niet hoort. Omklappen past modulo 2^16 toe, dus 1.0 schaalt naar 32768, wat terugleest als -32768 en dus -1.0, een volledige tekenomkering. DSP-hardware kiest precies daarom standaard voor verzadiging, maar gewone integerberekeningen in C klappen om.

Waarom moet je na het vermenigvuldigen van twee Q15-getallen 15 bits naar rechts schuiven?

Omdat fractiebits optellen. Q15 × Q15 levert een product in Q30 op, dus het gehele resultaat draagt 30 fractiebits in plaats van 15. Vijftien posities naar rechts schuiven zet het terug naar Q15: 16384 × 16384 = 268435456, en 268435456 >> 15 = 8192, wat decodeert naar 0.25. Tel er 1 << 14 bij op vóór de verschuiving om af te ronden naar dichtstbij in plaats van af te kappen, en houd het tussenresultaat in een int32_t zodat het 32-bits product niet overloopt.

Wanneer gebruik je Q-formaat in plaats van IEEE 754?

Gebruik Q-formaat als een protocol, een registerkaart, een DSP-algoritme of een codec de binaire schaal voor een integerwoord al heeft vastgelegd. De schaal hoort dan bij de interface en je mag hem niet zelf kiezen. Gebruik IEEE 754 als de waarde een breed dynamisch bereik nodig heeft, als het doelplatform een FPU in hardware heeft, of als het veld echt een teken, exponent en significand opslaat. Voor een kale wisseling van talstelsel zonder schaal geldt geen van beide; dat is gewoon omrekenen tussen talstelsels.

Kort samengevat

Q15 met vaste komma is een vermenigvuldiging, een afrondkeuze en een bereikcontrole. raw = round(value × 32768) erin, value = raw ÷ 32768 eruit. De formule is triviaal; de fouten zitten allemaal in de stukken die niemand vastlegt.

Leg ze dus vast. Schrijf naast elk Q-label of het getal signed is, plus W en F, in plaats van aan te nemen dat het label genoeg zegt. Noem de afrondmodus op dezelfde plek, want bij negatieve waarden lopen afkappen en floor een volle LSB uiteen. Zeg met zoveel woorden of overflow een fout geeft, verzadigt of omklapt, want het omklapgeval maakt van 1.0 een -1.0 en laat geen enkel spoor achter.

Spreken een registerwoord en een spreadsheet elkaar tegen, decodeer dan één bekende waarde in de converter voor Q-formaat. Die zet het opgeslagen gehele getal, het exacte decimale getal, de kwantisatiefout en het representeerbare bereik naast elkaar, en dat rekenwerk gebeurt in je browser. Meestal is dat genoeg om te zien wiens aanname over W en F fout was.

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

Gerelateerde artikelen

Alle artikelen bekijken