Как сделать библиотеку для ардуино
Перейти к содержимому

Как сделать библиотеку для ардуино

  • автор:

Как сделать библиотеку для Arduino? (пример прилагается)

Допустим, Вы решили оформить написанные Вами функции/классы для Arduino в библиотеку, чтобы Вам самим было проще с ними работать, и главное, чтобы кто-то другой мог с помощью Вашей библиотеки спокойно решать данные задачи, поминая Вас добрым словом, а не выдумывать новый велосипед.
Опишу минимум особенностей, в расчете на читателя, хотя бы немного знакомого с программированием на C/C++.
Библиотека должна иметь как минимум один .h файл и как правило, один .cpp файл. В .h файле, как водится, описываются все типы/функции/классы/константы, а в .cpp все это реализуется.
Библиотека будет компилироваться без дополнительных преобразований (подробнее тут), поэтому, если Вам потребуются какие-то функции/объекты/константы из стандартной библиотеки Arduino, нужно подключать соответствующие заголовочные файлы (вроде «WConstants.h», «WProgram.h»).
Если я верно понял, правилом «хорошего тона» является оформление кода в виде класса, и если по смыслу выполняемых задач экземпляр этого класса может быть только один, то заодно и объявление этого экземпляра в .h файле:

extern MyClass MyClassObject;

и создание одного глобального объекта в .cpp файле:

MyClass MyClassObject;

Например, если мы делаем класс для работы с аппаратным последовательным портом, то поскольку такой порт физически только один, то желательно в самой библиотеке экземпляр класса и создать. Собственно, в стандартной библиотеке объект Serial так и объявлен.
Написанную библиотеку нужно положить в папку с каким-нибудь подходящим именем в каталог «hardware\libraries», и перезапустить среду Arduino. При запуске среда попытается скомпилировать Вашу библиотеку, и если возникнут ошибки, о них будет сказано.
Неплохо создать в папке с библиотекой файл «keywords.txt», в котором описываются имена типов, методов, функций, констант, используемых в библиотеке, чтобы редактор «раскрашивал» их соответствующими цветами. Синтаксис у файла простой — вот пример (разделитель — TAB):

--- keywords.txt --- MyType KEYWORD1 MyFunction KEYWORD2 MY_CONSTANT LITERAL1 ---

Кроме того, можно создать папку «examples», куда сложить одну или несколько папок с проектами примеров работы с библиотекой (уже в формате .pde).
В качестве примера приведу простенькую библиотечку, которая организует ввод чисел с последовательного порта.
В ходе экспериментов с Arduino бывает удобно написать программку в диалоговой форме, и работать с ней через терминалку (ту же HyperTerminal). Ну и постоянно встает задача ввода чисел — этим библиотека и занимается. Работают с библиотекой примерно так:

#include void setup() < Serial.begin(9600); >void loop() < long int Number; Serial.print("Enter number: "); Number = SerialInput.InputNumber(); // Здесь ввод и происходит Serial.print("You entered: "); Serial.println(Number, DEC); >

Более полный пример идет с библиотекой, скачать которую можно тут: SerialInput.zip Архив разархивировать в папку «hardware\libraries».

  • arduino
  • микроконтроллеры
  • электроника

Пишем свою библиотеку под Arduino

Одна из довольно сильных сторон любого программного обеспечени — это возможность единожды написанной программы быть использованной многократно как в виде отдельных частей, так и целиком, что и привело к зарождению концепции «библиотеки».

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

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

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

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

Если вы до этого уже интересовались сутью библиотек и пытались разбирать существующие библиотеки Arduino, то наверняка успели заметить, что они состоят из двух отдельных файлов, один из которых имеет расширение .cpp — что означает «С Plus Plus». Так как язык Wiring для Arduino базируется, по сути, на языке C++, то и решили создавать файлы с таким расширением. Видимо, создатели подумали, что «а ещё это просто красиво» ©. Второй же компонент библиотеки имеет расширение .h ( «Headers»):

  • Файл .cpp — называется файлом реализации.
  • Файл .h — называется файлом заголовков.

Допустим, что у нас есть некий код, который управляет двигателями. Этот код состоит из ряда участков, среди которых инициализация каких-то переменных и какая-то функция:

Изначальный код

#include #include "esp32-hal-ledc.h" #include // мотор 1: int motor1Pin1 = 21; int motor1Pin2 = 19; //int enable1Pin = 14; // мотор 2: int motor2Pin1 = 23; int motor2Pin2 = 22; //int enable2Pin = 32; const int freq = 30000; const int pwmChannel = 3; const int resolution = 8; int dutyCycle = 0; const int freq2 = 30000; const int pwmChannel2 = 4; const int resolution2 = 8; int dutyCycle2 = 0; void setup() < pinMode(motor1Pin1, OUTPUT); pinMode(motor1Pin2, OUTPUT); pinMode(motor2Pin1, OUTPUT); pinMode(motor2Pin2, OUTPUT); ledcSetup(pwmChannel, freq, resolution); // первый двигатель ledcSetup(pwmChannel2, freq2, resolution2); // второй двигатель ledcAttachPin(motor1Pin1, pwmChannel); ledcAttachPin(motor2Pin1, pwmChannel2); ledcWrite(pwmChannel, dutyCycle); ledcWrite(pwmChannel2, dutyCycle2); >void loop() < // тут какая то логика работы >void Motors (String s) < if (s.equals ("Forward") ) < ledcWrite(pwmChannel, 155); ledcWrite(pwmChannel2, 155); digitalWrite(motor1Pin1, HIGH); digitalWrite(motor1Pin2, LOW); digitalWrite(motor2Pin1, HIGH); digitalWrite(motor2Pin2, LOW); >else if (s.equals ("Left") ) < ledcWrite(pwmChannel, 255); ledcWrite(pwmChannel2, 0); digitalWrite(motor1Pin1, HIGH); digitalWrite(motor1Pin2, LOW); digitalWrite(motor2Pin1, LOW); digitalWrite(motor2Pin2, HIGH); >else if (s.equals ("Right") ) < ledcWrite(pwmChannel, 0); ledcWrite(pwmChannel2, 255); digitalWrite(motor1Pin1, LOW); digitalWrite(motor1Pin2, HIGH); digitalWrite(motor2Pin1, HIGH); digitalWrite(motor2Pin2, LOW); >else if (s.equals ("Reverse") ) < ledcWrite(pwmChannel, 125); ledcWrite(pwmChannel2, 125); digitalWrite(motor1Pin1, LOW); digitalWrite(motor1Pin2, HIGH); digitalWrite(motor2Pin1, LOW); digitalWrite(motor2Pin2, HIGH); >>

