Перейти к содержанию

Арифметика с фиксированной точкой

Как считать дробные числа на микроконтроллере без блока плавающей точки: формат Q, перевод в целые и обратно, сложение и умножение со сдвигом, округление и насыщение, деление и типичные ошибки. С примерами на C, типами q7, q15 и q31 библиотеки CMSIS-DSP и встроенными типами _Fract и _Accum в avr-gcc по техническому отчёту ISO TR 18037.

Обновлено Редакция GAW

На микроконтроллере без блока плавающей точки дробные числа часто хранят как целые с неявным масштабом — с фиксированной точкой. Число 0,75 при масштабе 2^15 хранится как 24 576, а сложение и умножение выполняются обычными целочисленными командами. Ниже — формат Q, правила операций и типичные ошибки, а также что предлагают для этого CMSIS-DSP и GCC.

Коротко

  • Формат Qm.n: знаковое целое, у которого n младших бит — дробная часть, m — целая; значение = целое / 2^n.
  • Q15 (16 бит) — от −1 до 0,99997 с шагом 2^−15; Q31 (32 бита) — от −1 до 1 − 2^−31; в CMSIS-DSP это q15_t и q31_t.
  • Сложение — обычное целочисленное, при одинаковом формате операндов; переполнение надо ловить самому.
  • Умножение — в удвоенной разрядности и сдвиг вправо на n: ((int32_t)a * b) >> 15 для Q15.
  • Насыщение — ограничение результата границами формата вместо «заворачивания» через переполнение.

Формат Q

Число в формате Qm.n — это знаковое целое из 1 + m + n бит, которое интерпретируется как целое, делённое на 2^n. Отсюда:

ФорматТипДиапазонШаг
Q7 (1.7)int8_t−1 … 0,99222^−7 ≈ 0,0078
Q15 (1.15)int16_t−1 … 0,999972^−15 ≈ 3,05 · 10^−5
Q31 (1.31)int32_t−1 … 1 − 2^−312^−31 ≈ 4,66 · 10^−10
Q8.8int16_t−128 … 127,9962^−8 ≈ 0,0039
Q16.16int32_t−32 768 … 32 767,999982^−16 ≈ 1,53 · 10^−5

Перевод: целое = round(значение · 2^n), значение = целое / 2^n. Например, 0,3 в Q15 — round(0,3 · 32 768) = 9830, а обратно 9830 / 32 768 = 0,29999 — погрешность меньше половины шага. Обозначения 1.7, 1.15 и 1.31 использует CMSIS-DSP (в заголовке arm_math_types.h q7_t, q15_t и q31_t описаны как 8-, 16- и 32-битные дробные типы в этих форматах); технический отчёт TR 18037 записывает те же форматы как s.7, s.15 и s.31, где s — знаковый бит.

Формат выбирают по диапазону величины: коэффициенты цифрового фильтра, лежащие между −1 и 1, удобно держать в Q15, а температуру от −40 до 125 °C с шагом около 0,004 °C — в Q8.8.

Сложение и вычитание

Числа одного формата складываются как обычные целые: (a + b) / 2^n = a / 2^n + b / 2^n. Числа разных форматов сначала приводят к одному сдвигом. Результат может выйти за диапазон: 0,75 + 0,5 = 1,25 в Q15 не помещается, и при сложении в int16_t получится отрицательное число. Есть два выхода: считать в более широком типе и ограничить результат (насыщение) или заранее масштабировать данные так, чтобы переполнение было невозможно.

#include <stdint.h>

/* Q15 addition with saturation */
static int16_t q15_add_sat(int16_t a, int16_t b)
{
    int32_t s = (int32_t)a + b;
    if (s > INT16_MAX) return INT16_MAX;    /* 0x7FFF, +0.99997 */
    if (s < INT16_MIN) return INT16_MIN;    /* 0x8000, -1.0 */
    return (int16_t)s;
}

Умножение

Произведение двух чисел Qm.n имеет 2n дробных бит: (a / 2^n) · (b / 2^n) = (a · b) / 2^2n. Чтобы вернуться к формату Qm.n, произведение сдвигают вправо на n бит. Перемножать нужно в удвоенной разрядности — для Q15 в 32 битах, — иначе старшие биты потеряются:

/* Q15 multiplication with rounding and saturation */
static int16_t q15_mul(int16_t a, int16_t b)
{
    int32_t p = (int32_t)a * b;      /* Q30 */
    p += 1L << 14;                   /* round to nearest */
    p >>= 15;                        /* back to Q15 */
    if (p > INT16_MAX) p = INT16_MAX;   /* only -1.0 * -1.0 overflows */
    return (int16_t)p;
}

Приведение (int32_t)a обязательно на 8-битном AVR: там int 16-битный, и a * b без него вычислялось бы в 16 битах. Сдвиг вправо отрицательного произведения стандарт C оставляет на усмотрение реализации; GCC делает его арифметическим, с сохранением знака. Без прибавления 1 << 14 сдвиг округляет вниз, и при многократных умножениях ошибка накапливается в одну сторону.

Единственный случай переполнения при умножении Q15 — (−1) · (−1) = +1, которое не представимо: −32 768 · −32 768 = 2^30, после сдвига 32 768, а максимум int16_t — 32 767. Поэтому в конце стоит ограничение.

Деление

Деление обратно умножению: делимое сначала сдвигают влево на n бит в удвоенной разрядности, затем делят на делитель: (int16_t)(((int32_t)a << 15) / b). Результат помещается в Q15, только если |a| меньше |b|, а деление на ноль нужно исключить заранее. Деление — самая медленная операция на МК без аппаратного делителя (у AVR и Cortex-M0 его нет), поэтому в циклах деление на константу заменяют умножением на заранее вычисленную обратную величину.

