Математические операции
Одной из основных функций микроконтроллера является выполнение вычислений, как с числами напрямую, так и со значениями переменных. Начнём погружение в мир математики с самых простых действий:
= присваивание
+ сложение
- вычитание
* умножение
/ деление
% остаток от деления
Рассмотрим простой пример:
int a = 10;
int b = 20;
int c = a + b; // c = 30
int d = a * b; // d = 200
// так тоже можно
d = d / a; // d = 20
c = c * d; // c = 600
|
По поводу последних двух строчек из примера, когда переменная участвует в расчёте своего собственного значения: существуют также составные операторы, укорачивающие запись:
+= составное сложение: а += 10 равносильно а = а + 10
-= составное вычитание а -= 10 равносильно а = а - 10
*= составное умножение а *= 10 равносильно а = а * 10
/= составное деление а /= 10 равносильно а = а / 10
%= остаток от деления а %= 10 равносильно а = а % 10
С их использованием можно сократить запись последних двух строчек из предыдущего примера:
d /= a;
c *= d;
|
Очень часто в программировании используется прибавление или вычитание единицы, для чего тоже есть короткая запись:
++ (плюс плюс) инкремент а++ равносильно а = а + 1
-- (минус минус) инкремент а-- равносильно а = а - 1
Порядок записи инкремента играет очень большую роль: пост-инкремент
возвращает значение переменной до выполнения инкремента. Операция пре-инкремента
возвращает значение уже изменённой переменной.
Пример:
byte a, b;
a = 10;
b = a++;
// a получит значение 11
// b получит значение 10
a = 10;
b = ++a;
// a получит значение 11
// b получит значение 11
|
Локальные переменные нужно инициализировать, иначе в математических операциях получится непредсказуемый результат.
{
byte a; // просто объявляем
byte b = 0; // инициализируем 0
a++; // результат непредсказуем
b++; // результат 1
}
|
Порядок вычислений
Порядок вычисления выражений подчиняется обычным математическим правилам: сначала выполняются действия в скобках, затем умножение и деление, и в конце – сложение и вычитание.
Математические вычисления выполняются процессором некоторое время, оно зависит от типа данных и типа операции. Вот время выполнения (в микросекундах) не оптимизированных компилятором вычислений для Arduino Nano 16 МГц:
Тип данных | Сложение и вычитание | Умножение | Деление, остаток |
int8_t | 0.44 | 0.625 | 14.25 |
uint8_t | 0.44 | 0.625 | 5.38 |
int16_t | 0.89 | 1.375 | 14.25 |
uint16_t | 0.89 | 1.375 | 13.12 |
int32_t | 1.75 | 6.06 | 38.3 |
uint32_t | 1.75 | 6.06 | 37.5 |
float | 8.125 | 10 | 31.5 |
- Нужно понимать, что не все во всех случаях математические операции занимают ровно столько времени, так как компилятор их оптимизирует.
- Операции с
floatвыполняются гораздо дольше целочисленных, потому что в AVR нет аппаратной поддержки чисел с плавающей точкой и она реализована программно как сложная библиотека. В некоторых микроконтроллерах есть FPU – специальный аппаратный блок для вычислений сfloat.
- Операции целочисленного деления на AVR выполняются дольше по той же причине – они реализованы программно, а вот умножение и сложение с вычитанием МК делает аппаратно и очень быстро.
Целочисленное деление
При целочисленном делении результат не округляется по “математическим” правилам, дробная часть просто отсекается, фактически это округление вниз: и 9/10и 1/10дадут 0. При использовании floatсамо собой получится 0.9и 0.1. Если нужно целочисленное деление с округлением вверх, его можно реализовать так: вместо x / yзаписать (x + y - 1) / y. Рассмотренные выше примеры деления на 10 дадут результат 1.Для округления по обычным математическим правилам можно использовать функцию round(), но она довольно тяжёлая, так работает с loat.
Переполнение переменной
Что будет с переменной, если её значение выйдет из допустимого диапазона? Тут всё весьма просто: при переполнении в бОльшую сторону из нового значения вычитается максимальное значение переменной, и у неё остаётся только остаток. Для сравнения представим переменную как ведро. Будем считать, что при наливании воды и заполнении ведра мы скажем стоп, выльем из него всю воду, а затем дольём остаток. Вот так и с переменной, что останется – то останется. Если переполнение будет несколько раз – несколько раз опорожним наше “ведро” и всё равно оставим остаток.
При переполнении в обратную сторону (выливаем воду из ведра), будем считать, что ведро полностью заполнилось. Да, именно так =) Посмотрим пример:
// тип данных byte (0.. 255)
byte val = 255;
// тут val станет равным 0
val++;
// а тут из нуля станет 246
val -= 10;
// переполним! Останется 13
val = 525;
// и обратно: val равна 236
val = -20;
|
Особенность больших вычислений
Для сложения и вычитания по умолчанию используется ячейка 4 байта (long), но для умножения и деления – 2 байта (int). Если при умножении или делении в текущем действии результат превысит 32768– ячейка переполниться и мы получим некорректный результат. Для исправления ситуации нужно привести тип переменной к longперед вычислением, что заставит МК выделить дополнительную память. Например a = (long)b *c;Для цифр существуют модификаторы, делающие то же самое: U или u– перевод в uint16_t (от 0 до 65’535). Пример: 36000uL или – перевод в int32_t (-2 147 483 648… 2 147 483 647). Пример: 325646LULили ul – перевод в uint32_t (от 0 до 4 294 967 295). Пример: 361341ul
Посмотрим, как это работает на практике:
long val;
val = 2000000000 + 6000000; // посчитает корректно (т.к. сложение)
val = 25 * 1000; // посчитает корректно (умножение, меньше 32'768)
val = 35 * 1000; // посчитает НЕКОРРЕКТНО! (умножение, больше 32'768)
val = (long)35 * 1000; // посчитает корректно (выделяем память (long) )
val = 35 * 1000L; // посчитает корректно (модификатор L)
val = 35 * 1000u; // посчитает корректно (модификатор u)
val = 70 * 1000u; // посчитает НЕКОРРЕКТНО (модификатор u, результат > 65535)
val = 1000 + 35 * 10 * 100; // посчитает НЕКОРРЕКТНО! (в умножении больше 32'768)
val = 1000 + 35 * 10 * 100L; // посчитает корректно! (модификатор L)
val = (long)35 * 1000 + 35 * 1000; // посчитает НЕКОРРЕКТНО! Второе умножение всё портит
val = (long)35 * 1000 + (long)35 * 1000; // посчитает корректно (выделяем память (long) )
val = 35 * 1000L + 35 * 1000L; // посчитает корректно (модификатор L)
Особенности float
Помимо медленных вычислений, поддержка работы с
float
занимает много памяти, т.к. реализована в виде “библиотеки”. Использование математических операций с
float
однократно добавляет примерно 1.5 кБ в память программы.
С вычислениями есть такая особенность: если в выражении нет
float
чисел, то вычисления будут иметь целый результат (дробная часть отсекается). Для получения правильного результата нужно писать преобразование
(float)
перед действием, использовать
float
числа или
float
переменные. Также есть модификатор
f, который можно применять только к цифрам
float
. Смысла в нём нет, но такую запись можно встретить. Смотрим:
float val; // далее будем присваивать 100/3, ожидаем результат 33.3333
val = 100 / 3; // посчитает НЕПРАВИЛЬНО (результат 33.0)
int val1 = 100; // целочисленная переменная
val = val1 / 3; // посчитает НЕПРАВИЛЬНО (результат 33.0)
float val2 = 100; // float переменная
val = val2 / 3; // посчитает правильно (есть переменная float)
val = (float)100 / 3; // посчитает правильно (указываем (float) )
val = 100.0 / 3; // посчитает правильно (есть число float)
val = 100 / 3.0f; // посчитает правильно (есть число float и модификатор)
При присваивании
float
числа целочисленному типу данных дробная часть отсекается. Если хотите математическое округление – его нужно использовать отдельно:
int val;
val = 3.25; // val станет 3
val = 3.92; // val станет 3
val = round(3.25); // val станет 3
val = round(3.92); // val станет 4
Следующий важный момент: из за особенности самой модели “чисел с плавающей точкой” – вычисления иногда производятся с небольшой погрешностью. Смотрите (значения выведены через порт):
float val2 = 1.1 - 1.0;
// результат 0.100000023 !!!
float val4 = 1.5 - 1.0;
// результат 0.500000000
Казалось бы,
val2
должна стать ровно
0.1
после вычитания, но в 8-ом знаке вылезла погрешность! Будьте очень внимательны при сравнении
float
чисел, особенно со строгими операциями
<=
: результат может быть некорректным и нелогичным.
Список математических функций
Математических функций в Arduino довольно много, часть из них являются макросами, идущими в библиотеке Arduino.h, все остальные же наследуются из мощной C++ библиотеки math.h
Математические функции из math.h
Функция |
Описание |
cos (x) |
Косинус (радианы) |
sin (x) |
Синус (радианы) |
tan (x) |
Тангенс (радианы) |
fabs (x) |
Модуль для float чисел |
fmod (x, y) |
Остаток деления x на у для float |
modf (x, *iptr) |
Возвращает дробную часть, целую хранит по адресу iptr http://cppstudio.com/post/1137/ |
modff (x, *iptr) |
То же самое, но для float |
sqrt (x) |
Корень квадратный |
sqrtf (x) |
Корень квадратный для float чисел |
cbrt (x) |
Кубический корень |
hypot (x, y) |
Гипотенуза ( корень(x*x + y*y) ) |
square (x) |
Квадрат ( x*x ) |
floor (x) |
Округление до целого вниз |
ceil (x) |
Округление до целого вверх |
frexp (x, *pexp) |
|
ldexp (x, exp) |
x*2^exp http://cppstudio.com/post/1125/ |
exp (x) |
Экспонента (e^x) |
cosh (x) |
Косинус гиперболический (радианы) |
sinh (x) |
Синус гиперболический (радианы) |
tanh (x) |
Тангенс гиперболический (радианы) |
acos (x) |
Арккосинус (радианы) |
asin (x) |
Арксинус (радианы) |
atan (x) |
Арктангенс (радианы) |
atan2 (y, x) |
Арктангенс (y / x) (позволяет найти квадрант, в котором находится точка) |
log (x) |
Натуральный логарифм х ( ln(x) ) |
log10 (x) |
Десятичный логарифм x ( log_10 x) |
pow (x, y) |
Степень ( x^y ) |
isnan (x) |
Проверка на nan (1 да, 0 нет) |
isinf (x) |
Возвр. 1 если x +бесконечность, 0 если нет |
isfinite (x) |
Возвращает ненулевое значение только в том случае, если аргумент имеет конечное значение |
copysign (x, y) |
Возвращает x со знаком y (знак имеется в виду + -) |
signbit (x) |
Возвращает ненулевое значение только в том случае, если _X имеет отрицательное значение |
fdim (x, y) |
Возвращает разницу между x и y, если x больше y, в противном случае 0 |
fma (x, y, z) |
Возвращает x*y + z |
fmax (x, y) |
Возвращает большее из чисел |
fmin (x, y) |
Возвращает меньшее из чисел |
trunc (x) |
Возвращает целую часть числа с дробной точкой |
round (x) |
Математическое округление |
lround (x) |
Математическое округление (для больших чисел) |
lrint (x) |
Округляет указанное значение с плавающей запятой до ближайшего целого значения, используя текущий режим округления и направление |
Дата: 2022-12-21   Автор: Админ   Просмотров: 1596
Контакты
Если у Вас есть вопросы, мы с удовольствием на них ответим.
Адрес:
Мурманская область, г.Полярный ул. Красный Горн, д.16
Почта:
zatocdod@mail.ru
Телефон:
+7 (815-51) 7-59-64