Liczby stałoprzecinkowe Q15: Q7, Q31, zaokrąglanie i przepełnienie
Format stałoprzecinkowy Q15 przechowuje ułamek jako zwykłą 16-bitową liczbę całkowitą ze znakiem, z domyślną skalą binarną 2^15 = 32768. Kodowanie to jedno mnożenie i jedno zaokrąglenie:
raw = round(value × 32768)
Dekodowanie to jedno dzielenie:
value = raw ÷ 32768
Na tym cała arytmetyka się kończy. 0.5 × 32768 = 16384, więc 0.5 leży w pamięci jako liczba całkowita 16384, a 16384 ÷ 32768 zwraca dokładnie 0.5. Nie ma tu pola wykładnika ani ukrytego bitu. Przecinek binarny jest umową między tym, kto zapisuje słowo, a tym, kto je odczytuje; sprzęt widzi wyłącznie int16.
Z tego wzoru wynikają dwie konsekwencje i to na nie zwykle schodzi całe popołudnie.
0.1 × 32768 = 3276.8, a to nie jest liczba całkowita. Q15 zapamiętuje 3277, więc po odczycie wraca 0.100006103515625, a nie 0.1.
1.0 × 32768 = 32768, czyli o jeden więcej niż największa 16-bitowa liczba całkowita ze znakiem. 1.0 nie ma więc w Q15 żadnego kodowania, a to, co zrobi z tym kod, zależy od reguły, której większość projektów nigdy nie zapisuje. Przy nasyceniu wychodzi 0.999969482421875. Przy zawinięciu wychodzi -1.0.
Reszta tekstu krąży wokół tych dwóch awarii: skąd bierze się niejednoznaczność notacji Q, jak przeliczać w obie strony na kartce i po czym poznać, którą regułę zaokrąglania oraz przepełnienia po cichu wybrał używany toolchain.
Q15, Q1.15, Qm.n: czy to ten sam układ bitów?
Trzy źródła zapytane o znaczenie Q15 potrafią dać trzy różne odpowiedzi. Powód jest prozaiczny: notacja nigdy nie została ustandaryzowana, a cały spór toczy się o jeden bit.
Jak czytać Qm.n
Zapis dwuczłonowy nie zostawia miejsca na domysły. W Qm.n n to liczba bitów ułamkowych, a m to liczba bitów całkowitych. W tym tekście bit znaku liczy się wewnątrz m — to właśnie ta konwencja czyni z Q16.16 słowo 32-bitowe: 16 bitów całkowitych łącznie ze znakiem, 16 bitów ułamkowych, skala 2^16 = 65536. Zakodowanie 1.5 w Q16.16 daje 1.5 × 65536 = 98304, czyli hex 0x00018000, bez żadnego błędu zaokrąglenia.
Kłopot zaczyna się przy bicie znaku. Jedni autorzy liczą go wewnątrz m, inni doliczają ponad. Q1.15 w pierwszej konwencji to słowo 16-bitowe: jedna pozycja znaku i części całkowitej plus 15 bitów ułamkowych. W drugiej konwencji ta sama etykieta opisuje 17 bitów, których nie ma żadna maszyna.
Dlaczego ta sama etykieta Q15 oznacza różne szerokości w różnych dokumentach
Zapis jednoczłonowy Q15 pomija m w całości i pozostawia szerokość domyślną. W praktyce DSP oznacza on niemal zawsze 16-bitowe słowo ze znakiem w kodzie uzupełnień do dwóch, z 15 bitami ułamkowymi. Takie znaczenie przyjęły ekosystemy TI i ARM całe dekady temu.
W sieci krążą jednak szeroko kopiowane wyjaśnienia w rodzaju: Q15 oznacza 15 bitów ułamkowych, więc jeśli zdefiniujemy liczbę 32-bitową, mamy 1 bit znaku i 16 bitów całkowitych. Przy uważnej lekturze widać, że bit znaku doliczono tam ponad m, a nie wewnątrz, czyli sięgnięto po konwencję odwrotną niż przyjęta tutaj. Opisany układ istnieje naprawdę i zapisuje się go zwykle jako Q16.15. Błędna jest sama etykieta: 32-bitowe słowo z 16 bitami całkowitymi nie jest Q15 w żadnej z tych konwencji. Ten sam akapit powielono na tylu blogach, że przy części zapytań wyprzedza dziś poprawną definicję.
Samotne Q15 w nieznanym dokumencie warto traktować jak hipotezę, nie jak fakt. Sprawdzić je można na czymś mierzalnym: szerokości rejestru w mapie pamięci, typie C w pliku nagłówkowym (header) sterownika albo na znanej wartości wzorcowej.
Opis, który przetrwa zderzenie z cudzym kodem
Wystarczy zapisać trzy rzeczy, a niejednoznaczność znika:
- Znak — ze znakiem w kodzie uzupełnień do dwóch albo bez znaku
- W — łączna liczba bitów w słowie
- F — liczba bitów ułamkowych
signed, W=16, F=15 nie da się odczytać na dwa sposoby. Tak samo unsigned, W=32, F=16. Reszta jest pochodna: skala to 2^F, rozdzielczość to 2^-F, a zakres to całkowitoliczbowy zakres słowa podzielony przez 2^F. Te trzy wartości wpisane do dokumentu protokołu i do komentarza przy strukturze wystarczą, żeby spór nie wrócił.
Dlatego internetowy konwerter stałoprzecinkowy formatu Q wypisuje przy każdym wyniku znak, W, F oraz skalę. Gdy nota katalogowa mówi „Q15”, a parser kolegi z zespołu twierdzi co innego, zdekodowanie jednego znanego słowa rozstrzyga sprawę w jakieś dziesięć sekund.
Wzór konwersji, policzony ręcznie
Obie strony przeliczenia są na tyle krótkie, że da się je zrobić na kartce, co się przydaje, gdy na ekranie oscyloskopu widnieje surowy zrzut szesnastkowy.
Z float na fixed: round(x × 2^F)
Przeniesienie 0.5 do Q15 wygląda tak:
- Skalowanie:
0.5 × 32768 = 16384 - Zaokrąglenie: to już liczba całkowita, więc
16384zostaje bez zmian - Kontrola zakresu: 16 bitów ze znakiem mieści wartości od -32768 do 32767, a 16384 się w nich mieści
- Zapis:
16384, hex0x4000, binarnie0100000000000000
Informację traci tylko krok 2, a zawieść może tylko krok 3. Wszystko, co w arytmetyce stałoprzecinkowej ciekawe, dzieje się w jednym z tych dwóch miejsc.
Z fixed na float: raw ÷ 2^F
Teraz w drugą stronę, zaczynając od zrzutu rejestru, w którym pole Q15 ze znakiem pokazuje 0xC000.
- Odczyt tekstu jako 16-bitowego kodu bez znaku:
0xC000 = 49152 - Format jest ze znakiem, a najstarszy bit jest ustawiony, więc odejmujemy 2^16:
49152 - 65536 = -16384 - Skalowanie w dół:
-16384 ÷ 32768 = -0.5
Krok 2 jest tym, który zwykle wypada. Bez korekty uzupełnień do dwóch 0xC000 czyta się jako +1.5, a taka wartość nie mieści się nawet w zakresie Q15. To przydatne ostrzeżenie: jeśli zdekodowana wartość leży poza zakresem formatu, niemal na pewno pominięto krok ze znakiem.
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 odgrywa tu realną rolę. Skalowanie najpierw przez float wprowadziłoby z powrotem zaokrąglenie binarne dokładnie w chwili, w której próbujemy je zmierzyć, a Fraction(str(x)) czyta wpisany literał dziesiętny, a nie najbliższą mu liczbę double.
Odczyt i zapis słowa szesnastkowego
W podglądzie rejestrów wartości te pojawiają się w postaci szesnastkowej, a przeliczenie jest mechaniczne: 3277 w hex to 0xCCD, uzupełnione do W/4 cyfr daje 0x0CCD. Uzupełniać trzeba zawsze. Słowo Q15 ma cztery cyfry szesnastkowe, a słowo Q31 osiem; pominięcie wiodącego zera to typowy sposób na rozjechanie wartości w dekoderze wsadowym.
Surowe kody w hex lepiej też trzymać bez znaku. 0x8000 to najbardziej ujemne słowo Q15, a nie +32768, i zapis -0x8000 nikomu nie pomaga. Kolejność bajtów to osobna sprawa: format Q określa skalowanie liczbowe i nie mówi nic o endianness, więc zrzut 0x0CCD w porządku little-endian przychodzi jako bajty CD 0C. Do zwykłej zmiany podstawy przy czytaniu zrzutu wystarczy konwerter systemów liczbowych, który obsługuje system binarny, ósemkowy i szesnastkowy bez doczepionej skali i szerokości ze znakiem.
Q7, Q15, Q31 i Q16.16: zakres i rozdzielczość
Każda liczba w tej tabeli to -2^(W-1) podzielone przez 2^F na jednym końcu i (2^(W-1) - 1) podzielone przez 2^F na drugim. Wartości są dokładne, nie zaokrąglone na potrzeby wyświetlania.
| Format | Szerokość | Bity ułamkowe F | Skala 2^F | Minimum | Maksimum (dokładne) | Rozdzielczość |
|---|---|---|---|---|---|---|
| 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 |
Po wklejeniu dowolnej z tych wartości brzegowych do konwertera stałoprzecinkowego formatu Q wracają zapisana liczba całkowita, słowo szesnastkowe i dokładna wartość dziesiętna. Tyle wystarczy, żeby potwierdzić stałą w firmware, zanim trafi ona do wydania.
Dlaczego górna granica zakresu Q15 to 0.999969482421875
Zakres formatu Q15 jest niesymetryczny, a bierze się to z kodu uzupełnień do dwóch, nie z czegokolwiek swoistego dla arytmetyki stałoprzecinkowej. 16-bitowe słowo ze znakiem obejmuje liczby całkowite od -32768 do 32767. Po podzieleniu obu końców przez 32768 zakres staje się przedziałem od -32768/32768 do 32767/32768, czyli od -1 do 0.999969482421875.
Do 1.0 brakuje więc dokładnie jednego LSB. To nie jest „maksimum mniej więcej równe 1.0” ani „1.0 po zaokrągleniu”: ta wartość nie ma kodowania, a konwerter, który dla Q15 zwraca 1.0 bez informacji o nasyceniu, zwyczajnie kłamie.
Dlaczego -1 należy do zakresu
Wiele źródeł zapisuje zakres Q15 jako -1 < X < 0.9999695, z nawiasem otwartym po obu stronach. Dolna granica jest błędna. -32768 ÷ 32768 = -1 dokładnie, więc -1 jest reprezentowalne, a oba końce przedziału [-1, 0.999969482421875] są wartościami osiągalnymi. Spotyka się też zapis [-1, 1), który mówi dokładnie to samo, tyle że w odniesieniu do pełnej skali: samo 1.0 jest poza zasięgiem, ale największą wartością w zasięgu jest 0.999969482421875, a nie jakaś liczba leżąca tuż pod nią.
Konsekwencja jest praktyczna. -1 jest tą wartością, która rozbija mnożenie, bo -1 × -1 = 1, a 1 leży poza zakresem. Kto założy, że -1 jest nieosiągalne, ten nie napisze gałęzi nasycenia, która ten przypadek wyłapie.
Skrócona górna granica 0.9999695 w tych samych źródłach to artefakt wyświetlania. W tej wartości nie ma nic okresowego: to 32767/32768, a 32768 jest potęgą dwójki, więc rozwinięcie dziesiętne kończy się po 15 cyfrach na 0.999969482421875.
0.1 nie mieści się w Q15
Wystarczy przeskalować i problem widać od razu: 0.1 × 32768 = 3276.8. Zapis stałoprzecinkowy przechowuje wyłącznie liczby całkowite, więc coś musi ustąpić.
Przy zaokrąglaniu do najbliższej zapisane słowo to 3277, czyli hex 0x0CCD. Wartość, którą to słowo reprezentuje, wynosi:
3277 ÷ 32768 = 0.100006103515625
Błąd kwantyzacji to różnica między wartością żądaną a tym, co siatka była w stanie dać:
0.100006103515625 - 0.1 = +0.000006103515625
To około sześć milionowych, czyli jedna piąta LSB. W regulacji głośności nieszkodliwe. Już nie tak nieszkodliwe w integratorze, który dodaje ten błąd tysiąc razy na sekundę: tam zamienia się on w stały dryf rzędu 0.006 na sekundę w akumulowanej wartości.
Błąd stałoprzecinkowy jest równomierny, zmiennoprzecinkowy nie
Q15 rozkłada siatkę 65536 punktów oddalonych od siebie dokładnie o 1/32768 na przedziale [-1, 0.999969482421875]. Odstęp w okolicy 0.9 jest taki sam jak w okolicy 0.0001, więc najgorszy możliwy błąd bezwzględny wynosi wszędzie pół LSB. Analiza błędu robi się dzięki temu nudna, co akurat jest zaletą: poziom szumu filtra da się ograniczyć rachunkiem, który sprawdzi początkujący inżynier.
IEEE 754 robi odwrotnie. Utrzymuje stałą liczbę bitów znaczących i przesuwa wykładnik, więc bezwzględny odstęp między sąsiednimi liczbami double rośnie wraz z rzędem wielkości, podczas gdy błąd względny pozostaje niemal stały. W okolicy 1.0 odstęp ten wynosi około 2.2e-16, a w okolicy 1e12 już około 0.0001220703125.
Kłopot jest zresztą ten sam, tylko inaczej rozłożony. Liczba double też nie pomieści 0.1. Przechowuje 0.1000000000000000055511151231257827021181583404541015625 i dlatego 0.1 + 0.2 zwraca 0.30000000000000004. Ten przypadek rozłożono na części w towarzyszącym tekście o precyzji liczb zmiennoprzecinkowych. Zapis stałoprzecinkowy nie rozwiązuje problemu ułamków dziesiętnych w systemie binarnym, sprawia tylko, że rozmiar błędu staje się przewidywalny.
Trzy tryby zaokrąglania, różnica jednego LSB
Reguła zaokrąglania należy do kontraktu danych, a nie do szczegółów implementacji. Dwie poprawne implementacje różniące się zaokrąglaniem będą w nieskończoność produkować wektory testowe rozjeżdżające się na ostatnim bicie, a tropienie tego to niewdzięczna robota.
| Tryb | Reguła | 10911.744 | -3276.8 |
|---|---|---|---|
| Zaokrąglanie do najbliższej, połówki do parzystej | Najbliższy punkt siatki; dokładne połówki idą do liczby parzystej | 10912 | -3277 |
| Obcięcie w stronę zera | Odrzucenie części ułamkowej, moduł wyłącznie maleje | 10911 | -3276 |
| Podłoga w stronę minus nieskończoności | Zawsze w dół osi liczbowej | 10911 | -3277 |
Dwie kolumny pokazują, dlaczego jeden przykład nigdy nie wystarcza. Dla wartości dodatniej obcięcie i podłoga dają to samo. Dla ujemnej rozjeżdżają się o pełny LSB, bo obcięcie podciąga -3276.8 w stronę zera, a podłoga spycha ją w dół.
Przykład, na którym najłatwiej się pomylić
0.333 w Q15 to dobry przypadek testowy, bo leży blisko granicy. 0.333 × 32768 = 10911.744.
- Obcięcie:
10911, co po odczycie daje0.332977294921875 - Zaokrąglenie do najbliższej:
10912, co po odczycie daje0.3330078125
Starsze poradniki wypisują 10911 i idą dalej, nie mówiąc, która reguła je wyprodukowała. Ta sama liczba przeniesiona do projektu, którego koder zaokrągla, wywraca wzorcowe wektory testowe przy pierwszym uruchomieniu, a różnica o jeden wygląda wtedy na literówkę, a nie na rozjazd reguł.
Na liczbach ujemnych trzy reguły się rozchodzą. -0.1 × 32768 = -3276.8 daje -3276 przy obcięciu (wartość -0.0999755859375) i -3277 przy podłodze albo zaokrągleniu do najbliższej (wartość -0.100006103515625). Obcięcie i zaokrąglenie do najbliższej traktują oba znaki tak samo — odpowiednio ±3276 i ±3277 — więc symetryczna para współczynników pozostaje symetryczna. Podłoga już nie: +0.1 trafia na 3276, a -0.1 na -3277, więc para rozjeżdża się o jedną jednostkę.
Co robi domyślnie dany język
Żadne z tych domyślnych zachowań nie jest błędne. Po prostu się różnią i żadne nie ostrzega o sobie z góry.
- C/C++: rzutowanie z typu zmiennoprzecinkowego na całkowity obcina w stronę zera.
(int16_t)(0.333f * 32768)daje10911. - Python: wbudowane
round()stosuje regułę połówek do parzystej, więcround(3276.8)to3277, around(2.5)to2. - JavaScript:
Math.roundrozstrzyga połówki w stronę plus nieskończoności, czyli niesymetrycznie.Math.round(2.5)to3, aleMath.round(-2.5)to-2. - Sprzęt: wiele ścieżek mnożąco-akumulujących w DSP zaokrągla przy przesunięciu i udostępnia tryb „do najbliższej parzystej” jako bit konfiguracyjny, więc referencyjny model w C i krzem potrafią się nie zgadzać, dopóki ktoś nie zajrzy do rejestru trybu.
Wybrać jedną regułę, nazwać ją w specyfikacji formatu obok W i F, a potem zaszyć w wektorach testowych.
Przepełnienie: nasycenie kontra zawijanie
Przepełnienie w Q15 kończy się na jeden z trzech sposobów: odrzuceniem wartości, przycięciem jej do 0.999969482421875 albo zawinięciem do -1.0. O tym, który z nich zadziała, decyduje reguła wybrana przez toolchain, a trzecia możliwość po cichu odwraca znak.
Weźmy 1.0. Skalowanie daje 1.0 × 32768 = 32768, a największa 16-bitowa liczba całkowita ze znakiem to 32767. Wartość wykracza poza zakres dokładnie o jeden.
| Reguła | Zapisane słowo | Odczytana wartość | Jak to wygląda dalej w łańcuchu |
|---|---|---|---|
| Błąd | brak | konwersja odrzucona | Głośne i łatwe do wyłapania, zwykle właściwe dla narzędzi |
| Nasycenie | 0x7FFF = 32767 | 0.999969482421875 | Nie do odróżnienia od 1.0 ani uchem, ani okiem |
| Zawinięcie | 0x8000 = -32768 | -1.0 | Odwrócenie znaku przy pełnej skali |
Wiersz z nasyceniem gubi 0.000030517578125 i nikt tego nie zauważa. Wiersz z zawinięciem zamienia dodatnią próbkę o pełnej skali w ujemną o pełnej skali, a w torze audio jest to trzask słyszalny z drugiego końca pokoju. W pętli regulacji to komenda o pełnej skali w złą stronę.
Groźną cechą zawijania jest to, że powstaje z niego słowo wyglądające całkowicie poprawnie. 0x8000 jest legalnym kodowaniem -1.0 w Q15. Nic dalej w łańcuchu nie odróżni go od prawdziwej próbki -1.0, więc po fakcie nie ma czego szukać grepem. Jedyne wyjście to oprzyrządować miejsce, w którym dochodzi do przepełnienia.
Dlaczego krzem DSP dostarcza instrukcje nasycające
Nasycenie to zachowanie, którego oczekuje przetwarzanie sygnałów, więc procesory realizują je sprzętowo, zamiast zostawiać to instrukcji warunkowej. ARM ma QADD/QSUB oraz nasycające instrukcje przesunięcia SSAT/USAT, NEON ma VQADD i pokrewne, a x86 SSE — pakowane dodawania z nasyceniem, takie jak paddsw. Rodziny C6000 i C55x od TI wystawiają nasycenie jako bit trybu na ścieżce akumulatora.
W filtrze albo w mikserze sporadycznie przycięta próbka to drobne, lokalne zniekształcenie. Próbka zawinięta to nieciągłość niosąca energię w całym paśmie. Sprzęt domyślnie wybiera „trochę źle” zamiast „katastrofalnie źle”, ale tylko wtedy, gdy ktoś to włączył, bo zwykła arytmetyka całkowitoliczbowa w C na tym samym układzie nadal zawija.
Dlaczego mnożenie w Q15 wymaga przesunięcia o 15 bitów w prawo
Przemnożenie dwóch słów Q15 jak liczb całkowitych daje poprawny wynik, tyle że nie jest to już Q15. Bity ułamkowe podczas mnożenia się sumują: Q15 × Q15 daje Q30.
Prześledźmy 0.5 × 0.5, gdzie poprawna odpowiedź to oczywiście 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
Zinterpretowanie 268435456 jako Q15 dałoby odczyt 8192.0, czyli wynik zawyżony 32768 razy. Cały błąd sprowadza się do tego czynnika i dlatego filtry stałoprzecinkowe, które „prawie działają”, chybiają zwykle o potęgę dwójki.
Iloczyn potrzebuje też miejsca. Dwie wartości 16-bitowe mnożą się do nawet 32 bitów, więc wynik pośredni musi być typu int32_t. Akumulowanie wielu iloczynów wymaga jeszcze większego zapasu, dlatego akumulatory DSP w układach takich jak C55x mają 40 bitów.
Przesunięcie z zaokrągleniem zamiast wyrzucania bitów
Samo >> 15 wyrzuca 15 najmłodszych bitów, co dla wartości ze znakiem jest obcięciem w stronę minus nieskończoności. Dodanie najpierw połowy LSB wyjściowej precyzji zamienia to w zaokrąglenie do najbliższej:
#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;
}
Skompilowane poleceniem cc -std=c11 -Wall -o q15 q15.c && ./q15 wypisuje:
0.5*0.5 -> 8192
0.1*0.1 -> 328
-1*-1 -> 32767
no-round -> 327
Trzeci wiersz to omawiany wcześniej przypadek -1. -32768 × -32768 = 1073741824, czyli 1.0 w Q30 i wartość poza zakresem Q15, więc sat_q15 przycina ją do 32767. Bez tego przycięcia rzutowanie na int16_t zawija ją do -32768, więc -1 × -1 daje -1.
Dwa ostatnie wiersze pokazują różnicę w zaokrąglaniu. 3277 × 3277 = 10738729, a samo przesunięcie daje 327 (0.009979248046875), podczas gdy przesunięcie z zaokrągleniem daje 328 (0.010009765625). Prawdziwy iloczyn to 0.01, więc wersja z zaokrągleniem trafia ponad dwa razy bliżej. Kosztuje to jedno dodatkowe dodawanie.
Jedno zastrzeżenie do samego przesunięcia: przesuwanie w prawo ujemnej liczby całkowitej ze znakiem jest w C przed C23 zależne od implementacji, choć każdy spotykany w praktyce kompilator wykonuje przesunięcie arytmetyczne. Kogo to niepokoi, ten może podzielić przez 32768 i zostawić wygenerowanie instrukcji kompilatorowi albo policzyć wszystko na typie bez znaku, dodając wcześniej przesunięcie bazowe. Szerzej mechanikę przesunięć i masek omawia przewodnik po operacjach bitowych.
Dodawanie wymaga najpierw zgodnych wartości Q
Mnożenie zmienia wartość Q w przewidywalny sposób. Dodawanie nie toleruje rozbieżności w ogóle: dodanie słowa Q7 do słowa Q15 daje bzdurę, bo operandy nie mają wspólnej skali.
Najpierw trzeba je zrównać przesunięciem. 0.5 w Q7 to 64, a 64 << 8 to 16384, czyli 0.5 w Q15. Wielkość przesunięcia to różnica bitów ułamkowych, 15 - 7 = 8.
Przesunięcie w górę jest dokładne, ale kosztuje zapas, bo wartość Q7 podniesiona do Q15 potrzebuje szerszego pojemnika. Przesunięcie w dół jest stratne i wymaga tej samej decyzji o zaokrąglaniu co mnożenie. Tak czy inaczej, wartość Q każdego wyniku pośredniego powinna trafić do komentarza. Kod stałoprzecinkowy, w którym skale istnieją wyłącznie w głowie autora, po miesiącu przestaje nadawać się do utrzymania.
Stałoprzecinkowy Q15 czy zmiennoprzecinkowy IEEE 754: jak wybrać
Oba są binarnymi systemami pozycyjnymi, więc z zasady żaden nie ma przewagi w dokładności. Wybór sprowadza się do tego, ile liczy sobie za to docelowy sprzęt i jakich gwarancji się potrzebuje.
| Pytanie | Przemawia za stałoprzecinkowym | Przemawia za zmiennoprzecinkowym |
|---|---|---|
| Czy jest sprzętowa FPU? | Brak FPU albo biblioteka soft-float | Sprzętowa FPU z operacjami w jednym takcie |
| Jak szeroka jest dynamika? | Znana i ograniczona, jak znormalizowane audio | Rozciąga się na wiele rzędów wielkości |
| Czy format transmisji definiuje skalę? | Protokół albo rejestr ustala skalę binarną | Pole jest prawdziwą liczbą float |
| Czy wyniki muszą być bitowo identyczne między buildami? | Tak, liczby całkowite odtwarzają się wszędzie | Można tolerować różnice od FMA i optymalizacji |
| Czy pamięć albo pasmo są na styk? | 16-bitowe próbki zajmują połowę tego, co 32-bitowe liczby float | To nie jest ograniczenie |
| Kto utrzymuje kod? | Zespół biegły w notacji Q | Zespół mieszany, większym ryzykiem są błędy skalowania |
Ostatni wiersz nie jest żartem. Zapis stałoprzecinkowy przenosi błąd z czasu wykonania do fazy projektowania, a to opłacalna zamiana tylko wtedy, gdy ktoś tę fazę projektowania faktycznie przeprowadza. Na układzie Cortex-M4F ze sprzętową FPU pojedyncza precyzja bywa i szybsza, i bezpieczniejsza, a tradycyjny odruch sięgania po Q15 to nawyk z czasów układów, które nie są już dominujące.
Kiedy pałeczkę przejmuje IEEE 754
Po zapis zmiennoprzecinkowy warto sięgnąć wtedy, gdy samo słowo niesie znak, wykładnik i mantysę zamiast stałej skali. W tym momencie format Q przestaje mieć zastosowanie: nie ma jednego 2^F, przez które można podzielić, bo wykładnik zmienia się dla każdej wartości.
W praktyce obie reprezentacje spotykają się bez przerwy. Dane z czujnika przychodzą jako słowa rejestrowe Q15, awansują do typu float na czas dłuższego obliczenia i wracają jako Q15 do przetwornika DAC. Do obejrzenia zmiennoprzecinkowej połowy tej ścieżki bit po bicie służy konwerter IEEE 754, który rozkłada wartość na znak, wykładnik i mantysę oraz wypisuje dokładną zapisaną wartość dziesiętną, czyli robi to samo, co konwerter formatu Q dla stałej skali.
Oba stoją na tym samym fundamencie
Format Q, IEEE 754 i zwykłe liczby całkowite czytają te same bity, różniąc się jedynie regułą mówiącą, gdzie stoi przecinek i czy wolno mu się przesuwać. Jeśli arytmetyka pozycyjna wydaje się chwiejna albo przydałaby się większa wprawa w odczytywaniu 0x0CCD jako 0000 1100 1100 1101 bez sięgania po kalkulator, wprowadzenie do konwersji systemów liczbowych: binarnego, szesnastkowego i ósemkowego omawia podstawy, na których stoją oba formaty.
FAQ: liczby stałoprzecinkowe Q15
Co oznacza Q15?
W powszechnej konwencji DSP Q15 to 16-bitowe słowo ze znakiem w kodzie uzupełnień do dwóch, z 15 bitami ułamkowymi: jedna pozycja znaku i 15 pozycji ułamkowych. Skala wynosi 2^15 = 32768, rozdzielczość 1/32768 = 0.000030517578125, a zakres sięga od -1 do 32767/32768. Ponieważ ta sama etykieta Q znaczy w różnych dokumentach co innego, zamiast jej ufać lepiej potwierdzić łączną szerokość słowa i obecność znaku.
Czy Q15 i Q1.15 to to samo?
Zwykle opisują ten sam 16-bitowy układ ze znakiem, w którym 1 w Q1.15 odpowiada pozycji znaku. Notacja nie jest jednak uniwersalna i część autorów dolicza bit znaku ponad m, zamiast liczyć go wewnątrz. Wiarygodny opis to obecność znaku plus łączna liczba bitów W plus liczba bitów ułamkowych F: dla tego układu signed, W=16, F=15.
Ile wynosi 0.5 w Q15?
Wartość 0.5 w Q15 to zapisana liczba całkowita 16384, szesnastkowo 0x4000. Rachunek to 0.5 × 32768 = 16384, a wynik jest już liczbą całkowitą, więc nie ma tu ani zaokrąglenia, ani błędu kwantyzacji. Dekodowanie to potwierdza: 16384 ÷ 32768 = 0.5 dokładnie.
Jakie są wartości maksymalna i minimalna Q15?
Minimum to -1 i należy ono do zakresu, bo -32768 ÷ 32768 daje dokładnie -1. Maksimum to 32767/32768 = 0.999969482421875. Obie te wartości są osiągalne, więc zakres to [-1, 0.999969482421875]; poza nim leży dopiero 1.0. Źródła zapisujące dolną granicę jako przedział otwarty się mylą, a ten błąd ukrywa przypadek przepełnienia -1 × -1.
Co się dzieje przy przepełnieniu Q15?
Zależy to od obowiązującej reguły. Reguła błędu odrzuca konwersję. Nasycenie przycina do najbliższego krańca, więc 1.0 staje się 0.999969482421875, co kosztuje jeden LSB i zwykle jest niesłyszalne. Zawijanie działa modulo 2^16, więc 1.0 skaluje się do 32768, co odczytuje się jako -32768, a zatem -1.0 — pełne odwrócenie znaku. Sprzęt DSP domyślnie nasyca dokładnie z tego powodu, ale zwykła arytmetyka całkowitoliczbowa w C zawija.
Dlaczego po przemnożeniu dwóch liczb Q15 trzeba przesunąć wynik o 15 bitów w prawo?
Bo bity ułamkowe się sumują. Q15 × Q15 daje iloczyn w Q30, więc wynik całkowitoliczbowy niesie 30 bitów ułamkowych zamiast 15. Przesunięcie w prawo o 15 wyrównuje go z powrotem do Q15: 16384 × 16384 = 268435456, a 268435456 >> 15 = 8192, co dekoduje się do 0.25. Dodanie 1 << 14 przed przesunięciem daje zaokrąglenie do najbliższej zamiast obcięcia, a wynik pośredni należy trzymać w int32_t, żeby 32-bitowy iloczyn się nie przepełnił.
Kiedy używać formatu Q zamiast IEEE 754?
Formatu Q warto użyć wtedy, gdy protokół, mapa rejestrów, algorytm DSP albo kodek już ustaliły skalę binarną dla słowa całkowitoliczbowego. Skala jest wtedy częścią interfejsu i nie podlega wyborowi. Po IEEE 754 sięga się wtedy, gdy wartość potrzebuje szerokiej dynamiki, gdy układ docelowy ma sprzętową FPU albo gdy pole faktycznie przechowuje znak, wykładnik i mantysę. Przy zwykłej zmianie podstawy, bez doczepionej skali, nie stosuje się żaden z nich; to zwyczajna konwersja systemów liczbowych.
W skrócie
Stałoprzecinkowy Q15 to mnożenie, decyzja o zaokrągleniu i kontrola zakresu. raw = round(value × 32768) na wejściu, value = raw ÷ 32768 na wyjściu. Wzór jest trywialny; wszystkie awarie mieszkają w tych częściach, których nikt nie dokumentuje.
Warto je więc udokumentować. Obok każdej etykiety Q zapisać obecność znaku, W i F, zamiast zakładać, że sama etykieta wystarczy. W tym samym miejscu nazwać tryb zaokrąglania, bo obcięcie i podłoga rozjeżdżają się na liczbach ujemnych o pełny LSB. Wprost powiedzieć, czy przepełnienie zgłasza błąd, nasyca czy zawija, bo zawinięcie zamienia 1.0 w -1.0 i nie zostawia po sobie żadnego śladu.
Gdy słowo z rejestru i arkusz kalkulacyjny podają co innego, wystarczy zdekodować jedną znaną wartość w konwerterze stałoprzecinkowym formatu Q. Pokazuje on obok siebie zapisaną liczbę całkowitą, dokładną wartość dziesiętną, błąd kwantyzacji i zakres reprezentowalny, a wszystko liczy w przeglądarce. Zwykle to wystarczy, by ustalić, czyje założenie o W i F było błędne.