Advanced pointers and function pointers
Pointer arithmetic, double pointers, and the function pointers used for callbacks and state machines.
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
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
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.
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
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.