The Direct Answer: What #if defined(ARDUINO) Actually Does
The #if defined(ARDUINO) (or #ifdef ARDUINO) directive is a C/C++ preprocessor macro used to conditionally compile code specifically for the Arduino build environment. When the Arduino IDE or PlatformIO compiles your sketch, it automatically passes the -DARDUINO flag to the GCC compiler. This allows library authors to wrap Arduino-specific headers like <Arduino.h> and functions like Serial.print() inside this block, while providing alternative native code (like ESP-IDF or mbed-OS) in the #else block.
If you are writing a library intended to run on an Arduino Uno R4, an ESP32-S3, and a Raspberry Pi Pico W without modification, this directive is your primary tool for abstracting hardware abstraction layer (HAL) differences. According to the official GCC preprocessor documentation, using #if defined(MACRO) is strictly preferred over #ifdef MACRO in modern embedded C++ because it allows for complex boolean logic (e.g., #if defined(ARDUINO) && defined(ESP32)).
Preprocessor Macro Behavior Across Core Embedded Boards
A common mistake is assuming ARDUINO behaves identically across all toolchains. The value of the macro changes depending on whether you are using the legacy Arduino IDE or PlatformIO. Below is a data-dense breakdown of how the preprocessor evaluates across modern 2026-standard development boards.
| Board Variant | Core Framework | ARDUINO Macro Value | Architecture Macro | Native Fallback Macro |
|---|---|---|---|---|
| Arduino Uno R4 Minima | Arduino Core (Renesas) | 10813 (IDE) / 1 (PIO) | ARDUINO_ARCH_RENESAS_UNO | None (Renesas FSP not standardly exposed) |
| ESP32-S3-DevKitC-1 | ESP32 Arduino Core | 10812 / 1 | ARDUINO_ARCH_ESP32 | ESP_PLATFORM |
| Raspberry Pi Pico W | RP2040 Arduino Core | 10812 / 1 | ARDUINO_ARCH_RP2040 | PICO_SDK_PATH |
| ESP32-S3 (Native) | ESP-IDF (No Arduino) | Undefined | Undefined | ESP_PLATFORM |
#if ARDUINO >= 100 in PlatformIO. Because PlatformIO often defines ARDUINO as 1 rather than the IDE version number (like 10813), your condition will evaluate to false, silently triggering the wrong code path. Always use #if defined(ARDUINO) for environment detection, and rely on architecture macros for version-specific features.
Parts List & Pin Mapping for a Cross-Platform Test Rig
To demonstrate this in practice, we will build a hardware test rig using a BME280 environmental sensor. The code must compile on all three boards without changing the source file, relying entirely on preprocessor directives to map the correct I2C pins.
Required Hardware:
- MCU 1: Arduino Uno R4 Minima (Renesas RA4M1)
- MCU 2: ESP32-S3-DevKitC-1 (N8R2 variant, 8MB Flash / 2MB PSRAM)
- MCU 3: Raspberry Pi Pico W (RP2040 with Infineon CYW43439)
- Sensor: Adafruit BME280 I2C Breakout (Product ID: 2652)
- Wiring: 24 AWG silicone jumper wires, 4.7kΩ I2C pull-up resistors (if not populated on breakout)
| Sensor Pin | Uno R4 Minima | ESP32-S3-DevKitC-1 | Raspberry Pi Pico W |
|---|---|---|---|
| VIN / VCC | 5V | 3V3 | 3V3 (Pin 36) |
| GND | GND | GND | GND (Pin 38) |
| SDA | A4 (18) | GPIO 8 | GP4 (Pin 6) |
| SCL | A5 (19) | GPIO 9 | GP5 (Pin 7) |
Note: The Espressif Arduino Core updated the default I2C pins for the ESP32-S3 from GPIO 21/22 to GPIO 8/9 in recent releases to align with native USB routing. Always verify your specific core version.
Complete Compilable Code: The Cross-Platform BME280 Wrapper
This code targets all three board variants listed above. It uses #if defined() blocks to assign the correct I2C pins and initialize the Wire library appropriately. It includes robust error handling if the sensor fails to initialize on the I2C bus.
#include <Wire.h>
#include <Adafruit_BME280.h>
// --- PIN DEFINITIONS & PREPROCESSOR ROUTING ---
#if defined(ARDUINO_ARCH_RENESAS_UNO)
// Arduino Uno R4 Minima
#define BME_SDA_PIN 18 // A4
#define BME_SCL_PIN 19 // A5
#define WIRE_INTERFACE Wire
#elif defined(ARDUINO_ARCH_ESP32)
// ESP32-S3-DevKitC-1
#define BME_SDA_PIN 8
#define BME_SCL_PIN 9
#define WIRE_INTERFACE Wire
#elif defined(ARDUINO_ARCH_RP2040)
// Raspberry Pi Pico W
#define BME_SDA_PIN 4 // GP4
#define BME_SCL_PIN 5 // GP5
#define WIRE_INTERFACE Wire1 // Using I2C1 block on Pico
#else
#error "Unsupported board architecture. Please select a valid Arduino core."
#endif
#define SEALEVELPRESSURE_HPA (1013.25)
Adafruit_BME280 bme;
void setup() {
// Standard Arduino Serial initialization
#if defined(ARDUINO)
Serial.begin(115200);
while (!Serial && millis() < 5000) { delay(10); } // Wait for USB serial
#endif
Serial.println(F("Cross-Platform BME280 Initialization..."));
// Initialize I2C with architecture-specific pins
WIRE_INTERFACE.setSDA(BME_SDA_PIN);
WIRE_INTERFACE.setSCL(BME_SCL_PIN);
WIRE_INTERFACE.begin();
// Error handling: Check if sensor is found at 0x77 or 0x76
if (!bme.begin(0x76, &WIRE_INTERFACE)) {
Serial.println(F("ERROR: Could not find a valid BME280 sensor!"));
Serial.println(F("Check I2C wiring, pull-up resistors, and address."));
while (1) {
// Blink built-in LED to indicate hardware fault
#if defined(ARDUINO)
digitalWrite(LED_BUILTIN, HIGH);
delay(250);
digitalWrite(LED_BUILTIN, LOW);
delay(250);
#endif
}
}
Serial.println(F("BME280 initialized successfully."));
}
void loop() {
#if defined(ARDUINO)
Serial.print(F("Temperature = "));
Serial.print(bme.readTemperature());
Serial.println(F(" *C"));
Serial.print(F("Pressure = "));
Serial.print(bme.readPressure() / 100.0F);
Serial.println(F(" hPa"));
delay(2000);
#endif
}
Debugging Preprocessor and Compilation Failures
When working with conditional compilation, the compiler errors can be cryptic because the code you see in your editor isn't the code the compiler actually processes. When your cross-platform build fails, these are the first three things to check:
- Verify the Active Framework: Check your
platformio.inior Arduino IDE board selection. If you selected an ESP-IDF native framework instead of the Arduino framework, theARDUINOmacro will not be defined, and the compiler will skip to your#elseblock. - Check for Stray Semicolons: Preprocessor directives do not end with semicolons. A stray semicolon will break the macro evaluation.
- Inspect Verbose Compiler Output: Enable "Verbose Output" in the Arduino IDE or run
pio run -vin PlatformIO. Look at theg++command line to verify that-DARDUINOand-DARDUINO_ARCH_ESP32are actually being passed.
Below are the most common exact error strings and their ranked causes, referencing Adafruit's BME280 integration guides and standard GCC behaviors.
Error 1: "fatal error: Arduino.h: No such file or directory"
- Cause A (Most Likely): You forgot to wrap
#include <Arduino.h>inside an#if defined(ARDUINO)block, and you are compiling in a native ESP-IDF environment where that header does not exist. - Cause B: Your IDE is set to a non-Arduino core (e.g., mbed-OS for the Pico W instead of the Earle Philhower core).
Error 2: "error: expected unqualified-id before 'if'"
- Cause A (Most Likely): You wrote
#if defined(ARDUINO);with a semicolon at the end, or you missed the hash symbol and wroteif defined(ARDUINO)outside of a function body. - Cause B: A missing
#endifearlier in the file caused the preprocessor to lose track of the conditional block boundaries.
Error 3: "undefined reference to 'Serial'"
- Cause A (Most Likely): The
ARDUINOmacro evaluated to false, triggering a native fallback path, but you leftSerial.print()calls in the native block. Native ESP-IDF usesESP_LOGI()orprintf()instead of the Arduino Serial object.
Extending and Simplifying the Build
Once you have the basic #if defined(ARDUINO) structure working, you will inevitably need to adapt the library for production environments.
How to Extend the Build:
To support native environments alongside Arduino, chain your directives using #elif. For example, if you want to support the ESP32 via pure ESP-IDF (without the Arduino core overhead), extend the logic like this:
#if defined(ARDUINO_ARCH_ESP32)
// Arduino ESP32 Core initialization
#elif defined(ESP_PLATFORM)
// Native ESP-IDF initialization (use esp_log and i2c_master driver)
#include "driver/i2c.h"
#include "esp_log.h"
#endif
How to Simplify the Build:
If you realize your project will never be deployed outside the Arduino ecosystem (e.g., you are strictly building a hobbyist shield that only targets Uno R4 and Pico W), strip out the native fallback macros entirely. Rely solely on ARDUINO_ARCH_* macros to differentiate between the Renesas and RP2040 chips. This reduces cognitive load, eliminates dead code paths, and prevents the compiler from throwing warnings about unused variables in uncompiled #else blocks.






