Что такое 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 в Arduino IDE позволяет называть значения (константы), которые делают программу более понятной. Можно определить имя константы или фрагмента кода один раз в начале программы, а затем использовать только это имя в коде для Arduino Uno. Давайте рассмотрим правильные способы использования функции #define в языке программирования Arduino IDE с подробным описанием и примерами скетчей.
Необходимые компоненты:
- Arduino Uno / Arduino Nano / Arduino Mega
- светодиоды и резисторы
- макетная плата
- коннекторы
- Описание функции pinMode в Ардуино
- Что такое внешние прерывания в Ардуино
- Справочник по языку программирования Ардуино
Константы, определенные с помощью директивы #define, не занимают места в памяти, поскольку Arduino IDE подставляет значения вместо имен при компиляции скетча. Действие этой директивы можно сравнить с ‘СРАВНИТЬ’ и ‘ЗАМЕНИТЬ’. Во время компиляции Arduino IDE находит в программе фрагмент кода который нужно изменить> и заменяет его на фрагмент кода который нужно вставить> в программу.
#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 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])