Что такое define в ардуино
Перейти к содержимому

Что такое define в ардуино

  • автор:

Что такое define в ардуино

Директива #define

#define это удобная директива, который позволяет дать имя константе перед тем как программа будет скомпилирована. Определенные этой директивой константы не занимают программной памяти, поскольку компилятор заменяет все обращения к ним их значениями на этапе компиляции, соответственно они служат исключительно для удобства программиста и улучшения читаемости текста программы.

Стоит упомянуть о некотором нежелательном эффекте, который может иметь место при использовании директивы #define. Например, если имя константы, заданное с помощью директивы #define включить в имя другой константы или переменной, то оно будет заменено на свое значение.

В общем случае рекомендуется использовать выражение const для определения констант вместо #define

Синтаксис для Arduino такой же как и для C:

Синтаксис:

#define constantName value

Внимание! Символ # перед словом define обязателен.

Пример

#define ledPin 3 // компилятор заменит любое упоминание ledPin на занчение 3 во время компиляции

Замечание по использованию

Обратите внимание, что точка с запятой не ставится, иначе компилятор выдаст критическую ошибку.

#define ledPin 3; // это ошибка, ; здесь не нужна

Точно так же знак равно после имени константы тоже вызовет критическую ошибку компилятора.

#define ledPin = 3 // это тоже ошибка, знак = не нужен

Функция define в Ардуино описание, как работает

Функция define в Ардуино описание

Инструкция #define в Arduino IDE позволяет называть значения (константы), которые делают программу более понятной. Можно определить имя константы или фрагмента кода один раз в начале программы, а затем использовать только это имя в коде для Arduino Uno. Давайте рассмотрим правильные способы использования функции #define в языке программирования Arduino IDE с подробным описанием и примерами скетчей.

Необходимые компоненты:

  • Arduino Uno / Arduino Nano / Arduino Mega
  • светодиоды и резисторы
  • макетная плата
  • коннекторы
  1. Описание функции pinMode в Ардуино
  2. Что такое внешние прерывания в Ардуино
  3. Справочник по языку программирования Ардуино

Константы, определенные с помощью директивы #define, не занимают места в памяти, поскольку Arduino IDE подставляет значения вместо имен при компиляции скетча. Действие этой директивы можно сравнить с ‘СРАВНИТЬ’ и ‘ЗАМЕНИТЬ’. Во время компиляции Arduino IDE находит в программе фрагмент кода который нужно изменить> и заменяет его на фрагмент кода который нужно вставить> в программу.

#define в Ардуино что это значит, описание

Функция define в Ардуино описание

Вот пример программы с мигающими светодиодами. С помощью директивы #define мы дали имена портам 13 и 12, к которым подключены светодиоды. В программе удобнее использовать имена, а не цифры, чтобы не вспоминать каждый раз, какой цвет к какому выводу подключен. Утилита Arduino IDE автоматически заменит названия RED и BLU на соответствующие числовые значения во время компиляции программы.

#define RED 13 #define BLU 12 void setup() < pinMode(RED, OUTPUT); pinMode(BLU, OUTPUT); >void loop()

Функции #ifdef, #ifndef и #endif в Arduino IDE

#ifdef Arduino IDE проверяет, встречалось ли это определение ранее в программе; если да, то блок кода размещается со следующей строки по #endif. В следующем небольшом примере проверяется, был ли знак отладки ранее определен в #define, если да, то код будет выполнен (вывод сообщения на монитор порта — serial monitor Arduino IDE), если знак не определен, то сообщение не будет выведено на монитор порта.

#ifdef A PROPOS DE Serial.println ("Message"); #endif

#ifndef Arduino IDE проверит, встречалось ли уже это определение в программе, и, если нет, поместит блок кода из следующей строки в #endif. В следующем простом примере программы мы объявляем новую константу, если только мы уже не объявляли константу ранее в скетче. Если определение с таким именем уже использовалось, то программа будет игнорировать строки внутри конструкции #ifndef … #endif.

#ifndef RED #define RED 13 #endif

Замена функций с помощью define в Arduino IDE

Мигалка на Ардуино: мигание двумя светодиодами

Кроме использования define в программе для объявления констант, можно заменять целые фрагменты кода с помощью директивы #define. Это более сложный, но интересный вариант использования команды define в Ардуино, который позволяет создать много разных упрощающих инструкций в скетче. Например, мы можем в первом примере заменить функцию pinMode() на конструкцию с дефайн с заданными параметрами.

#define out(pin) pinMode(pin, OUTPUT) #define on(pin, del) digitalWrite(pin, HIGH); delay(del) #define off(pin, del) digitalWrite(pin, LOW); delay(del) void setup() < out(13); out(12); >void loop()

Обратите внимание, что on(13, 500) и другие строчки не являются функциями, конструкция просто подставляет в код нужный фрагмент кода. В более сложных программах есть риск создать самому ошибки, так как в скетче могут быть десятки подключаемых библиотек, где инструкция дефайн может что-то незаметно для вас поменять. При этом будут возникать ошибки компиляции или ошибки во время исполнения программы.

