Arduino has revolutionized the world of electronics thanks to the ease with which prototypes and functional projects can be created. However, for those who want to take their programming a step further and optimize resources, keep the code clean and gain efficiency, macros become a key tool.
In this article we are going to dive deep into the use of macros in Arduino: what they are, how to use them, their advantages and limitations. And we'll do so by gathering the most comprehensive and useful information from the best resources available online, rewritten in a clear and up-to-date way to be truly practical.
What are macros in Arduino?
Macros are preprocessor directives in C/C++ that allow you to replace text before the code is compiled. Instead of executing instructions like a traditional function, a macro acts by replacing parts of the source text, which has a direct impact on how the final binary code is generated.
The preprocessor It runs before the actual compilation, and is responsible for applying these substitutions. In Arduino, this allows from define constants, conditionally include files or even create small online features that save time and memory.
Basic example: a definition like #define LED_PIN 13
causes all code to be automatically replaced LED_PIN
by 13
before compiling.
This may seem trivial, but it offers a powerful way to write more flexible and maintainable code.
Advantages of using macros
Implementing macros in Arduino projects can offer a number of specific benefits:
- Improve code readability: By reusing symbolic names, it is easier to understand the purpose of each element.
- Optimize performance: By not generating function calls, macros can execute operations faster.
- Reduce RAM usage: especially useful on resource-limited boards, such as the Arduino UNO.
- Allows conditional adaptations: It is possible to compile different code fragments depending on the type of Arduino board used.
Basic Macros: Using #define
Directive #define It is the most widely used. It is used both for define constant values as if to create injected automatic functions at pre-compilation time.
Example 1: Define a pin
#define PINLED 13
void setup() {
pinMode(PINLED, OUTPUT);
}
void loop() {
digitalWrite(PINLED, HIGH);
delay(500);
digitalWrite(PINLED, LOW);
delay(500);
}
Example 2: Macro as inline function
int itemCounter = 0;
#define COUNT_ITEM() do { itemCounter++; } while(0)
void setup() {
Serial.begin(9600);
COUNT_ITEM();
COUNT_ITEM();
}
void loop() {
Serial.println(itemCounter);
}
As you can see, the use of the pattern do { … } while(0) ensures that the macro behaves safely even if used within conditional structures.
## operator and macro concatenation
The ## operator is a powerful preprocessor tool. which allows you to concatenate identifiers. This is very useful when you want to dynamically generate variable names.
Practical example:
#define GENERAR_VARIABLE(no) \
int var##no = no;
void setup() {
GENERAR_VARIABLE(3); // crea int var3 = 3
}
Important warning: This operator isn't equally compatible with all Arduino board models. For example, it may work fine on an Uno or Esplora, but fail on a Mega. Additionally, you can't nest macros within other macros using ## directly.
Macros and memory saving
One of the key advantages of using macros in Arduino is the saving RAM. Arduino has limited capacity, so loading text strings directly into RAM can become a significant problem.
An advanced technique to avoid this involves using FORCE_INLINE and load strings from program memory (PROGMEM):
#include <HardwareSerial.h>
#define MYSERIAL Serial
#define FORCE_INLINE __attribute__((always_inline)) inline
FORCE_INLINE void printFromFlash(const char *str) {
char ch = pgm_read_byte(str);
while (ch) {
MYSERIAL.write(ch);
ch = pgm_read_byte(++str);
}
}
#define SERIAL_LOG(x) (MYSERIAL.print(x))
#define SERIAL_LOGLN(x) (MYSERIAL.println(x))
Using these macros can make the difference between a project working or not, especially in applications with displays or multiple sensors.
Macros combined with functions
Macros can also facilitate dynamic function calls based on a type passed as a parameter. A clear and fairly graphic example is:
#define FUNC_LENTA(tipo) \
{ funcion_##tipo##_lenta(); }
#define FUNC_RAPIDA(tipo) \
{ funcion_##tipo##_rapida(); }
void funcion_caminar_lenta() {
Serial.println("Andando despacio");
}
void funcion_caminar_rapida() {
Serial.println("Andando rápido");
}
void setup() {
Serial.begin(9600);
FUNC_LENTA(caminar);
}
void loop() {
FUNC_RAPIDA(caminar);
}
Thanks to the ## operator and macros, we can avoid repeating structures and centralize dynamic logic..
Macros with output parameters
It is also possible to use macros to encapsulate small objects or conversions:
#define BOOL_OUT() (bool){false}
#define NUM_OUT(a,b) (float){a+b}
#define STR_OUT(msg) (String){msg}
void loop() {
Serial.println(BOOL_OUT());
Serial.println(NUM_OUT(1.2, 3.4));
Serial.println(STR_OUT("Mensaje"));
}
Good practices and precautions with macros
Using macros excessively or carelessly can lead to difficult-to-debug errors. For example, by making incorrect substitutions or defining names that collide with names in external libraries.
Some basic rules to avoid problems:
- Avoid unnecessary spaces or line breaks within the macro.
- Do not include comments within complex macros that use multiple lines.
- Use unique names or with prefixes (such as the project name) to avoid conflicts.
- Replace macros with real constants or functions whenever possible. Modern C++ allows for cleaner, safer alternatives.
On the other hand, overusing macros can reduce code clarity. The goal should be to improve efficiency and modularity without compromising maintainability.
Conditional directives and adaptive compilation
One of the most practical functionalities in scalable projects is the use of macros to conditionally generate code, something very useful when you want the same sketch to work on different boards.
Typical example:
#ifdef ARDUINO_MEGA
#define LEDPIN 53
#else
#define LEDPIN 13
#endif
It is also useful for controlling debugging or displaying compiler messages with #pragma message or even generate errors under certain conditions with #mistake.
Internal compiler macros
The GCC preprocessor for AVR (used in Arduino) includes several special macros that provide system information, very useful during development:
- __LINE__: current line number.
- __FILE__: name of the current file.
- __TIME__ and __DATE__: compilation time and date.
- __func__: name of the current function.
They allow for version control, log structures, and facilitate maintenance and error tracing without invading the main code.
Macros offer a powerful and flexible way to structure Arduino projects. They allow you to define constants, save memory, adapt the code depending on the runtime environment and create reusable blocks without duplicating lines. However, they require discipline, clarity, and knowledge to avoid subtle errors or loss of readability. When applied correctly, they are an invaluable advantage for intermediate and advanced developers.