Macros — Complete Guide for Embedded C
- Ashok Kumar Kumawat
- Feb 22
- 6 min read
Updated: May 11
What is a Macro and How It Works at Compilation Stage
Macros are handled by the C Preprocessor, which runs before the actual compilation begins. The compilation pipeline looks like this:
Source.c → [Preprocessor] → Expanded.i → [Compiler] → object.o → [Linker] → final.elfThe preprocessor performs textual substitution — it finds every macro usage and replaces it with the defined text, literally copy-pasting the expansion into the source before the compiler ever sees it. No type checking happens at this stage. You can inspect the expanded output using:
bash
gcc -E main.c -o main.i # see what preprocessor producesDefining Macros — Basic Syntax Rules
c
#define NAME // object-like, no value (flag macro)
#define NAME value // object-like with value
#define NAME(a, b) expression // function-like macro
#undef NAME // remove a macro definitionGolden rules every embedded developer must follow:
Always wrap the entire expression in outer parentheses.
Always wrap every parameter in its own parentheses.
Never put a space between macro name and ( in function-like macros — #define FOO (x) defines an object-like macro whose value starts with (x), not a function-like macro.
Use do { ... } while(0) wrapper for multi-statement macros.
Avoid side effects in arguments — never pass i++ into a macro.
Function-Like Macros vs Real Functions
Aspect | Macro | Function |
Expansion | Inline at call site (no call overhead) | Actual function call, stack frame |
Type checking | None — pure text substitution | Full type checking by compiler |
Debugging | Hard — shows expanded line in error | Easy — stack trace with function name |
Code size | Grows with each use | Single copy in memory |
Speed | Faster (critical in ISR, tight loops) | Slightly slower due to call/return |
Recursion | Not possible | Possible |
Best use | Single-expression, performance-critical | Complex, multi-line, reusable logic |
In embedded C, macros win for register bit manipulation, hardware abstractions, and ISR-level operations where even a function call overhead is unacceptable.
Practical Macro Examples
1. Greatest of Three Numbers
c
#define MAX2(a, b) ( (a) > (b) ? (a) : (b) )
#define MAX3(a, b, c) MAX2(MAX2((a), (b)), (c))
// Usage
int x = MAX3(10, 25, 17); // expands correctly → 25
// WHY parentheses matter — without them:
// #define MAX2(a,b) a > b ? a : b
// result = 2 * MAX2(3+1, 2) → expands to: 2 * 3+1 > 2 ? 3+1 : 2 → WRONG2. Bit Manipulation (Core of Embedded C)
c
// BIT SET — force a specific bit to 1
#define BIT_SET(reg, bit) ( (reg) |= (1U << (bit)) )
// BIT RESET — force a specific bit to 0
#define BIT_RESET(reg, bit) ( (reg) &= ~(1U << (bit)) )
// BIT TOGGLE — flip a specific bit
#define BIT_TOGGLE(reg, bit) ( (reg) ^= (1U << (bit)) )
// BIT READ — read a specific bit value (returns 0 or 1)
#define BIT_READ(reg, bit) ( ((reg) >> (bit)) & 1U )
// BIT CHECK — returns nonzero if bit is set
#define BIT_CHECK(reg, bit) ( (reg) & (1U << (bit)) )
// Usage — controlling GPIO on STM32 style register
BIT_SET(GPIOA->ODR, 5); // Set PA5 high
BIT_RESET(GPIOA->ODR, 5); // Set PA5 low
BIT_TOGGLE(GPIOA->ODR, 5); // Toggle PA5
if (BIT_READ(GPIOA->IDR, 0)) { /* PA0 is high */ }3. Bit Swap (swap bit positions i and j)
c
// Swap two specific bit positions in a value
#define BIT_SWAP(val, i, j) \
({ \
__typeof__(val) _v = (val); \
int _x = ((_v >> (i)) ^ (_v >> (j))) & 1U; \
_v ^ ((_x << (i)) | (_x << (j))); \
})
// Swap all bytes (endian swap) for 32-bit value
#define SWAP32(x) ( (((x) & 0xFF000000U) >> 24) | \
(((x) & 0x00FF0000U) >> 8) | \
(((x) & 0x0000FF00U) << 8) | \
(((x) & 0x000000FFU) << 24) )
#define SWAP16(x) ( (((x) & 0xFF00U) >> 8) | (((x) & 0x00FFU) << 8) )4. Typecasting Macros
c
// Safe casting macros — very common in embedded drivers
#define TO_U8(x) ( (uint8_t)(x) )
#define TO_U16(x) ( (uint16_t)(x) )
#define TO_U32(x) ( (uint32_t)(x) )
#define TO_S32(x) ( (int32_t)(x) )
#define TO_FLOAT(x) ( (float)(x) )
// Cast a raw address to a pointer of given type
#define REG(type, addr) ( *( (volatile type *)(addr) ) )
// Usage — accessing hardware register by address directly
#define PORTA_DATA REG(uint8_t, 0x40020014)
PORTA_DATA = 0xFF; // write to hardware register5. container_of — Get Structure Base Address from Member Address
This is one of the most powerful and elegant macros in C, used heavily in Linux kernel and embedded RTOS code:
c
/*
* container_of(ptr, type, member)
* ptr = pointer to the member variable
* type = type of the containing structure
* member = name of the member inside the structure
*
* HOW IT WORKS:
* offsetof() gives the byte distance of member from struct start.
* Subtracting that from member's address gives the struct's base address.
*/
#include <stddef.h> // for offsetof()
#define container_of(ptr, type, member) \
( (type *)( (char *)(ptr) - offsetof(type, member) ) )
// offsetof is itself usually defined as:
#define OFFSETOF(type, member) ( (size_t)&(((type *)0)->member) )
// --- Example ---
typedef struct {
uint8_t id;
uint16_t voltage; // ← suppose we have a pointer to this
uint32_t current;
} SensorData_t;
SensorData_t sensor = {1, 3300, 150};
uint16_t *v_ptr = &sensor.voltage;
// Recover the base structure pointer from member pointer
SensorData_t *s_ptr = container_of(v_ptr, SensorData_t, voltage);
// s_ptr now points to sensor — same as &sensor
// Real use case: linked list nodes, callback registrations in RTOS6. File Inclusion Guard (Header Guard)
c
// Every .h file must have this — prevents double inclusion
#ifndef SENSOR_DRIVER_H // if not already defined
#define SENSOR_DRIVER_H // define it now
// all your declarations, typedefs, macros go here
#endif /* SENSOR_DRIVER_H */ // end of guardModern alternative — #pragma once (compiler extension, not standard C):
c
#pragma once // simpler but not in C standard — works on GCC, Clang, MSVCAlways prefer the #ifndef guard for maximum portability in embedded projects.
7. Variadic and Utility Macros
c
// Array size — very useful, avoids magic numbers
#define ARRAY_SIZE(arr) ( sizeof(arr) / sizeof((arr)[0]) )
// Suppress unused variable warning
#define UNUSED(x) ( (void)(x) )
// Min / Max
#define MIN(a, b) ( (a) < (b) ? (a) : (b) )
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
// Clamp a value between lo and hi
#define CLAMP(val, lo, hi) MIN(MAX((val), (lo)), (hi))
// Align a value up to next power-of-2 boundary
#define ALIGN_UP(x, align) ( ((x) + (align) - 1) & ~((align) - 1) )
// String and token pasting
#define STRINGIFY(x) #x // converts token to string literal
#define TOSTRING(x) STRINGIFY(x) // expands macro first, then stringify
#define CONCAT(a, b) a##b // pastes two tokens together
// Debug print that includes file, line, function automatically
#define DBG_PRINT(fmt, ...) \
printf("[%s:%d %s] " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__)8. Multi-Statement Macro — The do{...}while(0) Pattern
c
// WRONG — breaks if/else logic
#define INIT_WRONG(x, y) x = 0; y = 0;
// if (flag)
// INIT_WRONG(a, b); // only a=0 is guarded by if — BUG!
// else
// do_something();
// CORRECT
#define INIT(x, y) do { (x) = 0; (y) = 0; } while(0)
// Now works perfectly with if/else, no extra semicolon issues
if (flag)
INIT(a, b);
else
do_something();When to Use Macros vs When to Avoid
You MUST use macros when:
Defining hardware register addresses and bit positions — they need to be compile-time constants
Header include guards — no alternative exists
Conditional compilation (#ifdef DEBUG, #ifdef STM32F4) for platform portability
The operation must work on multiple types without writing overloads (C has no templates)
Inside ISR (Interrupt Service Routine) where function call overhead is forbidden
offsetof / container_of type operations — impossible with functions
Prefer static inline functions over macros when:
Logic is more than a single expression
You want type safety and proper compiler error messages
Debugging matters — inline functions appear in stack traces
The argument might have side effects like i++ or a function call
c
// Prefer this over a MAX macro when type safety matters
static inline int32_t max_s32(int32_t a, int32_t b) {
return (a > b) ? a : b;
}
```
**Never use macros for:**
- Anything that can be a `const` variable — `const uint32_t BAUD = 115200;` is better than `#define BAUD 115200` because it has type, scope, and shows up in debugger
- Recursive or complex multi-step algorithms
- Hiding pointer semantics or creating confusing code flow
---
## Quick Reference Summary
```
Rule 1 → Parenthesize everything: #define SQ(x) ((x)*(x))
Rule 2 → do{}while(0) for multi-statement macros
Rule 3 → No space before ( in function-like macros
Rule 4 → Never pass expressions with side effects (i++, func())
Rule 5 → Use include guards in every header file
Rule 6 → Prefer static inline when type safety matters
Rule 7 → Use UPPER_CASE names for macros — makes them obvious
Rule 8 → Use #undef to limit macro scope if needed
Rule 9 → Use -E flag to debug surprising macro expansion behaviorMacros are a double-edged tool — used correctly they are the backbone of portable, hardware-abstracted embedded firmware. Used carelessly they produce bugs that are nearly impossible to trace.
Comments