MODULE 8 · LESSON 8.4 · EXAMPLES
Function Pointer Examples in Embedded C: Tables, Callbacks, Schedulers, Driver Interfaces and a Bootloader Check
Eight worked examples with real output: an operator table, map and filter, EXTI handlers, a command parser, a cooperative scheduler, a driver interface, record sorting and application vector-table validation. Study this one of the function pointer examples in embedded C closely.
Real output
Compiled and run
Function Pointer Examples in Embedded C: What This Page Covers
These function pointer examples in embedded C apply Lesson 8.4: an operator table, map and filter callbacks, EXTI line handlers, a command parser with help text, a cooperative task scheduler, driver interfaces built from a struct of function pointers, sorting records and a bootloader validity check on the vector table. Every program was compiled with GCC and run; the output is real. These function pointer examples in embedded C build on each other.
| # | Example | Skill |
|---|
| 1 | A calculator operator table | Array of function pointers |
| 2 | map and filter with callbacks | Generic helpers |
| 3 | EXTI line handlers with registration | NULL-safe callbacks |
| 4 | A command parser with help text | Dispatch struct |
| 5 | A cooperative task scheduler | Periodic callbacks |
| 6 | A driver interface (struct of function pointers) | Polymorphism in C |
| 7 | Sorting records with qsort | Comparator callbacks |
| 8 | Validating an application vector table | Bootloader |
| 9 | Mini exercises | Self-test |
Example 1: A Calculator Operator Table
Problem: Choose an arithmetic function by a numeric code without a switch. The table ops[] is a static const array of function pointers, so on the STM32F407 it would sit in Flash. The only check needed is that the code is within the table size; the table length comes from sizeof, so adding an operator updates the check automatically. The invalid code 4 is rejected before it can index past the array. Among the function pointer examples in embedded C this pattern is very common.
op_table.c
#include <stdio.h>
typedef int (*binop_t)(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 int dvd(int a, int b) { return b ? a / b : 0; } /* division by zero returns 0 here */
static const binop_t ops[] = { add, sub, mul, dvd }; /* index = operator code */
static const char sym[] = { ‘+’, ‘-‘, ‘*’, ‘/’ };
#define N_OPS (sizeof ops / sizeof ops[0])
static int apply(unsigned code, int a, int b, int *result)
{
if (code >= N_OPS) return –1; /* bounds check before indexing */
*result = ops[code](a, b);
return 0;
}
int main(void)
{
for (unsigned c = 0; c < 5; c++)
{
int r;
if (apply(c, 20, 5, &r) == 0) printf(“20 %c 5 = %d\n”, sym[c], r);
else printf(“code %u: invalid operator\n”, c);
}
return 0;
}
Output (verified by compiling and running)
20 + 5 = 25
20 – 5 = 15
20 * 5 = 100
20 / 5 = 4
code 4: invalid operator
Safety: Indexing a function pointer table with an unchecked number is a classic vulnerability: the call jumps to whatever follows the array. Always validate the index.
Example 2: map and filter with Callbacks
Problem: Write the loop once and let the caller supply the rule. map replaces each element by f(element); filter copies the elements for which keep() returns true and reports how many it kept. The same two helpers handle ADC samples, packet bytes or sensor ids; only the callback changes. The caller owns both buffers (Lesson 8.2) and the output array is sized for the worst case. Keep these function pointer examples in embedded C for revision.
map_filter.c
#include <stdio.h>
#include <stddef.h>
static void map(int *a, size_t n, int (*f)(int)) /* apply f to every element */
{
for (size_t i = 0; i < n; i++) a[i] = f(a[i]);
}
static size_t filter(const int *in, size_t n, int *out, int (*keep)(int))
{
size_t m = 0;
for (size_t i = 0; i < n; i++)
if (keep(in[i])) out[m++] = in[i];
return m;
}
static int triple(int x) { return 3 * x; }
static int is_even(int x) { return (x & 1) == 0; }
static int in_range(int x) { return x >= 10 && x <= 50; }
static void show(const char *l, const int *a, size_t n)
{
printf(“%-10s”, l);
for (size_t i = 0; i < n; i++) printf(” %d”, a[i]);
printf(“\n”);
}
int main(void)
{
int v[] = { 4, 15, 22, 7, 50, 61, 10 };
size_t n = sizeof v / sizeof v[0];
int out[7];
show(“input:”, v, n);
size_t m = filter(v, n, out, is_even); show(“even:”, out, m);
m = filter(v, n, out, in_range); show(“10..50:”, out, m);
map(v, n, triple); show(“tripled:”, v, n);
return 0;
}
Output (verified by compiling and running)
input: 4 15 22 7 50 61 10
even: 4 22 50 10
10..50: 15 22 50 10
tripled: 12 45 66 21 150 183 30
Cost: Each element costs one indirect call, a few cycles on the Cortex-M4. For a very hot loop, a static inline function lets the compiler inline the rule instead.
Example 3: EXTI Line Handlers with Registration
Problem: Let application code attach its own handler to an STM32 external interrupt line. The driver keeps a table of 16 callback and context pairs (zero at start because it is static), the registration function validates the line number and the dispatch loop acts on every pending bit. Line 9 has no handler and is reported instead of crashing: an unset callback is a normal case. Each pending bit is cleared with &= ~(1u << line); on the real EXTI_PR register you write a 1 to the bit to clear it, while all other bits written as 0 stay unchanged. The simulated register in this program only mimics that.
exti_cb.c
#include <stdio.h>
#include <stdint.h>
#define EXTI_LINES 16
typedef void (*exti_cb_t)(uint8_t line, void *ctx);
static struct { exti_cb_t cb; void *ctx; } s_table[EXTI_LINES]; /* all NULL at start (.bss) */
static int exti_register(uint8_t line, exti_cb_t cb, void *ctx)
{
if (line >= EXTI_LINES) return –1;
s_table[line].cb = cb;
s_table[line].ctx = ctx;
return 0;
}
static uint32_t EXTI_PR; /* simulated pending register */
static void exti_dispatch(void) /* what the shared ISR does */
{
for (uint8_t line = 0; line < EXTI_LINES; line++)
if (EXTI_PR & (1u << line))
{
EXTI_PR &= ~(1u << line); /* the real register: write 1 to clear */
if (s_table[line].cb) s_table[line].cb(line, s_table[line].ctx);
else printf(” line %u: no handler\n”, line);
}
}
static void on_user_button(uint8_t line, void *ctx)
{
unsigned *presses = ctx;
(*presses)++;
printf(” line %u: user button (press #%u)\n”, line, *presses);
}
static void on_sensor(uint8_t line, void *ctx)
{
(void)ctx;
printf(” line %u: sensor data ready\n”, line);
}
int main(void)
{
unsigned presses = 0;
exti_register(0, on_user_button, &presses); /* PA0 */
exti_register(5, on_sensor, NULL); /* PB5, for example */
printf(“register line 16 -> %d (rejected)\n”, exti_register(16, on_sensor, NULL));
EXTI_PR = (1u << 0) | (1u << 5) | (1u << 9); /* three events pending */
printf(“dispatch:\n”);
exti_dispatch();
printf(“pending after dispatch: 0x%X\n”, (unsigned)EXTI_PR);
EXTI_PR = 1u << 0;
exti_dispatch();
return 0;
}
Output (verified by compiling and running)
register line 16 -> -1 (rejected)
dispatch:
line 0: user button (press #1)
line 5: sensor data ready
line 9: no handler
pending after dispatch: 0x0
line 0: user button (press #2)
Real hardware: On the STM32F407 the lines 5 to 9 share the vector EXTI9_5_IRQHandler and lines 10 to 15 share EXTI15_10_IRQHandler. A shared ISR must read EXTI_PR and run a dispatch loop like the one above.
Example 4: A Command Parser with Help Text
Problem: A serial console should accept commands, check the argument count and print its own help. Each table row holds the name, the minimum number of arguments, the handler and a help string, all in one static const structure; the help command simply walks the same table, so documentation can never fall out of date. Handlers receive argc/argv like main. The line buffers are arrays rather than string literals because strtok writes into its input.
cmd_parser.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef int (*cmd_fn)(int argc, char *argv[]);
typedef struct { const char *name; int min_args; cmd_fn run; const char *help; } cmd_t;
static int cmd_led(int argc, char *argv[]) { (void)argc; printf(” LED %s = %s\n”, argv[1], argv[2]); return 0; }
static int cmd_add(int argc, char *argv[]) { int s = 0; for (int i = 1; i < argc; i++) s += atoi(argv[i]); printf(” sum = %d\n”, s); return 0; }
static int cmd_help(int argc, char *argv[]);
static const cmd_t cmds[] = {
{ “led”, 2, cmd_led, “led <n> <on|off> switch an LED” },
{ “add”, 1, cmd_add, “add <n> … add numbers” },
{ “help”, 0, cmd_help, “help list commands” },
};
#define NCMD (sizeof cmds / sizeof cmds[0])
static int cmd_help(int argc, char *argv[])
{
(void)argc; (void)argv;
for (size_t i = 0; i < NCMD; i++) printf(” %s\n”, cmds[i].help);
return 0;
}
static int run_line(char *line)
{
char *argv[8];
int argc = 0;
for (char *t = strtok(line, ” “); t && argc < 8; t = strtok(NULL, ” “)) argv[argc++] = t;
if (argc == 0) return 0;
for (size_t i = 0; i < NCMD; i++)
if (strcmp(argv[0], cmds[i].name) == 0)
{
if (argc – 1 < cmds[i].min_args) { printf(” usage: %s\n”, cmds[i].help); return –1; }
return cmds[i].run(argc, argv);
}
printf(” unknown command ‘%s’\n”, argv[0]);
return –1;
}
int main(void)
{
char l1[] = “help”, l2[] = “led 3 on”, l3[] = “add 4 5 6”, l4[] = “led 3”, l5[] = “fly”;
char *lines[] = { l1, l2, l3, l4, l5 };
for (int i = 0; i < 5; i++)
{
printf(“> %s\n”, lines[i]);
run_line(lines[i]);
}
return 0;
}
Output (verified by compiling and running)
> help
led <n> <on|off> switch an LED
add <n> … add numbers
help list commands
> led 3 on
LED 3 = on
> add 4 5 6
sum = 15
> led 3
usage: led <n> <on|off> switch an LED
> fly
unknown command ‘fly’
Embedded note: Over a UART the input arrives byte by byte: collect a line in a buffer with a size limit, then call the same run_line. The table costs no RAM.
Example 5: A Cooperative Task Scheduler
Problem: Run three activities at different periods without an RTOS. Each task is a record with a function pointer, a period and the time it is next due. The scheduler loop, called from main with the millisecond tick, runs a task when (int32_t)(now - next_due) >= 0; this subtraction trick keeps working when the 32-bit tick counter wraps around after about 49 days. The sensor runs every 20 ms, the LED every 50 ms and the status line every 100 ms; at t = 0 all three are due. Because the tasks run one after another in main, none of them may block.
scheduler.c
#include <stdio.h>
#include <stdint.h>
typedef void (*task_fn)(void);
typedef struct { const char *name; task_fn run; uint32_t period_ms; uint32_t next_due; } task_t;
static void read_sensor(void) { printf(” [read_sensor] sample ADC\n”); }
static void blink_led(void) { printf(” [blink_led] toggle PD12\n”); }
static void send_status(void) { printf(” [send_status] transmit status line\n”); }
static task_t tasks[] = {
{ “sensor”, read_sensor, 20, 0 },
{ “led”, blink_led, 50, 0 },
{ “status”, send_status, 100, 0 },
};
#define NTASK (sizeof tasks / sizeof tasks[0])
static void scheduler_run(uint32_t now_ms)
{
for (unsigned i = 0; i < NTASK; i++)
if ((int32_t)(now_ms – tasks[i].next_due) >= 0) /* wrap-safe comparison */
{
tasks[i].next_due = now_ms + tasks[i].period_ms;
tasks[i].run();
}
}
int main(void)
{
for (uint32_t t = 0; t <= 100; t += 10)
{
printf(“t=%3u ms\n”, (unsigned)t);
scheduler_run(t);
}
return 0;
}
Output (verified by compiling and running)
t= 0 ms
[read_sensor] sample ADC
[blink_led] toggle PD12
[send_status] transmit status line
t= 10 ms
t= 20 ms
[read_sensor] sample ADC
t= 30 ms
t= 40 ms
[read_sensor] sample ADC
t= 50 ms
[blink_led] toggle PD12
t= 60 ms
[read_sensor] sample ADC
t= 70 ms
t= 80 ms
[read_sensor] sample ADC
t= 90 ms
t=100 ms
[read_sensor] sample ADC
[blink_led] toggle PD12
[send_status] transmit status line
Rule: A task must return quickly. A long delay inside one task delays all the others; use a state machine (Lesson 8.4 theory) when an activity needs several steps.
Example 6: A Driver Interface (Struct of Function Pointers)
Problem: Let the same application code write to a real UART in firmware and to a memory buffer in a unit test. The interface serial_ops_t is a struct of function pointers; each implementation fills one static const instance. The function report receives a pointer to the operations table and never mentions UART registers. This is polymorphism in plain C and is how HAL layers, file systems and USB stacks are structured. The UART functions here are simulated with a counter so that the program runs on a PC.
serial_ops.c
#include <stdio.h>
#include <string.h>
#include <stddef.h>
/* the interface: a struct of function pointers */
typedef struct
{
int (*write)(const char *data, size_t len);
size_t (*bytes_sent)(void);
} serial_ops_t;
/* implementation A: the UART (simulated) */
static size_t uart_count;
static int uart_write(const char *d, size_t n) { (void)d; uart_count += n; printf(” UART TX %zu bytes\n”, n); return 0; }
static size_t uart_sent(void) { return uart_count; }
static const serial_ops_t uart_ops = { uart_write, uart_sent };
/* implementation B: a memory log used in tests */
static char logbuf[32];
static size_t loglen;
static int mem_write(const char *d, size_t n)
{
if (loglen + n >= sizeof logbuf) return –1;
memcpy(logbuf + loglen, d, n);
loglen += n;
logbuf[loglen] = ‘\0’;
return 0;
}
static size_t mem_sent(void) { return loglen; }
static const serial_ops_t mem_ops = { mem_write, mem_sent };
/* application code depends only on the interface */
static int report(const serial_ops_t *io, int value)
{
char msg[16];
int n = snprintf(msg, sizeof msg, “V=%d;”, value);
return io->write(msg, (size_t)n);
}
int main(void)
{
report(&uart_ops, 123);
report(&uart_ops, 45);
printf(“UART total: %zu bytes\n”, uart_ops.bytes_sent());
report(&mem_ops, 7);
report(&mem_ops, 8);
printf(“memory log: \”%s\” (%zu bytes)\n”, logbuf, mem_ops.bytes_sent());
return 0;
}
Output (verified by compiling and running)
UART TX 6 bytes
UART TX 5 bytes
UART total: 11 bytes
memory log: “V=7;V=8;” (8 bytes)
Testing: The memory log lets a test check exactly what would have been transmitted, without hardware.
Example 7: Sorting Records with qsort
Problem: Order sensor records by different keys. qsort knows nothing about reading_t; it receives the base address, the element count, the element size and a comparison callback. The callback casts the two const void * arguments back to the real type and returns a negative, zero or positive number. Passing a different callback changes the sort key without touching the sorting code. The expression (a > b) - (a < b) is the safe way to compare: subtracting the two values can overflow.
sort_records.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct { int id; int temp_c; const char *name; } reading_t;
static int by_temp(const void *a, const void *b)
{
const reading_t *x = a, *y = b;
return (x->temp_c > y->temp_c) – (x->temp_c < y->temp_c);
}
static int by_name(const void *a, const void *b)
{
const reading_t *x = a, *y = b;
return strcmp(x->name, y->name);
}
static void show(const char *label, const reading_t *r, int n)
{
printf(“%s\n”, label);
for (int i = 0; i < n; i++) printf(” id=%d %-8s %3d C\n”, r[i].id, r[i].name, r[i].temp_c);
}
int main(void)
{
reading_t r[] = { {1, 41, “motor”}, {2, 23, “board”}, {3, 67, “driver”}, {4, 30, “battery”} };
int n = sizeof r / sizeof r[0];
qsort(r, n, sizeof r[0], by_temp); show(“sorted by temperature:”, r, n);
qsort(r, n, sizeof r[0], by_name); show(“sorted by name:”, r, n);
return 0;
}
Output (verified by compiling and running)
sorted by temperature:
id=2 board 23 C
id=4 battery 30 C
id=1 motor 41 C
id=3 driver 67 C
sorted by name:
id=4 battery 30 C
id=2 board 23 C
id=3 driver 67 C
id=1 motor 41 C
Embedded caution: qsort may use recursion and a temporary buffer internally, depending on the C library. On a small MCU use a simple insertion sort with the same callback signature if the stack is tight.
Example 8: Validating an Application Vector Table (Bootloader)
Problem: A bootloader at 0x08000000 must decide whether the application at 0x08008000 is safe to start. The application image begins with its own vector table: word 0 is the initial stack pointer, word 1 the address of its Reset_Handler. The check requires the stack pointer to lie in SRAM and be word aligned, the reset address to have the Thumb bit set and to point inside the application region. The real jump loads the stack pointer into MSP (with the CMSIS function __set_MSP), then calls the reset address through a function pointer. Here the call is replaced by a stand-in function so that the program can run on a PC. The last test shows a deliberately strict policy: this bootloader accepts only a stack in the main 128 KB SRAM and rejects one in CCM RAM (0x10000000), even though the core could use it.
boot_check.c
#include <stdio.h>
#include <stdint.h>
#define SRAM_START 0x20000000u
#define SRAM_END 0x20020000u /* 128 KB: 0x20000000 + 0x20000 */
#define APP_START 0x08008000u /* application after a 32 KB bootloader */
#define FLASH_END 0x08100000u /* 1 MB device */
/* an application image starts with: [0] initial SP, [1] address of Reset_Handler (Thumb bit set) */
static int app_is_valid(const uint32_t *image)
{
uint32_t sp = image[0];
uint32_t reset = image[1];
if (sp < SRAM_START || sp > SRAM_END) return 0; /* stack pointer must point into SRAM */
if ((sp & 3u) != 0) return 0; /* and be word aligned */
if ((reset & 1u) == 0) return 0; /* Thumb bit must be set */
if ((reset & ~1u) < APP_START || (reset & ~1u) >= FLASH_END) return 0; /* inside application Flash */
return 1;
}
static void start_application(void) { printf(” jumping to Reset_Handler of the application\n”); }
typedef void (*entry_fn)(void);
int main(void)
{
uint32_t good[] = { 0x20020000u, 0x08008191u }; /* typical: SP at top of SRAM, odd address */
uint32_t erased[] = { 0xFFFFFFFFu, 0xFFFFFFFFu }; /* blank Flash reads 0xFF */
uint32_t no_thumb[] = { 0x20020000u, 0x08008190u }; /* bit 0 clear: UsageFault if executed */
uint32_t wrong_sp[] = { 0x10000000u, 0x08008191u }; /* CCM, not main SRAM: rejected by this policy */
const struct { const char *name; const uint32_t *img; } tests[] = {
{ “good”, good }, { “erased”, erased }, { “no Thumb bit”, no_thumb }, { “SP outside SRAM”, wrong_sp } };
entry_fn entry = start_application; /* stands in for the real jump */
for (int i = 0; i < 4; i++)
{
int ok = app_is_valid(tests[i].img);
printf(“%-16s -> %s\n”, tests[i].name, ok ? “valid” : “REJECTED”);
if (ok) entry();
}
return 0;
}
Output (verified by compiling and running)
good -> valid
jumping to Reset_Handler of the application
erased -> REJECTED
no Thumb bit -> REJECTED
SP outside SRAM -> REJECTED
Why this matters: Jumping to blank Flash (0xFFFFFFFF) loads a garbage stack pointer and a garbage entry, a guaranteed HardFault loop. A five-line check turns that into a clean “no application” message.
Example 9: Mini Exercises
Exercise 1: Write the declaration of a pointer h to a function taking uint8_t and returning void.
void (*h)(uint8_t);, or with a typedef typedef void (*handler_t)(uint8_t); handler_t h;
Exercise 2: What is wrong with ops[code](a, b) when code is 7 and the table has 4 entries?
It reads past the end of the array and calls whatever bits are found there. Check code < N_OPS first.
Exercise 3: Why does the scheduler compare (int32_t)(now - next_due) >= 0 instead of now >= next_due?
The subtraction is correct even when the 32-bit counter wraps around; the plain comparison fails at the wrap.
Exercise 4: Why is the dispatch table static const?
It is placed in Flash, uses no RAM and cannot be overwritten by a stray pointer.
Exercise 5: Why must app_is_valid test bit 0 of the reset address?
On Cortex-M the Thumb bit must be 1; with 0 the core would try to enter ARM state, which it does not support, and raises a UsageFault.
Exercise 6: Which function in Example 6 knows about UART registers?
Only the UART implementation (uart_write); report sees just the interface.
Exercise 7: How would you add a command reset to Example 4?
Write the handler, then add one row to the table: { "reset", 0, cmd_reset, "reset restart" }.
Key Takeaways from the Function Pointer Examples in Embedded C
- These function pointer examples in embedded C show that a table of function pointers replaces switch statements and keeps data and behaviour together.
- Callbacks let a driver or library call application code it knows nothing about; always handle an unset callback.
- A struct of function pointers defines an interface that hardware and test code can both implement.
- Check table indexes, NULL pointers and the Thumb bit before an indirect call or jump.
- Keep ISR-called callbacks short; use
static const tables to save RAM.
Ready for the quiz?
Next: predict function pointer output, spot the declaration errors and answer the function pointer interview questions.
2 Comments