Пишем свою библиотеку
Как написать свою библиотеку?
В этом уроке мы научимся писать собственные библиотеки для 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.cppИ вот уже у нас полноценная взрослая библиотека, разбитая на файлы. Можно дополнить её файлом 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.
Библиотека без класса
В библиотеке необязательно должен быть класс, может быть просто набор функций:
Очевидные вариации: более грамотно будет написать объявление отдельно от реализации функции. Или даже поместить реализацию в файл .cpp.Обернём в namespace
Данный пример относится к примеру выше: в "библиотеке" мы создали функции, имена этих функций могут совпасть с другими функциями в скетче, что приведёт к проблемам. Вместо написания класса, функции можно обернуть в "пространство имён" - namespace. Смотрите пример, я думаю всё станет понятно.
Использование namespace позволяет разделить функции с одинаковыми названиями из разных документов, обращение к функции из пространства имён выглядит точно так же, как к классу:имяПространстваИмён::имяФункции
.
Передача и вывод значения в класс
Рассмотрим такой пример: нужно передать в класс некое значение, обработать его и вернуть результат обратно в скетч. В качестве примера просто вернём умноженное на 10 число:
Рассмотрим более сложную ситуацию: нужно принять значение в класс, записать в приватную переменную, и отдельным методом получить её:Изменение переменной из класса
Рассмотрим такую ситуацию: нам нужно при помощи метода/функции библиотеки изменить значение переменной в скетче. Тут есть два варианта: присваивать напрямую, или использовать указатель. Рассмотрим оба варианта в одном примере:
В первом варианте мы передаём значение переменной, внутри метода умножаем его на 5 и возвращаем обратно, и можем приравнять эту же переменную в скетче к новому значению. В случае с указателем всё работает более интересно: мы передаём методу адрес переменной, умножаем эту переменную на 7 внутри класса, и всё. Грубо говоря, в этом примере *value является куклой вуду для переменной a: что мы будем делать с *value внутри метода - это сразу же будет отражаться на a. Данную тему можно развить до такого варианта: мы можем хранить в классе адрес переменной, и класс всегда будет иметь прямой доступ к значению переменной, его не нужно будет передавать каждый раз! Таким образом класс и его методы могут иметь полный контроль над переменной в основной программе!Передача массива в класс
Попробуем передать массив в класс, чтобы методами класса можно было, например, сложить сумму элементов массива и вернуть её!
Основной механизм я думаю понятен, оставлю тут ещё пример, как передать структуруПередача функции в класс
Я думаю вы помните, как работают штуки типа attachInterrupt: мы указываем имя функции, которую можно будет вызвать из другой функции. Делается это через указатель на функцию. Посмотрим простой пример без класса:
Теперь засунем это всё в класс и будем хранить адрес прикреплённой функции внутри класса. Тут есть два варианта, покажу оба, просто чтобы было тут в одном месте это всё. Первый вариант: Второй вариант:Автоматическое создание объекта
Создание класса подразумевает также создание объекта, но иногда библиотека пишется только для одного объекта (например - библиотека для работы с одним интерфейсом), и создание объекта в скетче выглядит как лишний код. Но, если вы откроете любой пример с использованием библиотеки Wire.h, вы не найдёте там создания объекта Wire, а он используется! Например:
#include <Wire.h> void setup() { Wire.begin(); } // .............
Мы используем объект Wire, но мы его не создавали! Иногда это может быть удобно, давайте покажу, как это сделать: нужно всего лишь добавить в заголовочный файл строчку:
extern имя_класса имя_объекта;
А в .cpp, если он есть, добавить:
имя_класса имя_объекта = имя_класса();
Таким образом объект будет создан внутри библиотеки, и мы сможем им пользоваться из скетча. Давайте возьмём самый первый пример из урока, из главы "Передача и вывод значения в класс", и избавимся от лишнего создания объекта:
Задание размера массива при создании объекта
Вы должны помнить из урока про массивы, что размер массива должен быть известен до начала выполнения программы. А что делать, если в классе нам нужен массив с возможностью задать его размер? Если объект в программе будет один, или для всех объектов размер массива будет одинаковый, то очевидно можно сделать вот так:
#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, что и даёт ей синий цвет. Давайте сделаем библиотеку, которая выводит текст в зависимости от указанной константы:
Вот так можно передать вместо значения - слово, и работать с такой библиотекой будет удобнее. Заметьте, мы использовали константы (дефайн), это не очень правильно: если в другом подключенном ниже документе или в самом скетче наш дефайн совпадёт с названием другой переменной, функции или другого дефайна, то программа будет работать некорректно! Дефайн ведь распространяется на другие документы, включая главную программу (скетч). Что же делать? Можно называть свои константы настолько уникально, чтобы никто никогда с ними не пересекался, например добавлять префикс с названием библиотеки: MYLIB_CONSTANT. Ещё можно заменить дефайн перечислением, тогда ваша библиотека не будет влиять на другие и на главный документ, но вот другие библиотеки и внешние дефайны могут залезть и в вашу библиотеку... Смотрим пример, такой же как выше, но на enum:
Использование enum в голом виде тоже может приводить к проблемам: имена enum (как имя самого enum, так и его констант) не должны пересекаться в подключенных документах, иначе будет ошибка двойного объявления. О чём конкретно я говорю: Чтобы разделить enum в разных файлах, т.е. изолировать их значения друг от друга, есть три способа:- Пространство имён
namespace
enum class
- Внесение enum в основной класс библиотеки
Рассмотрим пример через namespace
ИМЯ_КЛАССА::
Вмешательство в компиляцию
Далее рассмотрим такую ситуацию: мы умеем пользоваться директивами препроцессора и хотим влиять на процесс компиляции библиотеки, не трогая ничего в файле библиотеки. Возможно ли это? Да, возможно. Важный момент: данный трюк работает только в заголовочном файле библиотеки, то есть от файла реализации .cpp скорее всего придётся отказаться. Если сделать define до подключения файла библиотеки, то этот дефайн будет "виден" из заголовочного файла библиотеки и его можно использовать для операторов условной компиляции. Важный момент: при создании библиотеки не рекомендуется писать исполнительный код в заголовочном файле вне класса, потому что это приведёт к ошибкам при подключении библиотеки в разных файлах. Для использования "магии дефайнов" нужно правильно оформить реализацию в заголовочном файле, смотрим пример:
Ну и пример как работает дефайн, "влезающий" в библиотеку: Зачем это нужно? Условная компиляция позволяет управлять компиляцией кода, то есть жёстко задавать, какие части кода будут компилироваться, а какие - нет. Более подробно об опасностях и тонкостях работы с define, в том числе и для создания библиотек, читайте в предыдущем уроке про директивы препроцессора.Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])