Одна из довольно сильных сторон любого программного обеспечени — это возможность единожды написанной программы быть использованной многократно как в виде отдельных частей, так и целиком, что и привело к зарождению концепции «библиотеки».
Можно сказать, что она вполне вписывается в общую парадигму развития цивилизации, которая позволила человеку стать царём дикой природы и обеспечила технический, культурный и интеллектуальный прогресс — это накопление информации и возможность поделиться ею с другими людьми.
Итак, как вы уже поняли, в этом рассказе пойдёт речь о библиотеках. Если бы мы попытались охватить тему библиотек под разные платформы и языки, то это получился бы чудовищных размеров рассказ, поэтому ограничимся небольшой сферой — библиотеками для Arduino.
Рано или поздно любой проект для Arduino сталкивается с тем, что необходимо снова и снова использовать отдельные компоненты кода, так как они не только отработаны и содержат в себе удачные решения, но и к тому же разработчику хорошо знакомы.
Такие фрагменты имеет смысл упаковывать в библиотеки, кроме того, если над одним и тем же проектом работает несколько человек, можно с лёгкостью поделиться своими наработками с другими (не забыв дать им описание API, кстати говоря, подробнее расскажу об этом чуть ниже).
Если вы до этого уже интересовались сутью библиотек и пытались разбирать существующие библиотеки Arduino, то наверняка успели заметить, что они состоят из двух отдельных файлов, один из которых имеет расширение .cpp — что означает «С Plus Plus». Так как язык Wiring для Arduino базируется, по сути, на языке C++, то и решили создавать файлы с таким расширением. Видимо, создатели подумали, что «а ещё это просто красиво» ©. Второй же компонент библиотеки имеет расширение .h ( «Headers»):
- Файл .cpp — называется файлом реализации.
- Файл .h — называется файлом заголовков.
Теперь рассмотрим эту концепцию разделения на два файла на примере конкретного кода.
Допустим, что у нас есть некий код, который управляет двигателями. Этот код состоит из ряда участков, среди которых инициализация каких-то переменных и какая-то функция:
В принципе, весь этот код мы можем поместить в файл реализации, то есть с расширением .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; 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); } }
Я специально в качестве кода для примера взял код для 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.
Serge78rus
Пример файла реализации для класса Kotofey у Вас написан не правильно, так как
— это функция, не принадлежащая никакому классу. Функция (метод) класса Kotofey должна быть записана так:kot_review Автор
Безусловно, ваш вариант является более корректным. Я тут почему-то не посчитал нужным ограничить область видимости классом (почему-то держал в голове, что класс у нас может быть только один, что может быть некорректно в более-менее большом проекте).
Serge78rus
Дело не в области видимости. Если в заголовочнике Вы объявляете класс, то и реализацию надо писать для функций класса. Если же Вам нужны свободные функции, то и делайте их свободными и в заголовочнике, и в файле реализации.
Использовать класс для ограничения области видимости обычных функций конечно можно, объявив их статическими функциями класса, но вообще-то в C++ для этого давно существует namespace.