
Содержание
Директивы препроцессора в Arduino
Препроцессор
Процесс компиляции прошивки очень непростой и имеет несколько этапов, один из первых – работа препроцессора. Препроцессору можно давать команды, которые он выполнит перед компиляцией кода прошивки: это может быть подключение файлов, замена текста, условные конструкции и некоторые другие вещи. Также у препроцессора есть макросы, которые позволяют добавлять в код некоторые интересные вещи.
#include – подключить файл
С подключением файлов мы уже знакомы: директива #include
подключает новый документ в текущий, например библиотеку. После #include
нужно указать имя файла, который подключается. Указать можно в "двойных кавычках"
, а можно в <угловых скобках>
. В чём разница? Файл, имя которого указано в двойных кавычках, компилятор будет искать в папке с основным документом, если не найдёт – будет искать в папке с библиотеками. Если указать в скобках – будет сразу искать в папке с библиотеками, путь к которой обычно можно настроить.
#include "mylib.h" // подключить mylib.h, сначала поискать в папке со скетчем #include <mylib.h> // подключить mylib.h из папки с библиотеками
Также можно указать путь к файлу, который нужно подключить. Например у нас в папке со скетчем есть папка libs, а в ней – файл mylib.h. Чтобы подключить такой файл, пишем:
#include "libs/mylib.h"
Компилятор будет искать его в папке со скетчем, в подпапке libs.
#define / undef
Мы с вами уже сталкивались с #define
в предыдущих уроках, сейчас хочу рассказать о некоторых частных случаях. Напомню, #define
– это команда препроцессору заменить один порядок символов на другой, например #define MOTOR_SPEED 50
заменит все встречающиеся в коде MOTOR_SPEED
цифрой 50
при компиляции. Если не писать ничего после указания первого набора символов, препроцессор заменит их на “ничего”. То есть #define MOTOR_SPEED
просто удалит из кода все сочетания MOTOR_SPEED
.
Также #define
позволяет создавать макро-функции, об этом мы говорили в уроке про функции. Например при помощи дефайна можно создавать удобные конструкции в стиле вечного цикла
#define FOREVER for(;;) ...... FOREVER { // код крутится, байты мутятся }
Или быстрого и удобного отключения отладки в коде:
#ifdef DEBUG #define DEBUG_PRINT(x) Serial.println(x) #else #define DEBUG_PRINT(x) #endif
Или даже задефайнить целый кусок кода, используя переносы и обратный слэш
#define printWords(digit) \ Serial.print("Digit is "); \ Serial.print(digit); // на последней строке \ не нужен
Если DEBUG
задефайнен, то DEBUG_PRINT
– это макро-функция, которая выводит значение в порт. А если не задефайнен – все вызовы DEBUG_PRINT
просто убираются из кода и экономят память!
При разработке проекта важна отладка, мы делаем её средствами Serial.println()
. Чтобы после окончания разработки не убирать из кода все вызовы Serial и не нагружать код условными конструкциями #ifdef DEBUG…. #endif, можно сделать так:
#ifdef DEBUG_ENABLE #define DEBUG(x) Serial.println(x) #else #define DEBUG(x) #endif
Если DEBUG_ENABLE
задефайнен – все вызовы DEBUG()
в коде будут заменены на вывод в порт. Если не задефайнен – они будут заменены НИЧЕМ, то есть просто “вырежутся” из кода! Также по DEBUG_ENABLE
можно запустить сериал и получить полный контроль над отладкой: если она не нужна – убрали DEBUG_ENABLE
и из кода убрался запуск порта и все выводы, что резко сокращает объём занимаемой памяти:
// раздефайнить или задефайнить для использования //#define DEBUG_ENABLE #ifdef DEBUG_ENABLE #define DEBUG(x) Serial.println(x) #else #define DEBUG(x) #endif void setup() { #ifdef DEBUG_ENABLE Serial.begin(9600); #endif } void loop() { DEBUG("kek"); delay(100); }
#undef
Также есть директива #undef
, которая отменяет #define
, в некоторых случаях может оказаться полезным.
Проблемы
В чём же состоит опасность #define
? Он распространяется на все документы, которые подключаются в код после него, а также работают в коде, будучи описанными в другом документе. Рассмотрим подробнее:
Если ПЕРЕД подключением файла вы объявите #define
, то данный define будет распространяться на этот файл и заменит указанный текст.
Если что-то в подключаемом файле (имена функций и переменных) совпадёт в вашим дефайном – будет ошибка компиляции. Например, в библиотеке FastLED есть цвет DarkMagenta
, внутри библиотеки цвета объявлены как enum. Если я сделаю дефайн на такое имя – получу ошибку:
Но, если в подключаемом файле есть свой #define
с таким же именем, то работать будет #define
файла!
Важный момент: наш скетч в Arduino IDE по сути является .cpp файлом, и #define
из него могут распространяться только на заголовочные файлы .h! То есть в файле .h подключаемой библиотеки дефайн будет “видно”, а вот в .cpp – уже нет!
Как решить эту проблему? Например, мы хотим управлять компиляцией библиотеки при помощи define-ов, расположенных не в заголовочном файле библиотеки (потому что из заголовочного можно, это и так понятно). Есть два несложных варианта (сложные не рассматриваем):
- Поместить исполнительный код библиотеки в заголовочном .h файле (.cpp не создавать вообще), тогда дефайном из скетча можно будет влиять на компиляцию исполнительного кода. Этот пример мы рассматривали в самом первом скриншоте.
- Создать в папке с библиотекой отдельный заголовочный файл, например config.h, в нём собрать необходимые дефайны “настроек”, и этот файл подключать во все файлы библиотеки, в этом случае .cpp файл библиотеки сможет подхватить нужный define. Так сделано, например, в библиотеке FastLED.
На этом сложности не заканчиваются: #define
из одной библиотеки может пролезть в другую библиотеку, которая подключена после первой! Вернёмся к тому же примеру с DarkMagenta
– если в моей библиотеке я задефайню это слово и подключу библиотеку до подключения FastLED – я получу ошибку компиляции! Если поменять подключение местами – ошибки не будет. Но, если я захочу использовать DarkMagenta
в своём скетче, я буду неприятно удивлён =)
Что я хочу сказать в итоге: #define
– гораздо более мощный инструмент, чем может показаться на первый взгляд. Использование define с невнимательным отношением к именам может привести к ошибке, которую будет непросто отловить. Это палка о двух концах: с одной стороны хочется использовать в своей библиотеке define, чтобы никто другой случайно не пролез со своими дефайнами. В то же время, своя библиотека может начать конфликтовать с другими библиотеками. Какой тут выход? Очень простой! Делать имена дефайнов максимально уникальными: если это библиотека – оставлять префикс библиотеки, если это скетч – делать префикс с именем скетча. Также можно отказаться от define в пользу констант или enum, enum кстати удобнее define в плане создания набора констант, а места занимает совсем немного!
#if – условная компиляция
Условная компиляция является весьма мощным инструментом, при помощи которого можно вмешиваться в компиляцию кода и делать его очень универсальным как для пользователя, так и для железа. Рассмотрим директивы условной компиляции:
#if
– если#ifdef
– если определено#ifndef
– если не определено#else
– иначе#elif
– иначе если#endif
– конец условияdefined
– проверка, определён ли
При помощи условной компиляции можно буквально включать и выключать целые части кода из компиляции, то есть из финальной версии программы, которая будет загружена в микроконтроллер. Рассмотрим несколько конструкция для примера:
#define USE_DISPLAY 1 // настройка для пользователя #if (USE_DISPLAY == 1) #include <библиотека дисплея.h> #endif void setup() { #if (USE_DISPLAY == 1) // дисплей.инициализация #endif } void loop() { }
#define SENSOR_TYPE 3 // настройка для пользователя // подключение выбранной библиотеки #if (SENSOR_TYPE == 1 || SENSOR_TYPE == 2) #include <библиотека сенсора 1 и 2.h> #elif (SENSOR_TYPE == 3) #include <библиотека сенсора 3.h> #else <библиотека сенсора 4.h> #endif
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) // код для ATmega1280 и ATmega2560 #elif defined(__AVR_ATmega32U4__) // код для ATmega32U4 #elif defined(__AVR_ATmega1284__) // код для ATmega1284 #else // код для остальных МК #endif
Сообщения от компилятора
Для вывода сообщения можно использовать директиву #pragma message
, выглядит вот так:
Также есть директива #error
, она тоже выводит текст, но вызывает ошибку компиляции:
И pragma message и error можно вызывать при помощи условной компиляции, рассмотренной в предыдущей главе.
#pragma
#pragma
это целый класс директив с разными возможностями. Выше мы уже рассмотрели #pragma message
, здесь рассмотрим ещё некоторые.
#pragma once
Указывает компилятору, что данный файл нужно подключить только один раз. Является более удобной и современной заменой конструкции вида
#ifndef _MY_LIB #define _MY_LIB // код #endif
Такую конструкцию вы можете встретить в 99% библиотек, файлов ядра и вообще заголовочников с кодом.
#pragma pack/pop
Конструкция с #pragma pack
и #pragma pop
позволяет более рационально распределять структуры в памяти. Тема сложная, читайте на Хабре.
Макросы
У препроцессора есть несколько интересных макросов, которыми можно пользоваться в своём коде. Рассмотрим некоторые полезные из них, которые работают на Arduino (точнее, на компиляторе avr-gcc).
__func__ и __FUNCTION__
Макросы __func__
и __FUNCTION__
“возвращают” в виде символьного массива (строки) название функции, внутри которой они вызваны. Являются аналогом друг друга. Например:
void myFunc() { Serial.println(__func__); // выведет myFunc }
__DATE__ и __TIME__
__DATE__
возвращает дату компиляции по системному времени в виде символьного массива (строки) в формате <первые три буквы месяца> <число> <год>
__TIME__
возвращает время компиляции по системному времени в виде символьного массива (строки) в формате ЧЧ:ММ:СС
Serial.println(__DATE__); // Feb 27 2020 Serial.println(__TIME__); // 14:32:18
Работать напрямую с этим макросом очень неудобно, это ведь просто набор символов. У меня есть библиотека buildTime, которая позволяет получать отдельно каждый параметр (день, месяц, год, часы, минуты, секунды). Скачать/почитать можно здесь.
__FILE__ и __BASE_FILE__
__FILE__ и __BASE_FILE__
возвращают полный путь к текущему файлу, опять же как строку. Являются аналогами друг друга.
Serial.println(__FILE__); // вывод C:\Users\Alex\Desktop\sketch_feb27a\sketch_feb27a.ino
__LINE__
__LINE__
возвращает номер строки в документе, в которой вызван этот макрос
__COUNTER__
__COUNTER__
возвращает значение, начиная с 0. Значение __COUNTER__ увеличивается на единицу с каждым вызовом макроса в коде.
int val = __COUNTER__; void setup() { Serial.begin(9600); Serial.println(__COUNTER__); // 1 Serial.println(val); // 0 Serial.println(__COUNTER__); // 2 } void loop() {}
__COUNTER__ можно использовать для генерации уникальных имён переменных, но об этом мы поговорим когда нибудь в другой раз.
Важные страницы
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макро, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту (alex@alexgyver.ru)
- Articles coming soon