Maker.io main logo

How To Use Arduino IDE Compiler Macros

2025-11-12 | By Maker.io Staff

Arduino

Image of How To Use Arduino IDE Compiler Macros

Compiler macros, which are commonly used to define constant values or support debugging, are vital for writing code that targets different platforms. Still, even though macros are powerful, they are best used with care. Read on to learn how they work, where they can benefit your code, and when they should be avoided.

What are Compiler Macros?

C++ compiler macros are special instructions known as preprocessor directives. They come from a variety of sources but are generally categorized into predefined and user-defined groups. Predefined macros are supplied by the language, compiler, or build system, while user-defined macros come from libraries or custom code.

Many makers are familiar with the #define keyword, a macro that’s commonly used to set static values. However, macros can also control conditional blocks, allowing code to be excluded from compilation. Predefined macros enable the detection of the platform or CPU, allowing programmers to target features specific to certain boards. They can even provide useful constants for debugging or determining the size and maximum value of particular data types.

The preprocessor replaces macros before compilation. For static values, it substitutes every occurrence of a macro with its assigned value, even if the replacement results in incorrect code. When converting conditional macros, the preprocessor removes all blocks that evaluate to false, leaving the compiler to translate only the parts that evaluate to true.

Common Pitfalls When Using Compiler Macros

When the preprocessor encounters a macro, it performs simple text substitution and conditional removal, without checking the validity of the resulting code before passing it to the compiler. This can bypass type safety when replacing macros with static values, potentially causing compilation errors or unexpected behavior difficult to trace.

Conditional macros can introduce similar surprises. Code that compiles and runs correctly on one board when using the Arduino IDE might fail or behave unexpectedly on a similar board when built with a different IDE or core, purely because the set of predefined macros varies.

Leverage Built-In Macros For Debugging

One of the main uses of compiler macros is debugging. Programmers can use standard macros, such as __FILE__, __DATE__, __TIME__, and __FUNCTION__, to gather information about the execution environment and build process. Arduino-specific macros, such as ARDUINO, provide additional details about the toolchain and target platform. Including this information helps track and reproduce issues after release, making it easier to identify which builds contain problems:

Copy Code
void printVersionInfo(void) {
  Serial.print("Compiled on ");
  Serial.print(__DATE__);
  Serial.print(" at ");
  Serial.print(__TIME__);
  Serial.print(" with IDE version ");
  Serial.println(ARDUINO);
}

It’s vital to remember that the preprocessor performs simple text substitution operations, and it replaces macros with their values at compile time in a process called expansion. These values are static, so __TIME__, for example, expands to the time at compilation and remains unchanged until the code is recompiled.

These macros also let programmers add details like function names and the line number to debug logs, making them more informative:

Copy Code
void printDebugLogLine(const char* function, int line, const char* message) {
  char buffer[256];
  snprintf(buffer, sizeof(buffer), "[%s:%d in %s] %s",
    __FILE__, line, function, message
  );
  Serial.println(buffer);
}

Similar to the date and time, the expanded values are static and depend on their location in the code. In this example, it means that the calling function must pass along the function name and line number to ensure that they show up correctly in the output messages:

Copy Code
void setup() {
  Serial.begin(9600);
  printVersionInfo();
  printDebugLogLine(__FUNCTION__, __LINE__, "Ready!");
}

void loop() {
  printDebugLogLine(__FUNCTION__, __LINE__, "Iteration started!");
  /* Do some meaningful work */
  printDebugLogLine(__FUNCTION__, __LINE__, "Done!");
}

Running the example produces helpful debug information and log messages:

Image of How To Use Arduino IDE Compiler Macros Compiler macros can help build more descriptive debug log messages.

Defining Functions With Macros

In the previous example, each call to the printDebugLogLine had to include the current function name and line number as parameters. However, each call utilized the __FUNCTION__ and __LINE__ macros, which expand to static values. Therefore, it’s possible to simplify the code further by adding a macro that defines a wrapper function as follows:

Copy Code
#define DEBUG_LOG(msg) printDebugLogLine(__func__, __LINE__, msg)

