Embedded C track

Lesson 05 · Part 1: Foundations (shared — verified with PC gcc)

Functions, Header Files & #define Macros

Modularity and header guards

Part 1: Foundations (shared — verified with PC gcc)8 min readFree lesson

When the code gets long, you cannot cram everything into one file. Firmware is usually divided into modules such as the 'GPIO handling part,' 'UART handling part,' and 'application logic.' The tools for dividing into modules are functions (units of operation), separating headers (.h) and source (.c) (Interface/Implementation), and preprocessors (#include, #define, header guards).

In this lesson, as the last part of the basics, we separate the bit manipulation logic into the ledbank module (ledbank.h + ledbank.c) and create a structure where main.c/pc_test.c use it by looking only at its interface. We also cover header guards and macro pitfalls (parentheses!). The sense of dividing into modules is a key habit for expanding firmware, continuing through lesson 23 (HAL abstraction).

Learning Objectives

  • Separate function declaration (prototype) and definition, and split operations into functions.
  • Divide headers (.h, interface) and source (.c, implementation), and build multiple files together.
  • Prevent duplicate inclusion with header guards (#ifndef/#define/#endif).
  • Distinguish between #define constants and function-like macros, and avoid macro parenthesis traps.
  • Understand the concept of branching Platform/Option with conditional compilation (#ifdef).

Target Environment

  • Compiler: Either Keil C51 / Keil MDK-ARM (this edition is pure logic).
  • Device: Pure logic — PC + gcc or μVision simulator (actual board not required).
  • Verification: gcc on PC (main.c/pc_test.c + ledbank.c).

Key Concepts

1) Functions — Declaration and Definition

Functions give a name to an operation to make it easy to reuse and test.

  • Declaration (Prototype): Only promises the name, return type, and parameter types. uint8_t led_set(uint8_t bank, uint8_t pos);
  • Definition: The actual body. uint8_t led_set(...) { return ...; }

If you put the declaration in a header and the definition in the source file, other files can call the function by just looking at the header (they don't need to know the implementation).

2) Separation of Header (.h) and Source (.c)

File Role Contains
ledbank.h Interface #define, types, function declarations
ledbank.c Implementation function definitions (body)
main.c Usage include #include "ledbank.h" and then call functions

When building, compile the .c files you are using together.

gcc main.c ledbank.c -o main

In μVision, just add both main.c and ledbank.c to the project Source Group (.h does not need to be added because it can be found through #include).

3) Header Guard — Preventing Duplicate Inclusion

If the same header is included twice via different paths, types and macros get redefined causing errors. The idiom to prevent this is a header guard.

#ifndef LEDBANK_H      /*If not defined yet*/
#define LEDBANK_H      /*Mark as defined*/
... header content ...
#endif /* LEDBANK_H */ /* From the second one, skip entirely */

#pragma once does the same, but not all compilers guarantee it, so #ifndef guards are recommended for portability.

4) #define — Constant Macro vs Function-like Macro

The preprocessor replaces text before compilation.

#define LED_COUNT  8u            /*Constant Macro*/
#define BIT(n)     (1u << (n))   /*Function-style Macro*/

In function-style macros, it is an absolute rule to enclose both the arguments and the entire expression in parentheses. If you don't, it breaks due to precedence.

#define SQ(x) x*x          /*Bad: SQ(a+1) → a+1*a+1 (= a + a + 1)*/
#define SQ(x) ((x)*(x))    /*Good: SQ(a+1) → ((a+1)*(a+1))*/

Macros don't have type checks and evaluate arguments multiple times (SQ(i++) risk), so if it's not a simple constant or simple expression, the static function (or inline) is safer.

5) Conditional Compilation — #ifdef

Select code to compile according to On-platform/option. This is used when branching the same source for 8051, STM32, and PC.

#ifdef USE_UART
    uart_send(msg);     /*Included only in UART build*/
#else
    /*Does nothing*/
#endif

Viewing in μVision (This time, a PC is sufficient)

Pure logic is fine on a PC. In μVision, add main.c and ledbank.c to the project and build with F7; the two files will link together. Module separation works the same way in any IDE.

Examples

Example 1 — ledbank.h / ledbank.c / main.c: Module Separation

Move bit manipulation to the ledbank module, and main expresses the intention just with the function name.

ledbank.h (Interface):

#ifndef LEDBANK_H
#define LEDBANK_H

#include <stdint.h>

