Finite State Machines in C
This article is intended for intermediate and advanced C programmers. It introduces the finite state machine (FSM) programming model, explains how states, events, and transitions are represented in C, and demonstrates common implementation techniques used to build predictable and maintainable applications, particularly in embedded systems.
1. What Is a Finite State Machine?
A Finite State Machine (FSM) is a programming model used to represent systems whose behavior depends on their current state. At any given time, the system is in exactly one state, and it changes to another state only when a specific event occurs.
For example, consider a simple automatic door.
Button Press
Closed -----------> Opening
▲ │
│ │ Door Opened
Door Closed ▼
│ Open
│ │
└------ Closing <---┘
Timer Expired
At any moment, the door is in one of four states:
- Closed
- Opening
- Open
- Closing
Events such as pressing a button, the door fully opening, or a timer expiring cause the system to transition from one state to another.
An FSM consists of three fundamental components:
- State – The current operating mode of the system.
- Event – Something that occurs and may trigger a state change.
- Transition – The movement from one state to another in response to an event.
These components interact as follows:
Unlike a sequence of unrelated if or switch statements, an FSM organizes behavior around the current state of the system. The same event can therefore produce different behavior depending on the active state.
For example, pressing the door button while the door is closed starts the opening process.
However, pressing the same button while the door is already open may instead start the closing process.
The event is identical, but the outcome differs because the current state is different.
Finite state machines are widely used in embedded systems because many devices naturally operate as a series of well-defined states. Typical examples include:
- Traffic light controllers
- Elevators
- Washing machines
- Vending machines
- Communication protocols
- User interface menus
By explicitly modeling states and transitions, FSMs make program behavior easier to understand, implement, test, and maintain.
2. States, Events, and Transitions
Every finite state machine is built from three fundamental components: states, events, and transitions. Together, they define how the system behaves as it responds to changing conditions.
States
A state represents the current operating mode of the system. At any point in time, an FSM is in exactly one state.
For example, an automatic door may have the following states:
The active state determines how the system responds to incoming events.
Events
An event is something that occurs while the system is operating. Events may originate from user input, hardware, timers, sensors, or communication interfaces.
Typical events include:
- Button Pressed
- Timer Expired
- Sensor Triggered
- Data Received
- Error Detected
An event simply informs the FSM that something has happened. Whether it causes a change in behavior depends on the current state.
Transitions
A transition is the movement from one state to another in response to an event.
For example, if the door is currently closed, pressing the button causes it to begin opening.
Current State Event Next State
------------------------------------------------
Closed + Button Pressed -> Opening
After the transition completes, Opening becomes the current state.
Likewise, once the door is fully open, a timer may cause it to begin closing.
Current State Event Next State
------------------------------------------------
Open + Timer Expired -> Closing
The Current State Matters
One of the defining characteristics of an FSM is that the same event can produce different results depending on the current state.
For example, consider the event Button Pressed.
Now consider exactly the same event while the door is already open.
The event is identical, but the resulting transition is different because the FSM is in a different state.
Some events may not cause any transition at all.
Since the door is already closed, the event has no effect.
The relationship between states, events, and transitions can be summarized as follows.
+---------------+
| Current State |
+---------------+
│
▼
Receive Event
│
▼
Is a Transition Defined?
│ │
Yes│ │No
▼ ▼
Change State Stay in State
Every finite state machine follows this same principle. It continuously waits for events, determines whether the current state defines a valid transition for that event, and either changes state or remains where it is.
3. Event-Driven State Machines
In many embedded systems, events do not occur in a fixed sequence. Instead, they are generated by external sources such as interrupts, timers, communication peripherals, or user input. An event-driven state machine waits for these events and processes them as they occur.
A typical embedded application follows this pattern.
+------------+
| Initialize |
+------------+
│
▼
+----------------------+
| Wait for an Event |
+----------------------+
│
▼
+----------------------+
| Process the Event |
+----------------------+
│
▼
+----------------------+
| Update the State |
+----------------------+
│
└───────────────┐
▼
Wait for Next Event
Rather than continuously checking every possible condition, the application reacts only when an event is available.
Event Sources
Events can originate from many parts of an embedded system.
For example:
- A button is pressed.
- A timer expires.
- A UART receives a character.
- An ADC conversion completes.
- A sensor detects an object.
Each of these occurrences generates an event that is delivered to the state machine.
Regardless of their source, all events are processed in the same way.
The Main Event Loop
Many embedded applications spend most of their time waiting for events.
The function getEvent() retrieves the next available event, while processEvent() updates the current state based on that event.
This separates event detection from state processing, making the program easier to understand and maintain.
Interrupts and State Machines
In embedded systems, interrupts often detect events, but they typically do not implement the state machine.
Instead, the interrupt records that an event occurred.
The main application loop later removes the event from the queue and passes it to the FSM.
Keeping interrupt service routines short improves responsiveness and avoids performing complex processing at interrupt level.
An event-driven architecture naturally complements finite state machines. Interrupts, timers, and peripherals generate events, while the main application loop processes those events and updates the current state. This separation results in firmware that is modular, predictable, and easier to maintain.
4. Implementing an FSM in C
Once the states and events have been identified, implementing a finite state machine in C is relatively straightforward. The most common approach is to represent states and events using enumerations and use a switch statement to determine how each event is handled.
Defining the States
The possible states are typically represented using an enumeration.
A variable stores the current state of the system.
At any point during execution, currentState identifies the system's operating mode.
Defining the Events
Events are also commonly represented using an enumeration.
typedef enum
{
EVENT_BUTTON_PRESSED,
EVENT_DOOR_OPENED,
EVENT_TIMER_EXPIRED,
EVENT_DOOR_CLOSED
} Event;
Whenever something happens, the corresponding event is passed to the state machine.
Processing Events
A typical implementation uses a function that accepts an event and updates the current state.
void processEvent(Event event)
{
switch (currentState)
{
case STATE_CLOSED:
if (event == EVENT_BUTTON_PRESSED)
{
currentState = STATE_OPENING;
}
break;
case STATE_OPENING:
if (event == EVENT_DOOR_OPENED)
{
currentState = STATE_OPEN;
}
break;
case STATE_OPEN:
if (event == EVENT_TIMER_EXPIRED)
{
currentState = STATE_CLOSING;
}
break;
case STATE_CLOSING:
if (event == EVENT_DOOR_CLOSED)
{
currentState = STATE_CLOSED;
}
break;
}
}
The switch statement selects the current state, while the if statements determine whether the received event causes a transition.
Example Execution
Suppose the state machine begins in the Closed state.
The following events occur.
The state machine processes each event in sequence.
STATE_CLOSED
│
│ EVENT_BUTTON_PRESSED
▼
STATE_OPENING
│
│ EVENT_DOOR_OPENED
▼
STATE_OPEN
│
│ EVENT_TIMER_EXPIRED
▼
STATE_CLOSING
│
│ EVENT_DOOR_CLOSED
▼
STATE_CLOSED
Each event updates the current state, and that new state determines how the next event will be processed.
Representing states and events with enumerations, combined with a switch statement for state processing, is one of the simplest and most widely used techniques for implementing finite state machines in C. It is easy to understand, easy to debug, and suitable for many embedded applications.
The next section introduces table-driven finite state machines, an alternative implementation that replaces large switch statements with transition tables.
5. Table-Driven Finite State Machines
As a state machine grows, the number of states and events increases, and a large switch statement can become difficult to read and maintain. An alternative approach is to represent state transitions as a table.
Instead of embedding the transition logic directly in code, each transition is stored as a table entry.
Transition Table
Consider the automatic door example.
Current State Event Next State
----------------------------------------------------
Closed Button Pressed Opening
Opening Door Opened Open
Open Timer Expired Closing
Closing Door Closed Closed
Each row defines a valid transition.
Representing a Transition
A transition can be represented using a structure.
The complete state machine is then described as an array.
Transition transitions[] =
{
{ STATE_CLOSED, EVENT_BUTTON_PRESSED, STATE_OPENING },
{ STATE_OPENING, EVENT_DOOR_OPENED, STATE_OPEN },
{ STATE_OPEN, EVENT_TIMER_EXPIRED, STATE_CLOSING },
{ STATE_CLOSING, EVENT_DOOR_CLOSED, STATE_CLOSED }
};
Rather than writing a separate case for every state, the application searches the table for a matching state and event.
Processing an Event
When an event occurs, the FSM searches the transition table.
Current State = Open
Event = Timer Expired
│
▼
Search Transition Table
│
▼
Open + Timer Expired
│
▼
Next State = Closing
If no matching entry exists, the current state remains unchanged.
Advantages
A table-driven implementation separates the definition of the state machine from the code that executes it.
This offers several advantages:
- Adding a new transition often requires only adding another table entry.
- The state machine is easier to visualize.
- Transition logic is centralized.
- Large state machines become easier to maintain.
Choosing an Implementation
Both implementation techniques are widely used.
A switch statement is often preferred for:
- Small state machines.
- Performance-critical code.
- State-specific processing.
A transition table is often preferred for:
- Large state machines.
- Frequently changing transition rules.
- Applications where the FSM definition should be easy to modify.
The choice depends on the complexity of the application rather than on correctness.
The next section discusses the advantages and limitations of using finite state machines in C.
6. Advantages and Limitations
Finite state machines are widely used in embedded software because they provide a structured way to model system behavior. By explicitly defining states and transitions, they simplify many control-oriented applications. However, like any design technique, they also have limitations.
Advantages
Predictable Behavior
An FSM can only be in one state at a time, and every state transition is explicitly defined.
This makes the behavior of the system deterministic and easy to reason about.
Easy to Debug
Because the current state is always known, debugging often becomes much simpler.
For example, logging state transitions can reveal exactly how the system reached its current condition.
Following the sequence of transitions is usually easier than tracing deeply nested conditional statements.
Modular Design
An FSM separates the application's behavior into well-defined states.
Instead of scattering conditional logic throughout the program, state-specific behavior is organized into a single state machine.
This improves readability and makes the code easier to maintain.
Well Suited for Embedded Systems
Many embedded applications naturally operate as a collection of states driven by external events.
Examples include:
- Communication protocols
- User interfaces
- Motor controllers
- Traffic light controllers
- Industrial automation
FSMs provide a natural way to model these systems.
Limitations
State Explosion
As a system becomes more complex, the number of states and transitions can increase rapidly.
Large state machines can become difficult to visualize and maintain.
Complex State Logic
Some applications require significant processing within each state.
As the amount of state-specific logic grows, the state machine itself can become harder to understand.
Limited Support for Concurrent Behavior
A basic finite state machine represents only one active state at a time.
Applications that perform multiple independent activities simultaneously may require multiple cooperating state machines or a more advanced design.
Finite state machines provide a simple and effective way to model event-driven behavior. They produce predictable, maintainable code for many embedded applications, but very large or highly concurrent systems may require additional design techniques to manage their complexity.
The next section summarizes the key concepts discussed in this article.
7. Summary
A Finite State Machine (FSM) is a programming model used to represent systems whose behavior depends on their current operating state. Instead of relying on complex conditional logic, an FSM organizes behavior into well-defined states and explicitly specifies how events cause transitions between those states.
Throughout this article, the following key concepts were introduced:
- An FSM is always in exactly one state.
- Events trigger state transitions.
- The same event can produce different behavior depending on the current state.
- States, events, and transitions form the core of every finite state machine.
- A
switchstatement is a common technique for implementing FSMs in C. - Transition tables provide an alternative implementation that scales well for larger state machines.
The overall operation of an FSM can be summarized as follows.
Current State
│
▼
Receive Event
│
▼
Is Transition Defined?
│
┌────┴────┐
│ │
Yes No
│ │
▼ ▼
Change Remain in
State Current State
│
▼
Wait for Next Event
Finite state machines are particularly valuable in embedded systems because many real-world devices naturally operate as a sequence of discrete states. By explicitly modeling these states and their transitions, FSMs produce software that is easier to understand, debug, test, and maintain.
Although very large systems may require multiple cooperating state machines or more advanced architectural techniques, the FSM remains one of the most widely used design patterns for implementing predictable, event-driven behavior in C.
Relevant Links
C/C++ Material on this website
(such as memory managment, pointers, embedded C programming etc.)