В принципе, весь этот код мы можем поместить в файл реализации, то есть с расширением .cpp.

Я специально в качестве кода для примера взял код для esp32 (чуть ниже поясню почему).

Как можно было видеть в коде выше, в его начале расположены стандартные строки #include , которые импортируют библиотеки, используемые в вашем коде. То есть если ваша библиотека является своего рода надстройкой над чужим кодом, то в файле реализации, как и в обычном скетче, необходимо поместить импорты этих библиотек.

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

#include

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

Далее мы обратим внимание вот на какой момент. Дело в том, что любая работа с какой-либо периферией требует её подключения с использованием вышеназванной функции pinMode() как минимум, а есть ещё разнообразные настройки, как в нашем случае.

На первый взгляд всё хорошо и в файле присутствует подключение периферии. Однако самые внимательные уже заметили, что обычно подключение периферии в скетче у нас происходит внутри блока setup () <> .

Однако в данном случае мы работаем над созданием библиотеки, и здесь никакого блока setup () <> не существует, и если мы попытаемся оставить всё как есть, и функции подключения периферии останутся лежать «просто так, снаружи», то код с подключённой нашей самодельной библиотекой не сможет скомпилироваться, и компилятор выдаст ошибку, если мы используем вот такое содержимое файла реализации (.cpp):

Код с ошибкой

#include #include "esp32-hal-ledc.h" #include // мотор 1: int motor1Pin1 = 21; int motor1Pin2 = 19; //int enable1Pin = 14; // мотор 2: int motor2Pin1 = 23; int motor2Pin2 = 22; //int enable2Pin = 32; const int freq = 30000; const int pwmChannel = 3; const int resolution = 8; int dutyCycle = 0; const int freq2 = 30000; const int pwmChannel2 = 4; const int resolution2 = 8; int dutyCycle2 = 0; pinMode(motor1Pin1, OUTPUT); pinMode(motor1Pin2, OUTPUT); pinMode(motor2Pin1, OUTPUT); pinMode(motor2Pin2, OUTPUT); ledcSetup(pwmChannel, freq, resolution); // первый двигатель ledcSetup(pwmChannel2, freq2, resolution2); // второй двигатель ledcAttachPin(motor1Pin1, pwmChannel); ledcAttachPin(motor2Pin1, pwmChannel2); ledcWrite(pwmChannel, dutyCycle); ledcWrite(pwmChannel2, dutyCycle2); void Motors (String s) < if (s.equals ("Forward") ) < ledcWrite(pwmChannel, 155); ledcWrite(pwmChannel2, 155); digitalWrite(motor1Pin1, HIGH); digitalWrite(motor1Pin2, LOW); digitalWrite(motor2Pin1, HIGH); digitalWrite(motor2Pin2, LOW); >else if (s.equals ("Left") ) < ledcWrite(pwmChannel, 255); ledcWrite(pwmChannel2, 0); digitalWrite(motor1Pin1, HIGH); digitalWrite(motor1Pin2, LOW); digitalWrite(motor2Pin1, LOW); digitalWrite(motor2Pin2, HIGH); >else if (s.equals ("Right") ) < ledcWrite(pwmChannel, 0); ledcWrite(pwmChannel2, 255); digitalWrite(motor1Pin1, LOW); digitalWrite(motor1Pin2, HIGH); digitalWrite(motor2Pin1, HIGH); digitalWrite(motor2Pin2, LOW); >else if (s.equals ("Reverse") ) < ledcWrite(pwmChannel, 125); ledcWrite(pwmChannel2, 125); digitalWrite(motor1Pin1, LOW); digitalWrite(motor1Pin2, HIGH); digitalWrite(motor2Pin1, LOW); digitalWrite(motor2Pin2, HIGH); >>

Я сейчас говорю вот об этом участке, который лежит как «не пришей кобыле хвост»:

 pinMode(motor1Pin1, OUTPUT); pinMode(motor1Pin2, OUTPUT); pinMode(motor2Pin1, OUTPUT); pinMode(motor2Pin2, OUTPUT); ledcSetup(pwmChannel, freq, resolution); // первый двигатель ledcSetup(pwmChannel2, freq2, resolution2); // второй двигатель ledcAttachPin(motor1Pin1, pwmChannel); ledcAttachPin(motor2Pin1, pwmChannel2); ledcWrite(pwmChannel, dutyCycle); ledcWrite(pwmChannel2, dutyCycle2);

И что же делать в таком случае? А вот что: необходимо функции инициализации пинов обернуть в функцию! То есть они не должны лежать снаружи, их нужно поместить внутрь функции (setupMotors() ) :

Правильный код реализации

#include #include "esp32-hal-ledc.h" #include // мотор 1: int motor1Pin1 = 21; int motor1Pin2 = 19; //int enable1Pin = 14; // мотор 2: int motor2Pin1 = 23; int motor2Pin2 = 22; //int enable2Pin = 32; const int freq = 30000; const int pwmChannel = 3; const int resolution = 8; int dutyCycle = 0; const int freq2 = 30000; const int pwmChannel2 = 4; const int resolution2 = 8; int dutyCycle2 = 0; void setupMotors() < pinMode(motor1Pin1, OUTPUT); pinMode(motor1Pin2, OUTPUT); pinMode(motor2Pin1, OUTPUT); pinMode(motor2Pin2, OUTPUT); ledcSetup(pwmChannel, freq, resolution); // первый двигатель ledcSetup(pwmChannel2, freq2, resolution2); // второй двигатель ledcAttachPin(motor1Pin1, pwmChannel); ledcAttachPin(motor2Pin1, pwmChannel2); ledcWrite(pwmChannel, dutyCycle); ledcWrite(pwmChannel2, dutyCycle2); >void Motors (String s) < if (s.equals ("Forward") ) < ledcWrite(pwmChannel, 155); ledcWrite(pwmChannel2, 155); digitalWrite(motor1Pin1, HIGH); digitalWrite(motor1Pin2, LOW); digitalWrite(motor2Pin1, HIGH); digitalWrite(motor2Pin2, LOW); >else if (s.equals ("Left") ) < ledcWrite(pwmChannel, 255); ledcWrite(pwmChannel2, 0); digitalWrite(motor1Pin1, HIGH); digitalWrite(motor1Pin2, LOW); digitalWrite(motor2Pin1, LOW); digitalWrite(motor2Pin2, HIGH); >else if (s.equals ("Right") ) < ledcWrite(pwmChannel, 0); ledcWrite(pwmChannel2, 255); digitalWrite(motor1Pin1, LOW); digitalWrite(motor1Pin2, HIGH); digitalWrite(motor2Pin1, HIGH); digitalWrite(motor2Pin2, LOW); >else if (s.equals ("Reverse") ) < ledcWrite(pwmChannel, 125); ledcWrite(pwmChannel2, 125); digitalWrite(motor1Pin1, LOW); digitalWrite(motor1Pin2, HIGH); digitalWrite(motor2Pin1, LOW); digitalWrite(motor2Pin2, HIGH); >>

