Инструкция define в ардуино, как и в языке C++, нужна для того, чтобы упростить написание скетчей. Мы можем один раз определить название какого-то фрагмента кода, а затем везде использовать только это название. В этой статье мы на конкретных примерах разберемся с такими вопросами, как правильно использовать #define, что такое препроцессор, в каких случаях надо использовать define, а в каких – лучше const.
Содержание
Синтаксис define ардуино
Синтаксис использования инструкции достаточно прост:
#define <что меняем> <на что меняем>
- Знак # означает начало инструкции препроцессора.
- define – название инструкции.
- <что меняем> – имя макроса: словосочетание, которое будет находить препроцессор
- <на что меняем> – тело макроса: строка, которая будет подставлена в то место, где будет найдено <что меняем>
Обратите внимание, что в конце строки не нужно ставить знак точки с запятой.
Примеры использования define:
- #define PIN_LED 13
- #define RED 1
- #define BUTTON_LEFT 5
- #define test Serial.println(“test”)
- #define test(a) Serial.println(a)
Большой интерес вызывает последний пример. Мы можем попросить ардуино подставить во фрагмент кода тот аргумент, который мы указали в качестве аргумента для параметра <что меняем>. Мы поговорим об этом в статье ниже.
Тело макроса должно заканчиваться в той же строке. Но если мы хотим сделать многострочный блок, то добавляем символ “/” в конце. Например:
#define LONG_STRING "Очень длинный текст \ Который мы смогли разбить на две части"
Описание define
#define является одной из инструкций препроцессора ардуино и C++. Само слово препроцессор означает, что работа с такими инструкциями проходит до основного процесса компиляции кода. Во время такого «нулевого» этапа препроцессор компилятора пробегает по исходному коду, находит все наши инструкции и производит замену прямо в том же исходном коде. Все это делается незаметно от нас, мы результатов этого отдельного шага не видим. И только после работы препроцессора запускается сам компилятор, который будет анализировать и собирать код с учетом тех изменений, который уже сделал препроцессор.
В чем-то это напоминает механизм макросов и шаблонов. Расставив определенные с помощью #define шаблоны по всему коду, мы даем возможность компилятору автоматически заменить их перед компиляцией на то, что нам нужно. Единственное исключение – если идентификатор находится внутри скобок “”. Тогда подстановки не будет.
Все это нужно для того, чтобы во время создания программы тратить меньше времени на написание команд и их изменение. Мы можем «закодировать» коротким словом большую «длинную» конструкцию и писать в коде ее («короткую»), а не длинную. А компилятор сам подставит «длинную» в нужные места перед компиляцией. Также мы можем один раз изменить значение подстановки в начале программы, и новое значение само подставится во всех нужных местах. Возможностей использования #define – много. Как это работает – посмотрим на примерах ниже.
Примеры define в arduino
define pin
Самое частое использование define в Arduino – это определение констант для номеров пинов. С помощью инструкции define можем «дать название» какому то числу, и потом везде в коде использовать именно это название. Вот самый простой фрагмент примера со светодиодами:
// Arduino define pin #define PIN_LED 13 // Мы сказали Arduino, что будем использовать слово PIN_LED, но на самом деле это цифра 13 void setup(){ pinMode(PIN_LED, OUTPUT); } void loop(){ digitalWrite(PIN_LED,HIGH); digitalWrite(PIN_LED, LOW); // Вопрос внимательным читателям – что в этой функции маячка не так? J }
В данном случае мы определили новое слово PIN_LED и можем его использовать повсюду в коде. Перед тем, как компилировать программу ардуино пробежится по коду и везде, где встретит словосочетание PIN_LED, заменит его на цифру 13. Т.е. в конечном итоге команде pinMode все равно придет в параметрах номер пина 13. И функция digitalWrite тоже получит 13. Но при этом мы явно в коде цифру 13 не использовали. А это очень хорошо: если нам нужно будет переключить светодиод на другой порт (например, 12), нам не придется бегать в ужасе по коду и менять цифры.
В итоге мы просто поменяем цифру один раз (в блоке #define ) и теперь уже во все функции вместо PIN_LED будет передаваться цифра 12. Представьте, если у вас скетч длиной 1000 строк и в 50 разных местах вы обращаетесь к пину 13, а нужно поменять на 12. С помощью #define вы вместе с компилятором аруино сделаете все за 1 секунду. А вручную, да без ошибок, вам придется провозиться гораздо дольше.
define константы
С помощью #define мы можем определять «псевдо константы». Они будут работать как обычные константы, но самих переменных создаваться при этом не будет. Например, создав макрос BUTTON_UP со значением 1, мы можем использовать его в коде как константу. Например, как только мы определили, что была нажата кнопка вверх, мы можем запомнить код нажатой кнопки и передать его другим функциям, которые будут сравнивать этот код и делать какие-то действия. Сам код кнопки мы можем придумать любым: хотим – 1, хотим – 345 (такие цифры часто называют «магическими», т.к. их придумывают сами программисты и не всегда понятно, почему придумали именно так), главное, чтобы этот код оставался неизменным в тексте программы. Лучшим решением будет закодировать этот код в константе (например, BUTTON_UP) и использовать вместо цифры именно название константы.
Вложенные define
Вы вполне можете использовать в блоке инструкции define другие макросы. Например:
#define pin 13
#define delay delay(pin)
Во второе определение вместо pin подставится цифра 13 и в итоге мы получим:
#define delay delay(13)
Ошибки define
Главное при работе define – это выбрать такое название макроса, которое не совпадает с другими конструкциями языка. Потому что ардуино найдет все совпадающие фрагменты и заменит тем, что мы определил в define. Компилятору все равно, какие конструкции он заменяет, поэтому он с легкостью выполнит подстановку любого кода. Результат будет непредсказуемым.
Например, определив
#define setup 12345
,вы заставите ардуино заменить команду setup и безобидная функция превратится в
void 12345(){}.
В таком случае, когда ардуино приступит к компиляции, он уже не найдет обязательную функцию setup и очень рассердится. К тому же, он обнаружит соврешенно странную строку 12345, нарушающe правила наименование переменных и функций.
Вы также можете создать трудноуловимую проблему, просто нечаянно добавив символ точки с запятой в конец строки:
#define pin 13;
//…
digitalWrite(pin, HIGH);
После подстановки эта конструкция превратится в digitalWrite(13;, HIGH). Т.е. внутри блока аргументов появится точка с запятой – а это, без сомнения, нарушение синтаксиса.
Еще одной серьезной ошибкой может стать несоответствие типов, которое вы не сможете сразу же распознать. Например, если вы определите
#define PIN_LED pin12
То при использовании в функции
digitalWrite(PIN_LED, HIGH);
возникнет ошибка, т.к. на вход вы подадите не цифру, а строку «pin12». Компилятор подсветит вам строку с ошибкой, но вы в ней не увидите текста «pin12», а всю ту же строку digitalWrite(PIN_LED, HIGH); . В результате вы можете надолго зависнуть, разбираясь, что, откуда и куда подставилось. Поэтому при работе с #define нужно быть очень аккуратным и без надобности не использовать эту инструкцию.
#Ifdef, #ifndef и #endif – дополнительные команды препроцессора
Иногда бывает полезным знать, объявили ли мы уже в #define какую-либо инструкцию ранее. Это может быть полезным, если у нас много файлов в проекте и мы не всегда понимаем, в какой последовательности в итоге будут подключаться наши модули. Для всего этого мы и используем специальные инструкции #ifdef или #ifndef, которые проверят, было ли встречено данное определение ранее и, если было (или не было – для второго варианта), то оставит блок кода с последующей строки и до места встречи #endif.
Описание синтаксиса:
#ifdef имя_макроса
последовательность команд, которые будут оставлены в коде, только если данный макрос был определен ранее инструкцией #define. В противном случае данный участок кода будет исключен
#endif
#ifndef имя_макроса
последовательность команд, которые будут оставлены в коде, только если до данного момента макрос не был определен
#endif
Пример для #ifdef
Мы проверяем, определили ли ранее в #define признак отладки, и если да, то код будет выполнен, если нет, то ничего выводиться не будет.
#define DEBUG 1 // Если закоментировать всю эту строку, то отладочные сообщения будут отключены
#ifdef DEBUG Serial.println («Test messge»); #endf
Пример для ifndef. Мы объявляем константу, только если не делали этого ранее.
#ifndef PIN_LED #define PIN_LED 13 #endif
Все достаточно понятно и похоже на обычные конструкции
define как альтернатива функциям
Сейчас мы рассмотрим более сложный вариант использования define – мы попробуем передавать внутрь конструкции define какие-то параметры. Но сначала давайте разберемся, зачем это нужно.
Допустим, мы захотели упростить чрезмерно сложную для начального понимания функцию digitalWrite. Исходя , мы можем определить такую инструкцию и использовать ее в коде:
#define on digitalWrite(13, HIGH)
…
void loop(){
on;
}
В этом примере мы достаточно длинную команду digitalWrite закодировали одним коротким словом on. Теперь везде в коде, где встретится on, компилятор (препроцессор компилятора) вставит строку digitalWrite(13, HIGH). И в конечном итоге функция loop будет выглядеть так:
void loop(){
digitalWrite(13, HIGH);
}
Все будет замечательно до того момента, когда мы захотим добавить второй светодиод и помигать им. Использовать инструкцию on уже не получится, т.к. на ее месте появится вызов для того же светодиода на 13 порту. И у нас есть два выхода:
- Написать еще один макрос, например, #define on12 digitalWrite(12, HIGH)
- Воспользоваться замечательным механизмов подстановки параметров в препроцессорные инструкции.
Первый вариант – это путь в никуда. Потому что нам опять придется делать однообразную работу по копированию одного и того же текста. А вот второй вариант мы сейчас рассмотрим.
В препроцессоре С++ (и, следовательно, ардуино), можно определить параметры, которые препроцессор будет использовать при замене одного фрагмента на другой. Можно указать, что в первом случае on должен вставить digitalWrite для 13 пина, а во втором – для 12 и т.д. Для этого нужно сделать следующее:
- Определить define следующим образом:
#define on(pin) digitalWrite(pin, HIGH)
- Использовать в коде такой вариант:
on(13);
В инструкции #define мы ввели параметр, который сами назвали a(можно выбрать любое имя) и прописали его явно с помощью скобок: on(a). Во второй части инструкции мы просто используем название параметра в нужном месте: digitalWrite (pin, HIGH).
Теперь, встретив конструкцию on(13), препроцессор вставит слово digitalWrite (pin, HIGH), а вместо pin добавит то, что мы передали в скобках: цифру 13. В итоге в код вставится строка digitalWrite (13, HIGH).
Указав on(12), мы заставим вставить в код функцию digitalWrite (12, HIGH). И так далее, в зависимости от наших нужд.
Таким же образом мы можем сделать макрофункцию (псевдофункцию) off(pin), которая будет выполнять подстановку digitalWrite (pin, LOW).
В итоге код маячка со светодиодами может стать до чрезвычайности простым:
void loop() { on(13); delay(1000); off(13); delay(1000); }
Можно сократить еще сильнее, потому что на вход макрофункции можно передавать несколько параметров:
#define out(pin) pinMode(pin, OUTPUT) #define on(pin, d) digitalWrite(pin, HIGH); delay(d) #define off(pin, d) digitalWrite(pin, LOW); delay(d) void setup() { out(13); out(12); } void loop() { on(13, 1000); off(13, 500); }
Достаточно интересно, не правда ли? Также можно придумать очень много разных упрощающих инструкций. Но давайте немного остановимся и поговорим об ограничениях.
Все дело в том, что создавая такую «библиотеку» из собственных инструкций мы очень сильно рискуем:
- Во-первых, мы запутываем код, потому что on(13) выглядит как функция, но она не является функцией! Она просто подставляет в код текст, ничего не вызывая, не помещая переменные в стек, не сохраняя их между вызовами! И в более сложных примерах мы можем создавать трудноуловимые и опасные ошибки, которые могут возникнуть случайно на определенных наборах.
- Во-вторых, мы можем случайно переопределить уже существующие инструкции и потом долго мучиться или от ошибок компиляции или, что еще опаснее, ошибок времени исполнения. В вашем проекте могут быть десятки различных скетчей и подключаемых библиотек. Что-то незаметно поменять в этой сложной каше из подключаемого кода– проще простого.
- Мы не проверяем параметры на входе, что тоже может привести к проблемам на этапе компиляции или же нарушению приоритетов выполнения операций на этапе исполнения.
Ради справедливости скажем и о плюсах:
- Созданные с помощью #define псевдофункции будут выполняться быстрее, т.к. обрабатываются на этапе компиляции и не приводят к реальным вызовам функций, что уменьшает затраты на такие затратные операции как работа со стеком. Если данный фрагмент кода используется часто, это может быть важным преимуществом.
- Макрофункции быстрее пишутся: при их создании не нужно указывать типы аргументов и инструкцию возврата результата (return).
- Созданные функции можно включать/отключать или переопределять как и любые другие макроподстановки.
В общем случае совет такой: используйте конструкцию #define для псевдофункций только в том случае, если структура вашего проекта проста, сама псевдофункция умещается в одну строчку, вызывается часто, вы не используете вложенные вызовы макросов и функций и всегда можете визуально оценить, какие изменения в коде вызовут подстановки.
Define или const
Альтернативой #define является использование констант. Константа в arduino и C++ – это переменная, которая определяется c модификатором const. Пример:
сonst int PIN_LED = 10;
То же самое можно сделать с помощью define:
#define PIN_LED 10
Результат одинаковый – в коде вы просто используете конструкцию типа digitalRead(PIN_LED) и получите желаемое: в нужное место вставится цифра 10.
Но использование константы все-таки предпочтительней по следующим причинам:
- Мы не сможем непреднамеренно разрушить код программы, т.к. константы, в отличие от макросов, не вставляются во все подряд вхождения слова.
- Компилятор сообщит об ошибке, если мы пытаемся использовать константу не того типа/
- На константы действуют общие правила области видимости переменных.
Если оценивать объем памяти необходимой для скетча с использованием #define или const, то никаких преимуществ ни тот, ни другой способ не дают – компилятор выделит одинаковый объем памяти для переменной, объявленной явно или встроенной в виде макроса. Исходя из всего этого, старайтесь при использовании констант в коде все-таки брать за основу const.
Вот это я понимаю объяснение!) Спасибо. Гляну, что еще тут в таком ключе есть!
Если я правильно понял, всё что следует за #defune, вставляется препроцессором в текст кода как простой его “кусок”. Если так, то это очень удобно. Например у меня есть участи кода где нужно вставлять одно и то же значение без привязки к типу. Пример для USART:
modem = SoftwareSerial(RX, TX);
modem.begin(57600); // здесь тип long
modem.println(“AT+IPR=57600”); // здесь тип String
Поддерживаю!