top of page

Macros — Complete Guide for Embedded C

  • Writer: Ashok Kumar Kumawat
    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.elf

The 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 produces

Defining 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 definition

Golden rules every embedded developer must follow:

  1. Always wrap the entire expression in outer parentheses.

  2. Always wrap every parameter in its own parentheses.

  3. 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.

  4. Use do { ... } while(0) wrapper for multi-statement macros.

  5. 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  → WRONG

2. 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 register

5. 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 RTOS

6. 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 guard

Modern alternative — #pragma once (compiler extension, not standard C):

c

#pragma once   // simpler but not in C standard — works on GCC, Clang, MSVC

Always 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 behavior

Macros 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.

Recent Posts

See All
Q&A: C Code

Q1: How do you find the size of an array in C? Q2: What is an array of pointers in C, and why is it useful? Q3: How can function pointers be used to trigger callbacks in C? Q4: How can we determine th

 
 
 
Bubble Sort Concept

Bubble Sort repeatedly compares adjacent elements and swaps them if they are in the wrong order. After each pass, the largest element "bubbles up" to its correct position at the end. C Code Example #i

 
 
 
Storage Classes in Embedded C

Excellent, let’s now systematically cover Storage Classes in Embedded C with the structured breakdown you asked for. 1. Concept Overview Storage classes define scope, lifetime, and visibility of varia

 
 
 

Comments


© 2035 by Robert Caro. Powered and secured by Wix

bottom of page