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

Как написать свою библиотеку?


В этом уроке мы научимся писать собственные библиотеки для Arduino и разберём некоторые типовые вопросы по взаимодействию кода в библиотеке и кода в скетче (в главном файле программы). Это третий урок, который относится к библиотекам: обязательно прочитайте и усвойте урок про объекты и классы из блока программирования, и урок по использованию библиотек из блока базовых уроков, а также урок про создание функций. В этом уроке мы будем использовать все наши предыдущие знания, так что рекомендую разобраться со всем, что было непонятно. Писать библиотеки очень удобно в текстовом редакторе Notepad++ (официальный сайт) – так называемом блокноте программиста. Данный блокнот распознаёт и подсвечивает синтаксис, умеет в автодополнение текста и расширенный поиск, и многое многое другое. Безумно рекомендую работать именно в нём, если вы не умеете пользоваться Microsoft Visual Studio и прочими серьёзными средами разработки. Также рекомендую к прочтению вот этот урок с сайта Arduino.ru, в нём кратко пошагово рассказывают о создании библиотеки без излишеств. Если будете компилировать пример из этой статьи – замените WProgram.h на Arduino.h.

Разбираемся с файлами


Библиотека – это в первую очередь текстовый файл с кодом, который мы можем подключить в свой скетч и использовать имеющиеся там команды. Библиотека может иметь несколько файлов или даже папок с файлами, но подключается всегда один – главный заголовочный файл с расширением .h, а он в свою очередь подтягивает остальные необходимые файлы. В общем случае библиотека имеет такую структуру (название библиотеки testLib):

  • testLib – папка библиотеки
    • examples – папка с примерами
    • testLib.h – заголовочный файл
    • testLib.cpp – файл реализации
    • keywords.txt – карта подсветки синтаксиса

Иногда файлы .h и .cpp могут находиться в папке src. Все файлы и папки, кроме заголовочного .h, являются необязательными и могут отсутствовать, т.е. библиотека может состоять только из заголовочного файла. В таком виде библиотека лежит в папке со всеми остальными библиотеками и может быть подключена в скетч при помощи команды #include. Вообще есть два места, где программа будет искать библиотеку (именно файл библиотеки):

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

Соответственно команда include имеет два варианта поиска файла, название заключается в <> или “”:

  • #include <файл.h> – будет искать файл в папке с библиотеками
  • #include “файл.h” – попробует найти файл в папке со скетчем, если не найдёт – пойдёт искать в папку с библиотеками

Выглядит это вот так:

Основа библиотеки


Давайте заполним наш файл testLib.h, нашу тестовую библиотеку, минимальным кодом для работы:

#ifndef testLib_h
#define testLib_h
#include <Arduino.h>

// код библиотеки

#endif

Конструкция из директив препроцессора запрещает повторное подключение библиотеки и в целом является необязательной, но лучше не лениться и писать так. Файл библиотеки testLib.h находится в папке testLib в папке со всеми остальными библиотеками. Также мы подключаем основной файл Arduino.h для использования ардуино-функций в своём коде. Если таковых нет – его можно не подключать. Также подключаем testLib.h в наш тестовый скетч, как на скриншоте в прошлой главе. Конструкцию с #ifndef-define вы найдёте практически во всех библиотеках. На текущих версиях IDE (и, соответственно версии компилятора) можно делать так:

#pragma once
// подключаем Ардуино.н
// код библиотеки

Конструкция pragma once говорит компилятору, что данный файл нужно подключить только один раз, это просто короткая альтернатива #ifndef-define. Дальше будем использовать её

Пишем класс


Давайте воспользуемся наработками из урока объекты и классы и вставим финальную версию класса в testLib.h

testLib.h

#pragma once
#include <Arduino.h>

// описание класса
class Color {   // класс Color
public:
 Color(byte color = 5, byte bright = 30);
 void setColor(byte color);
 void setBright(byte bright);
 byte getColor();
 byte getBright();
private:
 byte _color;  // переменная цвета
 byte _bright; // переменная яркости
};

// реализация методов
Color::Color(byte color = 5, byte bright = 30) { // конструктор
 _color = color;   // запоминаем
 _bright = bright;
}
void Color::setColor(byte color) {_color = color;}
void Color::setBright(byte bright) {_bright = bright;}
byte Color::getColor() {return _color;}
byte Color::getBright() {return _bright;}

testSketch.ino

#include <testLib.h>

Color myColor(10);  // создаём объект myColor, указав _color (получим 10, 30)
Color myColor2(10, 20);  // указываем цвет и яркость! (получим 10, 20)
Color myColor3;  // без инициализации (получим 5, 30)

void setup() {
}

void loop() {
}

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

testSketch.ino

#include <testLib.h>
Color myColor(10);  // создаём объект myColor, указав _color (получим 10, 30)
Color myColor2(10, 20);  // указываем цвет и яркость! (получим 10, 20)
Color myColor3;  // без инициализации (получим 5, 30)

void setup() {
  Serial.begin(9600);
  Serial.println(myColor.getColor());   // 10
  Serial.println(myColor2.getBright()); // 20
  Serial.println(myColor3.getColor());  // 5
}

void loop() {
}

Код выводит значения из класса, выводит правильно. Собственно вот мы и написали свою библиотеку! Далее можно разделить описание и реализацию, создав файл testLib.cpp

testLib.h

#pragma once
#include <Arduino.h>

// описание класса
class Color {   // класс Color
public:
 Color(byte color = 5, byte bright = 30);
 void setColor(byte color);
 void setBright(byte bright);
 byte getColor();
 byte getBright();
private:
 byte _color;  // переменная цвета
 byte _bright; // переменная яркости
};

testLib.cpp

#include <testLib.h>	// подключаем заголовок обязательно

// реализация методов
Color::Color(byte color = 5, byte bright = 30) { // конструктор
 _color = color;   // запоминаем
 _bright = bright;
}

void Color::setColor(byte color) {_color = color;}
void Color::setBright(byte bright) {_bright = bright;}
byte Color::getColor() {return _color;}
byte Color::getBright() {return _bright;}

testSketch.ino

#include <testLib.h>
Color myColor(10);  // создаём объект myColor, указав _color (получим 10, 30)
Color myColor2(10, 20);  // указываем цвет и яркость! (получим 10, 20)
Color myColor3;  // без инициализации (получим 5, 30)

void setup() {
  Serial.begin(9600);
  Serial.println(myColor.getColor());   // 10
  Serial.println(myColor2.getBright()); // 20
  Serial.println(myColor3.getColor());  // 5
}

void loop() {
}

Важный момент: если в библиотеке есть файл имябиблиотеки.cpp, то реализация методов и функций должна находиться именно там! В файле имябиблиотеки.h реализацию указывать нельзя, будет ошибка.

Если библиотека состоит только из заголовочного файла имябиблиотеки.h, то реализацию можно расписать в нём.

И вот уже у нас полноценная взрослая библиотека, разбитая на файлы. Можно дополнить её файлом keywords.txt, чтобы наши методы подсвечивались в коде.

Keywords.txt


keywords.txt это файл, в котором содержится “карта” подсветки синтаксиса, то есть каким цветом какие слова подсвечивать. Синтаксис построения этого файла очень прост: с новой строки перечисляются названия функций/методов, и через табуляцию (нажатие клавиши TAB) – тип ключевого слова.

  • KEYWORD1 – жирный оранжевый, подсветка для типов данных и названий классов
  • KEYWORD2 – оранжевый цвет, для методов и функций
  • LITERAL1 – голубой цвет, для констант

Вот так будет выглядеть keywords.txt для нашей библиотеки:

# комментарий
testLib	KEYWORD1
Color	KEYWORD1

setColor	KEYWORD2
setBright	KEYWORD2
getColor	KEYWORD2
getBright	KEYWORD2

Можно оставлять комментарии, здесь они начинаются с решётки #. Констант у нас нет, поэтому LITERAL1 не использовал. Давайте посмотрим, как выглядит код с подсветкой наших команд из библиотеки. Важный момент: чтобы изменения вступили в силу, нужно закрыть все окна Arduino IDE и открыть скетч заново.   Почему Color не выделен жирным, да и вообще уже выделен в скетче без подсветки? Дело в том, что Arduino IDE собирает keywords из всех библиотек, и где-то имя Color видимо уже используется. Собственно вот и всё!

Примеры реализации


Структуру создания библиотеки мы разобрали, давайте рассмотрим некоторые частные варианты с примерами. Я буду делать примеры именно с классами, а не с функциями, потому что механика работы с классом, с библиотекой, гораздо сложнее, а мы тут с вами учимся библиотеки писать. Во всех примерах у меня создана тестовая библиотека testLib.h, и тестирую я её в скетче testSketch.

Библиотека без класса


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

testLib.h

#pragma once
#include <Arduino.h>

void printLol() {
  Serial.println("lol");
}

testSketch.ino

#include <testLib.h>

void setup() {
  Serial.begin(9600);
  printLol(); // выведет lol
}

void loop() {
}

Очевидные вариации: более грамотно будет написать объявление отдельно от реализации функции. Или даже поместить реализацию в файл .cpp.

testLib.h

#pragma once
#include <Arduino.h>

// объявление
void printLol();

testLib.cpp

#include <testLib.h>	// подключаем заголовок обязательно

// реализация
void printLol() {
  Serial.println("lol");
}

testSketch.ino

#include <testLib.h>

void setup() {
  Serial.begin(9600);
  printLol(); // выведет lol
}

void loop() {
}

Обернём в namespace


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

testLib.h

#pragma once
#include <Arduino.h>

// пространство имён myFunc
namespace myFunc {
  void printLol();
};

// реализация
void myFunc::printLol() {
  Serial.println("lol");
}

testSketch.ino

#include <testLib.h>
void setup() {
  Serial.begin(9600);
  // выведет kek из функции скетча
  printLol();

  // выведет lol из функции библиотеки
  myFunc::printLol();
}

void printLol() {
  Serial.println("kek");
}

void loop() {
}

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

Передача и вывод значения в класс


Рассмотрим такой пример: нужно передать в класс некое значение, обработать его и вернуть результат обратно в скетч. В качестве примера просто вернём умноженное на 10 число:

testLib.h

#pragma once
#include <Arduino.h>

class testClass {
  public:
    long get_x10(int value);
  private:
};

testLib.cpp

#include <testLib.h>	// подключаем заголовок обязательно

long testClass::get_x10(int value) {
  return value*10;
}

testSketch.ino

#include <testLib.h>
testClass testObject;

void setup() {
  Serial.begin(9600);
  Serial.println(testObject.get_x10(450)); // выведет 4500
}

void loop() {
}

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

testLib.h

#pragma once
#include <Arduino.h>

class testClass {
  public:
    void setValue(int val);
    int getValue();
  private:
    int _value = 0;
};

testLib.cpp

#include <testLib.h>	// подключаем заголовок обязательно

void testClass::setValue(int val) {
  // берём внешнюю val и пишем в свою _value
  _value = val;
}
int testClass::getValue() {
  return _value;	// вернуть переменную из класса
}

testSketch.ino

#include <testLib.h>
testClass testObject;

void setup() {
  Serial.begin(9600);
  testObject.setValue(666);
  Serial.println(testObject.getValue()); // выведет 666
}

void loop() {
}

Изменение переменной из класса


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

testLib.h

#pragma once
#include <Arduino.h>

class testClass {
  public:
    int multTo5(int value);
    void multTo7(int* value);
  private:
    
};

testLib.cpp

#include <testLib.h>	// подключаем заголовок обязательно

int testClass::multTo5(int value) {
  // вернуть значение, умноженное на 5
  return value * 5;
}

void testClass::multTo7(int* value) {
  // умножить переменную на 7
  *value = *value * 7;
}

testSketch.ino

#include <testLib.h>
testClass testObject;

void setup() {
  int a = 10;
  a = testObject.multTo5(a);
  // a == 50
  
  testObject.multTo7(&a);
  // a == 350
}

void loop() {
}

В первом варианте мы передаём значение переменной, внутри метода умножаем его на 5 и возвращаем обратно, и можем приравнять эту же переменную в скетче к новому значению. В случае с указателем всё работает более интересно: мы передаём методу адрес переменной, умножаем эту переменную на 7 внутри класса, и всё. Грубо говоря, в этом примере *value является куклой вуду для переменной a: что мы будем делать с *value внутри метода – это сразу же будет отражаться на a. Данную тему можно развить до такого варианта: мы можем хранить в классе адрес переменной, и класс всегда будет иметь прямой доступ к значению переменной, его не нужно будет передавать каждый раз!

testLib.h

#pragma once
#include <Arduino.h>

class testClass {
  public:
    void takeControl(int* value);
    void multTo6();
  private:
    int *_value;	// храним адрес
};

testLib.cpp

#include <testLib.h>	// подключаем заголовок обязательно

void testClass::takeControl(int* value) {
  _value = value;
}
void testClass::multTo6() {
  *_value = *_value * 6;
}

testSketch.ino

#include <testLib.h>
testClass testObject;
int a = 10;

void setup() {
  // передали адрес a
  testObject.takeControl(&a);

  // сейчас а == 10
  testObject.multTo6(); // тут а станет 60

  a = 5;
  testObject.multTo6(); // тут а станет 30
  testObject.multTo6(); // тут а станет 180
}

void loop() {
}

Таким образом класс и его методы могут иметь полный контроль над переменной в основной программе!

Передача массива в класс


Попробуем передать массив в класс, чтобы методами класса можно было, например, сложить сумму элементов массива и вернуть её!

testLib.h

#pragma once
#include <Arduino.h>

class testClass {
  public:
    long getSum(int *array, byte length);
  private:

};

testLib.cpp

#include <testLib.h>	// подключаем заголовок обязательно

