Ground Embedded KO Subscribe

Notes / C Language

Advanced pointers and function pointers

Pointer arithmetic, double pointers, and the function pointers used for callbacks and state machines.

GROUND EMBEDDED · NOTEC Language 07Advanced pointers and function pointers 1volatile uint32_t *ODR 2 = (uint32_t*)0x40020014; 3*ODR |= (1u << 5); 4while (1) { 5 if (flag) toggle(); 6}

Pointer arithmetic

p + 1 does not add 1 to the address; it moves by the size of the pointed-to type. For uint32_t *p it steps over 4 bytes. Because an array name behaves like the address of its first element, buf[i] and *(buf + i) are the same.

Double pointers

A pointer that holds the address of a pointer. Use it when a function needs to change the pointer itself, for example to allocate a buffer and hand it back.

Function pointers

on_rxvariable holding a function addresscall on_rx(byte)parse_command()jump to the stored address
A function pointer keeps "which function to call" in a variable.

Functions have addresses too. Store that address in a variable and you can choose later which function to run.

typedef void (*handler_t)(uint8_t);
handler_t on_rx = parse_command;
on_rx(byte);   // runs parse_command(byte)

In firmware they are used in these places.

  • Callbacks: a driver telling the code above it that something happened, such as "receive complete"
  • State machines: put each state's handler in a table and call the function for the current state
  • Vector table: the array of interrupt handler addresses is itself a table of function pointers

Functions have addresses too

A function is code placed at some address in Flash. A function pointer is a variable holding that address, and you can jump to the stored address and run it.

void led_on(void)  { /* ... */ }
void led_off(void) { /* ... */ }

void (*action)(void) = led_on;     // store the address
action();                          // run the function at that address
action = led_off;                  // can be changed at run time

If the declaration looks unfamiliar, give it a name with typedef.

typedef void (*handler_t)(uint8_t arg);

Use 1: a command table

Received 0x02Look up the table0x01 → read status0x02 → set speed0x10 → reset
Look up the table by command number and call the paired function. Adding a command is one more line in the table.

Instead of a switch that keeps growing, pair command numbers with handler functions in a table.

typedef struct { uint8_t id; void (*fn)(const uint8_t *data, uint8_t len); } cmd_t;

static const cmd_t table[] = {
    { 0x01, cmd_read_status },
    { 0x02, cmd_set_speed   },
    { 0x10, cmd_reset       },
};

void dispatch(uint8_t id, const uint8_t *data, uint8_t len) {
    for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++)
        if (table[i].id == id) { table[i].fn(data, len); return; }
    send_nack(id);                 // command not in the table
}

Adding a command means adding one line to the table. The table is const, so it sits in Flash and uses no RAM.

RoadmapSee where this note sits in C Language→

Use 2: separating layers with callbacks

If a driver calls application code directly, the two become tangled. Instead, the application registers "call this function when the work is done".

static void (*rx_cb)(uint8_t byte);
void uart_set_rx_callback(void (*cb)(uint8_t)) { rx_cb = cb; }

void USART1_IRQHandler(void) {
    uint8_t b = (uint8_t)USART1->RDR;
    if (rx_cb) rx_cb(b);           // check for NULL, then call
}

The UART driver does not know who uses the data. The same driver can be carried into another project unchanged.

Use 3: a state machine

state_idlestart button?state_runningfault?state_error
The current state is the function to run. When the condition is met, switch to the next state's function.
typedef void (*state_fn)(void);
static state_fn state = state_idle;

void state_idle(void)    { if (start_pressed()) state = state_running; }
void state_running(void) { if (fault())         state = state_error;   }

while (1) { state(); }

The current state is the function to run. However many states there are, the main loop stays one line.

The vector table is an array of function pointers

An MCU's interrupt vector table is an array of function pointers listing handler addresses in order. When an interrupt fires, the CPU reads the address in the slot for that number and jumps there.

Problems that come up in real work

  • Calling NULL: calling a callback that was never registered jumps to address 0 and hard-faults. Check before calling.
  • Assigning a function of a different type: casting in a function with a different number or kind of arguments compiles, but the stack no longer lines up.
  • A long callback called from an ISR: the callback runs in interrupt context. Agree that it must finish quickly.
  • Table order and numbers out of step: a table looked up directly by index needs numbers and positions to match. Check the range first.
FREE PDFGet the 3 free ebooksIf you want to keep reading this topic as one book: BLDC motor control, serial communication and sensors at book length. PDFs are in Korean.Get them free →