Skip to content
Назад к блогу
Руководства

Фиксированная точка Q15: перевод, округление и переполнение

0,1 в Q15 хранится как 3277, а 1,0 без насыщения переворачивается в -1,0. Читайте нотацию Q, считайте вручную и проверяйте онлайн бесплатно.

15 мин чтения

Фиксированная точка 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.

  1. Масштабирование: 0.5 × 32768 = 16384
  2. Округление: уже целое, значит 16384 остаётся как есть
  3. Проверка диапазона: знаковое 16-битное вмещает от -32768 до 32767, и 16384 туда попадает
  4. Запись: 16384, hex 0x4000, двоичное 0100000000000000

Потерять информацию может только шаг 2, провалиться — только шаг 3; всё интересное в фиксированной точке случается в одном из этих двух мест.

Из фиксированной точки во float: raw ÷ 2^F

Теперь в обратную сторону — от снимка регистра, в котором знаковое поле Q15 читается как 0xC000.

  1. Прочитать текст как беззнаковый 16-битный код: 0xC000 = 49152
  2. Формат знаковый и старший бит выставлен, поэтому вычитаем 2^16: 49152 - 65536 = -16384
  3. Масштабируем вниз: -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МинимумМаксимум (точно)Разрешение
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

Вставьте любое из этих граничных значений в конвертер 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 = 327670.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 было неверным.

Теги: fixed-point dsp embedded q-format number-representation

Похожие статьи

Все статьи