/*
 *ledbank.h — 'interface' of a module that handles 8 LED banks (8 bits).
 *The implementation is in ledbank.c, and users only need to #include this header.
 *
 *Header guard (#ifndef ~ #define ~ #endif): prevents the content from being processed more than once even if the same header is included multiple times.
 *If omitted, a duplicate definition error occurs.
 */

/*Function-like macro — the arguments must be enclosed in parentheses (to prevent precedence issues).*/
#define BIT(n)       (1u << (n))
#define LED_COUNT    8u

/*Function declaration (prototype). Only the return and argument types are promised, and the implementation is in the .c file.*/
uint8_t led_set(uint8_t bank, uint8_t pos);
uint8_t led_clear(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 */

ledbank.c (Implementation):

#include "ledbank.h"

/*
 *ledbank.c — Implementation of the functions declared in ledbank.h.
 *Wrapped bit manipulation idioms (Part 3) into functions with meaningful names.
 */

uint8_t led_set(uint8_t bank, uint8_t pos)
{
    return (uint8_t)(bank | BIT(pos));
}

uint8_t led_clear(uint8_t bank, uint8_t pos)
{
    return (uint8_t)(bank & ~BIT(pos));
}

uint8_t led_toggle(uint8_t bank, uint8_t pos)
{
    return (uint8_t)(bank ^ BIT(pos));
}

uint8_t led_count_on(uint8_t bank)
{
    uint8_t pos;
    uint8_t count = 0;
    for (pos = 0; pos < LED_COUNT; pos++) {
        count = (uint8_t)(count + ((bank >> pos) & 1u));
    }
    return count;
}

main.c (Usage):

#include <stdio.h>
#include "ledbank.h"   /*Our module's interface*/

/*Print an 8-bit value as 8-digit binary.*/
static void print_bin8(uint8_t v)
{
    int i;
    for (i = 7; i >= 0; i--) {
        putchar(((v >> i) & 1u) ? '1' : '0');
    }
}

/*
 *Lesson 05 Example 1 — the side that uses the module (ledbank).
 *Main expresses the intention just from the function name, even if it doesn't know the 'method' of bit operations.
 *Build: gcc main.c ledbank.c -o main
 */
int main(void)
{
    uint8_t bank = 0x00;

    bank = led_set(bank, 1);
    bank = led_set(bank, 4);
    bank = led_set(bank, 6);
    bank = led_toggle(bank, 4);   /*Turns number 4 on and then off again*/

    printf("bank   = ");
    print_bin8(bank);
    printf("\n");
    printf("LED on = %u\n", (unsigned)led_count_on(bank));

    return 0;
}

Example 2 — pc_test.c: Separate Module for Verification

Verify the same module with the test harness.

#include <stdio.h>
#include "ledbank.h"

/*
 *Lesson 05 Example 2 — Unit test the ledbank module functions.
 *You can test the module alone on a PC without hardware.
 *Build: gcc pc_test.c ledbank.c -o pc_test
 */
int main(void)
{
    uint8_t b = 0x00;

    b = led_set(b, 0);
    b = led_set(b, 3);
    printf("set 0,3   = 0x%02X\n", b);     /* 0x09 */

    b = led_clear(b, 0);
    printf("clear 0   = 0x%02X\n", b);     /* 0x08 */

    b = led_toggle(b, 7);
    printf("toggle 7  = 0x%02X\n", b);     /* 0x88 */

    printf("count on  = %u\n", (unsigned)led_count_on(b));  /* 2 */

    return 0;
}

Verification on a PC

This time, since the modules are separated, compile the .c you are using together.

gcc main.c ledbank.c -o main && ./main

Expected output (stdout):

bank   = 01000010
LED on = 2
gcc pc_test.c ledbank.c -o pc_test && ./pc_test

Expected output (stdout):

set 0,3   = 0x09
clear 0   = 0x08
toggle 7  = 0x88
count on  = 2

Common Mistakes

Q. I get errors in multiple definition / redefinition.

A. If you put the definition (body) of a function in a header and include it in multiple .c files, the definition will appear multiple times. Only put the declaration in the header, and put the definition in the .c file. Also, if you forget the header guard (#ifndef), errors will occur due to duplicate inclusion even within a single .c file.

Q. I'm getting a undefined reference to led_set link error.

A. This is because ledbank.c was not built together. Since the header only provides the "declaration," include the implementation-containing .c together in Needs-compile/link: gcc main.c ledbank.c -o main. If using μVision, add both .c files to the project.

Q. A macro created with #define SQ(x) x*x gives strange values.

A. It's because there are no parentheses. SQ(a+1) expands to a+1*a+1. Enclose both the arguments and the entire expression in parentheses for function-like macros: #define SQ(x) ((x)*(x)). For arguments with side effects (SQ(i++)), they will be evaluated multiple times, so use a function instead.

Q. Which should I use for constants, #define or const?

A. Both are possible. #define is a preprocessor substitution, so it has no type and may not be visible in the debugger. const has a type and is advantageous for debugging. Values needed at the preprocessor stage for bit Like-location/address should use #define; for general constants, const is also a good choice.

Summary

  • Split actions into functions, place declarations in headers (.h), and definitions in source files (.c).
  • The .c files used must be built together to link (gcc main.c ledbank.c).
  • Use a header guard (#ifndef/#define/#endif) to prevent duplicate inclusion.
  • Functional macros enclose both arguments and expressions in parentheses. If it's complex, functions are safer.
  • Select and compile By-Platform/Option code using #ifdef.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/funcs_preproc/ledbank.cC
#include "ledbank.h"

/*
 *ledbank.c — Implementation of the functions declared in ledbank.h.
 *Wrapped bit manipulation idioms (Part 3) into functions with meaningful names.
 */

uint8_t led_set(uint8_t bank, uint8_t pos)
{
    return (uint8_t)(bank | BIT(pos));
}

uint8_t led_clear(uint8_t bank, uint8_t pos)
{
    return (uint8_t)(bank & ~BIT(pos));
}

uint8_t led_toggle(uint8_t bank, uint8_t pos)
{
    return (uint8_t)(bank ^ BIT(pos));
}

uint8_t led_count_on(uint8_t bank)
{
    uint8_t pos;
    uint8_t count = 0;
    for (pos = 0; pos < LED_COUNT; pos++) {
        count = (uint8_t)(count + ((bank >> pos) & 1u));
    }
    return count;
}
src/funcs_preproc/ledbank.hC header
#ifndef LEDBANK_H
#define LEDBANK_H

#include <stdint.h>

/*
 *ledbank.h — 'interface' of a module that handles 8 LED banks (8 bits).
 *The implementation is in ledbank.c, and users only need to #include this header.
 *
 *Header guard (#ifndef ~ #define ~ #endif): prevents the content from being processed more than once even if the same header is included multiple times.
 *If omitted, a duplicate definition error occurs.
 */

/*Function-like macro — the arguments must be enclosed in parentheses (to prevent precedence issues).*/
#define BIT(n)       (1u << (n))
#define LED_COUNT    8u

/*Function declaration (prototype). Only the return and argument types are promised, and the implementation is in the .c file.*/
uint8_t led_set(uint8_t bank, uint8_t pos);
uint8_t led_clear(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 */
src/funcs_preproc/main.cC
#include <stdio.h>
#include "ledbank.h"   /*Our module's interface*/

/*Print an 8-bit value as 8-digit binary.*/
static void print_bin8(uint8_t v)
{
    int i;
    for (i = 7; i >= 0; i--) {
        putchar(((v >> i) & 1u) ? '1' : '0');
    }
}

/*
 *Lesson 05 Example 1 — the side that uses the module (ledbank).
 *Main expresses the intention just from the function name, even if it doesn't know the 'method' of bit operations.
 *Build: gcc main.c ledbank.c -o main
 */
int main(void)
{
    uint8_t bank = 0x00;

    bank = led_set(bank, 1);
    bank = led_set(bank, 4);
    bank = led_set(bank, 6);
    bank = led_toggle(bank, 4);   /*Turns number 4 on and then off again*/

    printf("bank   = ");
    print_bin8(bank);
    printf("\n");
    printf("LED on = %u\n", (unsigned)led_count_on(bank));

    return 0;
}
src/funcs_preproc/pc_test.cC
#include <stdio.h>
#include "ledbank.h"

/*
 *Lesson 05 Example 2 — Unit test the ledbank module functions.
 *You can test the module alone on a PC without hardware.
 *Build: gcc pc_test.c ledbank.c -o pc_test
 */
int main(void)
{
    uint8_t b = 0x00;

    b = led_set(b, 0);
    b = led_set(b, 3);
    printf("set 0,3   = 0x%02X\n", b);     /* 0x09 */

    b = led_clear(b, 0);
    printf("clear 0   = 0x%02X\n", b);     /* 0x08 */

    b = led_toggle(b, 7);
    printf("toggle 7  = 0x%02X\n", b);     /* 0x88 */

    printf("count on  = %u\n", (unsigned)led_count_on(b));  /* 2 */

    return 0;
}

Assignment

This assignment is all pure logic (PC verifiable). You can solve and grade it using only gcc without a board or μVision.

gcc main.c -o prog && ./prog

Problem 1 — Bit Order Reversal Function

  • File name: homework_01/main.c
  • Key Concept: Function separation, bit Extraction/Assembly
  • Platform: Common

Requirements

  • Create a uint8_t reverse_bits(uint8_t v) function that reverses the order of 8 bits (bit i → bit 7-i).
  • Print the result for the value 0x01, 0x0F, 0x12, 0x80.
  • The output format reverse(0xNN) = 0xMM must match the "expected output" character by character.

Expected output

reverse(0x01) = 0x80
reverse(0x0F) = 0xF0
reverse(0x12) = 0x48
reverse(0x80) = 0x01

Hint

  • If the i-th bit of v is 1, set the 7 - i-th bit of the result.
  • Initialize the result variable to 0 and accumulate.
  • Check: 0x12 = 00010010 → reversed becomes 01001000 = 0x48.

Problem 2 — CLAMP Macro

  • File name: homework_02/main.c
  • Key Concept: Function-style macro, parentheses rule
  • Platform: Common

Requirements

  • Define a CLAMP(x, lo, hi) macro: if x is less than lo then lo, if greater than hi then hi, else x.
  • Print CLAMP(5,0,10), CLAMP(15,0,10), CLAMP(-2,0,10).
  • The output format must match the "expected output" character by character.

Expected output

CLAMP(5,0,10)  = 5
CLAMP(15,0,10) = 10
CLAMP(-2,0,10) = 0

Hint

  • Nest the ternary operator: ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : (x))).
  • Enclose both the arguments and the entire expression in parentheses (if parentheses are omitted, it may break at the call site).

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Answer 1 — Bit Reversal Function
 *
 *Key Points:
 *- By separating the logic into the reverse_bits() function, the main function only needs to look at 'what' it does.
 *- Read the i-th bit and place it in the opposite position (7 - i).
 *
 *Common mistake:
 *- Cases where the result accumulation variable is mistakenly not initialized to 0.
 *- Cases where the bit is put back in the same position (not reversed), resulting in the same value as the input.
 */

static uint8_t reverse_bits(uint8_t v)
{
    uint8_t out = 0;
    int i;
    for (i = 0; i < 8; i++) {
        if ((v >> i) & 1u) {
            out = (uint8_t)(out | (1u << (7 - i)));
        }
    }
    return out;
}

int main(void)
{
    uint8_t values[] = { 0x01, 0x0F, 0x12, 0x80 };
    int n = (int)(sizeof(values) / sizeof(values[0]));
    int k;

    for (k = 0; k < n; k++) {
        printf("reverse(0x%02X) = 0x%02X\n",
               values[k], reverse_bits(values[k]));
    }

    return 0;
}
homework_02/main.cC
#include <stdio.h>

/*
 *Answer 2 — CLAMP Macro
 *
 *Key Points:
 *- Functional macros enclose both the arguments and the entire expression in parentheses. That's necessary in the calling code
 *It does not proceed incorrectly due to operator precedence or sign issues.
 *- CLAMP(x, lo, hi): If x goes outside [lo, hi], it is cut to the boundary value.
 *
 *Common mistake:
 *- #define CLAMP(x,lo,hi) x<lo?lo:x>hi?hi:x  If you use it without parentheses like this
 *In calls like CLAMP(a+1, ...), the expansion breaks.
 *- Passing a macro argument as an expression with side effects (e.g., i++) is dangerous because it can be evaluated multiple times.
 */

#define CLAMP(x, lo, hi)  ((x) < (lo) ? (lo) : ((x) > (hi) ? (hi) : (x)))

int main(void)
{
    printf("CLAMP(5,0,10)  = %d\n",  CLAMP(5, 0, 10));
    printf("CLAMP(15,0,10) = %d\n",  CLAMP(15, 0, 10));
    printf("CLAMP(-2,0,10) = %d\n",  CLAMP(-2, 0, 10));

    return 0;
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now