Математические действия


Одной из основных функций микроконтроллера является выполнение вычислений, как с числами напрямую, так и со значениями переменных. Начнём погружение в мир математики с самых простых действий:

  • = присваивание
  • % остаток от деления
  • * умножение
  • / деление
  • + сложение
  • вычитание

Рассмотрим простой пример:

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

По поводу последних двух строчек из примера, когда переменная участвует в расчёте своего собственного значения: существуют также составные операторы, укорачивающие запись:

  • += составное сложение: a += 10 равносильно a = a + 10
  • -= составное вычитание: a -= 10 равносильно a = a – 10
  • *= составное умножение: a *= 10 равносильно a = a * 10
  • /= составное деление: a /= 10 равносильно a = a / 10
  • %= прибавить остаток от деления: a %= 10 равносильно a = a + a % 10

С их использованием можно сократить запись последних двух строчек из предыдущего примера:

d /= a;      // (равносильно d=d/a) d = 20
c *= d;      // (равносильно c=c*d) c = 600

Очень часто в программировании используется прибавление или вычитание единицы, для чего тоже есть короткая запись:

  • ++ (плюс плюс) инкремент: a++ равносильно a = a + 1
  • (минус минус) декремент: a — равносильно a = a – 1

Порядок записи инкремента играет очень большую роль: если инкремент идёт после имени переменной, то она сразу получает новое значение, если же инкремент идёт перед именем – сначала выполняется действие, а затем изменяется величина. Пример:

byte a, b;

a = 10;
b = a++;
// a получит значение 11
// b получит значение 11

a = 10;
b = ++a;
// a получит значение 11
// b получит значение 10 !!!

Порядок вычислений


Порядок вычисления выражений подчиняется обычным математическим правилам: сначала выполняются действия в скобках, затем умножение и деление, и в конце – сложение и вычитание

Скорость вычислений


Производимые вычисления занимают у процессора некоторое время, оно зависит от типа действия и от типа данных, с которым действие производится. Нужно понимать, что не все во всех случаях действия тратят столько времени, сколько будет рассказано дальше: компилятор старается по возможности оптимизировать вычисления, как он это делает можно попробовать поискать в интернете. Оптимизированные вычисления занимают ничтожно мало времени по сравнению с не оптимизированными. Сложно сказать, будет ли оптимизировано отдельно взятое вычисление в вашем коде, поэтому нужно всегда готовиться к худшему и знать, как лучше делать. А именно:

  • Arduino (на AVR) не имеет “хардверной” поддержки вычислений с плавающей точкой (float), и эти вычисления производятся при помощи отдельных инструментов и занимают гораздо больше времени, чем с целочисленными типами
  • Чем “массивнее” тип данных, тем дольше производятся вычисления, т.е. действия с 1-байтными переменными производятся быстрее, чем с 4-х байтными
  • Деление (и поиск остатка от деления) производится отдельными инструментами (как операции с float), поэтому эта операция занимает больше времени, чем сложение/вычитание/умножение. Для оптимизации скорости вычислений есть смысл заменять деление умножением на обратное число (даже на float)

Резюмируя всё вышесказанное хочу показать вам вот такую табличку, в которой показано время не оптимизированных компилятором вычислений разных типов данных, время указано в микросекундах (мкс):

Эта информация дана чисто для ознакомления и париться о скорости вычислений не нужно, т.к. большинство из них будут оптимизированы. Когда могут возникнуть проблемы с недостатком скорости вычисления? Я столкнулся с этим только один раз в проекте “LED кубик“, где Ардуина вычисляла закон движения пары десятков точек по наклонной плоскости по подробной физической модели. Вот там да, я заметил, как микросекунды вычислений превращались в миллисекунды. Для простых проектов без тысяч вычислений, честно, не парьтесь =)

Переполнение переменной


Раз мы начали говорить о действиях, увеличивающих или уменьшающих значение переменной, стоит задуматься и о том, что будет с переменной, если её значение выйдет из допустимого диапазона? Тут всё весьма просто: при переполнении в бОльшую сторону из нового большого значения отсекается максимальное значение переменной, и у неё остаётся только остаток. Для сравнения представим переменную как ведро. Будем считать, что при наливании воды и переполнении ведра мы скажем стоп, выльем из него всю воду, и дольём остаток. Вот так и с переменной, что останется – то останется. Если переполнение будет несколько раз – несколько раз опорожним наше “ведро” и всё равно оставим остаток. Ещё один хороший пример – кружка Пифагора. При переполнении в обратную сторону, т.е. в минус, выливаем воду, будем считать, что ведро полностью заполнилось. Да, именно так =) Посмотрим пример:

