Table of Contents

1. What Is a Callback?
#

A callback is a function that you pass to another module so it can call you back when something happens. You don’t call it directly – the system calls it for you.

 ┌─────────────────┐                    ┌─────────────────┐
 │   Your Code     │                    │  Sensor Manager  │
 │   (main.c)      │                    │  (library)       │
 │                  │   1. register      │                  │
 │  my_handler() ──────────────────────►│  stores pointer  │
 │                  │                    │                  │
 │                  │   2. event occurs  │                  │
 │                  │◄──────────────────── calls my_handler │
 │  my_handler()   │                    │                  │
 │  executes!      │                    │                  │
 └─────────────────┘                    └─────────────────┘

 You write the function.
 The library decides WHEN to call it.

2. Function Pointers: The Mechanism Behind Callbacks
#

A function pointer stores the address of a function, just like a regular pointer stores the address of a variable.

void greet(int id) {
    printf("Hello from sensor %d\n", id);
}

int main(void) {
    void (*func_ptr)(int) = greet;   // store address of greet()
    func_ptr(42);                     // call greet(42) via pointer
}
 Memory layout:

 TEXT segment (code)
 ┌────────────────────────────────┐
 │ 0x4000: greet() instructions   │
 │         printf("Hello ...")    │
 └────────────────────────────────┘
 STACK   │
 ┌───────┴────────────────────────┐
 │ func_ptr = 0x4000              │── points to greet()'s code
 └────────────────────────────────┘

 func_ptr(42)  →  jump to 0x4000  →  greet(42) executes

Reading function pointer syntax
#

 void (*func_ptr)(int, float)
  │      │         │
  │      │         └── parameter types this function takes
  │      └── name of the pointer variable
  └── return type of the function

 Read it as: "func_ptr is a pointer to a function
              that takes (int, float) and returns void"

3. Cleaning Up with typedef
#

3.1 The Problem: Raw function pointers are hard to read
#

// Without typedef -- every declaration repeats the full signature
void (*on_temp_cb)(int sensor_id, float value);
void (*on_humid_cb)(int sensor_id, float value);
void (*on_motion_cb)(int sensor_id, int detected);

3.2 The Solution: typedef gives the type a name
#

typedef void (*TempCallback)(int sensor_id, float value);
typedef void (*HumidCallback)(int sensor_id, float value);
typedef void (*MotionCallback)(int sensor_id, int detected);
 typedef void (*TempCallback)(int, float);
 │        │      │              │
 │        │      │              └── parameter types
 │        │      └── new type name (you choose this)
 │        └── return type
 └── "define a type alias"

 After this, TempCallback IS a type, just like int or float.

Now declarations are clean:

// Use it like any other type
TempCallback   my_temp_handler;
HumidCallback  my_humid_handler;
MotionCallback my_motion_handler;
 Without typedef:                    With typedef:
 ──────────────────────────         ──────────────────────────
 void (*cb)(int, float);            TempCallback cb;

 void register(                     void register(
   void (*cb)(int, float)             TempCallback cb
 );                                 );

 Hard to read                        Reads like English

4. Practical Example: Sensor Callback System
#

4.1 Architecture Overview
#

 ┌──────────────────────────────────────────────────────────────┐
 │                    Smart Home System                         │
 │                                                              │
 │  ┌─────────────┐    ┌──────────────────┐    ┌────────────┐  │
 │  │  main.c     │    │ sensor_manager.c │    │ sensor_    │  │
 │  │             │    │                  │    │ manager.h  │  │
 │  │ App logic   │    │ Core engine      │    │ Interface  │  │
 │  │ + handlers  │    │ + callback       │    │ (types +   │  │
 │  │             │    │   invocation     │    │  API)      │  │
 │  └──────┬──────┘    └────────┬─────────┘    └─────┬──────┘  │
 │         │                    │                     │         │
 │         │   registers        │    #include         │         │
 │         │   callbacks ──────►│◄────────────────────┘         │
 │         │                    │                               │
 │         │◄── calls back ─────│                               │
 │         │    when event      │                               │
 │         │    occurs          │                               │
 └─────────┴────────────────────┴───────────────────────────────┘

