Hello! Welcome back to your Embedded Interview Preparation Bootcamp.
In our last lesson, we established a critical rule: while arithmetic on a single function pointer is undefined behavior, arithmetic on a pointer to an array of function pointers is perfectly valid. This array-based pattern is not just a theoretical curiosity; it's the foundation for one of the most powerful and efficient control structures in embedded C: the jump table.
Today, we will use this concept to tackle a core topic in embedded systems design: state machines. Your goal is to implement a state machine using a function pointer array (a jump table) and compare its performance against the more traditional switch-based implementation.
State machines are essential for managing complexity in event-driven systems, from simple device control to intricate communication protocols. Mastering their implementation and understanding the deep performance trade-offs is a hallmark of a senior embedded engineer and a frequent subject in interviews at companies like AMD, Nvidia, and Qualcomm.
The Two Faces of State Machine Implementation
State machines can be implemented in many ways, but two approaches dominate in C: the switch statement and the function pointer array. Let's build a simple state machine using both methods to compare them.
Our example will be a simple flashing LED with different modes:
- OFF: The initial state. A 'MODE' event transitions it to ON.
- ON: LED is solid on. A 'MODE' event transitions it to BLINK.
- BLINK: LED is blinking. A 'MODE' event transitions it back to OFF.
An enum will define our states and events:
typedef enum {
STATE_OFF,
STATE_ON,
STATE_BLINK,
STATE_COUNT
} state_t;
typedef enum {
EVENT_MODE_BUTTON,
EVENT_TICK, // Represents passage of time for blinking
EVENT_COUNT
} event_t;
1. The switch-based State Machine
This is often the most intuitive approach. A main switch statement branches on the current state, and nested switch statements can handle events within each state.
Here is a straightforward implementation:
#include <stdio.h> // For printf
// Assume led_off(), led_on(), led_toggle() are defined elsewhere
void led_off(void);
void led_on(void);
void led_toggle(void);
// ... (enums from above) ...
void state_machine_switch(state_t* current_state, event_t event) {
switch (*current_state) {
case STATE_OFF:
if (event == EVENT_MODE_BUTTON) {
printf("Transition: OFF -> ON\n");
led_on();
*current_state = STATE_ON;
}
break;
case STATE_ON:
if (event == EVENT_MODE_BUTTON) {
printf("Transition: ON -> BLINK\n");
// Start with LED on for the first part of the blink cycle
led_on();
*current_state = STATE_BLINK;
}
break;
case STATE_BLINK:
if (event == EVENT_MODE_BUTTON) {
printf("Transition: BLINK -> OFF\n");
led_off();
*current_state = STATE_OFF;
} else if (event == EVENT_TICK) {
// This event would be generated by a timer
printf("Action: Toggling LED in BLINK state\n");
led_toggle();
}
break;
default:
// Should not happen
break;
}
}
Analysis:
- Readability: For a simple machine like this, it's easy to follow.
- Maintainability: As states and events grow, this function can become very large and difficult to manage. Forgetting a
breakis a classic, hard-to-find bug. - Performance: We'll dive deep into this later, but performance can be unpredictable. The compiler might turn this into a highly efficient jump table, or it might generate a less efficient series of comparisons (an "if-else chain").
2. The Jump Table State Machine (Function Pointer Array)
This approach dedicates one function to handle all logic for a single state. The current state is then simply an index into an array of pointers to these "state-handler" functions.
This is a more advanced and scalable technique. The following video provides an exceptionally clear guide on how to build a robust, object-oriented state machine in C using this pattern.
#39 State Machines Part-5: Optimal Implementation in C
This video from Quantum Leaps, LLC, demonstrates what they call an 'optimal' state machine implementation. It's a masterclass in using function pointers to create clean, maintainable, and efficient state handlers.
Please watch the following segments carefully: State Handlers in C (7:03 - 14:36): This is the core of the lesson. It explains how to model each state as a C function (a 'state handler') and how the tran macro facilitates clean state transitions. Applying the Pattern (14:36 - 16:44): See how the concept is applied to all states in the example, including how to handle the initial state transition. The Dispatcher (16:44 - 18:33): This shows how the central dispatch function becomes incredibly simple and efficient: it just calls the current state handler via a function pointer.
Inspired by the video, let's implement our LED flasher using this state-handler pattern.
First, we define our state-handler functions. Each function takes the event as input and returns the next state.
// Forward declarations of our state handlers
static state_t handle_state_off(event_t event);
static state_t handle_state_on(event_t event);
static state_t handle_state_blink(event_t event);
// Array of function pointers (the jump table)
// The index corresponds to the state_t enum value.
static state_t (*const state_table[STATE_COUNT])(event_t) = {
[STATE_OFF] = handle_state_off,
[STATE_ON] = handle_state_on,
[STATE_BLINK] = handle_state_blink
};
// State handler for the OFF state
static state_t handle_state_off(event_t event) {
if (event == EVENT_MODE_BUTTON) {
printf("Transition: OFF -> ON\n");
led_on();
return STATE_ON; // Return the new state
}
return STATE_OFF; // No transition, remain in the same state
}
// State handler for the ON state
static state_t handle_state_on(event_t event) {
if (event == EVENT_MODE_BUTTON) {
printf("Transition: ON -> BLINK\n");
led_on();
return STATE_BLINK;
}
return STATE_ON;
}
// State handler for the BLINK state
static state_t handle_state_blink(event_t event) {
if (event == EVENT_MODE_BUTTON) {
printf("Transition: BLINK -> OFF\n");
led_off();
return STATE_OFF;
} else if (event == EVENT_TICK) {
printf("Action: Toggling LED in BLINK state\n");
led_toggle();
}
return STATE_BLINK;
}
// The dispatcher is now trivial
void state_machine_jumptable(state_t* current_state, event_t event) {
// Look up the function pointer and call it.
// The state handler returns the new state, which we update.
*current_state = state_table[*current_state](event);
}
Analysis:
- Readability & Maintainability: Each state's logic is cleanly encapsulated in its own function. Adding a new state involves creating a new function and adding its pointer to the
state_tablearray, without touching the dispatcher. This is highly scalable. - Performance: The lookup and call is a guaranteed O(1) operation, regardless of the number of states. This predictability is a significant advantage in real-time systems.
3. Performance Deep Dive: Compiler Magic vs. Explicit Design
A common argument is that a good compiler will optimize a switch statement into a jump table, making the manual implementation unnecessary. While this is often true for switch statements with dense, sequential case values (like 0, 1, 2, 3...), it's not guaranteed.
For an interview at a semiconductor company, you are expected to know what happens at the assembly level.
Lookup table vs switch in C embedded software - Stack Overflow
This Stack Overflow discussion is a goldmine for this topic. It features detailed answers from experts who inspect the assembly code generated by different compilers on different architectures (x86 and ARM).
Read the question and the top three answers. Focus on: The initial code examples (in the question itself). The discussion of indirect call costs (pipeline breakage, cache effects). The detailed assembly analysis by Peter Cordes for x86 and ARM. Notice how GCC for ARM can generate an inline jump table (tbb instruction) which is different from a standard memory lookup.
Here is a visual representation of a compiler-generated jump table, similar to what the Stack Overflow post describes. The compiler places the addresses of the code for each case into a table in a read-only data section.