long testClass::getSum(int *array, byte length) {
  long sum = 0;
  // вычисляем длину массива
  length = length / sizeof(int);
  
  for (byte i = 0; i < length; i++) {
    sum += array[i];
  }
  return sum;
}

testSketch.ino

#include <testLib.h>
testClass testObject;

void setup() {
  // делаем массив
  int myArray[] = {5, 33, 58, 251, 91};

  // передаём массив и его размер (в байтах)
  long arraySum = testObject.getSum((int*)myArray, sizeof(myArray));
  // arraySum == 438
}

void loop() {
}

Основной механизм я думаю понятен, оставлю тут ещё пример, как передать структуру

Передача структуры по указателю

// передаем структуру по указателю
struct foo_param_t
{
 float *u; int n; float b; float c;
}

void foo(foo_param_t *p)
{
 for (int i=0; i<p->n; i++)
 {
  float x = i*M_PI;
  p->u[i] = 1.0+p->b*x+p->c*x*x;
 }
}

void bar()
{
 const int N = 10;
 float a[N];
 foo_param_t p = {(float*)&a, N, 1.23, -4.56};
 foo(&p);
}

Передача структуры по ссылке

// передаем структуру по ссылке
struct foo_param_t
{
 float *u; int n; float c; float b;
}

void foo(foo_param_t& p)
{
 for (int i=0; i<p.n; i++)
 {
  float x = i*M_PI;
  p.u[i] = 1.0+p.b*x+p.c*x*x;
 }
}

void bar()
{
 const int N = 10;
 float a[N];
 foo_param_t p = {(float*)&a, N, 1.23, -4.56};
 foo(p);
}

Передача функции в класс


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

testLib.h

#pragma once
#include <Arduino.h>

// тут хранится приаттааченная функция
void (*atatchedF)();

// подключаем функцию
void attachFunction(void (*function)()) {
 atatchedF = *function;
}

// вызов приаттаченной функции
void callFunction() {
 (*atatchedF)();
}

testSketch.ino

#include <testLib.h>

void setup() {
  Serial.begin(9600);
  // подключили функцию printKek
  attachFunction(printKek);

  // вызвали подключенную функцию
  callFunction();
  // вызовет printKek
}

void printKek() {
  Serial.println("kek");
}

void loop() {
}

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

testLib.h

#pragma once
#include <Arduino.h>

class testClass {
  public:
    void attachFunction(void (*function)());
    void callFunction();
  private:
    void (*atatchedF)();
};

testLib.cpp

#include <testLib.h>	// подключаем заголовок обязательно

void testClass::attachFunction(void (*function)()) {
 atatchedF = *function;	
}

void testClass::callFunction() {
 (*atatchedF)();
}

testSketch.ino

#include <testLib.h>
testClass testObj;
void setup() {
  Serial.begin(9600);
  // подключили функцию printKek
  testObj.attachFunction(printKek);

  // вызвали подключенную функцию
  testObj.callFunction();
  // вызовет printKek
}

void printKek() {
  Serial.println("kek");
}

void loop() {
}

Второй вариант:

testLib.h

#pragma once
#include <Arduino.h>

extern "C" {
  typedef void (*func)(void);
}

class testClass {
  public:
    void attachFunction(func newFunc);
    void callFunction();
  private:
    func _attachedFunc;
};

testLib.cpp

#include <testLib.h>	// подключаем заголовок обязательно

void testClass::attachFunction(func newFunc) {
 _attachedFunc = newFunc;	
}

void testClass::callFunction() {
 _attachedFunc();
}

testSketch.ino

#include <testLib.h>
testClass testObj;
void setup() {
  Serial.begin(9600);
  // подключили функцию printKek
  testObj.attachFunction(printKek);

  // вызвали подключенную функцию
  testObj.callFunction();
  // вызовет printKek
}

void printKek() {
  Serial.println("kek");
}

void loop() {
}

Автоматическое создание объекта


Создание класса подразумевает также создание объекта, но иногда библиотека пишется только для одного объекта (например – библиотека для работы с одним интерфейсом), и создание объекта в скетче выглядит как лишний код. Но, если вы откроете любой пример с использованием библиотеки Wire.h, вы не найдёте там создания объекта Wire, а он используется! Например:

#include <Wire.h>

void setup() {
  Wire.begin();
}
// .............

Мы используем объект Wire, но мы его не создавали! Иногда это может быть удобно, давайте покажу, как это сделать: нужно всего лишь добавить в заголовочный файл строчку:

extern имя_класса имя_объекта;

А в .cpp, если он есть, добавить:

имя_класса имя_объекта = имя_класса();

Таким образом объект будет создан внутри библиотеки, и мы сможем им пользоваться  из скетча. Давайте возьмём самый первый пример из урока, из главы “Передача и вывод значения в класс”, и избавимся от лишнего создания объекта:

testLib.h

#pragma once
#include <Arduino.h>

class testClass {
  public:
    long get_x10(int value);
  private:
};
extern testClass testObject;

testLib.cpp

#include <testLib.h> // подключаем заголовок обязательно

long testClass::get_x10(int value) {
  return value*10;
}
testClass testObject = testClass();

testSketch.ino

#include <testLib.h>
// объект не создаём!
void setup() {
  Serial.begin(9600);
  Serial.println(testObject.get_x10(450)); // выведет 4500
}

void loop() {
}

Задание размера массива при создании объекта


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

#define ARRAY_LEN 20

class myClass {
  public:
    byte vals[ARRAY_LEN];
  private:    
};

myClass obj1;  // тут у obj1.vals будет 20 ячеек
myClass obj2;  // тут у obj2.vals будет 20 ячеек
myClass obj3;  // тут у obj3.vals будет 20 ячеек

Если мы хотим иметь возможность задать размер массива каждому объекту, то тут есть варианты:

При создании объектов появится соответственно <параметр>

template < int ARRAY_LEN >
class myClass {
  public:
    byte vals[ARRAY_LEN];
  private:    
};

myClass<10> obj1; // тут у obj1.vals будет 10 ячеек 
myClass<20> obj2; // тут у obj2.vals будет 20 ячеек 
myClass<30> obj3; // тут у obj3.vals будет 30 ячеек

Также можно записать размер массива в переменную, чтобы использовать в дальнейшем коде:

template < int ARRAY_LEN >
class myClass {
  public:
    byte vals[ARRAY_LEN];
    byte arrSize = ARRAY_LEN;
  private:
};

myClass<30> obj1;
// obj1.vals имеет 30 ячеек
// obj1.arrSize имеет значение 30

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

Можно выделить “массив” динамически и хранить его как указатель. В качестве “константного” значения используется фишка С++ под названием список инициализации (двоеточие после myClass(int x) ):

class myClass {
  public:
    int* arr;
    myClass(int x) : arr(new int[x]) {
      // конструктор
    }
  private:
};

myClass obj(5);

void setup() {
  Serial.begin(9600);
  obj.arr[0] = 1;
  obj.arr[1] = 2;
  obj.arr[2] = 3;
  obj.arr[3] = 4;
  obj.arr[4] = 5;

  for (byte i = 0; i < 5; i++) {
    Serial.println(obj.arr[i]);
    // выведет 1 2 3 4 5 с переносом
  }
}

void loop() {
}

Даже при глобальном создании объекта такой массив будет храниться в динамической памяти и компилятор не сможет посчитать его размер!

Делаем константы


Вы наверное часто видели в библиотеках передачу константы в функцию, далеко ходить не надо: digitalWrite(13, HIGH); , где HIGH – что это? Если вы откроете Arduino.h, то найдёте там HIGH, это – константа, дефайн:

#define HIGH 0x1

А в keywords.txt она указана как LITERAL1, что и даёт ей синий цвет. Давайте сделаем библиотеку, которая выводит текст в зависимости от указанной константы:

testLib.h

#pragma once
#include <Arduino.h>

// константы
#define KEK 0
#define LOL 1
#define AZAZA 2
#define HELLO 3

class testClass {
  public:
    void printer(byte value);
  private:
};

testLib.cpp

#include <testLib.h> // подключаем заголовок обязательно

void testClass::printer(byte value) {
 switch (value) {
 case 0: Serial.println("kek");
  break;
 case 1: Serial.println("lol");
  break;
 case 2: Serial.println("azaza");
  break;
 case 3: Serial.println("hello");
  break;  
 }	
}

testSketch.ino

#include <testLib.h>
testClass testObject;
void setup() {
  Serial.begin(9600);
  testObject.printer(KEK);  // выведет kek
  testObject.printer(LOL);  // выведет lol
  testObject.printer(AZAZA);  // выведет azaza
  testObject.printer(HELLO);  // выведет hello
}
void loop() {
}

Вот так можно передать вместо значения – слово, и работать с такой библиотекой будет удобнее. Заметьте, мы использовали константы (дефайн), это не очень правильно: если в другом подключенном ниже документе или в самом скетче наш дефайн совпадёт с названием другой переменной, функции или другого дефайна, то программа будет работать некорректно! Дефайн ведь распространяется на другие документы, включая главную программу (скетч). Что же делать? Можно называть свои константы настолько уникально, чтобы никто никогда с ними не пересекался, например добавлять префикс с названием библиотеки: MYLIB_CONSTANT. Ещё можно заменить дефайн перечислением, тогда  ваша библиотека не будет влиять на другие и на главный документ, но вот другие библиотеки и внешние дефайны могут залезть и в вашу библиотеку… Смотрим пример, такой же как выше, но на enum:

testLib.h

#pragma once
#include <Arduino.h>

// константы
enum printModes {
 KEK,
 LOL,
 AZAZA,
 HELLO,
};

class testClass {
public:
 void printer(printModes value);
private:
};

testLib.cpp

#include <testLib.h> // подключаем заголовок обязательно

void testClass::printer(printModes value) {
 switch (value) {
 case KEK: Serial.println("kek");
  break;
 case LOL: Serial.println("lol");
  break;
 case AZAZA: Serial.println("azaza");
  break;
 case HELLO: Serial.println("hello");
  break;
  
 }	
}

testSketch.ino

#include <testLib.h>
testClass testObject;
void setup() {
  Serial.begin(9600);
  testObject.printer(KEK);  // выведет kek
  testObject.printer(LOL);  // выведет lol
  testObject.printer(AZAZA);  // выведет azaza
  testObject.printer(HELLO);  // выведет hello
}
void loop() {
}

Использование enum в голом виде тоже может приводить к проблемам: имена enum (как имя самого enum, так и его констант) не должны пересекаться в подключенных документах, иначе будет ошибка двойного объявления. О чём конкретно я говорю:

Пересекающиеся enum

// документ doc1.h
enum engineControl {
  start,
  stop,
  restart,
};

// документ doc2.h
enum soundControl {
  play,
  pause,
  stop,
  replay,
};

// основной документ
#include "doc1.h"
#include "doc2.h"
// уже приведёт к ошибке "значение stop объявлено в другом месте"

Чтобы разделить enum в разных файлах, т.е. изолировать их значения друг от друга, есть три способа:

  • Пространство имён namespace
  • enum class
  • Внесение enum в основной класс библиотеки

Рассмотрим пример через namespace

namespace

// документ doc1.h
namespace engine {
enum engineControl {
  start,
  stop,
  restart,
};
};

// документ doc2.h
namespace sound {
enum soundControl {
  play,
  pause,
  stop,
  replay,
};
};

// основной документ
#include "doc1.h"
#include "doc2.h"

// используем соответствующие namespace'ы
engine::engineControl control1 = engine::stop;
sound::soundControl sound1 = sound::stop;

enum class

// документ doc1.h
enum class engineControl {
  start,
  stop,
  restart,
};

// документ doc2.h
enum class soundControl {
  play,
  pause,
  stop,
  replay,
};

// основной документ
#include "doc1.h"
#include "doc2.h"

// используем пространство имён для ЗНАЧЕНИЙ enum
engineControl control1 = engineControl::stop;
soundControl sound1 = soundControl::stop;

Переделаем наш самый первый пример под enum class, спрятав таким образом константы от других файлов:

testLib.h

#pragma once
#include <Arduino.h>
// константы
enum class printModes {
  KEK,
  LOL,
  AZAZA,
  HELLO,
};

class testClass {
  public:
    void printer(printModes value);
  private:
};

testLib.cpp

#include "testLib.h" // подключаем заголовок обязательно

void testClass::printer(printModes value) {
  switch (value) {
    case printModes::LOL: Serial.println("kek");
      break;
    case printModes::KEK: Serial.println("lol");
      break;
    case printModes::AZAZA: Serial.println("azaza");
      break;
    case printModes::HELLO: Serial.println("hello");
      break;
  }
}

testSketch.ino

#include "testLib.h"
testClass testObject;

void setup() {
  Serial.begin(9600);
  testObject.printer(printModes::KEK);  // выведет kek
  testObject.printer(printModes::LOL);  // выведет lol
  testObject.printer(printModes::AZAZA);  // выведет azaza
  testObject.printer(printModes::HELLO);  // выведет hello
}

void loop() {
}

Ещё одним вариантом является объявление enum внутри класса, тогда обращаться к нему нужно будет через ИМЯ_КЛАССА::

testLib.h

#include <Arduino.h> // нужно для ардуино-функций
class testClass {
  public:
    enum printModes {
      KEK,
      LOL,
      AZAZA,
      HELLO,
    };
    void printer(printModes value);
  private:
};

testLib.cpp

#include "testLib.h" // подключаем заголовок обязательно

void testClass::printer(printModes value) {
  switch (value) {
    case LOL: Serial.println("kek");
      break;
    case KEK: Serial.println("lol");
      break;
    case AZAZA: Serial.println("azaza");
      break;
    case HELLO: Serial.println("hello");
      break;
  }
}