4.2 Step 1: Header File (sensor_manager.h)
#

#ifndef SENSOR_MANAGER_H
#define SENSOR_MANAGER_H

// --- Callback type definitions ---
typedef void (*TempCallback)(int sensor_id, float temperature);
typedef void (*HumidCallback)(int sensor_id, float humidity);
typedef void (*MotionCallback)(int sensor_id, int detected);
typedef void (*ErrorCallback)(int error_code, const char *message);

// --- Bundle all callbacks into one struct ---
typedef struct {
    TempCallback   on_temperature;
    HumidCallback  on_humidity;
    MotionCallback on_motion;
    ErrorCallback  on_error;
} SensorCallbacks;

// --- API ---
void sensor_init(void);
int  register_callbacks(SensorCallbacks *callbacks);
void start_monitoring(void);
void process_temperature(int sensor_id, float value);
void process_humidity(int sensor_id, float value);
void process_motion(int sensor_id, int detected);

#endif

Why bundle callbacks in a struct?
#

 Individual registration:            Struct registration:
 ────────────────────────           ────────────────────────
 register_temp(handler1);           SensorCallbacks cb = {
 register_humid(handler2);              .on_temperature = h1,
 register_motion(handler3);             .on_humidity    = h2,
 register_error(handler4);              .on_motion      = h3,
                                        .on_error       = h4
 4 separate API calls                };
 Hard to add new types              register_callbacks(&cb);

                                    1 API call
                                    Easy to extend

4.3 Step 2: Implementation (sensor_manager.c)
#

#include "sensor_manager.h"
#include <stdio.h>
#include <string.h>

// --- Internal state (static = file-private) ---
static SensorCallbacks g_callbacks;
static int g_initialized = 0;

void sensor_init(void) {
    memset(&g_callbacks, 0, sizeof(g_callbacks));  // all pointers = NULL
    g_initialized = 1;
    printf("[Sensor Manager] Initialized\n");
}

int register_callbacks(SensorCallbacks *callbacks) {
    if (callbacks == NULL) return -1;
    if (!g_initialized)   return -2;

    g_callbacks = *callbacks;   // copy the struct contents
    printf("[Sensor Manager] Callbacks registered\n");
    return 0;
}

void process_temperature(int sensor_id, float value) {
    printf("[Sensor Manager] Temperature: sensor=%d, value=%.1f\n",
           sensor_id, value);

    if (g_callbacks.on_temperature) {           // NULL check first!
        g_callbacks.on_temperature(sensor_id, value);  // invoke callback
    }
}

void process_humidity(int sensor_id, float value) {
    printf("[Sensor Manager] Humidity: sensor=%d, value=%.1f\n",
           sensor_id, value);

    if (g_callbacks.on_humidity) {
        g_callbacks.on_humidity(sensor_id, value);
    }
}

void process_motion(int sensor_id, int detected) {
    printf("[Sensor Manager] Motion: sensor=%d, detected=%d\n",
           sensor_id, detected);

    if (g_callbacks.on_motion) {
        g_callbacks.on_motion(sensor_id, detected);
    }
}

void start_monitoring(void) {
    printf("[Sensor Manager] Monitoring started...\n");
    process_temperature(1, 25.5f);
    process_humidity(2, 65.0f);
    process_motion(3, 1);
    process_temperature(1, 28.0f);
}

The critical line: g_callbacks = *callbacks
#

