Theory-Function Pointers in Embedded C: Callbacks, Dispatch Tables and the Vector Table-Free Embedded C Course in Hyderabad

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

MODULE 8 · LESSON 8.4 · THEORY

Function Pointers in Embedded C: Callbacks, Dispatch Tables and the Vector Table

Function addresses, declaring and typedef-ing function pointers, indirect calls, callbacks with context, dispatch tables, state machines, the Cortex-M4 vector table and the Thumb bit. Keep this in mind whenever you use function pointers in embedded C.

55 min
Read Time
Intermediate
Level
M8 · L4
Module / Lesson

What You Will Learn

✓ That a function name is an address and a function pointer stores it
✓ How to read and write function pointer declarations, and simplify them with typedef
✓ Calling through a pointer: fp(x) and (*fp)(x)
✓ Callbacks: passing behaviour to another function
✓ Dispatch tables: an array of function pointers instead of a long switch
✓ The Cortex-M4 vector table: interrupt handlers are function pointers
✓ The Thumb bit, NULL pointers and signature mismatches
✓ Registering callbacks with a void * context

Key Terms

Function pointerCallbackDispatch tabletypedefSignatureVector tableHandlerThumb bitContext pointerState machineqsortNULL checkIndirect callReset_Handlerconst table

1. A Function Name Is an Address

Machine code lives in memory (Flash on the STM32F407, starting at 0x08000000), and each function begins at an address. In C the name of a function, used without parentheses, evaluates to that address. A function pointer is a variable that stores such an address, so you can decide at run time which function to call. Function pointers in embedded C are the mechanism behind callbacks, command tables, state machines and every interrupt vector. Many bugs with function pointers in embedded C start here.

fp_basic.c
#include <stdio.h> static int twice(int x) { return 2 * x; } static int square(int x) { return x * x; } int main(void) { int (*op)(int); /* op: pointer to a function taking int, returning int */ op = twice; /* the name twice is its address (&twice is the same) */ printf(“op = twice : op(7) = %d\n”, op(7)); op = square; /* same variable, different function */ printf(“op = square : op(7) = %d\n”, op(7)); printf(“twice and &twice are the same address: %s\n”, ((void *)twice == (void *)&twice) ? “yes” : “no”); return 0; }
Output (verified by compiling and running)
op = twice : op(7) = 14 op = square : op(7) = 49 twice and &twice are the same address: yes

The variable op holds either address. The call op(7) jumps to whichever function it currently points to; at the machine level the compiler emits an indirect branch, blx r3 on the Cortex-M4, instead of the direct bl twice. This is the core rule for function pointers in embedded C.

One pointer, two possible targets
op
holds an address
→ selects →
twice()
code in Flash
square()
code in Flash
Not a data pointer: A function pointer cannot be dereferenced to read the code as data, and converting it to void * is outside ISO C (GCC allows it as an extension, which the demonstration above uses). Pointer arithmetic on function pointers is not allowed.

2. Declaring Function Pointers

The declaration mirrors the function’s own prototype with (*name) in place of the function name. The parentheses are essential. Reviewers check function pointers in embedded C closely.

DeclarationMeaning
int (*fp)(int);fp is a pointer to a function (int) returning int
int *fp(int);NOT a pointer: a function returning int * (the * binds to the return type)
void (*cb)(void);pointer to a function with no parameters and no result
int (*tbl[4])(int);array of 4 pointers to functions (int) returning int
void (*(*f)(int))(void);pointer to a function (int) returning a pointer to a function (void) returning void

The last line shows why you should use typedef: it gives the function type a name and makes everything else readable. Interviewers test function pointers in embedded C on exactly this point.

