Arduino
Оформление программного кода
В больших организациях, в которых работают десятки и сотни программистов, всегда есть документ, который содержит список правил оформления кода. Обычно он включает такие пункты:
И еще много другого. Это позволяет легче читать и дорабатывать чужой код. Однако, даже если вы работаете в небольшой команде или один, определение таких правил позволит вам намного проще читать и дорабатывать свой старый код и улучшать его.
С появлением сторонних редакторов кода и IDE, таких, как PlatformIO, следить за соблюдением стилистики кода стало намного проще.
Отступы
Первое и самое важное правило: отступы нужно использовать. Обязательно. Давайте сравним такой код:
Даже такой небольшой пример показывает, что код с отступами гораздо проще читать. А когда в коде будет в несколько раз больше уровней вложенности и гораздо больше строк, что прочитать и понять код без отступов будет ну очень сложно.
Размер отступа, а также то, какой символ использовать (пробел или табуляция) выбирается исходя из личных предпочтений. Однако есть устоявшаяся рекомендация: использовать всегда пробелы и не использовать символ табуляции. Это связано с тем, что отображение символа табуляции может быть разным в разных редакторах кода, что делает неудобным чтение кода при открытии его в другом редакторе, не где он был написан. Многие редакторы по-умолчанию конвертируют символ табуляции в символы пробелов.
Пробелы
Пробелы делают код более читаемым. Например сравните:
Второй вариант явно удобнее читать. Есть еще такой вариант:
Каждый решает сам. Но именно второй вариант наиболее распространен и чаще принято использовать именно его.
Фигурные скобки
Фигурные скобки определяют начало и конец блока кода функции или таких операторов, как условный оператор или оператор цикла. Если есть открывающаяся скобка, то должна быть и закрывающаяся, иначе код не будет скомпилирован.
Если для функции фигурные скобки являются обязательным элементом, то для многих операторов их можно опустить. Например:
Все три вариант равносильны. Но при отсутствии скобок отработает только первая строчка, следующая за оператором, поэтому если в блоке кода несколько строчек, то использование скобок становится обязательным. Именно по этой причине настоятельно рекомендуется всегда использовать фигурные скобки, так как легко допустить ошибку например при доработке кода.
Комментарии
Использование комментариев сильно упрощает чтение кода программы, а также используется например, чтобы оставить пометку доработать что-то в будущем. В скетчах часто в самом начале оставляют комментарий с кратким описанием программы, а также с перечислением подключенных к Arduino устройств и способов их подключения.
Программный код
Перед тем как приступать к написанию кода, мы должны получить знания об основах программирования. Давайте познакомимся с основными конструкциями языка программирования, используемого в Arduino.
Комментарии
Комментариями называется часть текста в коде, которая помогает разъяснять назначение отдельных частей скетча. Этот текст никак не влияет на выполнение основного кода программы.
Есть два способа выделения комментариев в скетче:
1. Символ «//». Так выделяются комментарии, которые помещаются в одной строчке:
Код программы перед «//» никак не изменится.
2. Символы «/*» и «*/». Они применяются, когда необходимо написать длинный комментарий. Всё, что написано между этими символами, будет восприниматься как комментарий.
Типы данных/Data
В процессе программирования используются числовые, символьные и другие типы данных. Язык программирования С («Си») позволяет работать с несколькими типами данных, например:
Для того, чтобы познакомиться со всем набором типов данных, используемых в Arduino, вы можете посетить страничку в интернете: arduino.ru/Reference
Константы/Constants
Константы — это числа, используемые напрямую в коде, без определения переменной для их хранения.
Переменные/Varibles
Переменные — это значения, которые могут изменяться во время выполнения программы. Для удобства каждому такому значению задаётся определённое имя. Каждая переменная принадлежит какому-то одному типу данных.
Переменные нужно объявлять перед использованием в программе, например:
«int» указывает на тип переменной «i», а символ «;» обозначает конец оператора. После объявления переменной вы можете её использовать в программе, например:
Переменной, находящейся слева от оператора присваивания («=») присваивается значение переменной или выражения, находящегося справа. Переменная должна быть способна хранить присваиваемое значение, то есть соответствовать его типу и диапазону допустимых значений.
В одной строке может быть объявлено несколько переменных одного типа. Также значение одной переменной может присваиваться другой переменной. Например, как показано ниже:
Функция/Function
Функцией называется набор операторов, которые выполняются в определенном порядке и решают определенную задачу. Давайте для наглядного примера напишем функцию мигания светодиодом:
Код в скетче исполняется последовательно и, в случае вызова функции, выполнение основной программы приостанавливается и начинает выполняться вызываемая функция. Основная часть программы продолжит выполняться после выполнения всех операторов функции, как это показано ниже:
Некоторые функции могут иметь один или несколько параметров (передаваемых ей значений). В таких случаях параметры указывабтся внутри круглых скобок «( )»:
Здесь digitalWrite и delay — это тоже функции, только заранее определённые в самой среде программирования Arduino.
Список команд Arduino
На этой странице представлен список всех (или почти всех) доступных “из коробки” команд для Arduino с кратким описанием и примерами. Часть информации взята из Гугла, в основном некоторые особенности языка, часть получена методом проб и ошибок. Полную информацию о том, как этим пользоваться, можно получить из уроков или официального reference. Также изо всех сил рекомендую вот этот онлайн справочник по C++, из него можно узнать гораздо больше о некоторых особенностях использования операторов и типов данных.
А ещё у меня есть краткий сборник всех основных Ардуино-функций для печати! Смотри в этом уроке.
Структура скетча
Синтаксис, структура кода
Ставится в конце каждого действия
Функция, содержимое которой выполняется один раз при запуске микроконтроллера. Подробнее – в этом уроке.
Функция, содержимое которой выполняется (или пытается выполняться) “по кругу” на протяжении всего времени работы МК. Подробнее – в этом уроке.
Директива, позволяющая подключать в проект дополнительные файлы с кодом.
В чём отличие <> и “”? Когда указываем название “в кавычках”, компилятор сначала ищет файл в папке со скетчем, а затем в папке с библиотеками. При использовании компилятор ищет файл только в папке с библиотеками
Директива, дающая команду препроцессору заменить указанное название на указанное значение. Чаще всего таким образом объявляют константы:
После компиляции все встречающиеся в тексте программы слова MOTOR_PIN будут заменены на цифру 10, а LED_PIN – на цифру 3. Такой способ хранения констант не использует оперативную память микроконтроллера. Также define позволяет делать т.н. макро функции. Например Ардуиновская функция sq (квадрат) является макро, который при компиляции превращается в умножение:
Директивы препроцессору, позволяющие включать или исключать участки кода по условию
При помощи условной компиляции очень удобно собирать и настраивать сложные проекты с кучей настроек и библиотек, подключаемых “по условию”. Например:
Если параметру DEBUG установить 1, то будет подключена библиотека Serial, если 0 – то нет. Таким образом получаем универсальный оптимизированный проект с отладкой. Подробнее – в этом уроке.
Условные директивы препроцессору, позволяют включать или исключать участки кода по условию: ifdef – определено ли? ifndef – не определено ли? Подробнее – в этом уроке.
Оператор перехода в другую часть кода по метке. Не рекомендуется к использованию, всегда можно обойтись без него. Как пример использования – выход из кучи условий
Оператор прерывания функции, он же оператор возврата значения из функции. Подробнее – в этом уроке
Условия (if, switch)
Оператор сравнения и его друзья. Подробнее – в этом уроке.
Оператор выбора, заменяет конструкцию с else if. Подробнее – в этом уроке.
Оператор break очень важен, позволяет выйти из switch. Но можно использовать так:
Циклы (for, while)
Цикл – “счётчик”. for (инициализация; условие; инкремент). Подробнее – в этом уроке.
Также используется для создания замкнутых циклов, т.к. настройки for необязательны. Выход только через break или goto
Цикл с предусловием. Подробнее – в этом уроке.
Может быть использован для создания замкнутого цикла, выход только через break или goto
Цикл с постусловием. Подробнее – в этом уроке.
Отличается от while тем, что гарантированно выполнится хотя бы один раз
Пропускает все оставшиеся в теле цикла действия и переходит к следующей итерации
Операторы
Запятая тоже является оператором, используется в следующих случаях:
Рассмотрим третий случай здесь:
Арифметические
Арифметические операторы – самые простые и понятные из всех
Большой урок по математическим действиям.
Сравнение и логика
Подробный урок по сравнениям и условиям.
Составные операторы
По битовым операциям читай отдельный урок. По математическим операциям у меня тоже есть подробный урок.
Битовые операции
По битовым операциям читай урок у меня на сайте.
Указатели и ссылки
Подробнее об указателях и ссылках читайте в отдельном уроке.
Работа с данными
Типы данных, переменные
Переменная – элементарная ячейка для хранения данных (цифр). Переменные разных типов имеют разный “размер ячейки” и имеют разный лимит на размер числа.
Название | Альт. название | Вес | Диапазон | Особенность |
boolean | bool | 1 байт | 0 или 1, true или false | Логическая переменная. bool на Arduino тоже занимает 1 байт, а не бит! |
char | int8_t | 1 байт | -128… 127 | Хранит номер символа из таблицы символов ASCII |
byte | uint8_t | 1 байт | 0… 255 | |
int | int16_t, short | 2 байта | -32 768… 32 767 | |
unsigned int | uint16_t, word | 2 байта | 0… 65 535 | |
long | int32_t | 4 байта | -2 147 483 648… 2 147 483 647 | – 2 миллиарда… 2 миллиарда |
unsigned long | uint32_t | 4 байта | 0… 4 294 967 295 | 0… 4 миллиарда… |
float | — | 4 байта | -3.4028235E+38… 3.4028235E+38 | Хранит числа с плавающей точкой (десятичные дроби). Точность: 6-7 знаков |
double | — | 4 байта | Для AVR то же самое, что float. А так он 8 байт | |
— | int64_t | 8 байт | -(2^64)/2… (2^64)/2-1 | Очень большие числа. Serial не умеет такие выводить |
— | uint64_t | 8 байт | 2^64-1 | Очень большие числа. Serial не умеет такие выводить |
Существует еще несколько специальных типов данных для символов. Подробнее можно почитать здесь.
Создаёт тип данных под названием color, который будет абсолютно идентичен типу byte (то есть принимать 0-255). Теперь с этим типом можно создавать переменные:
Создали три переменные типа color, который тот же byte, только в профиль. Это всё!
Более подробно о переменных и данных можно почитать вот здесь.
Структуры
Структура (struct) – очень составной тип данных: совокупность разнотипных переменных, объединённых одним именем.
Ярлык будет являться новым типом данных, и, используя этот ярлык, можно объявлять уже непосредственно саму структуру:
Также есть вариант объявления структуры без создания ярлыка, т.е. создаём структуру, не объявляя её как тип данных со своим именем.
Более подробно про структуры читай тут.
Перечисления
Перечисления (enum – enumeration) – тип данных, представляющий собой набор именованных констант, нужен в первую очередь для удобства программиста.
Объявление перечисления чем-то похоже на объявление структуры:
Таким образом мы объявили ярлык. Теперь, используя этот ярлык, можно объявить само перечисление:
Также как и у структур, можно объявить перечисление без создания ярлыка (зачем нам лишняя строчка?):
Более подробно про перечисления читай тут.
Классы
Классы в С++ – это основной и очень мощный инструмент языка, большинство “библиотек” являются классами. Иерархия такая:
Класс объявляется следующим образом:
Более подробно про работу с классами читай вот в этом уроке про классы.
Массивы
Для объявления массива достаточно указать квадратные скобки после имени переменной, тип данных – любой. Более подробно про массивы читай в уроке.
Строки (объект String)
String – очень мощный инструмент для работы со строками, т.е. текстовыми данными. Объявить строку можно несколькими способами:
Подробнее про строки String и массивы символов читай в этом уроке.
Строки можно сравнивать, складывать и вычитать, также для работы с ними есть куча функций:
myString.charAt(index);
Возвращает элемент строки myString под номером index. Аналог – myString[index];
myString.setCharAt(index, val);
Записывает в строку myString символ val на позицию index. Аналог – myString[index] = val;
myString.compareTo(myString2);
myString.concat(value);
Присоединяет value к строке (value может иметь любой численный тип данных). Возвращает true при успешном выполнении, false при ошибке. Аналог – сложение, myString + value;
myString.endsWith(myString2);
Проверяет, заканчивается ли myString символами из myString2. В случае совпадения возвращает true
myString.startsWith(myString2);
Проверяет, начинается ли myString символами из myString2. В случае совпадения возвращает true
myString.equals(myString2);
Возвращает true, если myString совпадает с myString2. Регистр букв важен
myString.equalsIgnoreCase (myString2);
Возвращает true, если myString совпадает с myString2. Регистр букв неважен
myString.indexOf(val);
myString.indexOf(val, from);
myString.lastIndexOf(val);
myString.lastIndexOf(val, from);
myString.length();
Возвращает длину строки в количестве символов
myString.remove(index);
myString.remove(index, count);
Удаляет из строки символы, начиная с index и до конца, либо до указанного count
myString.replace(substring1, substring2);
В строке myString заменяет последовательность символов substring1 на substring2.
myString.reserve(size);
Зарезервировать в памяти количество байт size для работы со строкой
myString.c_str();
Преобразовывает строку в “СИ” формат (null-terminated string) и возвращает указатель на полученную строку
myString.trim();
Удаляет пробелы из начала и конца строки. Действует со строкой, к которой применяется
myString.substring(from);
myString.substring(from, to);
Возвращает кусок строки, содержащейся в myString начиная с позиции from и до конца, либо до позиции to
myString.toCharArray(buf, len);
Раскидывает строку в массив – буфер buf (типа char []) с начала и до длины len
myString.getBytes(buf, len);
Копирует указанное количество символов len (вплоть до unsigned int) в буфер buf (byte [])
myString.toFloat();
Возвращает содержимое строки в тип данных float
myString.toDouble();
Возвращает содержимое строки в тип данных double
myString.toInt();
Возвращает содержимое строки в тип данных int
myString.toLowerCase();
Переводит все символы в нижний регистр. Было ААААА – станет ааааа
myString.toUpperCase();
Переводит все символы в верхний регистр. Было ааааа – станет ААААА
Спецификаторы переменных
Преобразование типов данных
Таким образом можно преобразовывать обычные переменные, указатели и другие типы данных.
И для строк мы уже рассматривали выше
Как пользоваться: на примере предыдущего примера
Подробный урок по типам данных смотри тут.
“Символьные” функции
Все следующие функции принимают как аргумент символ (тип char), анализируют его и возвращают true или false в зависимости от предназначения.
Работа с цифрами
Целые и дробные числа
Arduino поддерживает работу с целыми числами в разных системах исчисления:
Базис | Префикс | Пример | Особенности |
2 (двоичная) | B или 0b (ноль бэ) | B1101001 | цифры 0 и 1 |
8 (восьмеричная) | 0 (ноль) | 0175 | цифры 0 – 7 |
10 (десятичная) | нет | 100500 | цифры 0 – 9 |
16 (шестнадцатеричная) | 0x (ноль икс) | 0xFF21A | цифры 0-9, буквы A-F |
ВАЖНО! Для арифметических вычислений по умолчанию используется ячейка long (4 байта), но при умножении и делении используется int (2 байта), что может привести к непредсказуемым результатам! Если при умножении чисел результат превышает 32’768, он будет посчитан некорректно. Для исправления ситуации нужно писать (тип данных) перед умножением, что заставит МК выделить дополнительную память для вычисления (например (long)35 * 1000). Также существую модификаторы, делающие примерно то же самое.
Посмотрим, как это работает на практике:
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 числа целочисленному типу данных дробная часть отсекается. Если хотите математическое округление – его нужно использовать отдельно:
Математические функции и константы
Математических функций Arduino поддерживает очень много, малая часть из них являются макро функциями, идущими в комплекте с Arduino.h, все остальные же наследуются из мощной C++ библиотеки math.h
Константа | Значение |
UINT8_MAX | 255 |
INT8_MAX | 127 |
UINT16_MAX | 65535 |
INT16_MAX | 32767 |
UINT32_MAX | 4294967295 |
INT32_MAX | 2147483647 |
Функция | Значение |
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 | Константа перевода рад в град |
Псевдослучайные числа
Биты и байты
Битовые операции – подробнее читай в отдельном уроке.
Ввод-вывод
Цифровые пины
Устанавливает режим работы пина pin (ATmega 328: D0-D13, A0-A5) на режим mode:
Читает состояние пина pin и возвращает :
Подаёт на пин pin сигнал value:
Запускает генерацию ШИМ сигнала (отдельный урок про ШИМ) на пине pin со значением value. Для стандартного 8-ми битного режима это значение 0-255, соответствует скважности 0-100%. Подробнее о смене частоты и разрядности ШИМ смотрите в этом уроке. ШИМ пины:
Аналоговые пины
Читает и возвращает оцифрованное напряжение с пина pin. АЦП на большинстве плат Arduino имеет разрядность 10 бит, так что возвращаемое значение 0 – 1023 при напряжении 0 – опорное на пине. Урок про аналоговые пины.
Устанавливает режим работы АЦП согласно mode:
Как это влияет на работу? Значение 1023 функции analogRead() будет соответствовать опорному напряжению или выше его, соответственно поставив INTERNAL можно измерять напряжение от 0 до 1.1V с точностью (1.1 / 1023
1.2 мВ), напряжение выше 1.1V будет всегда 1023. После изменения источника опорного напряжения (вызова analogReference) первые несколько измерений могут быть нестабильными.
Нельзя использовать напряжение меньше 0V или выше 5V в качестве внешнего опорного в пин AREF. Также при использовании режима EXTERNAL нужно вызвать analogReference(EXTERNAL) до вызова функции analogRead(), иначе можно повредить микроконтроллер. Также можно подключить опорное в пин AREF через резистор на
5 кОм, но так как вход AREF имеет собственное сопротивление в 32 кОм, реальное опорное будет например 2.5 * 32 / (32 + 5) =
Аппаратные прерывания
Подключить прерывание (читай урок про прерывания) на номер прерывания pin, назначить функцию ISR как обработчик и установить режим прерывания mode: