← Back to Embedded C series
🔌
Basics
Basics · Prereq: lesson 4

05. Functions · Header Splitting · #define Macros

Firmware is split into modules — "the GPIO part", "the UART part", "the app logic". The tools are functions (units of action), header(.h)/source(.c) separation (interface/implementation), and the preprocessor (#include·#define·include guards). We extract bit logic into a ledbank module and let main use only its interface, covering include guards and the macro-parenthesis trap. This modular sense carries through lesson 23 (HAL abstraction).

functionheader#definemacroinclude guardmodule
Duration
~1.5 hours
Level
📊 Beginner
Prerequisite
🎯 Lesson 4
OUTCOME
Split actions into functions and headers/sources, build multiple files together, and structure firmware modularly with include guards and the macro-parenthesis rule.

What you'll learn

  • 1Distinguish a function declaration (prototype) from its definition
  • 2Split header (.h, interface) from source (.c, implementation) and build together
  • 3Prevent double inclusion with include guards (#ifndef/#define/#endif)
  • 4Tell apart constant vs function-like macros and avoid the parenthesis trap
  • 5Branch by platform/option with conditional compilation (#ifdef)

Introduction

When code grows you can't pile it into one file. As the last basics lesson, we extract bit logic into a ledbank module (ledbank.h + ledbank.c) so main only sees the interface.

Key concepts

1) Functions / header vs source

FileRoleHolds
ledbank.hinterface#define·types·function declarations
ledbank.cimplementationfunction definitions (bodies)
main.cuse#include "ledbank.h" then call

Build the .c files you use together: `gcc main.c ledbank.c -o main`. In μVision, add both .c files to the Source Group.

2) Include guard — prevent double inclusion

c
#ifndef LEDBANK_H      /* if not yet defined */
#define LEDBANK_H      /* mark defined and */
... header contents ...
#endif /* LEDBANK_H */ /* skip entirely the second time */

#pragma once does the same but isn't guaranteed by all compilers, so the #ifndef guard is preferred for portability.

3) #define — constant vs function-like macro

c
#define LED_COUNT 8u            /* constant macro */
#define BIT(n)    (1u << (n))   /* function-like macro */

#define SQ(x) x*x          /* bad: SQ(a+1) → a+1*a+1 */
#define SQ(x) ((x)*(x))    /* good: wrap args and the whole expr */
ℹ️

Macros have no type checks and evaluate args multiple times (SQ(i++) is risky). For anything beyond simple constants/expressions, a static (or inline) function is safer.

4) Conditional compilation #ifdef

c
#ifdef USE_UART
    uart_send(msg);     /* included only in the UART build */
#endif

Core example

ledbank.h holds the interface (declarations), ledbank.c the implementation (definitions), and main expresses intent with function names only.

c
/* ledbank.h — interface */
#ifndef LEDBANK_H
#define LEDBANK_H
#include <stdint.h>
#define BIT(n) (1u << (n))
uint8_t led_set(uint8_t bank, uint8_t pos);
uint8_t led_toggle(uint8_t bank, uint8_t pos);
uint8_t led_count_on(uint8_t bank);
#endif /* LEDBANK_H */
c
/* ledbank.c — implementation */
#include "ledbank.h"
uint8_t led_set(uint8_t b, uint8_t p)    { return (uint8_t)(b |  BIT(p)); }
uint8_t led_toggle(uint8_t b, uint8_t p) { return (uint8_t)(b ^  BIT(p)); }
uint8_t led_count_on(uint8_t b) {
    uint8_t pos, n = 0;
    for (pos = 0; pos < 8u; pos++) n = (uint8_t)(n + ((b >> pos) & 1u));
    return n;
}
bash
gcc main.c ledbank.c -o main && ./main   # build both .c together

Common mistakes

Q. I get multiple definition / redefinition errors.

A. Putting a function definition (body) in a header and including it from several .c files creates multiple definitions. Put declarations in the header, definitions in .c, and don't drop the include guard.

Q. I get undefined reference to led_set at link time.

A. You didn't build ledbank.c too. The header only declares; compile/link the .c with the implementation: gcc main.c ledbank.c.

Q. #define SQ(x) x*x gives weird values.

A. Missing parentheses. SQ(a+1) expands to a+1*a+1. Wrap args and the whole expression: #define SQ(x) ((x)*(x)). For side-effecting args, use a function.

Summary

  • Split actions into functions; declarations in headers (.h), definitions in sources (.c)
  • Build the .c files you use together to link
  • Prevent double inclusion with include guards (#ifndef/#define/#endif)
  • Wrap function-like macro args and expressions in parentheses — use a function when complex
  • Pick platform/option code with #ifdef

Exercises

  1. Add led_clear() to the ledbank module and call it from main
  2. Remove the include guard and #include the header twice to reproduce the error, then fix with the guard
  3. Write a MIN(a,b) function-like macro following the parenthesis rule and explain why MIN(x++, y) is risky
Example code / lecture materials

All lecture materials and example code (with homework and answers) are openly available on GitHub.

View on GitHub ↗