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

от автора

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

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

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

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

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

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

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

Теперь рассмотрим эту концепцию разделения на два файла на примере конкретного кода.

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

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

#include <Arduino.h> #include "esp32-hal-ledc.h" #include <WiFi.h>   // мотор 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 <Arduino.h>

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

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

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

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

Код с ошибкой

#include <Arduino.h> #include "esp32-hal-ledc.h" #include <WiFi.h>   // мотор 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 <Arduino.h> #include "esp32-hal-ledc.h" #include <WiFi.h>   // мотор 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 <Наша_библиотека.h>  void setup() {      setupMotors();  }  void loop() {   // Какой-то код }

Вот и всё!

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

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

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

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

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

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

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

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

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

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

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

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

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

#include <некая библиотека(ки).h> #include <некая библиотека(ки).h>  void  Murlicat(String gladit) {   //......некая реализация }  void  DatPogladitPuziko(int x, int y, int l) {   //......некая реализация }  void  TrogatZadniyeLapki(int a, int b) {   //......некая реализация }

Файл заголовков(.h):

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 – у вас может быть другая).

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

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

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

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

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

KotofeyKEYWORD1 MurlicatKEYWORD2 DatPogladitPuzikoKEYWORD2 TrogatZadniyeLapkiKEYWORD2 KoluchestvoLapokLITERAL1 

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


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

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


ссылка на оригинал статьи https://habr.com/ru/company/first/blog/683894/


Комментарии

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

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