В качестве примера я приведу форматы вычислений для контроллера холодильника на элементе Пельтье. Программа для этой разработки написана на Ассемблере. Команд для работы с плавающей запятой на Ассемблере нет. Тем не менее, задача реализована без каких-либо ограничений.
Число с плавающей запятой состоит из мантиссы и показателя степени – порядка. Для двоичного исчисления:
мантисса * 2
порядокМантисса находится в интервале 0…1. У числа с плавающей запятой фиксированная относительная точность и меняющаяся абсолютная точность. В Ардуино тип float состоит из 4 байтов: 3 байта – мантисса и 1 байт порядок. В результате диапазон значений для чисел float составляет -3.4 10
38 … +3.4 10
38.
Удобно использовать числа с таким широким диапазоном и с такой высокой точностью. Но в большинстве случаев в этом нет необходимости.
Например, в контроллере холодильника ток вряд ли будет выше ампер 10. И точность его определена разрядностью АЦП. Для 10 разрядного АЦП это 0,1% или в абсолютном выражении 0,01 А. В этой разработке не нужны плавающие числа, нужны дробные числа.
Многие считают, что дробные числа представляются только типом данных с плавающей запятой. Из заявленных в языке C данных это правильно. Но в природе существует еще формат с фиксированной запятой.
Типы данных byte, int, long предназначены для целочисленных вычислений. Например, для типа int диапазон чисел составляет -32768 … -1, 0, 1, 2, … 32767. Только целые числа. Это частный случай формата с фиксированной запятой, в котором запятая зафиксирована в младшем разряде.
Но в принципе запятую можно сдвинуть. Вот пример числа с фиксированной запятой в 11 разряде.
Разряды для числа INT | Разряды для числа с фиксированной запятой | Десятичный вес разряда числа с фиксированной запятой | Пример числа С десятичным эквивалентом 1,375 |
0. | -11 | 0,00048828125 | 0 |
1 | -10 | 0,0009765625 | 0 |
2 | -9 | 0,001953125 | 0 |
3 | -8 | 0,00390625 | 0 |
4 | -7 | 0,0078125 | 0 |
5 | -6 | 0,015625 | 0 |
6 | -5 | 0,03125 | 0 |
7 | -4 | 0,0625 | 0 |
8 | -3 | 0,125 | 1 |
9 | -2 | 0,25 | 1 |
10 | -1 | 0,5 | 0 |
11 | 0. | 1 | 1 |
12 | 1 | 2 | 0 |
13 | 2 | 4 | 0 |
14 | 3 | 8 | 0 |
15 | 4 | 16 | 0 |
Я в программе объявил переменную типа unsigned int и решил, что для этой конкретной переменной 0й разряд соответствует 11 разряду формата int. Формат переменной с фиксированной запятой никак не отражается в программе. Просто надо это помнить и указать в комментариях.
Диапазон значений числа из моего примера составляет 0 … 31,999, абсолютная точность 0,00048828125. Для нашей задачи больше чем достаточно.
Если не хватает точности, то можно запятую сдвинуть в сторону старших разрядов.
Если не хватает диапазона – то сдвинуть в сторону младших разрядов.
Для большинства практических вычислений вполне достаточно данного типа int. Если использовать long, то диапазон и точность будут на много выше.
В данном примере я сдвинул запятую на 11 разрядов. Это равнозначно, что я умножил число на 2
11, т.е. на 2048.
unsigned int measureCurrent; // измеренный ток (*2048)
При объявлении переменной я указал это в комментариях и должен помнить, то переменная measureCurrent содержит значение реального тока, умноженное на 2048.
Как правило, разные переменные с фиксированной запятой могут содержать запятую в разных разрядах. Например, этот измеренный ток мы хотим умножить на корректирующий коэффициент в диапазоне 0 .. 2. Тогда мы выберем запятую в 15 разряде и получим диапазон 0 … 1,99999 (0 … 65535 / 32768). Точность будет максимальная.
unsigned int coeffCurrent; // масштабный коэффициент тока (*32768)
Теперь об операциях с такими форматами. Для языка C это будут обычные математические операции с целыми числами типа int.
Но мы должны помнить, что в результате произведения чисел с фиксированными запятыми получится число с запятой в позиции равной сумме сдвигов запятой для каждого множителя. Проще использовать множители, которые мы написали в комментариях. Например:
long corrCurrent; // откорректированный ток (* 67 108 864)
corrCurrent = measureCurrent * coeffCurrent;Мы умножили два числа int, получили результат типа long и в нем запятая сдвинута на 26 (11+15) разрядов. Это равносильно, что мы умножили его на 67 108 864.
Скорее всего такая точность не нужна. Поэтому мы можем отбросить 16 разрядов, получить число int, в котором запятая сдвинута на 10 (26-16) разрядов.
unsigned int realCurrent; // откорректированный ток (* 1024)
realCurrent = (unsigned int) ( corrCurrent >> 16);Отбросить 16 разрядов можно более эффективным способом за счет применения указателей. Сдвиг данных типа long требует очень много времени. Но это другой вопрос.
В результате мы, используя только целочисленную математику, получили дробный результат.
Сумма чисел с фиксированной запятой может производиться только с числами, в которых запятые находятся на одинаковых позициях. Но это проблем не вызывает. Для одинаковых физических величин мы всегда выбираем одинаковые форматы. Вряд ли нам захочется напряжение прибавлять к току.
Остался вопрос, как вводить и выводить числа с фиксированной запятой. Это надо делать, учитывая множители, которые мы написали в комментариях. Если мы хотим получить реальное десятичное число из переменной
unsigned int realCurrent; // откорректированный ток (* 1024)
то надо разделить его на 1024. Эта операция может вызвать некоторые сложности. Вариантов много.
Если информация выводится программой верхнего уровня на компьютере, то можно там и разделить. В резидентной программе времени это не потребует совсем.
Если мы хотим выдать число в монитор последовательного порта в формате float, то делать нечего. Надо преобразовывать в float и делить. Но все равно операций с типами float будет намного меньше.
Но главное, что мы стремимся не уничтожить все операции с числами float из программы, а убрать их с критичных ко времени программных блоках. Вывод данных через последовательный интерфейс занимает много времени и применение в нем операций над числами float ничего не изменит. А вот в быстром регуляторе тока или напряжения вычисления с фиксированной значительно изменят ситуацию.
При вводе данных надо выполнять обратную операцию. Т.е. если мы хотим занести из компьютера значение коэффициента
unsigned int coeffCurrent; // масштабный коэффициент тока (*32768)
равное 1,025832, то мы должны умножить его на 32768. В результате мы должны занести число 33614 (1,025832 * 32768).
Если данное считывается из АЦП, то запятую лучше привязать к данному из АЦП, т.е. к нулевому разряду. Число останется целочисленным, а при умножении на первый коэффициент запятая сместится.
Думаю, мало кто дочитал до конца. Тот, кто дочитал, наверное, поклялся никогда не использовать числа с фиксированной запятой. Но, поверьте, что есть ситуации, когда без подобных вычислений обойтись нельзя. Они значительно сокращают время выполнения программы. Надо только применять их там, где действительно без них не обойтись. В локальных кусках программы сделать это не так сложно.
Повторюсь,
цель не изжить из программы вычисления с плавающей запятой, а суметь реализовать задачу на ресурсах которые есть.