Типы данных в Arduino IDE

Компьютеры и Arduino в том числе, работают с различными типами данных.

В их основе лежит арифметически-логическое устройство (АЛУ), которое выполняет арифметические и логические операции с ячейками памяти: R1+R2, R3*R7, R4&R5 и т.п.

Для АЛУ нет разницы, какой тип данных отображать пользователю: текст, целые числа, числа с плавающей запятой или даже часть программного кода.

Команды для этих операций поступают от компилятора, а команды компилятору передаются от пользователя. Именно вы, программист, объясняете компилятору, что это значение - целое (integer), а то значение - число с плавающей запятой (floating point number). После этого компилятор начинает разбираться, что имеется в виду, когда вы сообщаете ему "добавь это целое к числу с плавающей запятой". Иногда это просто, иногда - нет. А бывают случаи, когда кажется, что это должно быть просто, а результат получается совершенно неожиданным.

В этой статье мы разберемся с основными типами данных, которые поддерживает Arduino. Узнаем, для чего обычно используется тот или иной тип данных. Выясним, как использовать различные типы данных для уменьшения размеров и скорости работы ваших программ.

Характеристика основных типов данных в Arduino IDE

Оболочка для программирования Arduino по сути представляет из себя язык C++ с поддержкой большого количества библиотек для облегчения процесса написания программ. C++ предлагает широкий выбор различных типов данных. В этой статье мы рассмотрим только о тех, которые используются для работы с Arduino и сделаем акценты на основных ловушках, которые ожидают начинающего программиста Arduino.

Ниже представлен список основных типов данных, которые используются в скетчах Arduino. Рядом с каждым типом данных указан его размер. Обратите внимание, сто переменные типа signed дают возможность оперировать положительными и отрицательными числами, а переменные типа unsigned допускают только работу с позитивными значениями.

  • boolean (8 бит) - простое логическое true/false (правда/ложь)
  • byte (8 бит) - unsigned число в диапазоне от 0-255
  • char (8 бит) - signed число в диапазоне от -128 до 127. В некоторых случаях компилятор будет интерпретировать этот тип данных как символ, что может привести к неожиданным результатам.
  • unsigned char (8 бит) - то же что и ‘byte’; для ясности кода рекомендуется вместо этого типа данных использовать ‘byte’.
  • word (16 бит) - unsigned число в диапазоне от 0 до 65535
  • unsigned int (16 бит)- то же, что и ‘word’. Рекомендуется заменять типом данных ‘word’ для сокращения кода и ясности
  • int (16 бит) - signed число в диапазоне от -32768 до 32767. Один из самых распространенных типов данных, который очень часто используется для объявления переменных в скетчах-примерах для Arduino, встроенных в Arduino IDE
  • unsigned long (32 бита) - unsigned число в диапазоне от 0-4,294,967,295. Чаще всего этот тип данных используется для хранения результатов функции millis( ), которая возвращает количество миллисекунд, на протяжении которого работал ваш код.
  • long (32 бита) - signed число в диапазоне от -2,147,483,648 до 2,147,483,647
  • float (32 бита) - signed число в диапазоне от -3.4028235E38 до 3.4028235E38. Числа с плавающей запятой не характерны для Arduino и компилятору придется прилично попотеть, чтобы их обработать. Так что рекомендуется по возможности их избегать. Более детально мы затронем этот вопрос позже.

В этой статье не рассматриваются arrays (массивы), pointers (указатели) и strings (строки); это специальные типы данных, с более сложной концепцией, которую надо рассматривать отдельно.

Время обработки и размер разных типов данных в Arduino IDE

Сердце платы Arduino - Atmel ATmega328P, 8-ми битный процессор, в котором не предусмотренная встроенная функция поддержки чисел с плавающей запятой. Для того, чтобы использовать типы данных больше чем 8 бит, компилятор должен обеспечить место для отдельного куска кода с большим объемом данных, обработать его (на что понадобится некоторое время) и поместить результат обработки в место, где он используется.

То есть, максимальная продуктивность будет при обработке 8-ми битных значений и минимальная - при работе со значениями с плавающей запятой. Рассмотрим пример скетча для Arduino, в котором проводятся простые математические операции. В программе можно легко поменять тип данных и сделать аналогичные расчеты, чем мы и займемся.

Сначала давайте загрузим скетч на ваш Arduino Uno и посмотрим, какой результат появится в консоли серийного монитора:

Время обработки и размер разных типов данных в Arduino IDE 1

Давайте разбираться с результатами.

Во-первых, проверьте размер скомпилированного кода. С использованием типа данных bytes мы вышли на размер скетча в 2458 байта. Не так уж много и большая часть - это операция вывода данных в окне серийного монитора.

Во-вторых, давайте взглянем на вывод данных в окне серийного монитора. Почему вместо чисел отображаются квадраты? Это происходит из-за того, что функция Serial.print( ) в Arduino изменяет режим своей работы в зависимости от типа данных, которые ей передаются. Для 8-ми битных значений (это может быть и char и byte), функция отобразит значение в бинарном виде. При этом консоль серийного монитора интерпретирует эти данные в виде ASCII символа, а ASCII символы для 1,2 и 3 - это ‘START OF HEADING’, ‘START OF TEXT’, и ‘END OF TEXT’. Согласитесь, на практике это не самый удобный вариант, не лучше ли отобразить это с помощью одного символа? Отсюда и появляется квадрат: консоль серийного монитора сообщает вам примерно следующее: "я без понятия как это отобразить, так что вот вам квадрат". Итак, урок первый при работе с различными типами данных Arduino: для того, чтобы отобразить 8-ми битное значение в десятичном формате с использованием функции Serial.print( ), при вызове функции надо добавить DEC:

Serial.print(x, DEC);