fp_typedef.c
#include <stdio.h> typedef int (*binop_t)(int, int); /* binop_t: pointer to int f(int, int) */ static int add(int a, int b) { return a + b; } static int sub(int a, int b) { return a – b; } static int mul(int a, int b) { return a * b; } static binop_t choose(char sign) /* returns a function pointer */ { switch (sign) { case ‘+’: return add; case ‘-‘: return sub; case ‘*’: return mul; default : return NULL; } } int main(void) { const char signs[] = { ‘+’, ‘-‘, ‘*’, ‘/’ }; for (int i = 0; i < 4; i++) { binop_t f = choose(signs[i]); if (f) printf(“12 %c 4 = %d\n”, signs[i], f(12, 4)); else printf(“12 %c 4 : unsupported operator\n”, signs[i]); } return 0; }
Output (verified by compiling and running)
12 + 4 = 16 12 – 4 = 8 12 * 4 = 48 12 / 4 : unsupported operator
Reading rule: Start at the name and work outward: fp is (*fp) a pointer, to a function (…), returning int. With a typedef you read binop_t f: f is a binop_t.

3. Calling Through a Pointer

Both fp(args) and (*fp)(args) are valid and identical: the function-to-pointer conversion makes the extra * harmless. Most embedded style guides prefer the shorter form, which reads like an ordinary call. The call passes arguments by value and returns the result just as in Lesson 8.1; the pointer is only a different way to reach the callee. Master function pointers in embedded C by practising this case.

fp_call.c
#include <stdio.h> static void hello(const char *who) { printf(“hello, %s\n”, who); } int main(void) { void (*say)(const char *) = hello; say(“world”); /* usual style */ (*say)(“again”); /* equivalent, explicit dereference */ (**say)(“still the same”); /* legal too: *say is again a function designator */ return 0; }
Output (verified by compiling and running)
hello, world hello, again hello, still the same
Check before calling: Calling a NULL function pointer branches to address 0. The address is even, so bit 0 (the Thumb bit) is clear and the Cortex-M4 raises a UsageFault for an invalid state, which normally escalates to a HardFault. Always test optional callbacks with if (cb != NULL).

4. Callbacks: Passing Behaviour to a Function

A callback is a function pointer given to another function that will call it later. The library does not know what the callback does; it only knows its signature. The C library’s qsort is the classic example: it sorts any array, and you supply the comparison.

callback_qsort.c
#include <stdio.h> #include <stdlib.h> static int by_value_asc(const void *a, const void *b) { int x = *(const int *)a, y = *(const int *)b; return (x > y) – (x < y); } static int by_value_desc(const void *a, const void *b) { return by_value_asc(b, a); } static void show(const char *label, const int *v, int n) { printf(“%-11s”, label); for (int i = 0; i < n; i++) printf(” %d”, v[i]); printf(“\n”); } int main(void) { int data[] = { 42, 7, 19, 3, 88, 25 }; int n = sizeof data / sizeof data[0]; qsort(data, n, sizeof data[0], by_value_asc); show(“ascending:”, data, n); qsort(data, n, sizeof data[0], by_value_desc); show(“descending:”, data, n); return 0; }
Output (verified by compiling and running)
ascending: 3 7 19 25 42 88 descending: 88 42 25 19 7 3

Embedded libraries use the same idea for events: you register a function with a driver, and the driver calls it when the transfer is complete or the button is pressed.

Registering and invoking a callback
1. Application
btn_set_handler(on_press)
stores pointer
↓
2. Driver
keeps handler in a variable
event happens
↓
3. Driver calls
handler() runs the application code
callback_register.c
#include <stdio.h> typedef void (*event_cb_t)(int pin); static event_cb_t s_handler; /* driver-private storage, NULL at start */ static void btn_set_handler(event_cb_t cb) { s_handler = cb; } static void btn_irq_simulated(int pin) /* what the ISR would do */ { if (s_handler != NULL) /* an unset callback is not an error */ s_handler(pin); else printf(” pin %d event ignored (no handler)\n”, pin); } static void on_press(int pin) { printf(” application: pin %d pressed\n”, pin); } int main(void) { btn_irq_simulated(0); /* before registration */ btn_set_handler(on_press); btn_irq_simulated(0); /* after registration */ return 0; }
Output (verified by compiling and running)
pin 0 event ignored (no handler) application: pin 0 pressed

5. Dispatch Tables Instead of switch