As the resources show, the key performance factors are:
switchStatement:- Best Case: The compiler generates an efficient jump table. If the functions are small, it might even inline them, avoiding function call overhead entirely. This can be faster than the manual jump table.
- Worst Case: For sparse
casevalues (e.g., 1, 10, 1000), the compiler generates a chain ofif-else-ifcomparisons. Performance degrades linearly with the number of states.
- Function Pointer Jump Table:
- Guaranteed O(1) dispatch: You always have a single array lookup and an indirect function call.
- Indirect Call Overhead: This is the main performance penalty. An indirect call (
call *raxon x86,blx r3on ARM) can be difficult for a CPU's branch predictor to handle, potentially causing a pipeline flush. This cost varies significantly between CPU architectures.
The Quantum Leaps video you watched earlier also includes a direct performance measurement on a Cortex-M4 CPU. Let's see the results.
#39 State Machines Part-5: Optimal Implementation in C
The presenter now quantifies the performance of the state-handler implementation and compares it to other methods.
Watch the segment from 27:22 to 29:45. Pay attention to the cycle counts for the 'state handler' (jump table) method versus the 'state table' and 'nested switch' methods. Notice that the state handler method is almost as fast as the most optimized table lookup while being far more maintainable.
The takeaway is clear: while the switch statement could be faster under ideal conditions, the function pointer array gives you predictable and consistently high performance, which is often more valuable in an embedded system.

Conclusion
In this lesson, you implemented two common state machine patterns and, more importantly, analyzed their trade-offs from a system-level perspective.
Key Takeaways for Your Interview:
switch-based machines are simple and effective for a small number of states but scale poorly in terms of maintainability and have less predictable performance.- Function pointer arrays (jump tables) provide a scalable, maintainable, and highly predictable O(1) dispatch mechanism. They are an excellent choice for complex, event-driven systems.
- Performance is nuanced. Be prepared to discuss the "it depends" scenario:
- A
switchwith dense cases often becomes a compiler-generated jump table. - The cost of an indirect function call (pipeline stalls, cache misses) in a manual jump table is a real factor.
- The only way to be certain is to profile and inspect the assembly on the target hardware. Mentioning this shows true senior-level thinking.
- A
In our next lesson, we will explore another powerful use of function pointers: implementing a callback-driven event handler. This pattern is the cornerstone of asynchronous, event-driven programming in embedded C.