Встроенные типы: TR 18037 и GCC

Технический отчёт ISO/IEC TR 18037 «Embedded C» вводит в язык типы _Fract (только дробная часть, значения от −1 до 1) и _Accum (с целой частью), их беззнаковые и насыщающие варианты _Sat. Минимальные форматы — s.7, s.15 и s.23 для short _Fract, _Fract и long _Fract и s4.7, s4.15, s4.23 для соответствующих _Accum (раздел 4.1). Насыщение гарантировано только для типов _Sat или при включённых прагмах FX_FRACT_OVERFLOW и FX_ACCUM_OVERFLOW, а без них переполнение не определено (раздел 4.1.3).

GCC поддерживает эти типы как расширение не на всех платформах. В avr-gcc они есть начиная с версии 4.8, поддержка неполная, а форматы такие:

ТипРазмерФорматСуффикс констант
short _Fract1 байтs.7hr
_Fract2 байтаs.15r
long _Fract4 байтаs.31lr
short _Accum2 байтаs8.7hk
_Accum4 байтаs16.15k
long _Accum8 байтовs32.31lk
/* avr-gcc: _Fract is the same Q15 format, handled by the compiler */
_Fract gain = 0.75r;
_Fract y = gain * x;         /* x is a _Fract: a Q15 multiply done by the compiler */

Беззнаковые варианты получают префикс u в суффиксе: 1.0uk — unsigned _Accum. Такой код короче, но привязан к компилятору и платформе, поэтому в переносимых библиотеках, например в CMSIS-DSP для ARM, фиксированная точка реализована на целых типах q7_t, q15_t и q31_t.

Типичные ошибки

  • Умножение без расширения разрядности — a * b двух int16_t на AVR считается в 16 битах и теряет старшую часть.
  • Сдвиг до умножения вместо сдвига после: (a >> 8) * (b >> 7) сохраняет диапазон, но выбрасывает младшие биты обоих сомножителей.
  • Смешение форматов — сложение Q15 с Q8.8 без приведения даёт бессмыслицу без всякой ошибки компиляции.
  • Переполнение промежуточной суммы в цикле фильтра — аккумулятор берут шире: сумму произведений Q15 копят в 32 или 64 битах и только в конце возвращают в Q15.
  • Округление только вниз — систематическая ошибка, которая накапливается в рекурсивных фильтрах и интеграторах ПИД-регулятора.

Таблицы синусов и других функций в формате с фиксированной точкой удобно генерировать калькулятором таблиц, а особенности целочисленной арифметики C на микроконтроллерах разобраны в статье «Язык C для микроконтроллеров».

Частые вопросы

Что такое формат Q15?

16-битное знаковое целое, в котором 15 младших бит — дробная часть. Значение равно целому, делённому на 2^15 = 32 768, поэтому диапазон — от −1 до 1 − 2^−15 ≈ 0,99997, а шаг — около 0,0000305. Например, 0,5 хранится как 16 384 (0x4000), −0,25 — как −8192. В CMSIS-DSP такой тип называется q15_t.

Как перемножить два числа Q15?

Перемножить как 32-битные целые и сдвинуть произведение вправо на 15 бит: (int16_t)(((int32_t)a * b) >> 15). Для округления к ближайшему перед сдвигом прибавляют 1 << 14. Единственный случай переполнения — (−1) · (−1): результат +1 в Q15 не помещается, поэтому его ограничивают значением 0x7FFF.

Когда фиксированная точка лучше float?

На ядрах без блока плавающей точки — AVR, Cortex-M0, M0+, M3 — где float считается программно, и там, где важна предсказуемая точность во всём диапазоне: в цифровых фильтрах, ПИД-регуляторах, обработке звука. Цена — ручной контроль диапазона и масштаба. На Cortex-M4F и M7 с FPU float одинарной точности обычно проще и не медленнее.

Есть ли в C встроенные типы с фиксированной точкой?

В стандарте C — нет, но технический отчёт ISO TR 18037 описывает типы _Fract и _Accum, и GCC поддерживает их как расширение на части платформ, в том числе в avr-gcc начиная с версии 4.8. Поддержка неполная и платформенная, поэтому в переносимом коде фиксированную точку обычно делают на целых типах из stdint.h.

Источники

  1. ISO/IEC TR 18037 (Embedded C), WG14 N1169, 2006 — 4.1 — типы _Fract и _Accum, диапазоны fract от −1 до 1, минимальные форматы s.7, s.15, s.23 и s4.7…s4.23; 4.1.3 — переполнение: насыщение для _Sat-типов, иначе по прагмам FX_FRACT_OVERFLOW и FX_ACCUM_OVERFLOW
  2. GCC Wiki: avr-gcc — Fixed-Point Support; GCC Manual: Fixed-Point Types — avr-gcc с версии 4.8: short _Fract s.7, _Fract s.15, long _Fract s.31, short _Accum s8.7, _Accum s16.15, long _Accum s32.31, суффиксы hr, r, lr, hk, k, lk; поддержка не полная, переполнение без насыщения не определено; GCC поддерживает типы TR 18037 не на всех платформах
  3. Arm. CMSIS-DSP: arm_math_types.h (github.com/ARM-software/CMSIS-DSP) — q7_t — int8_t в формате 1.7, q15_t — int16_t в формате 1.15, q31_t — int32_t в формате 1.31, q63_t — int64_t

Нашли ошибку?