When a program selects among many functions by a number or a command code, an array of function pointers (a dispatch table) replaces a long switch. Adding a command means adding one table row. If the table is static const, it lives in Flash (.rodata), uses no RAM and cannot be corrupted at run time.

dispatch.c
#include <stdio.h> #include <string.h> typedef void (*cmd_fn)(int arg); static void cmd_led(int arg) { printf(” LED mask set to 0x%X\n”, arg); } static void cmd_delay(int arg) { printf(” delay %d ms\n”, arg); } static void cmd_reset(int arg) { (void)arg; printf(” system reset requested\n”); } typedef struct { const char *name; cmd_fn run; } command_t; static const command_t commands[] = { { “led”, cmd_led }, { “delay”, cmd_delay }, { “reset”, cmd_reset }, }; #define N_COMMANDS (sizeof commands / sizeof commands[0]) static int dispatch(const char *name, int arg) { for (size_t i = 0; i < N_COMMANDS; i++) if (strcmp(name, commands[i].name) == 0) { commands[i].run(arg); return 0; } return –1; /* unknown command */ } int main(void) { const char *input[] = { “led”, “delay”, “bogus”, “reset” }; int args[] = { 5, 250, 0, 0 }; for (int i = 0; i < 4; i++) { printf(“> %s %d\n”, input[i], args[i]); if (dispatch(input[i], args[i]) != 0) printf(” error: unknown command\n”); } return 0; }
Output (verified by compiling and running)
> led 5 LED mask set to 0x5 > delay 250 delay 250 ms > bogus 0 error: unknown command > reset 0 system reset requested
switchDispatch table
Adding an entryEdit code in a functionAdd one table row
Lookup costCompiler picks (compare chain or jump table)Index or search, then an indirect call
Data-driven (names, flags)AwkwardNatural
Placed in FlashAs codeAs const data
Type safetyEach case can differAll functions share one signature

6. State Machines with Function Pointers

A state machine can store the current state as a function pointer: each state function does its work and returns the next state. The main loop is then a single line, state = state();.

state_fp.c
#include <stdio.h> typedef struct state { const struct state *(*run)(int input); const char *name; } state_t; static const state_t S_IDLE, S_RUN, S_FAULT; static const state_t *idle(int in) { return in == 1 ? &S_RUN : &S_IDLE; } static const state_t *run(int in) { return in == 0 ? &S_IDLE : in == 9 ? &S_FAULT : &S_RUN; } static const state_t *fault(int in) { (void)in; return &S_FAULT; } /* latched */ static const state_t S_IDLE = { idle, “IDLE” }; static const state_t S_RUN = { run, “RUN” }; static const state_t S_FAULT = { fault, “FAULT” }; int main(void) { const int inputs[] = { 0, 1, 1, 0, 1, 9, 1, 0 }; const state_t *s = &S_IDLE; for (int i = 0; i < 8; i++) { s = s->run(inputs[i]); printf(“input %d -> %s\n”, inputs[i], s->name); } return 0; }
Output (verified by compiling and running)
input 0 -> IDLE input 1 -> RUN input 1 -> RUN input 0 -> IDLE input 1 -> RUN input 9 -> FAULT input 1 -> FAULT input 0 -> FAULT

Each state is a constant record containing its handler. The structure of the machine is visible as data, and the handler for any state is one indirect call away.

7. Interrupt Handlers Are Function Pointers: The Vector Table

On the Cortex-M4 the first words of Flash form the vector table: an array of 32-bit values. Word 0 is the initial stack pointer; word 1 is the address of Reset_Handler; the next words are the exception handlers and then the device interrupts, in order. When an event occurs, the core reads the entry at 4 × exception number and branches to it. In C the table is just an array of function pointers placed at 0x08000000 by the linker script.

OffsetExceptionNumberEntry
0x00Initial stack pointer–top of SRAM, 0x20020000 for the 128 KB
0x04Reset1Reset_Handler
0x3CSysTick15SysTick_Handler
0x58EXTI line 0 (IRQ 6)22EXTI0_IRQHandler
0xD8USART2 (IRQ 38)54USART2_IRQHandler