Ну и в-третьих, посмотрите на строку, в которой отображено значение "Elapsed time" (Затраченное время). Для того, чтобы оценить затраченное время, используем функцию micros( ), которая выдает достаточно точный результат при оценке времени выполнения запрограммированных операций. Как вы можете заметить, операция суммирования двух 8-ми битных значений заняла около 4 микросекунд процессорного времени.

Тестируем разные типы данных (Сумма)

Давайте двигаться дальше и проведем тестирование других типов данных. Если вы захотите проделать подобный эксперимент с использованием своей Arduino IDE, измените скетч как это показано на рисунке ниже:

Тестируем разные типы данных (сумма) 1 Тестируем разные типы данных (сумма) 2

Теперь загрузите новый скетч на ваш Arduino. Проверьте размер после компиляции: 2488 байт при использовании типа данных int. Напомним, что при использовании byte у нас было 2458 байта. Не намного больше, но это всего лишь три переменные! Напомним, что тип данных int занимает 16 байт, а не 8 как, например, byte. Теперь откройте окно серийного монитора, на котором вы должны увидеть что вроде:

Тестируем разные типы данных (сумма) 3

Следующее: в этот раз данные отобразились корректно. Это из-за того, что новый тип данных, который мы использовали, int, корректно интерпретируется компилятором как числовой тип и функция Serial.print( ) правильно отображает данные в консоли. Итак, второй урок при работе с разными типами данных Arduino: если вы хотите отправить бинарный эквивалент данных типа числового, скажем, для передачи данных на другой компьютер, а не для отображения в консоли, используйте функцию Serial.write( ).

Теперь давайте посмотрим на строку “Elapsed time”. Мы потратили 12 микросекунд - в три раза больше времени! Почему такая разница? Как мы уже упоминали выше, у нас 8-ми битный процессор, а это значит, что для математических операций с 16-ти битными числами, нам потребуются дополнительная обработка данных.

Теперь давайте займемся типом данных long. Повторите код, заменив тип данных int на long. Загрузите скетч на Arduino и откройте ваш серийный монитор:

Тестируем разные типы данных (сумма) 4

Смотрим на размер скетча: 2516 байт, на 28 байт больше, чем при использовании int и на 58 больше, чем с использованием типа данных byte.

Смотрим дальше. Время, которое была затрачено на обработку данных уменьшилось! С 12 до 8 секунд! Почему? Это не логично! Это третий урок, который надо вынести: не всегда то, что вы предполагаете на самом деле происходит. Вероятно, подобное происходит благодаря оптимизации самого компилятора.

Итак, последнее, что мы должны рассмотреть - типы данных с плавающей запятой. Для этого замените тип данных long на float. Загрузите скетч на Arduino и обратите внимание на размер: 3864 байта! Используя тип данных с плавающей запятой вы заставили компилятор обрабатывать значения после запятой. А это дополнительный большой кусок кода. Урок четвертый - не используйте значения с плавающей запятой, если только вы на 60% не уверены, что без этого типа данных вам не обойтись. Например, если вы подключили сенсор и получаете значение с аналогового входа вроде 536, гораздо информативнее будет преобразовать это значение в вольты и отобразить пользователю что-то вроде 2.26 В.

Тестируем разные типы данных (сумма) 5

Теперь взгляните на время обработки данной программы: опять 12 миллисекунд. Кстати, заметьте, что значения x, y, z отобразились с двумя нулями в десятичной части. Если вам надо больше (или меньше) значков после запятой, вы можете добавить их с помощью команды:

Serial.print(x, 3); // отображает x с тремя знаками в дробной части.

Тестируем разные типы данных (Умножение/Деление)

Теперь давайте взглянем что будет в случае "более сложных" математических операций - умножение и деление.

Ниже представлены несколько скриншотов результатов операции умножения:

Тестируем разные типы данных (Умножение/Деление) 1 Тестируем разные типы данных (Умножение/Деление) 2
Тестируем разные типы данных (Умножение/Деление) 3 Тестируем разные типы данных (Умножение/Деление) 4

Оцените затраченное время: 4 миллисекунды для byte, 8 для int или long и 12 для float. Итак, умножение явно поддерживается на уровне встроенных инструкций в нашем процессоре на Arduino. В результате операция умножения обрабатывается достаточно быстро. А что же будет с делением?

Тестируем разные типы данных (Умножение/Деление) 5 Тестируем разные типы данных (Умножение/Деление) 6
Тестируем разные типы данных (Умножение/Деление) 7 Тестируем разные типы данных (Умножение/Деление) 8

Упс. Деление сданных типа byte не так уж критично по времени - 16 миллисекунд, но long тянет 48?! Как-то не очень впечатляет. Оказывается, в процессоре Atmega, которым оснащается Arduino, нет встроенных инструкций для операций деления и компилятору приходится каждый раз ее генерировать. Так что наш последний урок при работе с разными типами данных Arduino: не все математические операции отрабатывают по подобным алгоритмам. Операция деления занимает гораздо больше времени чем умножение или нахождение суммы (или операция нахождения разности, хотя это по сути тоже, что и сложение только со знаком минус). А более сложные операции вроде нахождения квадратного корня или синуса угла займут еще больше времени. Порой обработка результатов длится настолько долго, что гораздо проще просто получить список необходимых значений с помощью Arduino, передать их на персональный компьютер и уже там найти нужные вам синусы, косинусы, тангенсы, квадратные корни и т.д. и т.п.

Надеемся, что эта статья на наглядных примерах помогла вам разобраться с базовыми типами данных, которые используются для программирования плат Arduino.

Оставляйте Ваши комментарии, вопросы и делитесь личным опытом ниже. В дискуссии часто рождаются новые идеи и проекты!