testSketch.ino

#include "testLib.h"
testClass testObject;

void setup() {
  Serial.begin(9600);
  testObject.printer(testClass::KEK);  // выведет kek
  testObject.printer(testClass::LOL);  // выведет lol
  testObject.printer(testClass::AZAZA);  // выведет azaza
  testObject.printer(testClass::HELLO);  // выведет hello
}

void loop() {
}

Вмешательство в компиляцию


Далее рассмотрим такую ситуацию: мы умеем пользоваться директивами препроцессора и хотим влиять на процесс компиляции библиотеки, не трогая ничего в файле библиотеки. Возможно ли это? Да, возможно. Важный момент: данный трюк работает только в заголовочном файле библиотеки, то есть от файла реализации .cpp скорее всего придётся отказаться. Если сделать define до подключения файла библиотеки, то этот дефайн будет “виден” из заголовочного файла библиотеки и его можно использовать для операторов условной компиляции. Важный момент: при создании библиотеки не рекомендуется писать исполнительный код в заголовочном файле вне класса, потому что это приведёт к ошибкам при подключении библиотеки в разных файлах. Для использования “магии дефайнов” нужно правильно оформить реализацию в заголовочном файле, смотрим пример:

Вот так можно.h

// lib.h
class testClass {
  public:
    int func() {return 10;}
  private:
};

А вот так нельзя.h

// lib.h
class testClass {
  public:
    int func();
  private:
};

int testClass::func() {
  return 10;
}

Ну и пример как работает дефайн, “влезающий” в библиотеку:

testLib.h

#pragma once
#include <Arduino.h>

void printResult() {
// если определена SEND_NUDES
#ifdef SEND_NUDES
 Serial.begin(9600);
 Serial.println("nudes");
#endif
}

testSketch.ino

// дефайним SEND_NUDES
// ДО подключения библиотеки
#define SEND_NUDES
#include <testLib.h>

void setup() {
  // выведет "nudes" если задефайнен SEND_NUDES
  printResult();
}
void loop() {
}

Зачем это нужно? Условная компиляция позволяет управлять компиляцией кода, то есть жёстко задавать, какие части кода будут компилироваться, а какие – нет. Более подробно об опасностях и тонкостях работы с define, в том числе и для создания библиотек, читайте в предыдущем уроке про директивы препроцессора.

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


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

Время на прочтение
9 мин

Количество просмотров 3.8K

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

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

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

Данный документ описывает создание библиотеки для Arduino. Объяснение начнется с написания скетча передачи кода Морзе посредством светодиода. Затем будет показано как конвертировать скетч в библиотеку. Это позволит другим пользователям легко использовать созданный код, обновлять и дополнять его.

Скетч, воспроизводящий код Морзе:

int pin = 13;

void setup()
{
  pinMode(pin, OUTPUT);
}

void loop()
{
  dot(); dot(); dot();
  dash(); dash(); dash();
  dot(); dot(); dot();
  delay(3000);
}

void dot()
{
  digitalWrite(pin, HIGH);
  delay(250);
  digitalWrite(pin, LOW);
  delay(250);
}

void dash()
{
  digitalWrite(pin, HIGH);
  delay(1000);
  digitalWrite(pin, LOW);
  delay(250);
}

Данный скетч посредством мигания светодиода на выводе 13 выдает сигнал SOS.

Скетч содержит ряд частей кода, которые необходимо будет перенести в библиотеку. Во-первых, это функции dot() и dash(), которые управляют миганием светодиода. Во-вторых, это переменная ledPin, определяющая какой порт ввод/вывода использовать. И наконец, вызов функции pinMode(), устанавливающий режим вывода на используемом порту ввода/вывода.

Процесс конвертации скетча в библиотеку.

Библиотека содержит два файла: заголовочный файл (с расширением .h) и файлы реализации (с расширением .cpp). Заголовочный файл содержит характеристики библиотеки, т.е. список всего что содержится в ней. Создаваемый заголовочный файл будет называться Morse.h. Для дальнейшей работы с заголовочным файлом необходимо просмотреть содержание файла реализации.

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

class Morse
{
  public:
    Morse(int pin);
    void dot();
    void dash();
  private:
    int _pin;
};

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

Также заголовочный файл содержит еще несколько дополнительных строк. Во-первых, это директива #include, которая дает доступ к стандартным типам и постоянным языка программирования Arduino (директива по умолчанию добавляется к каждому скетчу, но не к библиотеке). Директива выглядит следующим образом (и находится выше объявления класса):

#include "WProgram.h"

В версиях Arduino 1.0 и выше нужно еще добавить:

#include Arduino.h

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

#ifndef Morse_h
#define Morse_h

// директивы #include и код помещается здесь

#endif

Это предотвращает повторное подключение нашей библиотеки, если кто-то по ошибке дважды подключит библиотеку директивой #include.

В начале кода библиотеки принято помещать комментарий о ее предназначении, авторе, дате и лицензии на библиотеку.

Готовый заголовочный файл содержит:

/*
  Morse.h - Library for flashing Morse code.
  Created by David A. Mellis, November 2, 2007.
  Released into the public domain.
*/
#ifndef Morse_h
#define Morse_h

#include "WProgram.h"

class Morse
{
  public:
    Morse(int pin);
    void dot();
    void dash();
  private:
    int _pin;
};

#endif

Рассмотрим файл реализации Morse.cpp.

В начале кода находятся несколько директив #include. Данными директивами разрешается доступ к стандартным функциям Arduino и к характеристикам в головном файле библиотеки:

#include "WProgram.h"
#include "Morse.h"

Далее по коду находится конструктор. Он используется для создания экземпляра создаваемого класса. В данном случае пользователь задает номер используемого порта ввода/вывода через параметр. Порта устанавливается в режим вывода, а номер сохраняется в частной переменной для использования в других функциях:

Morse::Morse(int pin)
{
  pinMode(pin, OUTPUT);
  _pin = pin;
}

Код Morse:: означает, что функция принадлежит классу Morse. Нижний пробел в начале имени переменной _pin — принятое обозначение для частных переменных. Вообще, имя может быть любое, но согласно принятым конвенциям именования для частных переменных принято использовать префикс «_». Это также позволяет отличить от аргумента функции (в данном случае pin).    

Далее код, который конвертируется в библиотеку из изначального скетча, добавилось только Morse:: и изменилось имя переменной с pin, на _pin:

void Morse::dot()
{
  digitalWrite(_pin, HIGH);
  delay(250);
  digitalWrite(_pin, LOW);
  delay(250);  
}

void Morse::dash()
{
  digitalWrite(_pin, HIGH);
  delay(1000);
  digitalWrite(_pin, LOW);
  delay(250);
}

Общепринято помещать некоторые поясняющие комментарии в начале кода файла реализации. Полный код библиотеки:

/*
  Morse.cpp - Library for flashing Morse code.
  Created by David A. Mellis, November 2, 2007.
  Released into the public domain.
*/

#include "WProgram.h"
#include "Morse.h"

Morse::Morse(int pin)
{
  pinMode(pin, OUTPUT);
  _pin = pin;
}

void Morse::dot()
{
  digitalWrite(_pin, HIGH);
  delay(250);
  digitalWrite(_pin, LOW);
  delay(250);  
}

void Morse::dash()
{
  digitalWrite(_pin, HIGH);
  delay(1000);
  digitalWrite(_pin, LOW);
  delay(250);
}
Использование библиотеки.

Во-первых, необходимо создать папку Morse в подпапке libraries директории блокнота. Во-вторых, требуется скопировать файлы Morse.h и Morse.cpp в созданную папку. После запуска программы Arduino в меню Sketch > ImportLibrary будет находиться библиотека Morse. Библиотека будет компилироваться совместно со скетчами, использующими ее. Если при компиляции библиотеки возникли проблемы, то необходимо проверить, чтобы ее файлы были с расширениями .cpp и .h (не должно быть никаких дополнительных расширений .pde и .txt).

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

#include <Morse.h>

Morse morse(13);

void setup()
{
}

void loop()
{
  morse.dot(); morse.dot(); morse.dot();
  morse.dash(); morse.dash(); morse.dash();
  morse.dot(); morse.dot(); morse.dot();
  delay(3000);
}

Несколько отличий от изначального скетча:

Во-первых, добавлена директивы #include в начало скетча. Таким образом определяется доступность библиотеки Morse и ее подключение. Неиспользуемую библиотеку можно удалить, убрав директиву #include.

Во-вторых, создается экземпляр класса Morse, называемый morse:

Morse morse(13);

При выполнении данной строки (перед выполнением функции setup()) вызывается конструктор для класса Morse и принимает аргумент, данный в примере (13).

При этом функция setup() ничего не содержит, т.к. вызов функции pinMode() произошел внутри библиотеки (когда был создан экземпляр класса).

В-третьих, для вызова функций dot() и dash() необходимо прибавить префикс morse. – имя используемого экземпляра. Может быть несколько экземпляров класса Morse, каждый со своим номером порта, хранящимся в локальной переменной _pin. Вызовом функции конкретного экземпляра определяются какие переменные, используются во время вызова. При наличии следующих двух строк:

Morse morse(13);
Morse morse2(12);

внутри вызова morse2.dot(), переменная _pin будет иметь значение 12.

К сожалению автоматическая подсветка кода не работает с подключаемыми библиотеками. Для того чтобы подсветка заработала необходимо создать файл с названием keywords.txt. Пример:

Morse	KEYWORD1
dash	KEYWORD2
dot	KEYWORD2

Напротив каждой строки через табуляцию стоит зарезервированное слово, и опять через табуляцию тип слова. Классы соответствуют зарезервированному слову KEYWORD1 и окрашены в оранжевый цвет; функции – KEYWORD2 и окрашены в коричневый. Для распознавания слов необходимо перезапустить среду разработки Arduino.

Созданную библиотеку желательно всегда сопровождают примером ее применения. Для этого создается папка examples в директории Morse. Затем копируется созданный ранее скетч SOS в данную папку. (Файл скетча можно найти через меню Sketch > ShowSketchFolder). После перезапуска Arduino в меню File > Sketchbook > Examples будет находиться пункт Library-Morse, содержащий пример. Также необходимо добавить комментарии о том, как лучше использовать библиотеку.

Arduino использует C++, поэтому, библиотеку придётся писать на этом языке. Для новой библиотеки нужно создать дополнительный класс с кодом, который будет взаимодействовать с устройством.

Нам понадобится один заголовочный файл .h, в котором будет описан ваш класс с функциями, константами. А также второй файл .cpp, в котором все эти объявленные функции и константы реализуются.

Для начала создадим в папке ..libraries собственную папку с именем библиотеки, например, Kitty.

Внутри папки Kitty создаём файлы kitty.h и kitty.cpp.

Библиотеки нужно писать на C++. Если внутри нашей библиотеки используем какие-нибудь функции или объекты из стандартной библиотеки Arduino, то следует подключать соответствующий заголовочный файл Arduino.h.


#include "Arduino.h"

Есть два используемых способа создания класса. Если экземпляр класса может быть только один, то объявляется экземпляр в этом же .h-файле. Примеры таких классов — HardwareSerial, Serial.

Заголовочный файл не содержит выполняемого кода, а лишь определяет имена классов и методов в библиотеке.

Полностью заголовочный файл будет следующего содержания.


/*
  Kitty.h - Library for cats.
  Created by Alexander Klimov, December 12, 2018.
  Released into the public domain.
*/

#ifndef kitty_h
#define kitty_h

#include "Arduino.h"

class Kitty
{
    public:
        Kitty();
        void init();
        void meow();
    private:
        int _counter;	
};

#endif

Команда #ifndef предотвращает многократный импорт библиотеки.

Закрытый раздел private содержит переменную. В нашем примере она просто для демонстрации и не используется.

В общедоступном разделе public указаны конструкторы и методы.

Теперь реализуем созданные функции в файле kitty.cpp:


#include "Arduino.h"
#include "kitty.h"

// конструктор вызывается всегда при создании экземпляра класса Kitty
Kitty::Kitty()
{

}

void Kitty::init()
{
    Serial.begin(9600);
}

// просто говорим "Meow"
void Kitty::meow()
{
    Serial.println("Meow");
}

Теперь нужно перезапустить Arduino IDE, если она у вас была запущена. Откройте меню Sketch | Include Library. Если всё было сделано правильно, то увидите собственную библиотеку.

Create Library

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

Если выбрать свою библиотеку, то в скетче появится строка:


#include <kitty.h>

Это означает, что библиотека подключена и мы можем её использовать в скетче.


#include <kitty.h>

Kitty kitty; // создаём экземпляр нашего класса

void setup() {
  kitty.init();
}

void loop() {
  kitty.meow(); // мяукаем в Serial Monitor
  delay(1000);
}

Запускаем скетч, открываем Serial Monitor и наблюдаем за мяуканьем кота. Жрать просит!

Правилом хорошего тона считается создать дополнительный файл keywords.txt и папку examples.

Файл keywords.txt содержит объявления ключевых слов вашей библиотеки, имена типов, методов, функций, констант. Это поможет редактору раскрасить указанные слова соответствующими цветами. Синтаксис файла (в качестве разделителя используйте TAB):


#######################################
# Syntax Coloring Map For Kitty
#######################################

#######################################
# Datatypes (KEYWORD1)
#######################################

Kitty	KEYWORD1

#######################################
# Methods and Functions (KEYWORD2)
#######################################

meow	KEYWORD2
init	KEYWORD2

#######################################
# Constants (LITERAL1)
#######################################

Перезапустите Arduino IDE ещё раз. Теперь названия вашего класса и её функций будут подсвечены.

В папке examples создаются примеры использования в формате pde. Создайте необходимый скетч, переименуйте его в нужный формат и снова перезапустите IDE. Примеры станут доступны для использования.

О созданной библиотеке можно сообщить сообществу на сайте playground.arduino.cc/Main/LibraryList. Можно создать zip-архив или выложить на GitHub.

Реклама

Разберемся, как создать свою собственную библиотеку для Arduino. Начнем с эскиза, который позволит мигать кодом Морзе и объясним, как преобразовать его функции в библиотеку. Это позволит пользователю, подключив библиотеку, использовать написанный код и легко обновлять его при обновлении библиотеки.

Начнем с эскиза, который воспроизводит код Морзе:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

int pin = 13;

void setup()

{

  pinMode(pin, OUTPUT);

}

void loop()

{

  dot(); dot(); dot();

  dash(); dash(); dash();

  dot(); dot(); dot();

  delay(3000);

}

void dot()

{

  digitalWrite(pin, HIGH);

  delay(250);

  digitalWrite(pin, LOW);

  delay(250);

}

void dash()

{

  digitalWrite(pin, HIGH);

  delay(1000);

  digitalWrite(pin, LOW);

  delay(250);

}

При запуске программы мы получим на выводе 13 цифровой сигнал — код SOS (сигнал бедствия).

В эскизе есть несколько частей, которые необходимо будет внести в нашу библиотеку. Во-первых, у нас есть две функции dot() и dash(), которые обеспечивают «мигание» на выходе. Во-вторых, есть переменная ledPin, которая указывает функциям, какой пин использовать. Наконец, есть вызов pinMode(), который инициализирует пин в качестве вывода.

Давайте начнем превращать эскиз в библиотеку!

Вам нужно как минимум два файла для библиотеки: заголовочный файл (с расширением .h) и исходный файл (с расширением .cpp). Файл заголовка имеет определения для библиотеки: в основном список всего, что находится внутри; в то время, как исходный файл имеет фактический код. Мы будем называть нашу библиотеку «Morse», поэтому нашим заголовочным файлом будет Morse.h. Давайте посмотрим, что в нем происходит. Сначала он может показаться немного странным, но смысл станет понятен, если посмотреть исходный файл, который будет с ним работать.

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

class Morse

{

  public:

    Morse(int pin);

    void dot();

    void dash();

  private:

    int _pin;

};

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

Вам нужно еще несколько вещей в файле заголовка. Один из них — это оператор #include, который дает вам доступ к стандартным типам и константам языка Arduino (это автоматически добавляется к эскизам, но не к библиотекам). Это выглядит так (и идет выше определения класса, данного ранее):

И второе — обычно весь заголовочный файл «обертывается» в следующую конструкцию:

#ifndef Morse_h

#define Morse_h

// the #include statment and code go here…

#endif

Данная конструкция предотвращает проблемы, если кто-то случайно подключит  вашу библиотеку дважды.

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

В конечном итоге получаем файл, следующего содержания:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

/*

  Morse.h — Library for flashing Morse code.

  Created by David A. Mellis, November 2, 2007.

  Released into the public domain.

*/

#ifndef Morse_h

#define Morse_h

#include «Arduino.h»

class Morse

{

  public:

    Morse(int pin);

    void dot();

    void dash();

  private:

    int _pin;

};

#endif

Теперь давайте рассмотрим различные части исходного файла Morse.cpp.

Сначала указывается пара операторов #include. Они предоставляют доступ ко всем стандартным функциям Arduino и определениям в вашем файле-заголовке:

#include «Arduino.h»

#include «Morse.h»

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

Morse::Morse(int pin)

{

  pinMode(pin, OUTPUT);

  _pin = pin;

}

Рассмотрим некоторые особенности данного кода. Прежде всего это Morse :: перед именем функции. Такая запись говорит о том, что эта функция является частью класса Morse. Вы увидите это снова в других функциях класса. Вторая необычная вещь — это символ подчеркивания в имени нашей частной переменной _pin . Эта переменная может иметь любое имя, которое вы хотите, если оно соответствует определению в файле-заголовке. Добавление подчеркивания к началу имени является общим соглашением, позволяющим понять, какие переменные являются частными, а также отличать имя от имени аргумента функции ( в нашем случае — pin).

Далее идет код из эскиза, который мы переводим в библиотеку. Код выглядит примерно так же, как в скетче. Только добавляется Morse :: перед именами функций и  _pin вместо pin :

void Morse::dot()

{

  digitalWrite(_pin, HIGH);

  delay(250);

  digitalWrite(_pin, LOW);

  delay(250);  

}

void Morse::dash()

{

  digitalWrite(_pin, HIGH);

  delay(1000);

  digitalWrite(_pin, LOW);

  delay(250);

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

/*

  Morse.cpp — Library for flashing Morse code.

  Created by David A. Mellis, November 2, 2007.

  Released into the public domain.

*/

#include «Arduino.h»

#include «Morse.h»

Morse::Morse(int pin)

{

  pinMode(pin, OUTPUT);

  _pin = pin;

}

void Morse::dot()

{

  digitalWrite(_pin, HIGH);

  delay(250);

  digitalWrite(_pin, LOW);

  delay(250);  

}

void Morse::dash()

{

  digitalWrite(_pin, HIGH);

  delay(1000);

  digitalWrite(_pin, LOW);

  delay(250);

}

СОДЕРЖАНИЕ ►

  • Создание новой библиотеки в Arduino IDE
  • Как написать библиотеку для Arduino IDE
    • Создание заголовочного файла .h
    • Создание файла библиотеки .cpp
    • Создание файла синтаксиса keywords.txt
  • Встраивание библиотеки в Arduino IDE

Как создать библиотеку Ардуино и встроить ее в Arduino IDE? Если у вас появился такой вопрос, значит вы уже достаточно хорошо знакомы с программированием на языке Arduno и знаете, что библиотеки значительно упрощают работу. Рассмотрим на двух простых примерах, как самому написать библиотеку в Ардуино для выполнения различных операций и вызывать их в скетче с помощью собственных команд.

Создание своей библиотеки для Ардуино

Для этого занятия потребуется:

  • Arduino Uno / Arduino Nano / Arduino Mega
  • светодиод и резистор;
  • макетная плата;
  • провода «папа-папа».

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

// таким способом подключаются библиотеки в скетче
#include <SoftwareSerial.h>
#include <DHT.h>

// команды, которые можно использовать при подключении библиотек
SoftwareSerial mySerial(2, 3);
DHT dht(7, DHT11);

Для работы нам потребуется лишь текстовый редактор — «Блокнот». Первым делом необходимо в директории «C:Program FilesArduinolibraries» создать папку для файлов библиотеки, именно сюда устанавливаются библиотеки для Arduino. Название папки, как и имена создаваемых файлов можно придумать свое. Для примера мы будем использовать название «BlinkLed», которое отражает назначение библиотеки.

Стандартные и популярные библиотеки для Ардуино

Создание своей собственной библиотеки для Arduino UNO

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

   digitalWrite(13, HIGH);
   delay(1000);
   digitalWrite(13, LOW);
   delay(1000);

Если требуется мигать светодиодом определенное количество раз, то следует уже использовать цикл for, где можно установить счетчик для повторения операций. Именно этот код мы будем использовать для библиотеки, но вместо заданного количества повторений цикла, номера порта светодиода и времени задержки между операциями зададим переменные, которые можно будет указывать в скетче.

for (int x=0; x<=5; x++) {
     digitalWrite(13, HIGH);
     delay(1000);
     digitalWrite(13, LOW);
     delay(1000); 
}

Как написать библиотеку для Arduino IDE

Библиотека Arduino IDE должна иметь минимум два файла: заголовочный файл (с расширением .h) и файл с исходным кодом (с расширение .cpp). В первом файле содержится описание класса и переменные, второй файл содержит программный код методов. Файл keywords.txt не обязателен, но он позволяет выделять цветом в среде Arduino IDE новые типы и методы из созданной вами библиотеки.

  1. Зайдите в папку C:Program FilesArduinolibraries;
  2. Создайте новую папку c названием BlinkLed;
  3. Создайте текстовый документ keywords.txt;
  4. Создайте текстовый документ BlinkLed с расширением .h;
  5. Создайте текстовый документ BlinkLed с расширением .cpp.

Код для файла BlinkLed.h:

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

/*
здесь размещают информацию о разработчике и инструкцию для пользователя
*/

//  включение стандартных функций Ардуино
#include "Arduino.h"
void blink (int pin, int pause, int repeat);

Код для файла BlinkLed.cpp:

Исходный файл содержит основной код библиотеки. В нашем примере в программе используются три переменные, которые следует указывать в скетче: pin — номер порта, pause задержка в миллисекундах, repeat — количество раз выполнения цикла for.

/*
здесь размещают информацию о разработчике и инструкцию для пользователя
*/

//  включение стандартных функций Ардуино и заголовочного файла
#include "Arduino.h"
#include "BlinkLed.h"

void blink (int pin, int pause, int repeat) {

   pinMode(pin, OUTPUT);

   for (int x=0; x<=repeat; x++) {
        digitalWrite(pin, HIGH);
        delay(pause);
        digitalWrite(pin, LOW);
        delay(pause); 
   }
}

Код для файла keywords.txt:

В файле указывается подсветка синтаксиса в Arduino IDE. Функция KEYWORD1 окрашивает слово в оранжевый цвет, LITERAL1 окрашивает слово в синий цвет.

	BlinkLed		KEYWORD1
	blink		KEYWORD1

Встраивание библиотеки в Arduino IDE

Новая библиотека BlinkLed в списке примеров Arduino IDE

Новая библиотека BlinkLed в списке примеров Arduino IDE

На этом написание библиотеки закончено. Также вы можете в папке BlinkLed создать папку examples, где можно разместить примеры программ для демонстрации работы вашей библиотеки. При открытии среды программирования Arduino IDE, созданную библиотеку можно найти через панель инструментов Файл > Примеры / BlinkLed. В итоге мы можем вызвать функцию мигания светодиода с помощью одной команды.

// подключаем библиотеку BlinkLed.h
#include <BlinkLed.h>

void setup() {
}

void loop() {
  /*
    используем новую команду blink и указываем в скобках через запятую:
    номер порта, время задержки, количество раз включения светодиода
  */
  blink (13, 500, 10);
}

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

странице.

Arduino довольно сильно основан на C ++ (язык программирования). Этот язык основан на вещах, называемых заголовками, функциями и библиотеками. Эти вещи, фактически, переносятся и используются в Arduino. Как правило библиотеки включаются в самое начало вашего кода и используются для упрощения кода всего проекта, например:

#include <LiquidCrystal.h>
#include <Servo.h>

Большинство нужных библиотек вы можете найти на нашем сайте в разделе Библиотеки. В этом уроке мы продемонстрируем, как создать собственную библиотеку для Ардуино.

Содержание

  • Шаг 1. Программное обеспечение
  • Шаг 2. Код Arduino
  • Шаг 3. Создание библиотеки
  • Шаг 4. Встраивание библиотеки

Шаг 1. Программное обеспечение

Существует много специализированного программного обеспечения, которое вы можете использовать для этого, но ваш основной текстовый редактор — это «Блокнот», которого вполне достаточно для работы.

Вы также можете попробовать что-то вроде Notepad++, VSCode. Мы обычно используем Sublime Text 3 для программирования.

Это базовый эскиз Blink для включения встроенного светодиода:

void setup() {
  pinMode(13, OUTPUT);
}

void loop() {
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  delay(1000);
}

Этот код не очень сложный, и ему вообще не нужна библиотека. Однако, ради демонстрации, мы все равно сделаем это.

Шаг 3. Создание библиотеки

  1. Найдите где расположена папка «библиотеки» (Arduino > Библиотеки / Arduino > Libraries).
  2. Создайте новую папку под названием «MyLibrary», но не оставляйте кавычки.
  3. В папке, которую вы только что создали, создайте новый .txt документ под названием «BlinkLED«.
  4. Сохраните файл с расширением .h.
  5. Повторите, но с расширением .cpp.

Код для .h файла:

/*Этот файл ищет файл .cpp, когда он запускается*/
#include "Arduino.h"
void blinkLED(int pin, int delayTime);

Код для .cpp файла:

/*Здесь вы пишете код, который хотите запустить*/
#include "Arduino.h"
#include "BlinkLED.h"

void blinkLED(int pin, int delayTime){
  pinMode(pin, OUTPUT);
  digitalWrite(pin, HIGH);
  delay(delayTime);
  digitalWrite(pin, LOW);
  delay(delayTime);  
}

Теперь сохраните оба файла и выйдите.

Шаг 4. Встраивание библиотеки

Закройте Arduino IDE и снова откройте её. Перейдите в:

Sketch > Include Library / Включить Библиотеку

Прокрутите страницу вниз до «Contributed Libraries«. Вы должны увидеть одну из них под названием MyLibrary. Нажмите на нее, и вы увидите:

#include <BlinkLED.h>

Если появится вышеприведенный код, то это значит мы на правильном пути. Просто загрузите остальную часть кода.

#include <BlinkLED.h>

void setup() {
}
void loop() {
  blinkLED(13, 500); // пин, частота в миллисекундах
}

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

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

Если вы хотите, чтобы ваша библиотека делала больше, чем просто мигала светодиодом, все, что вам нужно сделать, это отредактировать два файла, которые мы создали ранее. Если вы заинтересованы в создании более сложных библиотек, вам может помочь изучение C++.

20 ноября 2018 в 17:13
| Обновлено 7 декабря 2019 в 00:09 (редакция)
Опубликовано:

Уроки

Понравилась статья? Поделить с друзьями:
  • Как написать библиографию
  • Как написать библиографическое описание статьи
  • Как написать библиографическое описание книги
  • Как написать библиографический список интернет ресурсов
  • Как написать бешу или бесю