Arduino. синтаксис и структура кода

Синтаксис

  • Тела функций заключаются в фигурные скобки
  • Каждая команда заканчивается точкой с запятой
  • Метод применяется к объекту через точку. Пример:
  • Вызов функции или метода всегда заканчивается скобками, даже если функция не принимает параметров. Пример:
  • Разделитель десятичных дробей – точка. Пример: У запятой тут другое применение
  • Запятыми перечисляются аргументы функций и методов, а также членов массива. Пример: массив – Также запятая является самостоятельным оператором, но об этом поговорим отдельно в другом уроке
  • Одиночный символ заключается в одиночные кавычки
  • Строка и массив символов заключается в двойные кавычки
  • Имена переменных могут содержать латинские буквы в верхнем и нижнем регистре (большие и маленькие), цифры и нижнее подчеркивание. Пример: .
  • Имена переменных не могут начинаться с цифры. Только с буквы или нижнего подчёркивания
  • Регистр имеет значение, т.е. большая буква отличается от маленькой. Пример: переменные и – не одно и то же.

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

  • Однострочный комментарий
    // однострочный комментарий
    // компилятор меня игнорирует =(
  • Многострочный комментарий
    /* Многострочный
    комментарий */

Особенность работы с float

Arduino поддерживает работу с числами с плавающей точкой (десятичные дроби). Этот тип данных не имеет аппаратной поддержки, а реализован программно, поэтому вычисления с ним производятся в несколько раз дольше, чем с целочисленным типом, вы могли видеть это из таблицы выше. Помимо медленных вычислений, поддержка работы с занимает память, т.к. она реализована в виде “библиотеки”. Использование математических операций с float (* / + –) добавляет примерно 1000 байт во flash память, однократно, просто подключается инструмент для выполнения действий.

Arduino поддерживает три типа ввода чисел с плавающей точкой:

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

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

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 и модификатор)

При присваивании числа целочисленному типу данных дробная часть отсекается. Если хотите математическое округление – его нужно использовать отдельно:

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;
// val2 == 0.100000023 !!!

float val4 = 1.5 - 1.0;
// val4 == 0.500000000

Казалось бы, должна стать ровно после вычитания, но в 8-ом знаке вылезла погрешность! Будьте очень внимательны при сравнении чисел, особенно со строгими операциями ==, >= и <=: результат может быть некорректным и нелогичным.

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

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

Деструктор

Наряду с конструктором класса существует также деструктор (от англ. destruct – разрушать), который выполняет противоположное действие: уничтожает объект, убирает его из динамической памяти. Как и конструктор, деструктор создаётся автоматически, если не указывать его явно. Деструктор также можно объявить самостоятельно, для выполнения каких-то действий при уничтожении класса, например для освобождения динамической памяти. Деструктор объявляется точно так же, как конструктор, т.е. имя совпадает с именем класса, возвращаемого типа данных нет. Единственное отличие – тильда ~ перед именем. Рассмотрим наш класс из этого урока, у него деструктор будет 

Рассмотрим пример, заодно вспомним про область видимости переменных. Если создать объект вне функций – он будет создан глобальным, и будет существовать на протяжении всего времени работы программы. Если создать его внутри функции или блока кода – он будет существовать только в пределах этого блока, то есть переменные класса будут занимать память на протяжении выполнения этого блока кода. Рассмотрим вот такой класс:

class Color {   // класс Color
  public:
    Color() {}; // конструктор
    void printHello() {
      Serial.println("Hello");
    };
    ~Color() {    // деструктор
      Serial.println("destruct");
    };
    byte someVar;   // какая-то переменная
  private:
};

В нём есть пустой конструктор, печатающий hello метод и деструктор. Выполним вот такой код:

Color myColor3;
void setup() {
  Serial.begin(9600);  
  myColor3.printHello();
}

В выводе порта увидим Hello и всё, потому что объект глобальный и деструктор не вызвался в процессе работы, потому что объект не уничтожался.

Внесём создание объекта в блок функции и посмотрим, что будет

void setup() {
  Serial.begin(9600);
  Color myColor3;
  myColor3.printHello();
}
// тут myColor3 уничтожается

Объект создан внутри функции, и при выходе из этой функции, то есть сразу после прохождения через закрывающую фигурную скобку , объект будет уничтожен, будет вызван деструктор и в порт выведется .

Как и зачем применять это на практике: читайте урок про динамическую память, в жизни она вам врядли пригодится, но без неё цикл уроков не был бы полным. Если внутри объекта выделяется память под какие-то действия, то в деструкторе эту память хорошо-бы освобождать. Как пример можно рассмотреть стандартный класс , объекты которого – строки с символами, располагаются в динамической памяти, и если создавать строку локально – она уничтожается после выхода из блока её функции, потому что так написано в деструкторе:

String::~String()
{
 free(buffer);
}

Итак, мы с вами поэтапно создали класс и изучили большую часть особенностей работы с классами. На этом завершается раздел программирования, и начинается раздел базовых уроков Ардуино. А к классам мы ещё вернёмся, когда будем писать свою собственную библиотеку!

