Программирование AVR AVR - как избавиться от чисел с плавающей точкой Wed, September 20 2017  

Поделиться

нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


AVR - как избавиться от чисел с плавающей точкой Печать
Добавил(а) microsin   

Одно из сильных ограничений AVR (на платформе MCS51 это сказывается не так) - быстрый расход памяти при написании программ на Си.

Что поделаешь, за удобства Си и скорость RISC надо платить. Еще больший соблазн - использование для вычислений (например, при измерении напряжений с помощью ADC) чисел float (числа с плавающей запятой). Несмотря на то, что эти вычисления не точны, их слишком просто и удобно применять - можно делить и умножать, не задумываясь о переполнении, и легко представлять результаты вычисления в формате, понятном человеку.

Но плата за float слишком высока - линкер добавляет код библиотек математики, и память программ кончается очень быстро. Другое ограничение - вычисления с float выполняются слишком медленно (если конечно не используется аппаратный перемножитель типа [1]). Если нет возможности реализовать алгоритм программы с применением чисел float (из-за вышеуказанных ограничений по объему кода и скорости), одним из вариантов решения проблемы является переход на числа с фиксированной запятой. Их математические операции (вычитание, сложение, деление, умножение) ничем не отличаются от математических операций с простыми целыми числами, код получается компактный и быстрый.

Число с фиксированной запятой (обычно байт или слово из двух байт) состоит из целой части (находится в старших битах 8 или 16-разрядного числа) и дробной части (находится в младших битах). Пример числа с фиксированной запятой указан на рисунке, с разрядностью в 8 бит (1 байт).

Перед использованием чисел с фиксированной запятой главное - выбрать разрядность числа (байт или слово), и также выбрать положение запятой. С разрядностью вроде все понятно - если возьмем слово (16 бит), вычисления будут точнее, но скорость упадет и объем кода вырастет (и то и другое не меньше чем в 2 раза), а если возьмем байт (8 бит), то получим максимальное быстродействие и самый маленький код, но ухудшится точность. Как обычно, нужен компромисс, и Ваша задача - принять верное решение. Положение запятой никак не влияет на объем кода и быстродействие при математических операциях. Она просто распределяет соотношение точности между целой и дробной частью. Обычно вычисляют, сколько разрядов надо выделить на целую часть, и оставшуюся часть достается дробной части.

Например, наше 8-битное число будет хранить напряжение, которое будет меняться от 0 до 5 вольт. Число от 0 до 5 может кодироваться минимум тремя разрядами, поэтому разряды 7, 6 и 5 будут хранить целую часть (3 разряда), а разряды 4, 3, 2, 1 и 0 - остаются под дробную часть (5 разрядов). Число в дробной части будет показывать, сколько 1/32 от вольта будет в дробной части. Например, если в битах 7, 6 и 5 будет число 4, а в битах 4, 3, 2, 1 и 0 - число 30, то это будет кодировать напряжение 4.9375 вольта (0.9375 = 30/2^5 = 30/32). Значение байта при этом будет 100.11110b или 0x9E.

При вычислениях с фиксированной запятой (как и с простыми вычислениями на целых числах) нужно применять особые правила:

1. В результате сложения двух чисел возможно появление дополнительного разряда. Это происходит, если произошло переполнение. Если возможность переполнения нужно учитывать, то дополнительный 1 бит числа надо где-то хранить.

2. Результат умножения двух 8-битных чисел хранить в 16-разрядном числе, двух 16-битных в 32-разрядном, и т. п. (разрядность при умножении складывается).

3. При делении (малого числа на большое особенно) нужно предварительно делимое умножить на константу. Самое простое - сдвинуть число влево на нужное число раз (каждый сдвиг умножает на 2), поместить сдвинутый результат в число вдвое большей разрядности, и потом уже спокойно делить. В результате получим число с фиксированной запятой. Например, если делим 8-разрядное целое делимое, сдвинутое влево на 5 разрядов (получили 16-разрядное делимое), на целый делитель, то получаем дробное 16-битное число с фиксированной запятой, где запятая находится между 5 и 4 разрядами.

4. Лучше как можно больше пользоваться предварительно вычисленными на этапе компилирования константами, чтобы убрать код, который будет их генерировать. Например, если мы должны сравнить напряжение на аккумуляторе с напряжением 1.05 вольт, то это напряжение 1.05 вольт лучше сразу представить в нужном формате и определить директивой #define.