This is a value copy of the entire struct, not a pointer assignment.

 register_callbacks(&callbacks)

 ┌──── main.c (caller) ──────┐      ┌──── sensor_manager.c ──────┐
 │                            │      │                             │
 │  callbacks (on stack)      │      │  g_callbacks (static/DATA)  │
 │  ┌──────────────────────┐  │      │  ┌──────────────────────┐   │
 │  │ .on_temperature=0x40 │──┼─copy─┼─►│ .on_temperature=0x40 │   │
 │  │ .on_humidity   =0x41 │──┼─copy─┼─►│ .on_humidity   =0x41 │   │
 │  │ .on_motion     =0x42 │──┼─copy─┼─►│ .on_motion     =0x42 │   │
 │  │ .on_error      =0x43 │──┼─copy─┼─►│ .on_error      =0x43 │   │
 │  └──────────────────────┘  │      │  └──────────────────────┘   │
 │                            │      │                             │
 │  (destroyed after main     │      │  (lives for entire program  │
 │   returns -- that's OK,    │      │   lifetime -- safe to call  │
 │   values were copied)      │      │   anytime)                  │
 └────────────────────────────┘      └─────────────────────────────┘

4.4 Step 3: Application Code (main.c)
#

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

// --- Your callback implementations ---

void my_temp_handler(int sensor_id, float temperature) {
    printf("  [APP] Temp alert: sensor %d reads %.1f C\n",
           sensor_id, temperature);
    if (temperature >= 27.0f) {
        printf("  [APP] WARNING: High temp! Turning on AC.\n");
    }
}

void my_humid_handler(int sensor_id, float humidity) {
    printf("  [APP] Humidity alert: sensor %d reads %.1f%%\n",
           sensor_id, humidity);
    if (humidity >= 70.0f) {
        printf("  [APP] WARNING: High humidity! Turning on dehumidifier.\n");
    }
}

void my_motion_handler(int sensor_id, int detected) {
    printf("  [APP] Motion alert: sensor %d - %s\n",
           sensor_id, detected ? "movement detected!" : "idle");
    if (detected) {
        printf("  [APP] Turning on lights.\n");
    }
}

void my_error_handler(int error_code, const char *message) {
    printf("  [APP] Error %d: %s\n", error_code, message);
}

int main(void) {
    printf("=== Smart Home Sensor System ===\n\n");

    // 1. Initialize
    sensor_init();

    // 2. Set up callbacks
    SensorCallbacks callbacks;
    callbacks.on_temperature = my_temp_handler;
    callbacks.on_humidity    = my_humid_handler;
    callbacks.on_motion      = my_motion_handler;
    callbacks.on_error       = my_error_handler;

    // 3. Register
    register_callbacks(&callbacks);

    // 4. Start
    printf("\n--- Monitoring started ---\n\n");
    start_monitoring();

    printf("\n--- Done ---\n");
    return 0;
}

4.5 Full Execution Flow
#

 main()                          sensor_manager              my_temp_handler()
   │                                   │                           │
   │  sensor_init()                    │                           │
   │──────────────────────────────────►│ g_callbacks = {NULL}      │
   │                                   │                           │
   │  register_callbacks(&cb)          │                           │
   │──────────────────────────────────►│ g_callbacks = cb (copy)   │
   │                                   │                           │
   │  start_monitoring()               │                           │
   │──────────────────────────────────►│                           │
   │                                   │                           │
   │                                   │ process_temperature(1,25.5)
   │                                   │──── g_callbacks            │
   │                                   │     .on_temperature ──────►│
   │                                   │     (sensor=1, val=25.5)   │
   │                                   │◄──────────────────────────│
   │                                   │                           │
   │                                   │ process_humidity(2, 65.0)  │
   │                                   │──── g_callbacks            │
   │                                   │     .on_humidity ─────────►│
   │                                   │                     (similar)
   │                                   │                           │
   │◄──────────────────────────────────│ return                    │
   │                                   │                           │

4.6 Output
#

=== Smart Home Sensor System ===

[Sensor Manager] Initialized
[Sensor Manager] Callbacks registered

--- Monitoring started ---

[Sensor Manager] Monitoring started...
[Sensor Manager] Temperature: sensor=1, value=25.5
  [APP] Temp alert: sensor 1 reads 25.5 C
[Sensor Manager] Humidity: sensor=2, value=65.0
  [APP] Humidity alert: sensor 2 reads 65.0%