Такой код благополучно скомпилируется, после того как мы создадим библиотеку, подключим её, а после вызовем вот эту функцию, внутри блока setup :

#include void setup() < setupMotors(); >void loop() < // Какой-то код >

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

По сути, ваш файл реализации готов, и мы перейдём к файлу заголовков — с расширением .h.

Для создания файла заголовков вам всего лишь нужно перенести туда названия ваших функций из файла с расширением .cpp в виде простого списка, с точкой с запятой в конце каждой строки:

void Motors (String s); void setupMotors();

Файл тоже готов.

Кстати говоря, тут интересный момент: вы сами определяете, какие функции будут доступны «снаружи» для пользователей! То есть этот набор функций, перечисленных в файле с расширением .h — и есть Application Programming Interface (API), то есть набор способов, с помощью которых можно взаимодействовать с вашей программой. Причём, как я уже говорил, у вас в файле реализации могут внутри быть ещё и другие функции, которые вы просто не пожелали дать для использования. Имеете право, почему нет.

А теперь посмотрим чуть более сложный пример, «объектно-ориентированное программирование» у нас или где 🙂

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

На самом деле, даже в этой ситуации, код ненамного усложнится:

  • Принципиально подобная библиотека, содержащая класс, также будет состоять из двух отдельных файлов, сохранённых с расширениями .cpp и .h.
  • Вся реализация методов также будет собрана в файле .cpp.
  • Сами методы также будут перечислены в файле с расширением .h.

Чтобы всё это было несколько интересней, мы можем даже немного усугубить ситуацию, добавить модификаторы доступа: public и protected .

В результате всё это будет выглядеть примерно так. Файл реализации (.cpp):

#include #include void Murlicat(String gladit) < //. некая реализация >void DatPogladitPuziko(int x, int y, int l) < //. некая реализация >void TrogatZadniyeLapki(int a, int b) < //. некая реализация >
class Kotofey < public: void Murlicat(String gladit); protected: void DatPogladitPuziko(int x, int y, int l); void TrogatZadniyeLapki(int a, int b); >;

Ну и напоследок, если мы хотим, чтобы наша библиотека была «совсем модной», то можем включить туда предварительно настроенные примеры, чтобы люди могли сразу понять, как им взаимодействовать с этой библиотекой. Для этого необходимо в директории, где находится два основых файла этой библиотеки (.cpp и .h), создать ещё и отдельную папку под названием examples, внутри которой в отдельную, совпадающую по названию со скетчем папку, положить код вашего примера.

Таким образом, путь до вашего примера будет выглядеть следующим образом:

Ваша_библиотека/examples/пример.ino

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

  • В первом случае вы можете подключить заархивированную библиотеку изнутри Arduino IDE, пройдя по пути: скетч-подключить библиотеку-добавить zip. библиотеку .
  • Во втором случае вы можете просто положить её стандартную папку библиотек Arduino: C:\Arduino\libraries
  • Или если вы используете portable-версию среды разработки (т.к. я, например, ношу её везде с собой на флешке, и она не требует установки), то положить сюда: C:\arduino-1.8.19\portable\sketchbook\libraries (в моём случае используется версия Arduino 1.8.19 – у вас может быть другая).

В этом файле мы пишем, разделяя с помощью TAB-клавиши клавиатуры, определённое понятие и цвет его подсветки.

У нас есть 3 варианта подсветки:

  • KEYWORD1: толстый оранжевый шрифт (классы, типы данных).
  • KEYWORD2: оранжевый шрифт (методы, функции).
  • LITERAL1: голубой шрифт (константы).
Kotofey KEYWORD1 Murlicat KEYWORD2 DatPogladitPuziko KEYWORD2 TrogatZadniyeLapki KEYWORD2 KoluchestvoLapok LITERAL1 

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

НЛО прилетело и оставило здесь промокод для читателей нашего блога:

— 15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS .

  • FirstVDS
  • arduino
  • библиотеки
  • ардуино
  • ардуино головного мозга
  • разработка
  • разработка под arduino
  • Блог компании FirstVDS
  • Программирование
  • Разработка под Arduino

Как написать крупный проект?

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

  • Урок про структуры
  • Урок про классы
  • Урок про библиотеки

Улучшаем “Ардуино-код”

Разделить на функции

Используйте функции (урок про функции)! Практически любую часть кода можно обернуть в функцию и вынести из основного цикла. Например:

void loop() < // конструкция таймера // опрос датчика // фильтрация значений // другой код // другой код // другой код >
void loop() < sensorRead(); // другой код // другой код // другой код >void sensorRead() < // конструкция таймера // опрос датчика // фильтрация значений >

Эти функции можно располагать во вкладках и группировать по смыслу.

Глобальные переменные

Нужно стремиться к уменьшению количества глобальных переменных в скетче. Во-первых это просто “некрасиво”, а во вторых – в какой то момент могут начаться проблемы с придумыванием уникальных имён и распознаванием их в полотне кода.

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

uint32_t tmr1; // переменная таймера void setup() <> void loop() < if (millis() - tmr1 >= 1000) < tmr1 = millis(); // выполнить действие >>

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

void setup() <> void loop() < sensorTimer(); timeoutTimer(); >void sensorTimer() < static uint32_t tmr; if (millis() - tmr >= 1000) < tmr = millis(); // выполнить действие >> void timeoutTimer() < static uint32_t tmr; if (millis() - tmr >= 1000) < tmr = millis(); // выполнить действие >>

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

float tempRaw; float tempFilter; int filterPeriod; bool btnState; bool btnFlag; int btnPeriod; void setup() <> void loop()

Объединим в структуры и упростим имена переменных:

struct < float raw; float filter; int period; >temp; struct < bool state; bool flag; int period; >btn; void setup() <> void loop() < // обращение к элементам temp.raw = sensorRead(); btn.state = pinRead(); >

Разделить данные и код

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

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

float temp; // температура с датчика void setup() <> void loop() < sensorRead(); regulator(); >void sensorRead() < temp = читаем датчик; // фильтруем/обрабатываем temp >void regulator() < // например PID регулятор output = temp * k; >

Давайте избавимся от глобальной переменной, например так:

void setup() <> void loop() < float temp = sensorRead(); regulator(temp); >float sensorRead() < // читаем датчик // фильтруем/обрабатываем return значение; >void regulator(float temp) < // например PID регулятор output = temp * k; >

Можно пойти дальше и избавиться даже от локальной переменной:

void setup() <> void loop() < regulator(sensorRead()); >float sensorRead() < // читаем датчик; // фильтруем/обрабатываем temp return значение; >void regulator(float temp) < // например PID регулятор output = temp * k; >

Использовать классы

Ещё более правильным будет создание класса (урок про классы), в котором реализуется полностью независимая часть программы, со своими переменными и функциями. Удобство класса также состоит в том, что оформленные в виде классов наработки можно использовать в других проектах, не меняя код класса. В качестве примера можно рассмотреть любую библиотеку – для датчика, дисплея или какого-то алгоритма.

Разбиваем на файлы

Чтобы проект можно было разделить на несколько файлов, он изначально должен состоять из частей, которые могут функционировать независимо друг от друга и не пересекаться друг с другом. Такую часть можно назвать “подпрограммой”, которая содержат свой код и набор переменных и точно так же может состоять из нескольких файлов. Знакомая история? Ведь именно так и реализованы библиотеки! Библиотека содержит набор инструментов, который не зависит от основной программы может использоваться даже в другом проекте. Используя библиотеку, мы уже разделяем свой проект на несколько файлов.

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

Пример 1

Давайте рассмотрим пример превращения ужасного “винигретного” кода с кучей глобальных переменных и бардаком в главном цикле в понятную программу с отдельными независимыми подпрограммами. В этом примере у нас подключены две кнопки (на пины D2 и D3) и два светодиода (используем бортовой на пине D13 и внешний на D12).

