Арифметика с фиксированной точкой
Как считать дробные числа на микроконтроллере без блока плавающей точки: формат 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,9922 | 2^−7 ≈ 0,0078 |
| Q15 (1.15) | int16_t | −1 … 0,99997 | 2^−15 ≈ 3,05 · 10^−5 |
| Q31 (1.31) | int32_t | −1 … 1 − 2^−31 | 2^−31 ≈ 4,66 · 10^−10 |
| Q8.8 | int16_t | −128 … 127,996 | 2^−8 ≈ 0,0039 |
| Q16.16 | int32_t | −32 768 … 32 767,99998 | 2^−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 _Fract | 1 байт | s.7 | hr |
_Fract | 2 байта | s.15 | r |
long _Fract | 4 байта | s.31 | lr |
short _Accum | 2 байта | s8.7 | hk |
_Accum | 4 байта | s16.15 | k |
long _Accum | 8 байтов | s32.31 | lk |
/* 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.
Источники
- 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
- 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 не на всех платформах
- 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
Нашли ошибку?
Смотрите также
- Программирование МКC для МК, битовые операции, фиксированная точка, регистры
- C для микроконтроллеровТипы, stdint, volatile, атомарность, регистры, константы во Flash
- Битовые операции в CМаски, сдвиги, установка и проверка битов, ловушки стандарта C
- Система команд Thumb-2Команды по группам, Operand2, флаги и условия, IT, переходы, DSP и FPU, M0
- Таблицы синуса и гаммыСинус, гамма, CIE L*, массив на C