Числа з фіксованою комою

Розрахунки в наших пристроях не завжди (м'яко кажучи) бувають цілочисельними, а використовувати числа з плаваючою комою (float або double) - вельми ресурсовитратності і не завжди коректно. Що ж робити?

Є кілька варіантів. Наприклад, можна використовувати отмасштабовані числа. Якщо потрібно вимірювати вольти, то можна уявити значення в мілівольтах. Таким чином, ми отримаємо і ціле число і точні розрахунки. Якщо не вистачає мілівольт, можна використовувати мікровольт, і так далі.

Такий підхід часто використовується в різних CADах і фінансовій сфері. Наприклад, в KiCad, відстані вимірюються в нанометрів і зберігаються в int'ах (звідси, до речі. Максимальний розмір плати - 4 * 4 метри).

Часто уявити величину в різних масштабах не особливо хочеться (наприклад, на індикаторі потрібно відображати цілу частину числа, а дрібна частина служить тільки для підвищення точності розрахунків), і ось тоді доводиться використовувати числа з фіксованою комою. Число з фіксованою комою - це ті-ж цілі числа, але, умовно, під дробову частину виділено кілька біт. Давайте, наприклад, з 32-бітного цілого числа зробимо кілька чисел з фіксованою комою:



Як видно, числа з фіксованою комою позначають як qm.n, де m - ціла частина, а n - дрібна. Якщо число має бути зі знаком, то біт для знака відбирають у цілій частині. Про знаковості числа побічно говорить відсутність одного біта в назві. Наприклад:

Так, як зберігати числа з фіксованою комою ми будемо в звичайних цілочисельних контейнерах, можна визначити їх ось так:

Максимальні і мінімальні значення

У будь-якого числа є максимальні і мінімальні значення. Максимальне значення для числі з фіксованою комою - 2 ^ m-1 / (2 ^ n-1), а мінімальний крок 1 / (2 ^ n-1). (Тут, як і раніше, m - ціла частина, а n - дрібна)

приклади:
q16.16: максимальне число: 65535.99998474. крок - 1.53e-5
q0.32: максимальне число: 0.9999999997671. крок - 2.328e-10
q5.27: максимальне число: 31.9999999925. крок - 7.45e-9

Перетворення в число з фіксованою комою і назад

Для того, щоб перетворити число з плаваючою комою в число з фіксованою, потрібно просто помножити його на 2 ^ n.

Ось, наприклад, макроси для такого перетворення:

Якщо ви перетворювати ціле число, то годі й множити, а просто зрушувати, так як множення на ступеня двійки - це той-же зрушення, але варіант з множенням - більш загальний:


Для зворотного перетворення, как не странно, ми зрушуємо наше число назад.


Можна перетворити і в число з плаваючою комою:


Для перетворення числа з одного положення фіксованою комою в інше, його потрібно зрушити на різницю в кількості дробовий біт:

Додавання, віднімання, множення

Додавання і віднімання числі з фіксованою комою точно таке ж, як і у цілих чисел. Можна навіть не визначати макроси для цього

А ось для множення і ділення нам знадобиться контейнер в два рази більшого розміру для зберігання проміжного результату, адже при множенні 32 біт на 32 біта може вийти 64 біта:

До речі, якщо вам потрібно розділити на 10, або будь-яку іншу константу, можна змусити препроцесор порахувати 1/10, а потім - множити на це число. Такий фінт вухами дозволяє сильно прискоритися на процесорах без апаратного дільника. Наприклад:

Якщо вам потрібно помножити або розділити на ціле число, то можна робити цю операцію прямо на місці, без жодних зрушень і перетворень

Природно, головна причина, по якій ми працюємо з цими числами - швидкість. Для ефективної роботи, нам знадобиться дуже багато зрушень.

Для того, щоб зрушення відбуватися швидко, в процесорі повинен бути так званий barrel-shifter. barrel-shifter - це шматок АЛУ, яка дозволяє зрушувати числа на будь-яку кількість біт за один такт.

Часто, barrel-shifter емулюється помножувачем. Наприклад, щоб зрушити число вліво на 3 біти, його можна просто помножити на 8.

Є ще одна хитрість. Якщо під дробову частину відвести кількість біт кратне 8, то процесор може просто вибрати байти зі зміщенням і не використовувати зрушень взагалі. Це особливо актуально, якщо числа разрадность процесора менше розрядності використовуваного числа (наприклад, 32-бітові числа на AVR).

Переповнений

Як і інші числа, числа з фіксованою комою можна переповнити. Наприклад:

Для того, щоб таке не відбувалося, дуже часто використовують числа з нульовим кількістю цілих біт. Наприклад, q0.32. Так як при множенні числа менше одиниці на число менше одиниці завжди буде число менше одиниці, переповнення q0.32 при множенні просто неможливо.

При додаванні, числа з фіксованою комою поводяться точно так-же, як і звичайні цілі числа.

Налагодження фіксованих ком може бути дуже хитромудрій, тому, що замість простого і зрозумілого «5», в отладчике ви побачите «327680». На щастя, багато програм вміють показувати обчислені результати в watch-вікні. Наприклад, в IAR'е можна написати:

Числа з фіксованою комою

Після того, як така можливість з'явилася, налагодження стала значно легше.

висновок

Не дивлячись на те, що вже з'явилися доступні контролери з FPU, числа з фіксованою комою повинні бути в арсеналі будь-якого ембеддера. Штука дуже корисна, швидка, витончена і переноситься. Особливо, це актуально для ПЛІС, де зрушення робляться просто підключеннями до відповідних регістрів, і, тому числа з фіксованою комою там працюють просто надзвичайно швидко.

На рахунок множення і ділення чисел з фіксованою точкою. 64-х розрядне множення і особливо поділ може бути дуже повільним. Особливо на 8-ми і 16-ти бітниках. А множення і ділення можна реалізувати і без використання типів з більшою розрядністю. Наприклад множення q16:

Потрібно 4 множення 16х16 => 32 біта.
Розподіл q16:

Хоч воно і побітовое, але працює в рази бsстрее, ніж х64 розподіл на Cortex-m3. Може бути можна зробити ще швидше використовуючи апаратне поділ 32/32 => 32 і алгоритм Кнута.

Ухти, дякую, не знав про такий підхід :) Завжди використовував 64 бітове множення. Потрібно буде спробувати себе на своїх завданнях.

Додав до статті. І ще додав про множення на цілі числа.