[Sensor Manager] Motion: sensor=3, detected=1
  [APP] Motion alert: sensor 3 - movement detected!
  [APP] Turning on lights.
[Sensor Manager] Temperature: sensor=1, value=28.0
  [APP] Temp alert: sensor 1 reads 28.0 C
  [APP] WARNING: High temp! Turning on AC.

--- Done ---

5. volatile and Callbacks in Embedded Systems
#

5.1 The Problem Without volatile
#

In embedded systems, hardware interrupts can change variables at any time. The compiler doesn’t know this, so it may optimize the variable into a register and never re-read it from memory.

int data_ready = 0;  // shared between main loop and interrupt

void main_loop(void) {
    while (!data_ready) {
        // compiler may optimize this into an infinite loop!
        // it thinks data_ready never changes inside this loop
    }
}
 What the compiler sees:              What actually happens:

 data_ready = 0                       data_ready = 0
      │                                    │
      ▼                                    ▼
 ┌─────────────────┐                 ┌─────────────────┐
 │ Load data_ready  │                 │ Load data_ready  │
 │ into register    │                 │ into register    │
 │ (value = 0)      │                 │ (value = 0)      │
 └────────┬────────┘                 └────────┬────────┘
          │                                    │
          ▼                               INTERRUPT fires!
 ┌─────────────────┐                 data_ready = 1 (in memory)
 │ Loop forever    │                       │
 │ (register = 0,  │                       ▼
 │  never re-reads │                 ┌─────────────────┐
 │  from memory)   │                 │ But register     │
 └─────────────────┘                 │ still holds 0!   │
                                     │ Loop continues   │
  BUG: infinite loop                 └─────────────────┘

5.2 The Fix: volatile
#

volatile tells the compiler: “this variable can change at any time – always read it from memory.”

volatile int data_ready = 0;
volatile float last_temperature = 0.0f;

// Interrupt handler (called by hardware)
void sensor_interrupt_handler(void) {
    last_temperature = read_sensor_value();
    data_ready = 1;
}

// Main loop
void main_loop(void) {
    while (!data_ready) {
        // volatile forces a fresh read from memory every iteration
    }
    process_temperature(1, last_temperature);
    data_ready = 0;
}
 With volatile:

 Memory              CPU Register
 ┌──────────┐
 │ data_ready│
 │   = 0     │◄──── read (iteration 1) ──── register = 0 → loop
 │           │◄──── read (iteration 2) ──── register = 0 → loop
 │           │
 │   = 1     │◄──── INTERRUPT writes 1
 │           │
 │           │◄──── read (iteration 3) ──── register = 1 → EXIT!
 └──────────┘

 Every loop iteration re-reads from actual memory.
 The interrupt's write is visible immediately.

5.3 Callbacks in Interrupt Context: Be Careful
#

Interrupt handlers must be fast. Calling a callback inside an interrupt is risky because the callback might do slow work (printf, I/O, etc.).

 BAD: callback inside interrupt       GOOD: flag + main loop
 ─────────────────────────────       ──────────────────────────

 ┌──────────────┐                    ┌──────────────┐
 │  Interrupt   │                    │  Interrupt   │
 │              │                    │              │
 │  callback()  │ ← might be slow   │  flag = 1;   │ ← fast!
 │  printf()    │   blocks other     │              │
 │  I/O ops     │   interrupts       └──────┬───────┘
 │              │                            │
 └──────────────┘                            ▼
                                     ┌──────────────┐
                                     │  Main Loop   │
                                     │              │
                                     │  if (flag) { │
                                     │   callback() │ ← safe here
                                     │   flag = 0;  │
                                     │  }           │
                                     └──────────────┘
// BAD -- slow callback blocks interrupts
void sensor_interrupt_handler(void) {
    if (g_callbacks.on_temperature) {
        g_callbacks.on_temperature(id, value);  // risky!
    }
}

// GOOD -- set flag, handle in main loop
static volatile int    temp_ready = 0;
static volatile float  temp_value = 0;
static volatile int    temp_sensor_id = 0;