Because DEBUG_LOG is a macro, the preprocessor replaces each occurrence with a call to printDebugLogLine, automatically inserting the current function and line number. This simplifies calls throughout loop and setup without changing the behavior:

Copy Code
void setup() {
  Serial.begin(9600);
  printVersionInfo();
  DEBUG_LOG("Ready!");
}

void loop() {
  DEBUG_LOG("Iteration started!");
  /* Do some meaningful work */
  DEBUG_LOG("Done!");
}

Conditional Compilation With Macros

Removing debug code and unnecessary log messages before release is a good practice because it produces smaller executables and improves security. At the same time, debug messages are valuable during development, and deleting them manually is tedious. You can address these issues with macros for conditional compilation, for example, by adding a flag that determines whether the preprocessor should remove or keep specific code segments:

Copy Code
 #define LOG_LEVEL 1 // 0 = OFF; 1 = DEBUG; 2 = PRODUCTION

Conditional macros can then check the presence, absence, or value of a macro. For example, programmers can determine whether a macro is present or absent using the following syntax:

Copy Code
#ifdef MACRO_NAME
// Run code if MACRO_NAME exists
#else
// Run code otherwise
#endif

#ifndef MACRO_NAME
// Run code if MACRO_NAME does NOT exist
#endif

Likewise, programmers can employ the #if, #elif, and #else macros to check the value of a macro, for example, to toggle which debug messages show up during program execution:

Copy Code
void printVersionInfo(void) {
  #if LOG_LEVEL > 0
    Serial.print("Compiled on ");
    Serial.print(__DATE__);
    Serial.print(" at ");
    Serial.print(__TIME__);
    Serial.print(" with IDE version ");
    Serial.println(ARDUINO);
  #endif
}

void printDebugLogLine(const char* function, int line, const char* message) {
  #if LOG_LEVEL == 1
    // Full log messages in DEBUG mode
    char buffer[256];
    snprintf(buffer, sizeof(buffer), "[%s:%d in %s] %s",
      __FILE__, line, function, message
    );
    Serial.println(buffer);
  #elif LOG_LEVEL == 2
    // Minimal output in PRODUCTION mode
    Serial.println(message);
  #endif
}

The preprocessor removes all contents from the printVersionInfo function if the LOG_LEVEL flag is set to zero or a negative number, thus omitting the debug startup message with the compilation date and time. Similarly, it removes the file name, function, and line number from the log messages if the log level is not equal to one.

When LOG_LEVEL is undefined or zero, the preprocessor leaves the two functions empty, which means that the compiler subsequently removes them to optimize the code, effectively deleting them from the binary, reducing its size and enhancing security.

Using Macros for Platform-Specific Code

Some features exist only on certain platforms or processors, and platform-specific macros allow developers to target them without duplicating code. For example, ESP32 boards have a 12-bit ADC, while the ATmega328 on the Arduino Uno has a 10-bit ADC. The voltage references and analog pin numbers also differ, so developers aiming to support both platforms must adjust these parameters to maintain compatibility. One way to accomplish this is by checking whether platform-specific macros, like ARDUINO_ARCH_ESP32 or ARDUINO_AVR_UNO, are set, and to adjust parameters as needed:

Copy Code
#if defined(ARDUINO_ARCH_ESP32) || defined(ESP32)
  #define ADC_VOLTAGE 3.3
  #define ADC_RESOLUTION 4096
  #define ANALOG_PIN 34
  #define BOARD_NAME "ESP32"
#else
  #define ADC_VOLTAGE 5.0
  #define ADC_RESOLUTION 1024
  #define ANALOG_PIN A0
  // You can also nest conditional macros!
  #ifdef ARDUINO_AVR_UNO
    #define BOARD_NAME "Arduino Uno"
  #else
    #define BOARD_NAME "Unknown"
  #endif
#endif

void setup() {
  Serial.begin(9600);
  Serial.print("Detected ");
  Serial.println(BOARD_NAME);
  
  pinMode(ANALOG_PIN, INPUT);
}

void loop() {
  float raw = analogRead(ANALOG_PIN);
  float voltage = raw * (ADC_VOLTAGE / ADC_RESOLUTION);
}