Когда нужно отобразить число с фиксированной запятой как набор десятичных цифр, действуют по простому алгоритму:
- сначала берут целую часть, и преобразуют её в символьный вид обычным образом.
- за целой частью рисуют запятую (или точку).
- берут дробную часть, приводят её к десятичной дроби, просто домножая и числитель, и знаменатель дробной части на дробное число (при этом значение дроби, как мы знаем, не изменится) - константу. Эта константа выбирается так, чтобы знаменатель стал числом - степенью десятки, а не двойки - при этом получится десятичная дробь. Фраза "домножая на дробное число" означает набор целочисленных операций (сначала умножить на целую константу, а потом разделить на целую константу), результат которых и будет это умножение на дробное число. При операциях умножения и деления либо множитель будет четным, либо делитель, либо они оба - и множитель, и делитель, будут нечетными (мы ведь формируем таким образом умножение на нецелое число). В качестве четной удобно использовать константу, являющуюся степенью двойки (2, 4, 8 и т. д.), потому что умножение и деление на эту константу заменяется простым сдвигом влево и вправо соответственно.
- после этого полученное значение числителя переводим в набор десятичных цифр и приписываем их после запятой.

Чтобы пояснить эти "премудрости" возьмем все тот же пример - переведем дробное число с фиксированной запятой 100.11110b (== 0x9E, наши 4.9375 вольта) в символьное представление:
- целая часть у нас равна 100b, т. е. 4, рисуем цифру 4
- рисуем за целой частью дробную точку: 4.
- берем дробную часть 11110b. Она равна 30, т. е. наша дробь - числитель 30, а знаменатель 32. Наша задача - подобрать такое дробное число, чтобы при его умножении на знаменатель 32 получилось число, которое можно представить степенью десятки, причем какая была степень десятки, столько десятичных знаков после запятой и получим. Пусть надо получить 3 десятичных знака после запятой, т. е. знаменатель 32 приводим к 1000. Число, на которое нужно домножить и числитель, и знаменатель, равно 1000/32 = 31.25. Отлично, но как умножить на дробное число, имея в распоряжении только целочисленную арифметику? Все просто - умножаем сначала на 125, а потом делим на 4 (т. к. 125/4 равно 31.25). Именно в таком порядке - сначала умножение (понадобится временное 16-битное число для хранения результата умножения), а потом деление, чтобы не потерять точность при отбрасывании остатка деления. На 125 умножаем как обычно, а делим на 4, сдвигая число на 2 бита вправо. Итак, 30 * 31.25 = (30 * 125) / 4 = 3750 / 4 = 937.5, округляем до 938. Таким образом, дробь 30/32 превратилась в дробь 938/1000.
- числитель 938 дописываем после запятой, получаем 4.938.

При подборе констант для умножения/деления удобно использовать смекалку и старые добрые электронные таблицы Microsoft Excel.

[Ссылки]

1. AVR201: использование аппаратного перемножителя AVR.

 

Комментарии  

 
+2 #2 Evgeny1969 13.09.2013 12:29
Чтобы вы знали, переменная float представляет мантиссу числа 23 битами. И вы называете эти вычисления неточными.

Но в случае 16-битных переменных точность будет еще более низкой. А 8-битные конструкции годятся разве что для вычислений с погрешностями в несколько процентов!

Бояться надо не переменных float, бояться надо поверхностных знаний о программировани и. Ибо неэффективное проектирование кода сожрет вашу память программ в 10 раз быстрее, чем просто использование float. Которое, к слову, добавит в ваш код библиотеку функций плавающей математики РОВНО ОДИН РАЗ. Хоть для одной переменной, хоть для тысячи.

microsin: каждой технологии, в том числе и кодировке float - свое время и место. Конечно же, если программа работает на ARM или PC, и/или если Вам не нужны финансовые расчеты, и/или если у Вас аппаратный перемножитель float, устраивающий Вас по разрядности и точности, и/или если Вам по большому счету наплевать на быстродействие и размер кода - пожалуйста, используйте float. Все зависит от конкретных прикладных условий, и даже предпочтений разработчика.
Цитировать
 
 
+1 #1 Goodman 29.03.2011 14:24
8) Good!
Цитировать
 

Добавить комментарий


Защитный код
Обновить

Top of Page