Arduino define и const

Инструкция 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.

ПОДЕЛИТЬСЯ

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here