Основы Arduino

В первую очередь – это специальный микроконтроллер с одноимённой системой управления и библиотеками, построенными на языке С++. Соответственно, если вы планируете создавать что-то уникальное, вам следует изучить все нюансы, которые имеет программирование Arduino.

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

Поэтому давайте рассмотрим, что вообще обозначает этот термин. Абсолютно любой проект построен на поэтапной блок-модели, в которой описывается, что необходимо сделать вашему микроконтроллеру и как это сделать

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

Для упрощения работы пользователей в Ардуино созданы готовые библиотеки функций, вам достаточно лишь вводить команды из них, чтобы добиться какой-то цели. Естественно, таким образом вы многого не добьётесь, но для создания собственных библиотек потребуется знание языка С++ на котором и построена прошивка чипа.

Ключевая особенность системы в том, что характеристики Arduino могут быть улучшены с помощью докупаемых компонентов, и вы всегда можете их подстроить под конкретный проект. Соответственно, единственным вашим ограничением является знание языка и его возможностей, а также собственная фантазия.

Все функции строятся из простейших операнд, которые характерны для С++. Этими операндами являются переменные различных типов и способы их применения. Поэтому любая функция, используемая в микроконтроллере для получения сведений или отправки сигнала, – это набор простейших операций, который записан в главной библиотеке. И вы будете ограничены до тех пор, пока не получите достаточно опыта и практики, чтобы понимать, какую библиотеку и для какой цели вам стоит написать.

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

Также, учитывайте, что язык написания библиотек системы – низкоуровневый, а соответственно, состоит из простейших команд, в отличие от высокоуровневых python или pascal, удобных для пользователей. С другой стороны, он также является мультипарадигмальным, поэтому подходит для решения любой задачи с помощью удобной вам парадигмы программирования.

Чаще всего применяется ООП. Сам С++ имеет ядро из многочисленных библиотек и дополнительных функций или методов, поэтому, если вы собираетесь разобраться во всём кардинально, стоит начинать с освоения языка с нуля.

Пространство имён (Pro)

Пространство имён – очень удобная возможность языка, с её помощью можно разделить функции или переменные с одинаковыми именами друг от друга, то есть защитить свой набор данных инструментов от конфликтов имён с другими именами. “Именная область” определяется при помощи оператора :

namespace mySpace {
  // функции или данные
};

Чтобы использовать содержимое из пространства имён, нужно обратиться через его название и оператор разрешения области видимости

mySpace::имя_функции

Более подробный пример:

namespace mySpace {
byte val;
void printKek() {
  Serial.println("kek");
}
};

void setup() {
  Serial.begin(9600);
  // printKek(); // приведёт к ошибке
  mySpace::printKek();
}

Также есть оператор , позволяющий не использовать каждый раз обращение к пространству имён. Например, в отдельном файле у нас есть пространство имён с различными функциями. Чтобы в основном файле программы каждый раз не писать ярлык пространства имён с , можно написать

using имя_пространства_имён;

И ниже по коду можно будет пользоваться содержимым пространства имён без обращения через

Область видимости

Переменные, константы и другие типы данных (структуры и перечисления) имеют такое важное понятие, как область видимости. Она бывает

  • Глобальной
  • Локальной
  • Формальной (параметр)

Глобальная

Глобальная переменная объявляется вне функций и доступна для чтения и записи в любом месте программы, в любой её функции.

byte var;
void setup() {
  // спокойно меняем глобальную переменную
  var = 50;
}
void loop() {
  // спокойно меняем глобальную переменную
  var = 70;
}

Локальная

Локальная переменная живёт внутри функции или внутри любого блока кода, заключённого в , доступна для чтения и записи только внутри него. При попытке обратиться к локальной переменной из другой функции (за пределами её ) вы получите ошибку, потому что локальная переменная создаётся заново при выполнении содержащего её блока кода (или функции) и удаляется из памяти при завершении выполнения этого блока (или функции):

void setup() {
  byte var;  // локальная для setup переменная
  // спокойно меняем локальную переменную
  var = 50;
}
void loop() {
  // приведёт к ошибке, потому что в этом блоке кода var не объявлена
  var = 70;
  
  // сделаем тут отдельный блок кода
  {
    byte var2 = 10;
    // var2 существует только внутри этого блока!
  }
  // вот тут var2 уже будет удалена из памяти
}

Важный момент: если имя локальной переменной совпадает с глобальной, то приоритет обращения по имени в функции отдаётся локальной переменной:

byte var;   // глобальная var
void setup() {
  byte var; // создаём локальную var
  // меняем локальную var
  var = 50;
}
void loop() {
  // а тут меняем уже глобальную var!
  var = 70;
}

Формальная (параметр)

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

void setup() {
  // передаём 10 как аргумент
  myFunc(10);
}

void loop() {
}

void myFunc(byte var) {
  // тут var является "локальной"
  // прибавим к ней 20
  var += 20;
  // после } переменная будет удалена из памяти!
}