The offset follows from the exception number: for interrupt n (IRQ number) the number is 16 + n and the offset 4 × (16 + n). IRQ 6 gives 4 × 22 = 0x58; IRQ 38 gives 4 × 54 = 0xD8.

vector_sim.c
#include <stdio.h> #include <stdint.h> typedef void (*handler_t)(void); static void Reset_Handler(void) { printf(” [Reset_Handler] start-up code runs\n”); } static void SysTick_Handler(void) { printf(” [SysTick_Handler] 1 ms tick\n”); } static void EXTI0_IRQHandler(void) { printf(” [EXTI0_IRQHandler] PA0 button\n”); } static void USART2_IRQHandler(void) { printf(” [USART2_IRQHandler] byte received\n”); } static void Default_Handler(void) { printf(” [Default_Handler] unexpected interrupt\n”); } /* simulated table, indexed by exception number (word index = offset / 4) */ static const handler_t vectors[60] = { [1] = Reset_Handler, [15] = SysTick_Handler, [16 + 6] = EXTI0_IRQHandler, [16 + 38] = USART2_IRQHandler, }; static void take_exception(unsigned n) { handler_t h = vectors[n] ? vectors[n] : Default_Handler; printf(“exception %2u -> table offset 0x%02X\n”, n, n * 4u); h(); } int main(void) { take_exception(1); take_exception(15); take_exception(16 + 6); take_exception(16 + 38); take_exception(16 + 10); /* no handler installed for this IRQ */ return 0; }
Output (verified by compiling and running)
exception 1 -> table offset 0x04 [Reset_Handler] start-up code runs exception 15 -> table offset 0x3C [SysTick_Handler] 1 ms tick exception 22 -> table offset 0x58 [EXTI0_IRQHandler] PA0 button exception 54 -> table offset 0xD8 [USART2_IRQHandler] byte received exception 26 -> table offset 0x68 [Default_Handler] unexpected interrupt

Real start-up files (for example startup_stm32f407xx.s) fill unused entries with weak aliases of Default_Handler, an endless loop, so that an unexpected interrupt is easy to spot in a debugger. You override a handler simply by defining a function with the same name.

The Thumb bit: The Cortex-M4 executes only Thumb code. Every address stored in the vector table (and every function pointer that the compiler creates on this core) has bit 0 set to 1 to signal Thumb state. A function at 0x08000190 appears as 0x08000191. If you ever build a pointer by hand from an integer, set bit 0, or the branch raises a UsageFault.

8. Callbacks with a Context Pointer

A plain callback has no way to know which object triggered it. The remedy is a second parameter, a void * context, chosen by the registrant and passed back unchanged. The callback casts it to the real type.

callback_ctx.c
#include <stdio.h> typedef void (*timer_cb_t)(void *ctx); typedef struct { timer_cb_t cb; void *ctx; unsigned period; unsigned left; } soft_timer_t; static void timer_start(soft_timer_t *t, unsigned period, timer_cb_t cb, void *ctx) { t->period = t->left = period; t->cb = cb; t->ctx = ctx; } static void timer_tick(soft_timer_t *t) /* call every 1 ms */ { if (t->left && –t->left == 0) { t->left = t->period; /* periodic */ if (t->cb) t->cb(t->ctx); } } typedef struct { const char *name; unsigned pin; unsigned toggles; } led_t; static void led_toggle_cb(void *ctx) { led_t *led = (led_t *)ctx; /* recover the object */ led->toggles++; printf(” %s (PD%u) toggled, count=%u\n”, led->name, led->pin, led->toggles); } int main(void) { led_t green = { “green”, 12, 0 }, red = { “red”, 14, 0 }; soft_timer_t t1, t2; timer_start(&t1, 3, led_toggle_cb, &green); timer_start(&t2, 4, led_toggle_cb, &red); for (int ms = 1; ms <= 12; ms++) { printf(“ms %2d\n”, ms); timer_tick(&t1); timer_tick(&t2); } return 0; }
Output (verified by compiling and running)
ms 1 ms 2 ms 3 green (PD12) toggled, count=1 ms 4 red (PD14) toggled, count=1 ms 5 ms 6 green (PD12) toggled, count=2 ms 7 ms 8 red (PD14) toggled, count=2 ms 9 green (PD12) toggled, count=3 ms 10 ms 11 ms 12 green (PD12) toggled, count=4 red (PD14) toggled, count=3

