Mastering C State Machine Implementations: Switch‑Case, Table‑Driven, and Function‑Pointer Techniques
This article explains three core ways to implement finite state machines in C—switch‑case, table‑driven, and function‑pointer approaches—detailing their structures, code examples, advantages, pitfalls, and how they relate to extended and hierarchical state machines.
Overview
A finite state machine (FSM) consists of three elements: states, events, and responses. Implementing an FSM in C can be done using three main techniques: the switch‑case method, the table‑driven method, and the function‑pointer method.
1. Switch‑Case Method
The switch‑case method nests two switch statements: one for the current state and another for the event. Each state case contains a full inner switch handling all possible events. Example code (List 4) shows the generic structure:
switch(StateVal){
case S0:
switch(EvntID){
case E1:
action_S0_E1();
StateVal = new_state_value; // state transition
break;
case E2:
action_S0_E2();
StateVal = new_state_value;
break;
// ... other events ...
default:
break;
}
break;
case S1:
// similar nested switch
break;
// ... other states ...
default:
break;
}In practice many events are irrelevant for a given state, so unnecessary case branches can be omitted. Two ordering styles exist: “state‑nest‑event” and “event‑nest‑state”. The order of cases affects lookup time; high‑frequency states/events should be placed early.
2. Table‑Driven Method
The table‑driven method stores the state‑event relationship in a two‑dimensional array, where rows represent states and columns represent events. Each cell contains a struct (List 5) that holds a function pointer fpAction and the next‑state value u8NxtStat:
struct fsm_node {
void (*fpAction)(void *pEvnt);
INT8U u8NxtStat;
};The driver code (List 6) looks up the appropriate node based on the current state and event, calls the action, and then updates the state:
extern struct fsm_node g_arFsmDrvTbl[][]; // driver table
INT8U u8CurStat = 0; // current state
INT8U u8EvntTyp = 0; // current event type
void *pEvnt = NULL; // event data pointer
struct fsm_node stNodeTmp = {NULL, 0};
u8CurStat = get_cur_state();
u8EvntTyp = get_cur_evnt_typ();
pEvnt = (void*)get_cur_evnt_ptr();
stNodeTmp = g_arFsmDrvTbl[u8CurStat][u8EvntTyp];
stNodeTmp.fpAction(pEvnt);
set_cur_state(stNodeTmp.u8NxtStat);Enums are recommended for both state and event sets because they provide a compact, incrementing integer sequence (starting at 0 with step 1) that matches array indexing.
3. Compressed Table‑Driven Method
To combine the simplicity of the switch‑case method with the efficiency of the table‑driven method, the compressed table‑driven approach uses a one‑dimensional array indexed by state. Each element (List 7) contains a function pointer, a state‑check value, and the action returns the next state:
struct fsm_node {
INT8U (*fpAction)(void *pEvnt); // action returns next state
INT8U u8StatChk; // verifies the node matches current state
};Framework code checks that u8StatChk matches the current state before invoking fpAction. If the check fails, an error handler is called to avoid illegal jumps.
Action functions (List 8) follow the pattern:
INT8U action_S0(void *pEvnt) {
INT8U u8NxtStat = 0;
INT8U u8EvntTyp = get_evnt_typ(pEvnt);
switch(u8EvntTyp) {
case E1:
action_S0_E1();
u8NxtStat = new_state_value;
break;
case Em:
action_S0_Em();
u8NxtStat = new_state_value;
break;
default:
; // ignore unrelated events
break;
}
return u8NxtStat; // next state
}This method solves the “Extended State Machine” limitation of the plain table‑driven approach because the action can decide the next state based on additional conditions.
4. Function‑Pointer Method
The function‑pointer method treats the action function’s address itself as the state representation. A global function pointer holds the current state handler, and each action returns the address of the next handler. This is conceptually similar to the compressed table‑driven method but without an explicit table.
Safety is a concern because there is no straightforward way to verify that the stored function pointer is valid; corruption can cause the program to jump to arbitrary code.
5. Conclusions and Practical Tips
Choose the switch‑case method for simple, small FSMs where readability outweighs performance.
Use the table‑driven method for larger FSMs to gain O(1) lookup and separate data from logic.
Adopt the compressed table‑driven method when you need both efficiency and the flexibility of extended state machines.
Be cautious with the pure function‑pointer method; add validation if possible.
Always define states and events as enums to ensure proper indexing and avoid out‑of‑bounds accesses.
Understanding these techniques enables developers to model complex behavior in embedded C projects, from simple device control to hierarchical state machines.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
