Mastering C State Machines: Switch‑Case, Table‑Driven, and Function‑Pointer Techniques
This article explains how to implement finite state machines in C using three classic approaches—switch‑case, table‑driven, and function‑pointer—detailing their structures, code examples, trade‑offs, and safety considerations for embedded and general‑purpose software development.
1. Switch‑Case Method
The core of a state machine consists of three elements: state, event, and response. In C, the simplest implementation nests two switch statements: the outer one selects the current state, the inner one selects the event, and each case calls an action function and optionally updates the state variable.
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;
// ...
default:
break;
}
break;
case S1:
// ...
break;
// ...
default:
break;
}Two nesting styles are possible—state‑nested‑event or event‑nested‑state—each with its own pros and cons. Because switch compares sequentially, frequently used states and events should be placed early to minimise lookup time.
2. Table‑Driven Method
The table‑driven approach stores the relationship between states and events in a two‑dimensional array. Rows represent states, columns represent events, and each cell contains a structure describing the action function pointer and the next state.
struct fsm_node {
void (*fpAction)(void *pEvnt);
INT8U u8NxtStat;
};Typical usage:
extern struct fsm_node g_arFsmDrvTbl[][]; // driver table
INT8U u8CurStat = 0; // current state
INT8U u8EvntTyp = 0; // current event type
void *pEvnt = NULL; // pointer to event data
u8CurStat = get_cur_state();
u8EvntTyp = get_cur_evnt_typ();
pEvnt = (void*)get_cur_evnt_ptr();
struct fsm_node stNodeTmp = g_arFsmDrvTbl[u8CurStat][u8EvntTyp];
stNodeTmp.fpAction(pEvnt); // execute action
set_cur_state(stNodeTmp.u8NxtStat); // state transitionTo keep the table safe, the state set must be a contiguous integer sequence starting at 0 with a step of 1—typically an enum. This guarantees that array indexing stays within bounds.
3. Compressed (One‑Dimensional) Table‑Driven Method
To combine the efficiency of a one‑dimensional array with the flexibility of a full table, the compressed method stores only a function pointer per state. The action function now returns the next state, eliminating the need for a separate next‑state field in the table.
struct fsm_node {
INT8U (*fpAction)(void *pEvnt); // action returns next state
INT8U u8StatChk; // sanity‑check index
};Framework code checks that the current state matches u8StatChk before invoking the function pointer, preventing illegal state accesses.
if (stNodeTmp.u8StatChk == u8CurStat) {
u8CurStat = stNodeTmp.fpAction(pEvnt); // action decides next state
set_cur_state(u8CurStat);
} else {
state_crash(u8CurStat); // illegal state handling
}Action functions 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;
// ...
default:
; // ignore unrelated events
break;
}
return u8NxtStat; // new state
}This design allows an Extended State Machine (ESM) to be used because the action can examine the full event payload via pEvnt and decide both the response and the next state.
4. Function‑Pointer Method
The function‑pointer method treats the action function address itself as the state representation. A global function pointer holds the current state's handler, and each handler returns the address of the next handler. Safety is a concern because there is no built‑in range check for the pointer.
5. Summary and Recommendations
All three techniques are valid for implementing finite state machines in C. The switch‑case method is straightforward but can become unwieldy for many states/events. The table‑driven method offers better scalability and constant‑time lookup, while the compressed table‑driven method adds flexibility for Extended State Machines without sacrificing performance. When using table‑driven approaches, ensure state and event enumerations form a dense, zero‑based sequence to avoid out‑of‑bounds accesses, and consider adding sanity‑check fields (e.g., u8StatChk) to detect corrupted state indices.
Understanding how to model a problem as a state‑transition diagram—identifying states, events, actions, and transitions—is the most valuable skill; the code implementations are merely the concrete realization of that model.
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.
