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