Arduino #define или const, что лучше использовать

Иногда бывает не удобно применять директиву #define для создания констант, в этом случае используют ключевое слово const. В отличие от глобальных переменных, значение const должно быть определено сразу при объявлении константы. Помните, что при использовании #define имена следует делать уникальными, чтобы не было совпадений с командами или функциями, которые используются в подключаемых библиотеках.

const int RED = 13; const int BLU = 12; void setup() < pinMode(RED, OUTPUT); pinMode(BLU, OUTPUT); >void loop()

Заключение. Если использовать константу вместо дефайн из последнего примера, то результат будет одинаковый – в коде вместо переменной RED будет автоматически подставляться цифра 13. На константы в программе действуют общие правила области видимости глобальных и локальных переменных. Кроме того, использование #define Arduino или const не дает никаких преимуществ, с точки зрения экономии объема памяти.

Директива define

#define это удобная директива, который позволяет дать имя константе перед тем как программа будет скомпилирована. Определенные этой директивой константы не занимают программной памяти, поскольку компилятор заменяет все обращения к ним их значениями на этапе компиляции, соответственно они служат исключительно для удобства программиста и улучшения читаемости текста программы.

Стоит упомянуть о некотором нежелательном эффекте, который может иметь место при использовании директивы #define. Например, если имя константы, заданное с помощью директивы #define включить в имя другой константы или переменной, то оно будет заменено на свое значение.

В общем случае рекомендуется использовать выражение const для определения констант вместо #define

Синтаксис для Arduino такой же как и для C:

#define ledPin 3 //компилятор заменит любое упоминание ledPin на значение 3 во время компиляции 
#define constantName value

Внимание! Символ # перед словом define обязателен.
Пример

Замечание по использованию

Обратите внимание, что точка с запятой не ставится, иначе компилятор выдаст критическую ошибку.

#define ledPin 3; // это ошибка, ; здесь не нужна 

Точно так же знак равно после имени константы тоже вызовет критическую ошибку компилятора.

 #define ledPin = 3 // это тоже ошибка, знак = не нужен 
Железо

Стартовый набор с Arduino Mega и RFID

Стартовый набор с Arduino Mega и RFID Это расширенный стартовый набор. В комплект входит Arduino Mega R3, макетные платы, множество датчиков, управляемые механизмы и необходимые радиоэлектронные компоненты. Полный список.

Плата Arduino Uno R3

Плата Arduino Uno R3 Arduino Uno — плата на базе микроконтроллера ATmega328P с частотой 16 МГц. На плате есть все необходимое для удобной и быстрой работы.

Директивы препроцессора

Процесс компиляции прошивки очень непростой и проходит в несколько этапов, один из первых – работа препроцессора. Препроцессору можно давать команды, которые он выполнит перед компиляцией кода прошивки: это может быть подключение файлов, замена текста, условные конструкции и некоторые другие вещи. Также у препроцессора есть макросы, которые позволяют добавлять в код некоторые интересные вещи.

#include – подключить файл

С подключением файлов мы уже знакомы: директива #include подключает новый документ в текущий, например библиотеку. После #include нужно указать имя файла, который подключается. Указать можно в «двойных кавычках» , а можно в . В чём разница? Файл, имя которого указано в двойных кавычках, компилятор будет искать в папке с основным документом, если не найдёт – будет искать в папке с библиотеками. Если указать в скобках – будет сразу искать в папке с библиотеками, путь к которой обычно можно настроить.

#include "mylib.h" // подключить mylib.h, сначала поискать в папке со скетчем #include // подключить 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 просто убираются из кода и экономят память!

Более подробный пример

При разработке проекта важна отладка, мы делаем её средствами 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()

Также есть директива #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 .

При помощи условной компиляции можно буквально включать и выключать целые части кода из компиляции, то есть из финальной версии программы, которая будет загружена в микроконтроллер. Рассмотрим несколько конструкций для примера:

#define USE_DISPLAY 1 // настройка для пользователя #if (USE_DISPLAY == 1) #include #endif void setup() < #if (USE_DISPLAY == 1) // дисплей.инициализация #endif >void loop()

#define SENSOR_TYPE 3 // настройка для пользователя // подключение выбранной библиотеки #if (SENSOR_TYPE == 1 || SENSOR_TYPE == 2) #include #elif (SENSOR_TYPE == 3) #include #else #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 позволяет более рационально распределять структуры в памяти. Тема сложная, читайте на Хабре.

Операторы

Оператор # превращает следующее за ним слово в строку, т.е. оборачивает в дойные кавычки. Например:

#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__ можно использовать для генерации уникальных имён переменных, но об этом мы поговорим когда нибудь в другой раз.

Полезные страницы

  • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
  • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
  • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
  • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
  • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
  • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
  • Поддержать автора за работу над уроками
  • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *