Директивы препроцессора
Препроцессор
Процесс компиляции прошивки очень непростой и проходит в несколько этапов, один из первых - работа препроцессора. Препроцессору можно давать команды, которые он выполнит перед компиляцией кода прошивки: это может быть подключение файлов, замена текста, условные конструкции и некоторые другие вещи. Также у препроцессора есть макросы, которые позволяют добавлять в код некоторые интересные вещи.
#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
Если DEBUG
задефайнен, то DEBUG_PRINT
- это макро-функция, которая выводит значение в порт. А если не задефайнен - все вызовы DEBUG_PRINT
просто убираются из кода и экономят память!
Также есть директива #undef
, которая отменяет #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, чтобы никто другой случайно не пролез со своими дефайнами. В то же время, своя библиотека может начать конфликтовать с другими библиотеками.
Какой тут выход? Очень простой! Делать имена дефайнов максимально уникальными: если это библиотека - оставлять префикс библиотеки (например библиотека FastBot, префиксы дефайнов FB_MY_CONST), а если это скетч - делать префикс с именем скетча. Также можно отказаться от define в пользу констант или enum, enum кстати удобнее define в плане создания набора констант, а места занимает совсем немного!
#if - условная компиляция
Условная компиляция является весьма мощным инструментом, при помощи которого можно вмешиваться в компиляцию кода и делать его очень универсальным как для пользователя, так и для железа. Рассмотрим директивы условной компиляции:
#if
- аналогif
в логической конструкции#elif
- аналогelse if
в логической конструкции#else
- аналогelse
в логической конструкции#endif
- директива, завершающая условную конструкцию#ifdef
- если "определено"#ifndef
- если "не определено"defined
- данный оператор возвращаетtrue
если указанное слово "определено" через#define
, иfalse
- если нет. Используется для конструкций условной компиляции.
Пример:
#define TEST 1 // определяем TEST как 1 #if (TEST == 1) // если TEST 1 #define VALUE 10 // определить VALUE как 10 #elif (TEST == 0) // TEST 0 #define VALUE 20 // определить VALUE как 20 #else // если нет #define VALUE 30 // определить VALUE как 30 #endif // конец условия
Таким образом мы получили задефайненную константу VALUE
, которая зависит от "настройки" TEST
.
При помощи условной компиляции можно буквально включать и выключать целые части кода из компиляции, то есть из финальной версии программы, которая будет загружена в микроконтроллер. Рассмотрим несколько конструкций для примера:
Сообщения от компилятора
Для вывода сообщения можно использовать директиву #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
позволяет более рационально распределять структуры в памяти. Тема сложная, читайте на Хабре.
Операторы
#
Оператор #
превращает следующее за ним слово в строку, т.е. оборачивает в дойные кавычки. Например:
#define MAKE_STR(x) #x MAKE_STR(text); // равносильно записи "text"
##
Оператор ##
"склеивает" переданные названия в одно:
#define CONCAT(x,y) x##y int CONCAT(my, val); // равносильно записи int myval;
Макросы
Помимо простой замены текста программы #define
может использоваться для создания макро-функций, об этом я писал в уроке про функции.
Константы
У препроцессора есть несколько интересных макросов, которыми можно пользоваться в своём коде. Рассмотрим некоторые полезные из них, которые работают на 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__
можно использовать для генерации уникальных имён переменных, например:
#define _CONCAT(a, b) a##b #define CONCAT_INNER(a, b) _CONCAT(a, b) #define MAKE_VAR(name) CONCAT_INNER(name, __COUNTER__) int MAKE_VAR(var) = 123; // создана переменная var20 int MAKE_VAR(var) = 456; // создана переменная var21
Полезные страницы
- Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
- Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
- Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
- Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
- Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
- Поддержать автора за работу над уроками
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])