On the Arduino Uno, the preprocessor removes all ESP32-related values before compilation and then replaces all macro occurrences with their corresponding static values, resulting in the following code:

Copy Code
void setup() {
  Serial.begin(9600);
  Serial.print("Detected ");
  Serial.println("Arduino Uno");
  
  pinMode(A0, INPUT);
}

void loop() {
  float raw = analogRead(A0);
  float voltage = raw * (5.0 / 1024);
}

F_CPU is another commonly used macro that expands to the nominal CPU frequency in Hertz, allowing developers to adjust time-sensitive calculations to accommodate different architectures and boards.

Board-specific macros help detect the presence of hardware features. For example, development boards based on the ATmega32u4 (Arduino Micro or Leonardo) or SAMD21 (MKR and Zero platforms) have native USB support. Developers can check for the presence of the USB_VID and USB_PID macros to determine the vendor and product IDs, and, thus, whether native USB support is available. Note that these macros are not universal; their exact name and values depend on the target board’s core definition files.

Arduino-Specific Compiler Macros for Memory Management

The F()-macro stores long strings in the MCU’s flash memory instead of SRAM. This is especially useful on devices like the Arduino Uno, which has only 2048 bytes of SRAM, or just enough to hold a single string of 2047 characters plus the null terminator. Therefore, even a handful of shorter strings can quickly fill the entire SRAM, leaving little room for variables or buffers.

F() is best used on constant strings, such as those used in static output messages. Programmers need only wrap the string to store in flash in parentheses and prepend a capital F. For example:

Copy Code
Serial.println(F("Initializing WiFi module..."));
if (connection_success()) {
  Serial.println(F("Connection established!"));
  Serial.println(F("Failed to read sensor data."));
}

Similar to F(), PROGMEM is a macro that stores constant data in the MCU’s flash memory instead of SRAM. It is more versatile than F because it supports arbitrary values, not just strings. However, the values must be constants, and they are baked into the code and stored alongside the instructions in flash. Therefore, PROGMEM is best used for large lookup tables of static values that are costly to compute at runtime, such as logarithms, trigonometric functions, or square roots.

To store an array of constant values in flash, you need only add the PROGMEM keyword after the array’s name:

Copy Code
#include <avr/pgmspace.h>

// Square roots between 1 and 10
const float sqrtTable[] PROGMEM = {1.0, 1.41, 1.73, 2.0, 2.24, 2.45, 2.65, 2.83, 3.0, 3.16};

To retrieve a value, use pgm_read_xxx where xxx corresponds to the target data type, for example:

Copy Code
float val = pgm_read_float(&sqrtTable[i]);

Summary

Compiler macros are a powerful tool in C++ that lets makers define static values, control which blocks of code compile, and adapt programs for different architectures. Predefined macros like __FILE__, __LINE__, and __FUNCTION__ give insight into where and when code runs, which is useful for debugging.

User-defined macros can be used to define custom constants and simplify repetitive tasks, like logging messages with a single call. However, remember that macros work through simple text substitution before compilation, so they bypass type checking. Therefore, they are best used sparingly.

Platform-specific macros allow targeting features unique to certain boards: for example, the ESP32 and Arduino Uno have different ADC resolutions and voltage references. Macros can ensure the right values are used automatically. Finally, the Arduino-specific macros F() and PROGMEM help manage memory on devices with limited SRAM by storing constant strings and data in flash instead.

Mfr Part # A000053
ARDUINO MICRO ATMEGA32U4 EVAL BD
Arduino
458,89 Kč
View More Details
Mfr Part # A000057
ARDUINO LEONARDO W/ HDRS ATMEGA3
Arduino
500,99 Kč
View More Details
Mfr Part # ABX00012
ARDUINO MKR ZERO W/ HDR ATSAMD21
Arduino
656,76 Kč
View More Details
Mfr Part # ABX00003
ARDUINO ZERO ATSAMD21G18 EVAL BD
Arduino
1 072,29 Kč
View More Details
Mfr Part # A000066
ARDUINO UNO R3 ATMEGA328P BOARD
Arduino
580,98 Kč
View More Details
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.