// тип данных byte
// мин. значение 0
// макс. значение 255
byte val = 255;

// тут val станет равным 0
val++;

// а тут из нуля станет 246
val -= 10;

// переполним! Останется 13
val = 525;

// и обратно: val равна 236
val = -20;

 

Особенность больших вычислений


Для сложения и вычитания по умолчанию используется ячейка long (4 байта), но при умножении и делении используется int (2 байта), что может привести к непредсказуемым результатам! Если при умножении чисел результат превышает 32’768, он будет посчитан некорректно. Для исправления ситуации нужно писать (тип данных) перед умножением, что заставит МК выделить дополнительную память для вычисления (например (long)35 * 1000). Также существую модификаторы, делающие примерно то же самое.

  • u или U – перевод в формат unsigned int (от 0 до 65’535). Пример: 36000u
  • l или L – перевод в формат long (-2 147 483 648… 2 147 483 647). Пример: 325646L
  • ul или UL – перевод в формат unsigned long (от 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


Arduino поддерживает работу с числами с плавающей точкой (десятичные дроби). Этот тип данных не является для неё “родным”, поэтому вычисления с ним производятся в несколько раз дольше, чем с целочисленным типом (около 7 микросекунд на действие). Arduino поддерживает три типа ввода чисел с плавающей точкой:

Тип записи Пример Чему равно
Десятичная дробь 20.5 20.5
Научный 2.34E5 2.34*10^5 или 234000
Инженерный 67e-12 67*10^-12 или 0.000000000067

С вычислениями есть такая особенность: если в выражении нет float чисел, то вычисления будут иметь целый результат (дробная часть отсекается). Для получения правильного результата нужно писать (float) перед действием, или использовать float числа при записи. Смотрим:

float val;
val = 100 / 3;          // посчитает НЕПРАВИЛЬНО (результат 3.0)
val = (float)100 / 3;   // посчитает правильно (указываем (float))
val = 100.0 / 3;        // посчитает правильно (есть число float)
val = 100 / 3.0;        // посчитает правильно (есть число float)

float val2 = 100;
val = val2 / 3;         // посчитает правильно (есть число 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

Список математических функций


Математических функций Arduino поддерживает очень много, малая часть из них являются макро функциями, идущими в комплекте с Arduino.h, все остальные же наследуются из мощной C++ библиотеки 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) http://cppstudio.com/post/1121/
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) Округляет указанное значение с плавающей запятой до ближайшего целого значения, используя текущий режим округления и направление

Функция Значение
min(a, b) Возвращает меньшее из чисел a и b
max(a, b) Возвращает большее из чисел
abs(x) Модуль числа
constrain(val, low, high) Ограничить диапазон числа val между low и high
map(val, min, max, outMin, outMax) Перевести диапазон числа val (от min до max) в новый диапазон (от outMin до outMax). val = map(analogRead(0), 0, 1023, 0, 100); – получить с аналогового входа значения 0-100 вместо 0-1023
round(x) Математическое округление
radians(deg) Перевод градусов в радианы
degrees(rad) Перевод радиан в градусы
sq(x) Квадрат числа

Константа Значение Описание
INT8_MAX 127 Максимальное значение для char, int8_t
UINT8_MAX 255 Максимальное значение для byte, uint8_t
INT16_MAX 32767 Максимальное значение для int, int16_t
UINT16_MAX 65535 Максимальное значение для unsigned int, uint16_t
INT32_MAX 2147483647 Максимальное значение для long, int32_t
UINT32_MAX 4294967295 Максимальное значение для unsigned long, uint32_t
M_E 2.718281828 Число e
M_LOG2E 1.442695041 log_2 e
M_LOG10E 0.434294482 log_10 e
M_LN2 0.693147181 log_e 2
M_LN10 2.302585093 log_e 10
M_PI 3.141592654 pi
M_PI_2 1.570796327 pi/2
M_PI_4 0.785398163 pi/4
M_1_PI 0.318309886 1/pi
M_2_PI 0.636619772 2/pi
M_2_SQRTPI 1.128379167 2/корень(pi)
M_SQRT2 1.414213562 корень(2)
M_SQRT1_2 0.707106781 1/корень(2)
NAN __builtin_nan(“”) nan
INFINITY __builtin_inf() infinity
PI 3.141592654 Пи
HALF_PI 1.570796326 пол Пи
TWO_PI 6.283185307 два Пи
EULER 2.718281828 Число Эйлера е
DEG_TO_RAD 0.01745329 Константа перевода град в рад
RAD_TO_DEG 57.2957786 Константа перевода рад в град

Видео


Важные страницы


  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
Оцени пост!

Последнее обновление Май 06, 2019
2019-05-06T11:12:13+03:00