One callback function serves both LEDs; the context tells it which LED to act on. The pattern removes the need for global variables and makes the code reusable, the same trick that HAL libraries use with their void * user-data parameters.

9. Pitfalls

PitfallWhat happensPrevention
Calling NULLBranch to address 0: UsageFault, usually HardFaultCheck optional callbacks, initialise tables
Wrong signature through a castUndefined behavior; wrong registers readNever cast between function pointer types; use one typedef
Function pointer stored as integerThumb bit lost (UsageFault)Keep the pointer type; set bit 0 if you must convert
Callback changes state used by the callerRe-entrancy surprisesDocument what the callback may call
Callback called from an ISR does slow workMissed interruptsSet a flag in the callback; do the work in main
Table not constTable in RAM, can be overwrittenstatic const places it in Flash
fpcast.c
static int add(int a, int b) { return a + b; } int main(void) { void (*fp)(void) = add; /* the signatures differ */ fp(); return 0; }
GCC warns about an incompatible function pointer
fpcast.c: In function ‘main’: fpcast.c:5:24: warning: initialization of ‘void (*)(void)’ from incompatible pointer type ‘int (*)(int, int)’ [-Wincompatible-pointer-types] 5 | void (*fp)(void) = add; /* the signatures differ */ | ^~~

The warning is real: calling add through a void (*)(void) pointer would leave its arguments undefined. Keep the warnings on and treat this one as an error (-Werror=incompatible-pointer-types).

10. Best Practices for Function Pointers in Embedded C

  • Give every function pointer type a typedef ending in _t or _fn.
  • Initialise pointers; check optional ones for NULL before the call.
  • Make dispatch tables static const so they sit in Flash.
  • Pass a void * context with every callback registration.
  • Keep callbacks short; if called from an ISR, only set flags or copy data.
  • Do not cast between function pointer types; write a wrapper with the correct signature instead.
  • Document the context in which a callback runs (interrupt or main) and what it may call.
  • Limit the depth of callback chains; they hide the control flow from reviewers and from stack analysis.

Key Takeaways — Lesson 8.4

  • A function name is its address; a function pointer stores it and allows an indirect call.
  • Use typedef to make declarations such as int (*fp)(int) readable.
  • Callbacks pass behaviour to a library; add a void * context to identify the caller’s object.
  • A static const dispatch table in Flash replaces a long switch.
  • The vector table is an array of handler addresses with the Thumb bit set; interrupt entry is an indirect call by the hardware.
  • Check for NULL, match signatures exactly and keep ISR callbacks short.

Frequently Asked Questions

What is a function pointer in C?
A variable that holds the address of a function. You can assign different functions with the same signature to it and call whichever it points to through the pointer.
How do you declare a function pointer?
Write the function prototype and replace the name by (*name), for example int (*fp)(int, int). Use a typedef such as typedef int (*binop_t)(int, int); to keep the code readable.
What is a callback function?
A function that you pass (by pointer) to another function or driver so that it is called later, for example when a timer expires or a byte arrives.
How are interrupts related to function pointers?
The vector table at the start of Flash is an array of addresses of the exception and interrupt handlers. When an interrupt occurs the core loads the handler address from the table and branches to it, which is an indirect call.
What happens if I call a NULL function pointer?
The program branches to address 0. Because the Thumb bit of that address is clear, the Cortex-M4 raises a UsageFault, which normally escalates to a HardFault. Always check optional callbacks for NULL.

Ready to practise?

Next: worked examples with command parsers, timers, event handlers, sort callbacks and a simulated vector table.

Need Help or More Information?

Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *