Фиксированная точка Q15: перевод, округление и переполнение
Фиксированная точка Q15 (fixed point) хранит дробь как обычное 16-битное знаковое целое с подразумеваемым двоичным масштабом 2^15 = 32768. Кодирование — это одно умножение плюс один шаг округления:
raw = round(value × 32768)
Декодирование — одно деление:
value = raw ÷ 32768
Больше в кодировании ничего нет: 0.5 × 32768 = 16384, поэтому 0.5 лежит в памяти как целое 16384, а 16384 ÷ 32768 возвращает ровно 0.5. Ни поля порядка, ни скрытого бита здесь нет. Положение двоичной точки — это соглашение между вами и тем, кто будет читать это слово; железо всегда видит только int16.
Из формулы следуют два вывода, и на каждом из них кто-нибудь регулярно теряет полдня.
0.1 × 32768 = 3276.8 — не целое число. Q15 оставляет 3277, и обратно вы читаете 0.100006103515625, а не 0.1.
1.0 × 32768 = 32768 — на единицу больше наибольшего знакового 16-битного целого. Значит, у 1.0 вообще нет кодировки в Q15, а что с этим сделает ваш код, зависит от политики, которую в большинстве проектов нигде не записывают. Насыщение (saturation) даст 0.999969482421875. Заворачивание (wraparound) даст -1.0.
Оба случая упираются в то, чего в формуле не видно: в чтение Q-нотации, когда два даташита противоречат друг другу, в перевод вручную в обе стороны и в правило округления и переполнения, которое ваш тулчейн выбрал молча.
Q15, Q1.15, Qm.n — это одна и та же раскладка?
Спросите три источника, что такое Q15, и получите три разных ответа. Дело не в том, что вы их неправильно читаете. Нотацию так и не стандартизировали, и спор идёт об одном-единственном бите.
Как читать Qm.n
Двухчисловая форма — честная. В Qm.n n — это количество дробных битов, а m — количество целых. В этой статье знаковая позиция считается внутри m — именно при таком прочтении Q16.16 оказывается 32-битным словом: 16 целых битов вместе со знаковым, 16 дробных, масштаб 2^16 = 65536. Закодируйте 1.5 в Q16.16 — получится 1.5 × 65536 = 98304, в шестнадцатеричном виде 0x00018000, вообще без ошибки округления.
Проблемы начинаются со знакового бита. Одни авторы считают его внутри m, другие добавляют сверху. Q1.15 по первому соглашению — это 16-битное слово: одна знаково-целая позиция плюс 15 дробных битов. По второму соглашению то же самое обозначение описывает 17 битов, которых нет ни у одной машины.
Почему одна и та же метка Q15 в разных документах означает разную разрядность
Одночисловая форма Q15 выбрасывает m целиком и оставляет разрядность подразумеваемой. В практике DSP она почти всегда означает знаковое 16-битное слово в дополнительном коде с 15 дробными битами — именно это значение десятилетия назад закрепили экосистемы TI и ARM.
Но вам попадутся широко растиражированные объяснения вроде: Q15 означает 15 дробных битов, поэтому, если мы определим 32-битное число, в нём будет 1 знаковый бит и 16 целых. Вчитайтесь: знаковый бит здесь считается сверх m, а не внутри него — то есть по другому соглашению, не по тому, которым пользуемся мы. Описанная раскладка настоящая, и записывают её обычно как Q16.15. Неверна именно метка: 32-битное слово с 16 целыми битами не является Q15 ни по одному из соглашений. Этот абзац перепечатали в таком количестве блогов, что по некоторым запросам он теперь стоит выше правильного определения.
Считайте голое Q15 в незнакомом документе гипотезой, а не фактом. Проверьте его по чему-нибудь измеримому: по разрядности регистра в карте памяти, по типу C в заголовке драйвера или по известному образцовому значению.
Единственное описание, которое переживает встречу с чужим кодом
Запишите три вещи — и двусмысленность исчезает:
- Знаковость — знаковое в дополнительном коде или беззнаковое
- W — всего битов в слове
- F — дробных битов
signed, W=16, F=15 прочитать неправильно невозможно. Как и unsigned, W=32, F=16. Всё остальное выводится: масштаб равен 2^F, разрешение — 2^-F, а диапазон — это целочисленный диапазон слова, делённый на 2^F. Положите эти три значения в описание протокола и в комментарий к структуре — и спорить об этом больше не придётся.
Онлайн-конвертер Q-формата печатает знаковость, W, F и масштаб рядом с каждым результатом по той же причине. Когда даташит говорит «Q15», а парсер коллеги — что-то другое, декодирование одного известного слова закрывает вопрос секунд за десять.
Формула перевода, посчитанная вручную
Оба направления достаточно коротки, чтобы проделать их на бумаге, — а это важно, когда вы смотрите на шестнадцатеричный дамп на экране осциллографа.
Из float в фиксированную точку: round(x × 2^F)
Переведём 0.5 в Q15.
- Масштабирование:
0.5 × 32768 = 16384 - Округление: уже целое, значит
16384остаётся как есть - Проверка диапазона: знаковое 16-битное вмещает от -32768 до 32767, и 16384 туда попадает
- Запись:
16384, hex0x4000, двоичное0100000000000000
Потерять информацию может только шаг 2, провалиться — только шаг 3; всё интересное в фиксированной точке случается в одном из этих двух мест.
Из фиксированной точки во float: raw ÷ 2^F
Теперь в обратную сторону — от снимка регистра, в котором знаковое поле Q15 читается как 0xC000.
- Прочитать текст как беззнаковый 16-битный код:
0xC000 = 49152 - Формат знаковый и старший бит выставлен, поэтому вычитаем 2^16:
49152 - 65536 = -16384 - Масштабируем вниз:
-16384 ÷ 32768 = -0.5
Шаг 2 как раз и пропускают. Без поправки на дополнительный код 0xC000 читается как +1.5, а такое значение в Q15 даже не попадает в диапазон — полезный признак. Если декодированное значение вышло за границы диапазона формата, вы почти наверняка забыли про знак.
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 здесь стоит не для красоты. Масштабирование сначала через float вернуло бы двоичное округление как раз в тот момент, когда вы пытаетесь его измерить, а Fraction(str(x)) читает набранный вами десятичный литерал, а не ближайший к нему double.
Как читать и записывать шестнадцатеричное слово
Именно в шестнадцатеричном виде эти значения и появляются в окнах регистров, а перевод здесь механический: 3277 в hex — это 0xCCD, дополненное до W/4 разрядов — 0x0CCD. Дополнять нужно всегда: слово Q15 — это четыре шестнадцатеричных разряда, слово Q31 — восемь, а потерянный ведущий ноль приводит к тому, что значение съезжает в пакетном декодере.
Сырые коды в шестнадцатеричном виде тоже держите беззнаковыми. 0x8000 — это самое отрицательное слово Q15, а не +32768, и запись -0x8000 не помогает никому. Порядок байтов — отдельный вопрос: формат Q задаёт числовое масштабирование и про порядок байтов не говорит ничего, поэтому little-endian-дамп 0x0CCD приходит байтами CD 0C. Для простой смены системы счисления при чтении дампа конвертер систем счисления справляется с двоичной, восьмеричной и шестнадцатеричной, без всякого масштаба и знаковой разрядности.
Q7, Q15, Q31 и Q16.16: диапазон и разрешение
Каждое число в этой таблице — это -2^(W-1), делённое на 2^F, с одного конца и (2^(W-1) - 1), делённое на 2^F, с другого. Значения точные, а не округлённые для показа.
| Формат | Разрядность | Дробных битов F | Масштаб 2^F | Минимум | Максимум (точно) | Разрешение |
|---|---|---|---|---|---|---|
| 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 |
Вставьте любое из этих граничных значений в конвертер Q-формата — он вернёт сохранённое целое, шестнадцатеричное слово и точную десятичную запись. Константу прошивки лучше прогнать через него до отгрузки, а не после.
Почему верхняя граница диапазона Q15 равна 0.999969482421875
Диапазон формата Q15 несимметричен, и асимметрия идёт от дополнительного кода, а не от чего-то специфичного для фиксированной точки. Знаковое 16-битное слово покрывает целые от -32768 до 32767. Разделите оба конца на 32768 — и диапазон станет от -32768/32768 до 32767/32768, то есть от -1 до 0.999969482421875.
Так что 1.0 не хватает ровно одного LSB — младшего значащего разряда. Это не «максимум примерно равен 1.0» и не «1.0 с округлением»: у значения просто нет кодировки, и конвертер, который выдаёт для Q15 1.0 и не сообщает о насыщении, вам врёт.
Почему -1 входит в диапазон
Во множестве источников диапазон Q15 записан как -1 < X < 0.9999695, с открытой скобкой с обеих сторон. Нижняя граница здесь неверна. -32768 ÷ 32768 = -1 точно, значит -1 представимо, и оба конца [-1, 0.999969482421875] — достижимые значения. Вам встретится и запись [-1, 1) — то же самое утверждение, но отнесённое к полной шкале: само значение 1.0 недостижимо, а наибольшее достижимое равно 0.999969482421875, а не чему-то чуть меньшему.
Это важнее, чем придирка к нотации. Именно -1 ломает умножение, потому что -1 × -1 = 1, а 1 выходит за диапазон. Тот, кто считает -1 недостижимым, не напишет ветку насыщения, которая этот случай ловит.
Обрезанная верхняя граница 0.9999695 в тех же источниках — артефакт отображения. Никакой периодичности в этом значении нет: это 32767/32768, а 32768 — степень двойки, поэтому десятичная запись обрывается на пятнадцатом знаке, на 0.999969482421875.
0.1 не помещается в Q15
Домножьте — и проблема видна сразу: 0.1 × 32768 = 3276.8. Фиксированная точка умеет хранить только целые, так что чем-то придётся пожертвовать.
При округлении к ближайшему сохранённое слово равно 3277, hex 0x0CCD. Значение, которое это слово представляет:
3277 ÷ 32768 = 0.100006103515625
Ошибка квантования — это разница между тем, что вы просили, и тем, что смогла дать сетка:
0.100006103515625 - 0.1 = +0.000006103515625
Это примерно шесть миллионных, или пятая часть LSB. В регуляторе громкости безобидно. В интеграторе, который тысячу раз в секунду прибавляет её обратно, — уже нет: там накопленное значение равномерно уплывает примерно на 0.006 в секунду.
У фиксированной точки ошибка равномерная, у плавающей — нет
Q15 раскладывает сетку из 65536 точек с шагом ровно 1/32768 по отрезку [-1, 0.999969482421875]. Шаг возле 0.9 такой же, как возле 0.0001, поэтому худшая абсолютная ошибка везде равна половине LSB. Считать ошибку от этого скучно, зато оценку можно доказать: шум фильтра ограничивается арифметикой, которую перепроверит и джуниор.
IEEE 754 делает наоборот. Он держит фиксированное число значащих битов и двигает порядок, поэтому абсолютный зазор между соседними double растёт вместе с величиной, а относительная ошибка остаётся почти постоянной. Возле 1.0 этот зазор около 2.2e-16; возле 1e12 — уже около 0.0001220703125.
Отказ тот же, только форма другая: double тоже не может хранить 0.1. Он хранит 0.1000000000000000055511151231257827021181583404541015625, поэтому 0.1 + 0.2 возвращает 0.30000000000000004. Этот случай разобран в соседней статье про точность чисел с плавающей запятой. Фиксированная точка не решает проблему десятичных дробей в двоичной системе — она лишь делает размер ошибки предсказуемым.
Три режима округления, между которыми один LSB
Правило округления — часть контракта на данные, а не деталь реализации. Две корректные реализации, разошедшиеся в округлении, будут бесконечно выдавать тест-векторы с разницей в последнем бите, а разбираться в этом придётся вручную.
| Режим | Правило | 10911.744 | -3276.8 |
|---|---|---|---|
| Округление к ближайшему, половины к чётному | Ближайшая точка сетки; точные половины уходят к чётному целому | 10912 | -3277 |
| Отбрасывание в сторону нуля | Отбросить дробную часть, модуль только уменьшается | 10911 | -3276 |
| Округление вниз, к минус бесконечности | Всегда вниз по числовой оси | 10911 | -3277 |
Два столбца показывают, почему одного примера никогда не хватает. Для положительного значения отбрасывание и округление вниз совпадают. Для отрицательного они расходятся на целый LSB, потому что отбрасывание тянет -3276.8 вверх, к нулю, а округление вниз толкает его ещё ниже.
Пример, на котором расходятся руководства
0.333 в Q15 — хороший тестовый случай, потому что значение стоит близко к границе. 0.333 × 32768 = 10911.744.
- Отбрасывание:
10911, что читается обратно как0.332977294921875 - Округление к ближайшему:
10912, что читается обратно как0.3330078125
Старые руководства печатают 10911 и идут дальше, не сказав, каким правилом это получено. Принесите это число в проект, где кодировщик округляет, — и эталонные векторы упадут на первом же прогоне, причём разница в единицу выглядит как опечатка, а не как расхождение в политике.
На отрицательных значениях три правила и расходятся. -0.1 × 32768 = -3276.8 даёт -3276 при отбрасывании (значение -0.0999755859375) и -3277 при округлении вниз или к ближайшему (значение -0.100006103515625). Отбрасывание и округление к ближайшему обходятся с обоими знаками одинаково — ±3276 и ±3277 соответственно, — поэтому симметричная пара коэффициентов симметричной и остаётся. А вот округление вниз — нет: +0.1 оно отправляет в 3276, а -0.1 — в -3277, и пара выходит перекошенной на единицу.
Что ваш язык делает по умолчанию
Ни одно из этих умолчаний не ошибочно, они просто разные — и ни одно о себе не сообщает.
- C/C++: приведение из плавающей точки к целому типу отбрасывает дробную часть в сторону нуля.
(int16_t)(0.333f * 32768)даёт10911. - Python: встроенная
round()округляет половины к чётному, поэтомуround(3276.8)— это3277, аround(2.5)— это2. - JavaScript:
Math.roundразрешает половины в сторону плюс бесконечности, что несимметрично.Math.round(2.5)— это3, аMath.round(-2.5)— это-2. - Железо: многие DSP-тракты умножения с накоплением округляют на сдвиге и предлагают режим «к ближайшему чётному» отдельным битом — поэтому эталонная модель на C и кремний расходятся до того самого момента, когда кто-нибудь прочитает регистр режима.
Выберите одно правило, назовите его в спецификации формата рядом с W и F и заставьте тест-векторы его нести.
Переполнение: насыщение против заворачивания
Переполнение Q15 делает одно из трёх: отвергает значение, зажимает его до 0.999969482421875 или заворачивает до -1.0. Что именно вы получите — это политика, выбранная вашим тулчейном, и третий вариант молча меняет знак на противоположный.
Возьмём 1.0. Масштабирование даёт 1.0 × 32768 = 32768, а наибольшее знаковое 16-битное целое — 32767. Значение выходит за диапазон ровно на единицу.
| Политика | Сохранённое слово | Значение при чтении | Как это выглядит дальше по тракту |
|---|---|---|---|
| Ошибка | нет | перевод отклонён | Громко и заметно; для инструментов обычно правильный выбор |
| Насыщение | 0x7FFF = 32767 | 0.999969482421875 | На слух и на глаз неотличимо от 1.0 |
| Заворачивание | 0x8000 = -32768 | -1.0 | Инверсия знака на полной шкале |
Строка с насыщением теряет 0.000030517578125, и этого никто не замечает. Строка с заворачиванием превращает положительный отсчёт полной шкалы в такой же отрицательный, а в звуковом тракте это щелчок, который слышно через всю комнату. В контуре управления это команда полной величины в противоположную сторону.
Опасное свойство заворачивания в том, что на выходе получается совершенно нормальное на вид слово. 0x8000 — легальная кодировка -1.0 в Q15. Ничто ниже по тракту не отличит его от настоящего отсчёта -1.0, поэтому постфактум искать нечего: вы найдёте это, только поставив измерение в той точке, где переполнение произошло.
Почему в DSP-кремний закладывают насыщающие инструкции
Обработке сигналов нужно насыщение, поэтому процессоры реализуют его сами, а не оставляют ветвлению. У ARM есть QADD/QSUB и насыщающие сдвиги SSAT/USAT, у NEON — VQADD и родственные ему, у x86 SSE — упакованные насыщающие сложения вроде paddsw. Семейства C6000 и C55x от TI выводят насыщение отдельным битом режима на тракте аккумулятора.
В фильтре или микшере случайный ограниченный отсчёт — это небольшое локальное искажение. Завёрнутый отсчёт даёт разрыв с энергией по всему спектру. Железо по умолчанию выбирает «чуть-чуть неправильно» вместо «катастрофически неправильно» — но только если вы это включили, а обычная целочисленная арифметика C на том же кристалле по-прежнему заворачивает.
Почему умножению в Q15 нужен сдвиг вправо на 15
Перемножьте два слова Q15 как целые — результат будет верным, но это уже не Q15. При умножении дробные биты складываются: Q15 × Q15 даёт Q30.
Разберём 0.5 × 0.5, где правильный ответ очевидно равен 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
Прочитайте 268435456 как Q15 — и увидите 8192.0, что отличается в 32768 раз. Этот множитель и есть весь баг целиком, и он объясняет, почему фильтры с фиксированной точкой, которые «почти работают», часто промахиваются ровно на степень двойки.
Произведению нужно ещё и место. Два 16-битных значения перемножаются в число шириной до 32 бит, поэтому промежуточный результат должен быть int32_t. Накопление многих произведений требует ещё большего запаса — поэтому на таких деталях, как C55x, аккумуляторы DSP 40-битные.
Округляйте сдвиг, а не просто отбрасывайте биты
Голый >> 15 выбрасывает младшие 15 битов, а для знаковых значений это отбрасывание вниз, к минус бесконечности. Если сначала прибавить половину LSB исходящей точности, получится округление к ближайшему:
#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;
}
Собранная командой cc -std=c11 -Wall -o q15 q15.c && ./q15, программа печатает:
0.5*0.5 -> 8192
0.1*0.1 -> 328
-1*-1 -> 32767
no-round -> 327
Третья строка — тот самый случай с -1 из начала статьи. -32768 × -32768 = 1073741824, что в Q30 равно 1.0 и выходит за диапазон Q15, поэтому sat_q15 зажимает результат до 32767. Уберите зажим — и приведение к int16_t завернёт его в -32768, превратив -1 × -1 в -1.
Последние две строки — это разница в округлении. 3277 × 3277 = 10738729, и голый сдвиг даёт 327 (0.009979248046875), а округлённый — 328 (0.010009765625). Истинное произведение равно 0.01, так что округлённый вариант оказывается более чем вдвое ближе. Стоит это одного лишнего сложения.
Одна оговорка про сам сдвиг: до C23 сдвиг вправо отрицательного знакового целого в C относится к поведению, определяемому реализацией, хотя любой компилятор, который вам встретится, выполняет арифметический сдвиг. Если это вас смущает, делите на 32768 и позвольте компилятору самому выпустить сдвиг — или сдвигайте беззнаковый тип, предварительно добавив смещение. Механика сдвигов и масок в целом разобрана в руководстве по побитовым операциям.
Сложению сначала нужны совпадающие Q
Умножение меняет Q предсказуемо. Сложение расхождения не терпит вовсе: прибавить слово Q7 к слову Q15 — значит получить бессмыслицу, потому что у операндов разный масштаб.
Сначала выровняйте их сдвигом. 0.5 в Q7 — это 64, а 64 << 8 — это 16384, то есть 0.5 в Q15. Величина сдвига равна разнице в числе дробных битов, 15 - 7 = 8.
Сдвиг вверх точен, но стоит запаса разрядности, потому что значению Q7, поднятому до Q15, нужен более широкий контейнер. Сдвиг вниз теряет точность и требует того же решения об округлении, что и умножение. В любом случае пишите Q каждого промежуточного значения в комментарии. Код с фиксированной точкой, где масштабы живут только в голове автора, перестаёт поддерживаться в течение месяца.
Фиксированная точка Q15 или плавающая IEEE 754: как выбрать
Обе системы двоичные и позиционные, поэтому в принципе ни у одной нет преимущества по точности. Выбор сводится к тому, сколько с вас возьмёт целевое железо и какие гарантии вам нужны.
| Вопрос | В пользу фиксированной точки | В пользу плавающей |
|---|---|---|
| Есть ли аппаратный FPU? | FPU нет, либо есть только программная эмуляция | Аппаратный FPU с однотактными операциями |
| Насколько широк динамический диапазон? | Известен и ограничен, как у нормализованного звука | Растянут на много порядков |
| Задаёт ли формат передачи масштаб? | Протокол или регистр фиксируют двоичный масштаб | Поле — настоящий float |
| Нужна ли побитовая воспроизводимость между сборками? | Да, целые воспроизводятся везде одинаково | Допустимы расхождения из-за FMA и оптимизаций |
| Жёстко ли с памятью или полосой? | 16-битные отсчёты вдвое компактнее 32-битных float | Это не ограничение |
| Кто сопровождает код? | Команда уже свободно читает Q-нотацию | Смешанная команда, ошибки масштабирования — больший риск |
Последняя строка — не шутка. Фиксированная точка переносит ошибку из времени выполнения в фазу проектирования, и это хорошая сделка только тогда, когда проектированием кто-то действительно занимается. На Cortex-M4F с аппаратным FPU одинарная точность часто оказывается и быстрее, и безопаснее, а привычка сразу тянуться к Q15 осталась от деталей, которые давно не определяют рынок.
Где начинается территория IEEE 754
Берите плавающую точку, когда само слово несёт знак, порядок и мантиссу, а не фиксированный масштаб. Именно в этот момент формат Q перестаёт применяться: единого 2^F, на который надо делить, просто нет, потому что порядок у каждого значения свой.
На практике два представления встречаются постоянно. Данные датчика приходят словами регистров Q15, поднимаются во float для длинного расчёта и возвращаются в Q15 для ЦАП. Чтобы разобрать плавающую половину этого пути по битам, конвертер IEEE 754 раскладывает значение на знак, порядок и мантиссу и печатает точную сохранённую десятичную запись — ту же работу конвертер Q-формата делает для фиксированного масштаба.
Обе стоят на одном фундаменте
Формат Q, IEEE 754 и обычные целые читают одни и те же биты, различаясь лишь правилами о том, где стоит точка и может ли она двигаться. Если позиционная часть кажется шаткой или вы хотите быстрее читать 0x0CCD как 0000 1100 1100 1101, не хватаясь за калькулятор, введение в перевод между двоичной, шестнадцатеричной и восьмеричной закрывает основу, на которой стоят оба формата.
FAQ по фиксированной точке Q15
Что означает Q15?
В общепринятом соглашении DSP Q15 — это знаковое 16-битное слово в дополнительном коде с 15 дробными битами: одна знаковая позиция и 15 дробных. Масштаб равен 2^15 = 32768, разрешение — 1/32768 = 0.000030517578125, а диапазон — от -1 до 32767/32768. Поскольку метки Q в разных документах различаются, подтверждайте общую разрядность и знаковость, а не доверяйте одной лишь метке.
Q15 и Q1.15 — это одно и то же?
Обычно они описывают одну и ту же знаковую 16-битную раскладку, где 1 в Q1.15 считает знаковую позицию. Но нотация не универсальна, и часть авторов добавляет знаковый бит сверх m, а не внутрь него. Надёжное описание — это знаковость плюс общее число битов W плюс число дробных битов F: для этой раскладки signed, W=16, F=15.
Чему равно 0.5 в Q15?
Значение 0.5 в Q15 хранится как целое 16384, а шестнадцатеричное слово — 0x4000. Арифметика такая: 0.5 × 32768 = 16384 — это уже целое, поэтому ни округления, ни ошибки квантования здесь нет. Декодирование это подтверждает: 16384 ÷ 32768 = 0.5 точно.
Каковы максимальное и минимальное значения Q15?
Минимум равен -1, и он входит в диапазон, потому что -32768 ÷ 32768 равно ровно -1. Максимум равен 32767/32768 = 0.999969482421875. Оба конца достижимы, поэтому диапазон записывается как [-1, 0.999969482421875], а за его пределами оказывается 1.0. Источники, записывающие нижнюю границу открытым интервалом, ошибаются, и эта ошибка прячет случай переполнения -1 × -1.
Что происходит при переполнении Q15?
Зависит от действующей политики. Политика ошибки отклоняет перевод. Насыщение зажимает до ближайшего конца диапазона, поэтому 1.0 становится 0.999969482421875 — потеря в один LSB, которую обычно не слышно. Заворачивание применяет вычет по модулю 2^16, поэтому 1.0 масштабируется в 32768, что читается обратно как -32768 и, следовательно, -1.0 — полная инверсия знака. Именно поэтому DSP-железо по умолчанию насыщает, а обычная целочисленная арифметика C заворачивает.
Почему после умножения двум числам Q15 нужен сдвиг вправо на 15?
Потому что дробные биты складываются. Q15 × Q15 даёт произведение Q30, поэтому целый результат несёт 30 дробных битов вместо 15. Сдвиг вправо на 15 возвращает его в Q15: 16384 × 16384 = 268435456, а 268435456 >> 15 = 8192, что декодируется в 0.25. Прибавьте 1 << 14 перед сдвигом, чтобы округлять к ближайшему вместо отбрасывания, и держите промежуточный результат в int32_t, чтобы 32-битное произведение не переполнилось.
Когда использовать формат Q вместо IEEE 754?
Берите формат Q, когда протокол, карта регистров, DSP-алгоритм или кодек уже зафиксировали двоичный масштаб для целого слова: масштаб — часть интерфейса, и выбирать его вам не дают. Берите IEEE 754, когда значению нужен широкий динамический диапазон, когда на целевой платформе есть аппаратный FPU или когда поле действительно хранит знак, порядок и мантиссу. Для простой смены системы счисления, без всякого масштаба, не подходит ни то, ни другое — это обычный перевод между системами счисления.
Коротко
Фиксированная точка Q15 — это умножение, решение об округлении и проверка диапазона. raw = round(value × 32768) на входе, value = raw ÷ 32768 на выходе. Формула тривиальна; все отказы живут в том, что никто не документирует.
Значит, документируйте. Пишите знаковость, W и F рядом с каждой меткой Q, а не считайте, что метки достаточно. Называйте там же режим округления, потому что отбрасывание и округление вниз расходятся на целый LSB на отрицательных значениях. Говорите прямо, что делает переполнение — выдаёт ошибку, насыщает или заворачивает, — потому что случай с заворачиванием превращает 1.0 в -1.0 и не оставляет после себя никаких следов.
Когда слово регистра и таблица противоречат друг другу, декодируйте одно известное значение в конвертере Q-формата — он показывает рядом сохранённое целое, точную десятичную запись, ошибку квантования и представимый диапазон, и всё это считается в браузере. Обычно этого достаточно, чтобы понять, чьё предположение про W и F было неверным.