Напишем программу, которая будет:

  • Асинхронно опрашивать кнопки с программным гашением дребезга контактов
  • Мигать светодиодом 1 с периодом, который настраивается кнопками
  • Мигать светодиодом 2 с периодом, который:
    • Задаётся случайно при помощи алгоритма аппаратного рандома
    • Делает это каждые 2 секунды по таймеру

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

    Плохой код

    // пины const byte btn1 = 2; const byte btn2 = 3; const byte led1 = 13; const byte led2 = 12; const byte analogPin = 0; // таймеры дебаунса кнопок uint32_t btn1Tmr; uint32_t btn2Tmr; // флаги опроса кнопок bool btn1Flag; bool btn2Flag; // переменные для светодиода 1 uint32_t ledTmr1; uint16_t ledPeriod1 = 1000; // начальный период 1 с bool ledState1 = false; const int step = 50; // шаг изменения // переменные для светодиода 2 uint32_t ledTmr2; uint16_t ledPeriod2 = 1000; bool ledState2 = false; // таймер для случайного периода uint32_t rndTmr; uint16_t rndPeriod = 2000; // переменная рандома uint32_t seed = 0; void setup () < // настраиваем пины pinMode(btn1, INPUT_PULLUP); pinMode(btn2, INPUT_PULLUP); pinMode(led1, OUTPUT); pinMode(led2, OUTPUT); >void loop() < // таймер светодиода 1 if (millis() - ledTmr1 >= ledPeriod1) < ledTmr1 = millis(); ledState1 = !ledState1; digitalWrite(led1, ledState1); >// таймер светодиода 2 if (millis() - ledTmr2 >= ledPeriod2) < ledTmr2 = millis(); ledState2 = !ledState2; digitalWrite(led2, ledState2); >// таймер рандома if (millis() - rndTmr >= rndPeriod) < rndTmr = millis(); for (int i = 0; i < 16; i++) < seed *= 4; seed += analogRead(analogPin) & 3; >// ограничим 1000 мс (1 секунда) ledPeriod2 = seed % 1000; > // опрос первой кнопки с дебаунсом 100мс bool btn1State = digitalRead(btn1); if (!btn1State && !btn1Flag && millis() - btn1Tmr >= 100) < btn1Flag = true; btn1Tmr = millis(); ledPeriod1 += step; // увеличить период >if (btn1State && btn1Flag && millis() - btn1Tmr >= 100) < btn1Flag = false; btn1Tmr = millis(); >// опрос второй кнопки с дебаунсом 100мс bool btn2State = digitalRead(btn2); if (!btn2State && !btn2Flag && millis() - btn2Tmr >= 100) < btn2Flag = true; btn2Tmr = millis(); ledPeriod1 -= step; // уменьшить период >if (btn2State && btn2Flag && millis() - btn2Tmr >= 100) < btn2Flag = false; btn2Tmr = millis(); >>

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

    Давайте обернём обработку кнопки в класс, ведь у нас уже есть две одинаковые кнопки и в будущем в программу можно будет добавить ещё. Сразу вынесем класс в отдельный файл и оформим как библиотеку. Я делаю это в одном файле, чтобы не дублировать один и тот же код.

    // класс кнопки #pragma once #include #define _BTN_DEB_TIME 100 // таймаут антидребезга class Button < public: Button (byte pin) : _pin(pin) < pinMode(_pin, INPUT_PULLUP); >bool click() < bool btnState = digitalRead(_pin); if (!btnState && !_flag && millis() - _tmr >= _BTN_DEB_TIME) < _flag = true; _tmr = millis(); return true; >if (btnState && _flag && millis() - _tmr >= _BTN_DEB_TIME) < _flag = false; _tmr = millis(); >return false; > private: const byte _pin; uint32_t _tmr; bool _flag; >;

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

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

    #pragma once #include class Timer < public: Timer(uint16_t nprd = 0) < setPeriod(nprd); >void setPeriod(uint16_t nprd) < prd = nprd; >uint16_t getPeriod() < return prd; >bool ready() < if (millis() - tmr >= prd) < tmr = millis(); return true; >return false; > private: uint32_t tmr = 0; uint16_t prd = 0; >;

    Теперь достаточно объявить и настроить таймер, а для проверки – опрашивать метод ready() , который вернёт true при срабатывании. Настроить период можно через setPeriod() , а получить – через getPeriod() .

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

    #pragma once #include #include "timer.h" class LED < public: LED (byte pin, int period) : _pin(pin) < pinMode(_pin, OUTPUT); tmr.setPeriod(period); >void setPeriod(uint16_t prd) < tmr.setPeriod(prd); >uint16_t getPeriod() < return (tmr.getPeriod()); >void blink() < if (tmr.ready()) < digitalWrite(_pin, flag); flag = !flag; >> private: const byte _pin; bool flag; Timer tmr; >;

    Теперь достаточно объявить светодиод с указанием пина и периода мигания и просто вызывать в цикле метод blink() .

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

    #pragma once #include uint32_t getRandom(byte pin);
    #include "rnd.h" static uint32_t seed = 0; uint32_t getRandom(byte pin) < for (int i = 0; i < 16; i++) < seed *= 4; seed += analogRead(pin) & 3; >return seed; >

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

    #include «timer.h» #include «button.h» #include «led.h» #include «rnd.h» #define BTN1_PIN 2 #define BTN2_PIN 3 #define LED1_PIN 13 #define LED2_PIN 12 #define LED1_STEP 50 LED led1(LED1_PIN, 1000); LED led2(LED2_PIN, 1000); Button btn1(BTN1_PIN); Button btn2(BTN2_PIN); Timer rndTimer(2000); void setup() < >void loop()

    Программа стала гораздо компактнее, а также занимает меньше памяти: 1618 байт против 1796. Легче почти на 200 байт!

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

    #pragma once #include #include "timer.h" class LED < public: LED (byte pin, int period) : _pin(pin) < pinMode(_pin, OUTPUT); tmr.setPeriod(period); >void setPeriod(uint16_t prd) < tmr.setPeriod(prd); >uint16_t getPeriod() < return (tmr.getPeriod()); >void blink() < if (state) < if (tmr.ready()) < flag = !flag; digitalWrite(_pin, flag); >> else < if (flag) digitalWrite(_pin, 0); >> void toggle() < state = !state; >private: const byte _pin; bool flag, state = true; Timer tmr; >;

    И финальная программа:

    #include «timer.h» #include «button.h» #include «led.h» #include «rnd.h» #define BTN1_PIN 2 #define BTN2_PIN 3 #define BTN3_PIN 4 #define BTN4_PIN 5 #define LED1_PIN 13 #define LED2_PIN 12 #define LED1_STEP 50 LED led1(LED1_PIN, 1000); LED led2(LED2_PIN, 1000); Button btn1(BTN1_PIN); Button btn2(BTN2_PIN); Button btn3(BTN3_PIN); Button btn4(BTN4_PIN); Timer rndTimer(2000); void setup() < >void loop()

    Пример 2

    Далее давайте вспомним пример с метеостанцией и попробуем его немного “причесать”:

    Метеостанция
    // НАСТРОЙКИ #define ONE_WIRE_BUS 2 // пин ds18b20 // БИБЛИОТЕКИ #include #include #include #include #include // ОБЪЕКТЫ И ПЕРЕМЕННЫЕ // адрес может быть 0x27 или 0x3f LiquidCrystal_I2C lcd(0x3f, 16, 2); // Устанавливаем дисплей RTC_DS3231 rtc; OneWire oneWire(ONE_WIRE_BUS); DallasTemperature sensors(&oneWire); uint32_t myTimer1, myTimer2, myTimer3; boolean LEDflag = false; float tempSum = 0, temp; byte tempCounter; void setup() < pinMode(13, 1); // светодиод // дисплей lcd.init(); lcd.backlight(); // Включаем подсветку дисплея // термометр sensors.begin(); sensors.setWaitForConversion(false); // асинхронное получение данных // часы rtc.begin(); // установка времени равному времени компиляции if (rtc.lostPower()) < rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); >> void loop() < // 2 раза в секунду if (millis() - myTimer1 >= 500) < myTimer1 = millis(); // сбросить таймер toggleLED(); >// 5 раз в секунду if (millis() — myTimer2 >= 200) < myTimer2 = millis(); // сбросить таймер getTemp(); >// каждую секунду if (millis() — myTimer3 >= 1000) < myTimer3 = millis(); // сбросить таймер redrawDisplay(); >> void toggleLED() < digitalWrite(13, LEDflag); // вкл/выкл LEDflag = !LEDflag; // инвертировать флаг >void getTemp() < // суммируем температуру в общую переменную tempSum += sensors.getTempCByIndex(0); sensors.requestTemperatures(); // счётчик измерений tempCounter++; if (tempCounter >= 5) < // если больше 5 tempCounter = 0; // обнулить temp = tempSum / 5; // среднее арифметическое tempSum = 0; // обнулить >> void redrawDisplay() < // ВРЕМЯ DateTime now = rtc.now(); // получить время lcd.setCursor(0, 0); // курсор в 0,0 lcd.print(now.hour()); // часы lcd.print(':'); // первый ноль для красоты if (now.minute() < 10) lcd.print(0); lcd.print(now.minute()); lcd.print(':'); // первый ноль для красоты if (now.second() < 10) lcd.print(0); lcd.print(now.second()); // TEMP lcd.setCursor(11, 0); // курсор в 11,0 lcd.print("Temp:"); lcd.setCursor(11, 1); // курсор в 11,1 lcd.print(temp); // ДАТА lcd.setCursor(0, 1); // курсор в 0,1 // первый ноль для красоты if (now.day() < 10) lcd.print(0); lcd.print(now.day()); lcd.print('.'); // первый ноль для красоты if (now.month()

    Что тут можно сделать? В проекте используются:

    • Дисплей LCD1602
    • Датчик температуры ds18b20
    • Микросхема реального времени DS3231

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

    Также сделаем класс для таймера, потому что там он используется в трёх местах.

    #pragma once #include #include #include void disp_init(); void print_time(byte hour, byte minute, byte second); void print_temp(float temp); void print_date(byte day, byte month, int year);

    display.cpp
    #include «display.h» // адрес может быть 0x27 или 0x3f static LiquidCrystal_I2C lcd(0x3f, 16, 2); // Устанавливаем дисплей void disp_init() < lcd.init(); lcd.backlight(); // Включаем подсветку дисплея >void print_time(byte hour, byte minute, byte second) < lcd.setCursor(0, 0); lcd.print(hour); lcd.print(':'); if (minute < 10) lcd.print(0); // первый ноль для красоты lcd.print(minute); lcd.print(':'); if (second < 10) lcd.print(0); lcd.print(second); >void print_date(byte day, byte month, int year) < lcd.setCursor(0, 1); // первый ноль для красоты if (day < 10) lcd.print(0); lcd.print(day); lcd.print('.'); // первый ноль для красоты if (month < 10) lcd.print(0); lcd.print(month); lcd.print('.'); lcd.print(year); >void print_temp(float temp)

    #pragma once #include #include #include void sensor_init(byte pin); void read_temp(); float get_temp();

    #include «sensor.h» static OneWire oneWire; static DallasTemperature sensors(&oneWire); static float tempSum = 0, temp; static byte tempCounter; void sensor_init(byte pin) < oneWire.begin(pin); sensors.begin(); sensors.setWaitForConversion(false); >void read_temp() < tempSum += sensors.getTempCByIndex(0); sensors.requestTemperatures(); // счётчик измерений tempCounter++; if (tempCounter >= 5) < // если больше 5 tempCounter = 0; // обнулить temp = tempSum / 5; // среднее арифметическое tempSum = 0; // обнулить >> float get_temp()

    #pragma once #include #include #include void time_init(); void read_time(); byte time_hour(); byte time_minute(); byte time_second(); byte time_day(); byte time_month(); int time_year();

    #include «time.h» static RTC_DS3231 rtc; static DateTime now; void time_init() < rtc.begin(); // установка времени равному времени компиляции if (rtc.lostPower()) < rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); >> void read_time() < now = rtc.now(); >byte time_hour() < return now.hour(); >byte time_minute() < return now.minute(); >byte time_second() < return now.second(); >byte time_day() < return now.day(); >byte time_month() < return now.month(); >int time_year()

    #pragma once #include class Timer < public: Timer(uint16_t nprd = 0) < setPeriod(nprd); >void setPeriod(uint16_t nprd) < prd = nprd; >uint16_t getPeriod() < return prd; >bool ready() < if (millis() - tmr >= prd) < tmr = millis(); return true; >return false; > private: uint32_t tmr = 0; uint16_t prd = 0; >;

    Располагаем файлы рядом с основным скетчем. Финальный код:

    #define SENSOR_PIN 2 #define LED_PIN 13 #include «display.h» #include «sensor.h» #include «time.h» #include «timer.h» Timer tmr1(500); Timer tmr2(200); Timer tmr3(1000); void setup() < pinMode(13, 1); disp_init(); sensor_init(SENSOR_PIN); time_init(); >void loop() < if (tmr1.ready()) toggleLED(); if (tmr2.ready()) read_temp(); if (tmr3.ready()) redrawDisplay(); >void toggleLED() < static bool LEDflag; digitalWrite(LED_PIN, LEDflag); LEDflag = !LEDflag; >void redrawDisplay()

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

    Полезные страницы

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

    Начинающим на Arduino: Упаковываем конечный автомат в отдельный класс и библиотеку

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

    Папки с библиотеками

    Зачем всё это нужно?

    Arduino IDE позволяет использовать синтаксис C++11, оказывается. То есть, там очень развитый объектно-ориентированный язык. Нам же хочется сосредотачиваться на нашем гениальном коде и размазанная по программе лишняя логика частенько мешает сосредоточиться. Взять, например, всякие дисплейчики, кнопочки, датчики и релюшки — у каждого же своя логика, зачем её смешивать с общей логикой программы. Тот же, например, дисплей. У него много полей, статических и изменяемых. Ой, поле — это же класс. Поле может входить в меню (класс меню) или нет, быть часть частью виртуального дисплея (класс), которых на физическом эеране может быть насколько (дисплеи: рабочий, настроек, диагностики и т.п.). Меню, в свою очередь, управляется кнопками (классы кнопок могут быть разными) или джойстиком (класс). Всё это вместе — класс «дисплей», который можно объявить в своей программе как:

    #include "Display.h" Display disp(куча параметров и настроек);

    Если вы делаете проект не совсем на коленке не кое как и собираетесь что-то потом менять или повторно использовать какие-то свои наработки — лучше оформить сделанное в виде библиотек Arduino. В идеале, конечно же, положить в Github для других людей, если вам не жалко и вы не против, что кто-то ваш код исправит или дополнит.

    Раз уж мы в прошлой статье делали кнопочку, давайте её оформим как класс и библиотеку?

    Итак, наша задача сделать так, чтобы мы могли в своих скетчах писать:

    #include "myButton.h" myButton b1(4),b2(5),b3(12); // три кнопки на пинах 4, 5 и 12. loop() < b1.run(); b2.run(); b3.run(); // . if (b1.clicked()) doSomething(); // так или другим каким способом, есть варианты. // . >

    Как сделать библиотеку Arduino?

    Сначала надо решить, как ваша библиотека будет называться. Пусть для примера, это будет MyLib.

    Найдите, где лежат ваши скетчи на компьютере. Они лежат каждый в своей папочке, а рядом с ними есть папка libraries (библиотеки). Например, на маке /Users/Пользователь/Documents/Arduino/libraries и на виндоусе c:\Users\Пользователь\Документы\Arduino\libraries. Я сам сижу на маке и пути в виндах не знаю. Найдёте.

    Вот в этой папке libraries создайте новую папку MyLib, то есть с именем своей библиотеки. Перейдите туда.

    В этой новой папке надо создать как минимум один файл MyLib.h, тот, что вы будет включать в ваш проект. Минимальное его содержимое выглядит примерно так:

    #ifndef MYLIB_H #define MYLIB_H #if ARDUINO >= 100 #include #else #include #endif // Ваш код здесь #endif

    Расскажу, что здесь зачем. Конструкция ниже позволяет включать вашу библиотеку в код несколько раз без ошибок. Лучше использовать название вашей библиотеки большими буквами. Это не сурово прямо обязательно, но все так делают и вы не выделяйтесь. Задача стоит придумать уникальное слово, в нашем случае MYLIB_H, идентификатор для этого заголовочного файла.

    #ifndef MYLIB_H #define MYLIB_H // Ваш код #endif

    То есть, в вашем скетче может оказаться несколько таких строк:

    #include "MyLib.h"

    Вы скажете «тю, да я, да я слежу, да я. » и будете неправы. Лучше один раз написать в одном файле вот такую конструкцию, чем исправлять ваши готовые скетчи, если вдруг вы захотите вложить один в другой или ваша библиотека будет включена в другую итд. Данный код проверяет, определено ли слово MYLIB_H, если нет, то определяет его и включает дальнейший код. Если же слово уже определено, то второй раз код компилировать не нужно.

    Следующий важный кусок кода:

    #if ARDUINO >= 100 #include #else #include #endif

    Включает определения из исполняющей системы Arduino UDE. Без этого ваша библиотека просто не скомпилируется.

    Всё. Закройте Arduino IDE, Откройте заново. Создайте новый скетч, пропишите там #include «MyLib.h» и ура, ваша библиотека есть и подключена!

    Я смотрел, в библиотеке вроде как много файлов должно быть?

    Да, конечно. Мы сделали минимальные действия, чтобы создать библиотеку. Теперь настало время планирования.

    Чтобы я мог помещать сюда куски своего кода копипастом, я назову библиотеку SmartButton, ладно? Болванку MyLib можно прибить за ненадобностью.

    По аналогии с предыдущим пунктом, создаём папку SmartButton, в ней:

    • SmartButton.h — То, что мы будем включать в наши программы. Там будут только определения, без кода.
    • SmartButton.cpp — Программный код класса. Это не скетч! Обратите внимание, что расширение файла cpp (C++).
    • README.md — Файл описания библиотеки «для людей», то есть, документация. «md» означает MarkDown, то есть с разметкой. Достаточно назвать просто README.
    • library.json — описание библиотеки для Arduino IDE в хитром формате JSON.
    • examples — папка с примерами, которые будут потом видны в Arduino IDE. В ней должны лежать папки с именами примеров, в а них с тем же именем файлы с расширением ino — скетчи.

    Расположение файлов в папке libraries

    #ifndef SMART_BUTTON_H #define SMART_BUTTON_H #if ARDUINO >= 100 #include #else #include #endif // Можно выше до include переопределить эти значения #ifndef SmartButton_debounce #define SmartButton_debounce 10 #endif #ifndef SmartButton_hold #define SmartButton_hold 1000 #endif #ifndef SmartButton_long #define SmartButton_long 5000 #endif #ifndef SmartButton_idle #define SmartButton_idle 10000 #endif class SmartButton < // Это внутренние переменный класса. // Они свои у каждого объекта и конфликта // за имена переменных не будет. // не надо выдумывать для каждой кнопки свои названия. private: byte btPin; // Точно, как мы делали в [предыдущей статье про МКА](https://habrahabr.ru/post/345960/) enum state ; enum input ; enum state btState = Idle; enum input btInput = Release; unsigned long pressTimeStamp; // Это скрытый метод, его снаружи не видно. private: void DoAction(enum input in); // Это то, чем можно пользоваться. public: // Конструкторы и деструкторы. // То есть то, что создаёт и убивает объект. SmartButton(); SmartButton(int pin); SmartButton(int pin, int mode) ~SmartButton(); // В стиле Arduino IDE определим метод begin void begin(int p, int m) // Генератор событий для помещения в loop(). void run(); // Методы для переопределения пользователем. public: inline virtual void onClick() <>; // On click. inline virtual void onHold() <>; // On hold. inline virtual void onLongHold() <>; // On long hold. inline virtual void onIdle() <>; // On timeout with too long key pressing. inline virtual void offClick() <>; // On depress after click. inline virtual void offHold() <>; // On depress after hold. inline virtual void offLongHold() <>; // On depress after long hold. inline virtual void offIdle() <>; // On depress after too long key pressing. >; #endif

    Давайте поясню суть затеи. Мы не знаем, что нам будет нужно от кнопки. Наш МКА умеет находиться в состояниях Клик, Нажатие, Удержание и СлишкомДолгоеУдержание, а так же выходить из этих состояний в состояние Выключен. Так как мы делаем библиотеку универсальную, то надо предоставить возможность другому программисту вставить свой код в обработчики состояний. В ООП есть для этого замечательное средство — наследование.

    Мы делаем класс, у которого есть несколько методов (функций) и они пустые. То есть, они есть, они будут вызываться в нужный момент, но кода в них нет. Зачем это? Затем, что в скетче можно будет создать свой класс на базе нашего, определить там только нужные из методов и наполнить их своим кодом.

    Например, мы захотим сделать кнопку-переключатель, то есть, одно нажатие — включено, другое — выключено. Будем зажигать и гасить светодиод и предоставим функцию isOn() для использования в классическом виде в функции loop().

    #include "SmartButton.h" #define LED_PIN (13) // Порождаем наш новый класс от SmartButton class Toggle : public SmartButton < private: byte sw = 0; // состояние переключателя byte led; // нога для лампочки public: Toggle(byte bt_pin, byte led_pin) : SmartButton(bt_pin) < // конструктор. led=led_pin; >; // Наши методы // Включена кнопка или нет. byte isOn() < return sw; >// Что делать на клик. virtual void onClick() < if (sw) < // Был включен. Выключаем. digitalWrite(led,LOW); // Здесь может быть любой ваш код на выключение кнопки. >else < // Был выключен. Включаем. digitalWrite(led,HIGH); // Здесь может быть любой ваш код на включение кнопки. >sw=!sw; // Переключаем состояние. > >; // Объявляем переменную bt нашего нового класса. Можно не одну. Toggle bt(4,LED_PIN); // Нога 4, встроенный светодиод. Toggle drill(12,8) // Нога 12, светодиод на ноге 8. void loop() < bt.run(); drill.run(); if (bt.isOn()) < // что-то делать >else < // что-то другое делать >if (drill.isOn()) < // что-то делать >else < // что-то другое делать >>

    Как видите, нас совершенно здесь не интересует МКА кнопочки из предыдущей статьи, кода этой кнопки нет, он спрятан. Мы добавили свою функциональность к базовому классу и сделали переключатель по клику. Наш новый класс Toggle тоже можно оформить в виде библиотеки, кстати или положить в отдельный файл Toggle.h рядом с вашим скетчем, вам достаточно будет его подключить директивой #include. Мы так же задаём ногу со светодиодом для подсветки кнопки. Обратите внимание, что мы просто создали два объекта (bt и drill) нового класса Toggle, а МКА обработки кнопки для нас скрыт и не заботит.

    Основываясь на классе SmartButton можно сделать свои классы, что понимают двойной клик, например, водят курсор по меню или поворачивают пулемётную турель медленно-быстрее в зависимости от времени удержания кнопки. Для этого достаточно определить свои методы, описанные в SmartButton.h как virtual. Все определять не обязательно, только нужные вам.

    По просьбе целевой аудитории, вот пример класса PressButton, который предоставляет методы:

    • pressed() — кнопка была нажата, можно вызывать много раз.
    • ok() — я понял, слушай кнопку дальше, то есть сброс.
    #include "SmartButton.h" #define LED_PIN (13) // Порождаем наш новый класс от SmartButton class PressButton : public SmartButton < private: byte sw = 0; // состояние переключателя public: PressButton(byte bt_pin) : SmartButton(bt_pin) <>; // конструктор. // Наши методы // Была кликнута кнопка или нет. byte pressed() < return sw; >; // Я всё понял, слушаем кнопку дальше. void ok() < sw=0; >; // Что делать на клик. virtual void onClick() < sw=1; >; >; // Объявляем переменную bt нашего нового класса. Можно не одну. PressButton bt(4); // Нога 4. PressButton drill(12) // Нога 12. void loop() < bt.run(); drill.run(); if (bt.pressed()) < // что-то делать bt.ok(); >else < // что-то другое делать >if (drill.pressed()) < // что-то делать if (какое_то_условие) drill.ok(); >else < // что-то другое делать >>

    Таким образом мы получаем две независимо работающие «залипающие» кнопки, которые после нажатия находятся в состоянии pressed пока их не сбросить методом ok().

    Если у вас есть меню, вы можете определить методы onClick() у кнопок «вверх» и «вниз», которые будут вызывать перемещение курсора меню на дисплее с соответствующем направлении. Определение onHold() у них может вызывать перемещение курсора в начало и конец меню, например. У кнопки «ентер» можно определить onClick() как выбор меню, onHold() как выход с сохранением, а onLongHold() как выход без сохранения.

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

    SmartButton — это просто МКА, это инструмент для реализации поведения ваших кнопок.

    Где же скрыта вся магия? Магия кроется в файле SmartButton.cpp

    #include "SmartButton.h" // Конструктор и деструктор пустые. SmartButton::SmartButton() <> SmartButton::~SmartButton() <> // Конструктор с инициализацией. // Он используется чаще всего. SmartButton::SmartButton(int pin) < btPin = pin; pinMode(pin, INPUT_PULLUP); >// Машина конечных автоматов сидит здесь: // Обратите внимание - это ровно та же функция, // Что мы писали в [прошлой статье](https://habrahabr.ru/post/345960/). // Обратите внимание на вызов виртуальных функций on* и off*. void SmartButton::DoAction(enum input in) < enum state st=btState; switch (in) < case Release: btState=Idle; switch (st) < case Click: offClick(); break; case Hold: offHold(); break; case LongHold: offLongHold(); break; case ForcedIdle: onIdle(); break; >break; case WaitDebounce: switch (st) < case PreClick: btState=Click; onClick(); break; >break; case WaitHold: switch (st) < case Click: btState=Hold; onHold(); break; >break; case WaitLongHold: switch (st) < case Hold: btState=LongHold; onLongHold(); break; >break; case WaitIdle: switch (st) < case LongHold: btState=ForcedIdle; break; >break; case Press: switch (st) < case Idle: pressTimeStamp=millis(); btState=PreClick; break; >break; > > // А это наш генератор событий. // Его надо помещать в loop() void SmartButton::run() < unsigned long mls = millis(); if (!digitalRead(btPin)) DoAction(Press); else DoAction(Release); if (mls - pressTimeStamp >SmartButton_debounce) DoAction(WaitDebounce); if (mls - pressTimeStamp > SmartButton_hold) DoAction(WaitHold); if (mls - pressTimeStamp > SmartButton_long) DoAction(WaitLongHold); if (mls - pressTimeStamp > SmartButton_idle) DoAction(WaitIdle); >

    Логика местами спорная, я знаю 🙂 Но это работает.

    Теперь осталось заполнить файл README описанием вашей библиотеки и заполнить по аналогии файлик library.json, где поля вполне очевидны:

    < "name": "SmartButton", "keywords": "button, abstract class, oop", "description": "The SmartButton abstract class for using custom buttons in Arduino sketches.", "repository": < "type": "git", "url": "https://github.com/nw-wind/SmartButton" >, "version": "1.0.0", "authors": < "name": "Sergei Keler", "url": "https://github.com/nw-wind" >, "frameworks": "arduino", "platforms": "*" >

    Если у вас нет репозитория, можно эту секцию не указывать.

    Ура! Библиотека готова. Можно запаковать папку в ZIP и раздавать друзьям или копировать на другие свои компьютеры.

    По аналогии, можно сделать класс для любой МКА. Принцип общий: вы делаете класс, определяете виртуальные методы, которые потом надо будет переопределить, чтобы вставить свой код или готовые методы, если универсальность не требуется.

    Что за Github и зачем он мне?

    Github — это огромное сообщество программистов. Да, ваш код будет публично светиться на весь интернет, но… любой человек может предложить свои правки к вашему коду. Мне, например, очень помогли с SmartDelay два человека, один из которых сделал свою подобную библиотеку и мы поподсматривали чуть-чуть код друг у друга. Лучше две хорошие библиотеки, чем две глюкавые, правда?

    Чтобы поместить вашу библиотеку в Github надо сделать там аккаунт, сгенерить ключ и создать репозиторий с там же именем, что ваша библиотека (папка). Файлы можно загрузить через web-шнтерфейс.

    Для установки библиотеки из Github в Arduino IDE достаточно скопировать URL и воспользоваться утилитой git:

    Или загрузить ZIP — это будет как раз библиотека Arduino, как и все прочие библиотеки.

    Как пользоваться git вообще и Github в частности, есть много статей наверняка. Попробуйте поискать. Если не найдёте, я напишу как им пользуюсь я.

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

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