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

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

Итак, как вы уже поняли, в этом рассказе пойдёт речь о библиотеках. Если бы мы попытались охватить тему библиотек под разные платформы и языки, то это получился бы чудовищных размеров рассказ, поэтому ограничимся небольшой сферой — библиотеками для 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 файла может выглядеть следующим образом:

Kotofey		KEYWORD1
Murlicat	KEYWORD2
DatPogladitPuziko	KEYWORD2
TrogatZadniyeLapki	KEYWORD2
KoluchestvoLapok	LITERAL1

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

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

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

Комментарии (3)


  1. Serge78rus
    31.08.2022 14:17
    +2

    Пример файла реализации для класса Kotofey у Вас написан не правильно, так как

    void  Murlicat(String gladit)
    {
      //......некая реализация
    }
    
    — это функция, не принадлежащая никакому классу. Функция (метод) класса Kotofey должна быть записана так:
    void  Kotofey::Murlicat(String gladit)
    {
      //......некая реализация
    }
    


    1. kot_review Автор
      02.09.2022 17:10

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


      1. Serge78rus
        02.09.2022 22:09

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

        Использовать класс для ограничения области видимости обычных функций конечно можно, объявив их статическими функциями класса, но вообще-то в C++ для этого давно существует namespace.