void sensor_interrupt_handler(void) {
    temp_value     = read_sensor_value();
    temp_sensor_id = get_sensor_id();
    temp_ready     = 1;                         // just set the flag
}

void main_loop(void) {
    if (temp_ready) {
        temp_ready = 0;
        if (g_callbacks.on_temperature) {
            g_callbacks.on_temperature(temp_sensor_id, temp_value);
        }
    }
}

6. Advanced: Passing User Data to Callbacks
#

6.1 The Problem
#

Your callback signature is fixed by the library. But sometimes you need extra context that isn’t in the parameters.

void my_temp_handler(int sensor_id, float temperature) {
    // I want to know WHICH ROOM this sensor is in...
    // but there's no room_name parameter!
}

6.2 The Solution: void *user_data
#

Add a generic pointer to the callback signature. The caller stores whatever extra data they want, and the callback casts it back.

// Extended callback type with user_data
typedef void (*TempCallbackEx)(int sensor_id, float temperature,
                                void *user_data);
 How void* user_data works:

 ┌──── main.c ──────────────────────────────────────────────┐
 │                                                           │
 │  RoomInfo living_room = { .name = "Living Room",          │
 │                           .floor = 1 };                   │
 │                                                           │
 │  register(my_handler, &living_room);                      │
 │                  │            │                            │
 │                  │            └── void* user_data          │
 │                  └── callback function                     │
 └──────────────────┬────────────┬───────────────────────────┘
                    │            │
                    ▼            ▼
 ┌──── sensor_manager.c ────────────────────────────────────┐
 │                                                           │
 │  stores: callback = my_handler                            │
 │          data     = &living_room (as void*)               │
 │                                                           │
 │  on event:                                                │
 │    callback(sensor_id, temp, data);                       │
 │         │                    │                             │
 └─────────┼────────────────────┼────────────────────────────┘
           │                    │
           ▼                    ▼
 ┌──── my_handler() ────────────────────────────────────────┐
 │                                                           │
 │  void my_handler(int id, float temp, void *user_data) {   │
 │      RoomInfo *room = (RoomInfo *)user_data;              │
 │      //                  ▲                                │
 │      //                  cast void* back to original type │
 │      printf("%s: %.1f C\n", room->name, temp);            │
 │  }                                                        │
 │                                                           │
 │  Output: "Living Room: 25.5 C"                            │
 └───────────────────────────────────────────────────────────┘
typedef struct {
    char name[32];
    int floor;
} RoomInfo;

RoomInfo living_room = { .name = "Living Room", .floor = 1 };

void my_handler(int sensor_id, float temp, void *user_data) {
    RoomInfo *room = (RoomInfo *)user_data;  // cast back
    printf("[%s, Floor %d] sensor %d: %.1f C\n",
           room->name, room->floor, sensor_id, temp);
}

// Register with user data
register_callback_ex(my_handler, &living_room);

6.3 Why void*?
#

 void* = "pointer to anything"

 ┌──────────┐     ┌──────────┐     ┌──────────┐
 │ RoomInfo  │     │ Config   │     │ Logger   │
 │ struct    │     │ struct   │     │ struct   │
 └─────┬────┘     └─────┬────┘     └─────┬────┘
       │                │                │
       └───────┬────────┴────────────────┘
          ┌─────────┐
          │  void*  │   accepts ANY pointer type
          └─────────┘

 The library doesn't need to know your data type.
 You cast it back inside your callback.

7. Summary
#

ConceptWhat It DoesWhen to Use
Function pointerStores address of a functionWhen behavior must be decided at runtime
typedefNames a function pointer typeAlways – makes code readable
Callback structBundles related callbacksWhen a module has multiple event types
g_callbacks = *cbCopies struct by valueSafe: original can be destroyed after
NULL checkif (cb.on_temp) before callAlways – callback may not be registered
volatileForces memory re-readVariables shared with interrupts
Flag patternSet flag in ISR, handle in mainKeep interrupt handlers fast
void *user_dataPass extra context to callbacksWhen callbacks need app-specific data