Константы

Что такое константа понятно из её названия – что-то, значение чего мы можем только прочитать и не можем изменить. Задать (объявить) константу можно двумя способами:

  • Точно так же как переменную, указав перед типом данных слово . Если значение переменной не будет изменяться в процессе выполнения программы – рекомендуется объявить её как константу, это позволит компилятору лучше оптимизировать код и в большинстве случаев он будет чуточку легче и быстрее.
    const byte myConst = 10;  // объявить константу типа byte
  • При помощи директивы препроцессору , которая делает следующее: на этапе компиляции кода препроцессор заменяет указанные все последовательности символов в текущем документе (напомню, что вкладки Arduino IDE являются одним документом) на соответствующие им значения. Константа, определённая при помощи не занимает места в оперативной памяти, а хранится как код программы во Flash памяти, это самый большой плюс данного способа. Синтаксис: . Точка запятой не ставится. Таким способом обычно указывают пины подключения, настройки, различные величины и всё такое. Пример:
    #define BTN_PIN 10
    #define DEFAULT_VALUE 3423

Ещё пару слов о константах и переменных: если обычная переменная нигде не изменяется в процессе выполнения программы – компилятор может самостоятельно сделать её константой и она не займёт места в оперативной памяти, т.е. будет помещена во Flash.

Резюмируя

Теперь по сути дела: датчики, их куча кучная, измерять можно ну просто всё, что вообще измеряется. Электроника: напряжение, ток, сопротивление, работа с переменным током, поля. Параметры микроклимата: температура, влажность, давление, содержание газов, скорость ветра, освещенность, что угодно. Интересных модулей тоже очень много: Bluetooth, сотовая связь, GPS, дисплеи различных типов и размеров, датчики присутствия, как ИК, так и микроволновые, модули для беспроводной связи ардуинок и многое другое.

Можно управлять абсолютно любой железкой, которая выполняет свою функцию просто при подаче питания: лампочка, светодиодная лента, электронагреватель, мотор или любой электропривод, электромагнит, соленоид-толкатель, и это все с любым напряжением питания. Но тут нужно кое что понять: Ардуино (точнее микроконтроллер) – логическое устройство, то есть по-хорошему она должна только отдавать команды другим устройствам, или принимать их от них. Это я к тому, что напрямую от ардуино не работают ни лампочки, ни моторчики, ни нагреватели, ни-хуче-го. Максимум – светодиод. С пониманием этого идём дальше. Чтобы ардуино включила или выключила (подала питание) на другое устройство, нужно устройство – посредник, например реле или транзистор. Ардуино управляет реле, а реле в свою очередь включает любую нужную нагрузку с любым напряжением питания и все такое, подробнее об этом поговорим отдельно.

Как суть всего выше написанного – возможности Ардуино по подключению и управлению различными железками практически безграничны, можно воплотить любую идею, даже самую безумную. Датчики что то измеряют, исполнительные устройства что то контролируют, в это же время ведётся отсылка данных куда-нибудь, что-то отображается на дисплее и контролируется при помощи кнопок. Романтика!

У меня в каталоге ссылок на Ардуино-компоненты можно найти практически все существующие датчики, модули и прочие железки для Ардуино, и практически у каждого есть ссылка на статью с примером и библиотекой. Пользуйтесь!

Многомерные массивы

Выше мы рассмотрели одномерные массивы, в которых элементы определяются просто порядковым номером. Можно задавать и многомерные массивы, в которых элемент будет иметь несколько номеров. Например двумерный массив, он же матрица, он же таблица, каждый элемент имеет номер строки и столбца. Задаётся такой массив вот так:

// двумерный массив, 5 строк 10 столбцов
byte myTable;

// матрица 3х3
byte myMatrix[] = {
  {10, 11, 12},
  {13, 14, 15},
  {16, 17, 18},
};

Очень важно помнить, что при объявлении массива с вручную вписанными данными нужно обязательно указать размер количества ячеек в измерении на 1 меньше размерности массива (для двумерного – обязательно указать размер одного из измерений, для трёхмерного – два, и т.д.). После последнего члена массива можно ставить запятую, это не приведёт к ошибке (пример кода выше).

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

В рассмотренном выше двумерном массиве элемент с адресом 0, 2 (строка 0 столбец 2) имеет значение 12. Обращение к этому элементу например с целью перезаписи будет выглядеть так:

// меняем 12 на 20, ячейка 0,2
myMatrix = 20;

Очень полезным бывает массив строк (массивов букв), позволяющий упорядоченно хранить названия пунктов меню или других подобных вещей. Такой массив должен быть объявлен при помощи адресного оператора * (звёздочка):

const char *names[]  = {
  "Period",   // 0
  "Work",     // 1
  "Stop",     // 2
};

Обращение к поможет вывести слово Stop в монитор порта или на дисплей, например

// вывести в порт слово Stop
Serial.println(names);

Но к этому мы ещё вернёмся в будущем.

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

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector