Своя библиотека Arduino: файлы .h и .cpp и зачем #include <Arduino.h>
Библиотека Arduino — это не что-то особенное, а обычная папка с двумя-тремя файлами на C++: заголовок .h говорит, что библиотека умеет, файл .cpp — как именно. Всё остальное — описание для среды, чтобы она показала библиотеку в меню и в менеджере.
Ниже — путь от вкладки в скетче до настоящей библиотеки с примером в меню «Файл → Примеры». По дороге разберём, что такое Arduino.h, почему в скетче его писать не надо, а в своём файле — обязательно, и пять ошибок, на которых застревают почти все. Весь код собран arduino-cli под Arduino UNO, тексты ошибок — дословные.
Зачем выносить код в библиотеку
Пока скетч один, библиотека не нужна. Она появляется, когда один и тот же кусок кода кочует из проекта в проект: опрос кнопки с защитой от дребезга, мигание без delay(), работа с самодельным датчиком. Копировать его руками — значит чинить одну и ту же ошибку в пяти местах.
Библиотека делает из этого кода одно место. Исправили — исправилось везде. А ещё она прячет подробности: снаружи видно led.update(), а сколько там миллисекунд и флагов внутри — дело самой библиотеки.
Что такое .h и .cpp
Заголовок .h — это оглавление: какие функции и классы есть и какие у них аргументы. Кода внутри функций в нём обычно нет. Файл .cpp — реализация: те же функции, но уже с телом.
Строка #include буквально вставляет текст файла в то место, где она стоит. Когда скетч подключает led.h, компилятор узнаёт, что функция ledOn существует и какие у неё аргументы, — и спокойно собирает вызов. Саму функцию он найдёт в led.cpp на следующем шаге, при сборке.
Скобки важны. #include <Servo.h> — искать среди установленных библиотек. #include "led.h" — сначала рядом с текущим файлом. Свои файлы из папки скетча подключают в кавычках.
Что такое Arduino.h и когда его подключать
Arduino.h — заголовок самой платформы. В нём объявлено всё, что вы привыкли считать частью языка: pinMode, digitalWrite, millis, HIGH, тип byte. Скачивать его не нужно и неоткуда: он приходит вместе с поддержкой платы.
В файле .ino его никто не пишет, потому что среда добавляет строку #include <Arduino.h> сама, перед сборкой. А вот в .h и .cpp она этого не делает. Свой файл без этой строки не знает ни byte, ни digitalWrite, и сборка падает с ошибкой 'byte' was not declared in this scope. Поэтому первые две строки любого заголовка библиотеки одинаковы:
#pragma once
#include <Arduino.h>Шаг первый: вкладка в скетче
Начинать удобно без всякой библиотеки. В Arduino IDE откройте меню справа от вкладок — три точки в IDE 2 или стрелка в IDE 1.8 — выберите «Новая вкладка» и назовите её led.h, затем ещё одну — led.cpp. Файлы лягут в папку скетча рядом с .ino.
led.h
#pragma once
#include <Arduino.h>
void ledOn(byte pin);led.cpp
#include "led.h"
void ledOn(byte pin) {
pinMode(pin, OUTPUT);
digitalWrite(pin, HIGH);
}сам скетч
#include "led.h" // свой файл — в кавычках
void setup() {
ledOn(13);
}
void loop() {}Для одного проекта этого уже достаточно. Библиотекой код станет, когда переедет из папки скетча в общую папку libraries — тогда его увидит любой скетч.
Шаг второй: класс вместо функций
Почти все библиотеки устроены как класс: вы создаёте объект, а он хранит своё состояние. Возьмём задачу, которая встречается в каждом втором проекте, — мигать светодиодом без delay(), чтобы программа не останавливалась. Если светодиодов два и у каждого свой ритм, отдельные переменные для каждого быстро превращаются в кашу. Класс прячет их внутрь объекта.
В заголовке — объявление класса. Раздел public — то, что можно вызывать снаружи. Раздел private — внутреннее состояние, к нему из скетча не добраться; подчёркивание в начале имени — просто договорённость, чтобы отличать поля от аргументов.
Blinker.h
#pragma once
#include <Arduino.h>
class Blinker {
public:
Blinker(uint8_t pin, unsigned long interval);
void begin();
void update();
void setInterval(unsigned long interval);
bool isOn() const;
private:
uint8_t _pin;
unsigned long _interval;
unsigned long _last = 0;
bool _on = false;
};В реализации перед каждым методом стоит имя класса и два двоеточия: Blinker::update — это метод update класса Blinker. Строка после двоеточия в конструкторе — список инициализации: так поля получают значения сразу при создании объекта.
Blinker.cpp
#include "Blinker.h"
Blinker::Blinker(uint8_t pin, unsigned long interval)
: _pin(pin), _interval(interval) {}
void Blinker::begin() {
pinMode(_pin, OUTPUT);
digitalWrite(_pin, LOW);
}
void Blinker::update() {
if (millis() - _last >= _interval) {
_last = millis();
_on = !_on;
digitalWrite(_pin, _on ? HIGH : LOW);
}
}
void Blinker::setInterval(unsigned long interval) {
_interval = interval;
}
bool Blinker::isOn() const {
return _on;
}Сравнение millis() - _last >= _interval записано именно так, через вычитание, не случайно: оно продолжает работать, когда через 49 дней счётчик millis() переполнится и начнёт с нуля.
Можно и без .cpp: библиотека из одного файла
Если класс маленький, его пишут целиком в заголовке: тела методов прямо внутри объявления класса. Такую библиотеку проще носить — это один файл, который достаточно положить рядом со скетчем. Методы, написанные внутри класса, компилятор считает inline, поэтому заголовок можно подключать в нескольких файлах без ошибки о повторном определении — мы проверили сборкой.
Debounce.h
#pragma once
#include <Arduino.h>
// кнопка с защитой от дребезга: весь класс в одном файле, .cpp не нужен
class Debounce {
public:
explicit Debounce(uint8_t pin) : _pin(pin) {}
void begin() { pinMode(_pin, INPUT_PULLUP); }
// true — ровно один раз на каждое нажатие
bool pressed() {
bool now = digitalRead(_pin) == LOW;
if (now && !_was && millis() - _t > 30) {
_t = millis();
_was = true;
return true;
}
if (!now) _was = false;
return false;
}
private:
uint8_t _pin;
bool _was = false;
unsigned long _t = 0;
};Цена удобства: каждый файл, который подключает заголовок, заново разбирает весь код класса, и при правке любой строки пересобирается всё. Для сотни строк это незаметно; когда класс вырастает — время делить на .h и .cpp.
Защита от двойного подключения
Первая строка заголовка, #pragma once, говорит компилятору: «вставляй этот файл один раз, даже если его подключили дважды». Без неё класс, подключённый из скетча и из другой библиотеки, окажется объявлен два раза — и сборка остановится.
В старых библиотеках вместо неё встречается конструкция из трёх строк — #ifndef BLINKER_H, #define BLINKER_H в начале и #endif в конце. Делает то же самое; #pragma once короче и понимается всеми компиляторами, которые собирают скетчи Arduino.
Одна переменная на несколько файлов: extern
Функции разложили по файлам — следующим захочется так же разложить переменную: счётчик нажатий увеличивается в одном файле, а печатается в скетче. Первое, что приходит в голову, — объявить её в заголовке вместе с функцией:
// counter.h — так нельзя
#pragma once
#include <Arduino.h>
int pressCount = 0; // определение прямо в заголовке
void countPress();Заголовок подключён и в скетче, и в counter.cpp — значит, переменная создана дважды, и сборка останавливается на multiple definition of `pressCount'. #pragma once тут не помогает: он защищает от двойного подключения внутри одного файла, а файлов два.
Правильно — разделить так же, как функцию. В заголовке слово extern обещает, что переменная существует, но ничего не создаёт. Создаётся она ровно один раз, в одном .cpp:
// counter.h
#pragma once
#include <Arduino.h>
extern int pressCount; // «где-то такая переменная есть»
void countPress();// counter.cpp
#include "counter.h"
int pressCount = 0; // а живёт она здесь, ровно в одном месте
void countPress() {
pressCount++;
}Теперь pressCount одна на всю программу: counter.cpp её увеличивает, скетч читает. Правило простое — в заголовке только обещания (объявления), всё, что занимает память или содержит код, — в .cpp.
static: что видно только внутри файла
Обратная задача: у библиотеки есть вспомогательная функция или переменная, которой незачем торчать наружу. Если в двух файлах окажутся две функции helper, сборка упадёт с тем же multiple definition — даже если никто не подключал заголовков. Слово static перед функцией или глобальной переменной ограничивает её одним файлом:
// a.cpp
static int helper() { return 1; } // видна только внутри a.cpp
int fromA() { return helper(); }
// b.cpp
static int helper() { return 2; } // своя, другая — конфликта нет
int fromB() { return helper(); }С static этот код собирается, без него — нет. Внутри класса те же задачи решает раздел private, поэтому в библиотеках-классах static нужен реже.
Имена: чтобы не столкнуться с чужой библиотекой
В скетче живут десяток библиотек одновременно, и все их имена лежат в одном общем пространстве. Если ваша библиотека объявит функцию begin или константу LED, рано или поздно найдётся соседняя с таким же именем. Два приёма снимают проблему почти целиком.
Приставка к макросам. Всё, что объявлено через #define, не подчиняется никаким областям видимости — поэтому у макросов библиотеки должна быть приставка с её именем: BLINKER_DEFAULT_INTERVAL, а не DEFAULT_INTERVAL. А ещё лучше вместо макроса написать обычную константу.
Пространство имён. Свободные функции и константы заворачиваются в namespace, и снаружи к ним обращаются с приставкой. Классам это нужно реже — у них уже есть своё имя, но для набора функций это лучший способ.
#pragma once
#include <Arduino.h>
namespace blinker {
const unsigned long DEFAULT_INTERVAL = 500;
void blinkOnce(uint8_t pin, unsigned long ms = DEFAULT_INTERVAL);
}#include "blink_tools.h"
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
}
void loop() {
blinker::blinkOnce(LED_BUILTIN); // приставка blinker:: — не спутать с чужим
delay(1000);
}Превращаем в библиотеку: папка и где она лежит
Библиотека — это папка с таким устройством. Главное правило: файл library.properties лежит прямо в папке библиотеки, а исходники — в подпапке src.
libraries/
└── Blinker/
├── library.properties
├── keywords.txt
├── src/
│ ├── Blinker.h
│ └── Blinker.cpp
└── examples/
└── TwoLeds/
└── TwoLeds.inoСама папка libraries находится в папке скетчей: в Windows и macOS это Документы/Arduino/libraries, в Linux — ~/Arduino/libraries. Точный путь видно в Arduino IDE: Файл → Параметры → «Размещение папки скетчей». После того как папка библиотеки легла туда, среду нужно перезапустить — список библиотек она читает при старте.
library.properties: паспорт библиотеки
Простой текстовый файл: по строке на поле. По нему среда понимает, что библиотека устроена по-современному и исходники лежат в src. Без него она считает библиотеку старого формата и ищет файлы прямо в корне папки — при раскладке с src этот файл обязателен.
name=Blinker
version=1.0.0
author=Ivan Petrov
maintainer=Ivan Petrov <ivan@example.com>
sentence=Blink LEDs without delay().
paragraph=Several LEDs blink at their own pace and never block the sketch.
category=Signal Input/Output
url=https://example.com/blinker
architectures=*| поле | зачем |
|---|---|
| name | Имя в менеджере и в меню «Подключить библиотеку». Латиница, без пробелов в начале. |
| version | Версия из трёх чисел: 1.0.0. Меняйте при каждом изменении — по ней среда понимает, что вышла новая. |
| author, maintainer | Кто написал и кто поддерживает. У maintainer принято указывать почту. |
| sentence, paragraph | Одна строка и абзац описания — их показывает менеджер. |
| category | Раздел каталога: Display, Sensors, Communication, Signal Input/Output и другие из списка Arduino. |
| architectures | Для каких плат: * — для любых, avr — только UNO, Nano, Mega; через запятую можно несколько. |
| depends | Необязательно: какие библиотеки нужны этой. Менеджер предложит поставить их вместе. |
Пример в меню «Файл → Примеры»
Каждая подпапка в examples — отдельный скетч, и имя файла .ino должно совпадать с именем папки. После перезапуска среды пример появится в меню Файл → Примеры → Blinker. Это лучшая документация к библиотеке: человек открывает готовый скетч и сразу видит, как ей пользоваться.
Обратите внимание: из скетча библиотека подключается уже в угловых скобках — она больше не лежит рядом, среда ищет её среди установленных.
examples/TwoLeds/TwoLeds.ino
#include <Blinker.h>
Blinker led(LED_BUILTIN, 500); // встроенный светодиод, полсекунды
Blinker second(9, 150); // второй светодиод на девятом выводе
void setup() {
led.begin();
second.begin();
}
void loop() {
led.update(); // каждый мигает в своём ритме,
second.update(); // и ни один не останавливает программу
}keywords.txt: подсветка в редакторе
Необязательный файл, но с ним имена из библиотеки подсвечиваются цветом, как встроенные команды. Одно имя на строку, между именем и типом — табуляция, а не пробелы: KEYWORD1 — классы, KEYWORD2 — методы и функции, LITERAL1 — константы.
Blinker KEYWORD1
begin KEYWORD2
update KEYWORD2
setInterval KEYWORD2
isOn KEYWORD2Как поделиться библиотекой
Упакуйте папку Blinker целиком в ZIP-архив. На другом компьютере: Скетч → Подключить библиотеку → Добавить .ZIP библиотеку и указать архив. Среда сама разложит его в папку libraries.
Проверьте одно: внутри архива сразу должна лежать папка библиотеки, а в ней — library.properties. Если между ними оказалась ещё одна папка, среда библиотеку не увидит. Так чаще всего бывает с архивами, скачанными с GitHub: там папка называется Blinker-main, а настоящая библиотека лежит на уровень глубже.
Положите рядом с library.properties файл README.md: зачем библиотека, как подключить, короткий пример и список методов. Это первое, что человек увидит на странице репозитория, — без него хорошую библиотеку просто не поймут.
Как попасть в общий менеджер библиотек
Для своих проектов хватит архива. Если библиотека пригодится другим, её можно добавить в реестр Arduino — тогда она будет находиться по имени в менеджере, как Servo или FastLED. Порядок такой:
- Выложите библиотеку в открытый репозиторий на GitHub. Файл
library.propertiesдолжен лежать в корне репозитория. - Создайте релиз — тег с номером версии, совпадающим с полем
version. Менеджер берёт только отмеченные тегом версии, а не последнее состояние кода. - В репозитории
arduino/library-registryдобавьте адрес своего репозитория в файлrepositories.txtи отправьте изменение на рассмотрение. Робот проверит структуру библиотеки и напишет, что поправить. - После принятия библиотека появляется в менеджере в течение суток. Каждая следующая версия — это новое значение
versionи новый тег; менеджер подхватит её сам.
Как ставить чужие библиотеки и где их брать, — на общей странице про библиотеки.
Пять ошибок, на которых застревают все
'byte' was not declared in this scope
В своём .h или .cpp нет строки #include <Arduino.h>. В скетче она добавляется сама, в остальных файлах — нет, поэтому типы и функции Arduino там неизвестны. Рядом обычно стоит ещё одна строка — variable or field 'ledOn' declared void: это последствие той же причины. Добавьте подключение второй строкой заголовка.
fatal error: util.h: No such file or directory
Свой файл из папки скетча подключён в угловых скобках. <util.h> ищется среди установленных библиотек, а там его нет. Для файлов рядом со скетчем — кавычки: #include "util.h".
fatal error: Blinker.h: No such file or directory — хотя библиотека в папке
Лишний уровень вложенности: libraries/Blinker-main/Blinker/…. Среда смотрит только на один уровень внутрь libraries. Переложите папку с library.properties прямо туда и перезапустите среду.
multiple definition of `twice(int)'
Функция написана целиком, с телом, прямо в .h, а заголовок подключён в двух файлах. Каждый из них получил свою копию, и при сборке их оказалось две. Либо перенесите тело функции в .cpp, а в заголовке оставьте только объявление, либо допишите перед ней слово inline:
#pragma once
// inline разрешает одинаковому определению встречаться в нескольких файлах
inline int twice(int x) {
return x * 2;
}multiple definition of `pressCount'
То же самое, но с переменной: она создана в заголовке, а заголовок подключён в двух файлах. В заголовке оставьте extern int pressCount;, а саму переменную создайте в одном .cpp — как в разделе про extern выше.
Что дальше
- Библиотеки: где брать и как ставить
- Программирование Arduino: основы
- Скетч не собирается или не заливается
Библиотека Blinker из этого урока собрана arduino-cli под Arduino UNO вместе с примером; ошибки получены сборкой намеренно сломанных вариантов.