What’s a State Machine, Anyway?
A Finite State Machine (FSM) is a mathematical model used in the logical design of sequential circuits. It is an abstract machine that can exist in a finite number of distinct states. At any given moment, the machine is in one specific state and it transitions to another state based on external inputs and predefined rules. In addition to state transitions, the FSM can also generate outputs. The method by which these outputs are computed determines the classification of the FSM into one of two main types:
- Moore FSM, where the ouput solely depends on the current state
- Mealy FSM, where the output depends on both the current state and the current input
While it's entirely possible to implement FSMs using flip-flops and hardware logic, this discussion will focus exclusively on software-based FSMs using C, targeting embedded systems.
The Nested Switch Approach
Imagine a scenario where you need to implement a multi-line menu on a microcontroller using a small HD44780 LCD. You have four buttons that serve as navigation controls, allowing to move up, down, select a menu item or return. Suppose the selected item leads to a specific clock settings function. Once inside this submenu, the display shows the current time in the format hh:mm:ss, and you can use the buttons to move left, right, select one of the time components (hours, minutes, or seconds) or return to the upper level. After selecting a component, you're presented with options to increase, decrease, or return to the previous screen.
It becomes evident that the behavior of the four buttons must dynamically change depending on the current state of the system. Let's address the problem with a simple nested switch statement:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <ctype.h>
//---Custom Types---
typedef enum {
STATE_MENU_NAV,
STATE_OPTION_NAV,
STATE_OPTION_EDIT,
STATE_COUNT
} menu_state_t;
typedef enum {
BTN_UP,
BTN_DOWN,
BTN_SELECT,
BTN_ESC,
BTN_NONE
} button_t;
typedef struct{
const unsigned char* text;
void (*menu_handler)(void);
} menu_item_t;
#define MENU_SIZE sizeof(menu)/sizeof(menu[0])
typedef struct {
uint8_t var;
const uint8_t min;
const uint8_t max;
} option_item_t;
typedef struct {
option_item_t *option_tbl_ptr;
uint8_t option_items;
const char* text;
char separator;
} option_t;
//---Globals---
static menu_state_t state = STATE_MENU_NAV;
static uint8_t v_index = 0;
static uint8_t h_index = 0;
static option_t active_options;
//---Function Prototypes---
static void Clock(void);
static void Date(void);
static void ExitMenu(void);
static void ShowMenu(uint8_t index);
static void ShowOptions(uint8_t index);
static void EditOptions(uint8_t index);
//---Menu---
static const menu_item_t menu[] = {
{"Set Clock", &Clock},
{"Set Date", &Date},
{"Exit", &ExitMenu}
};
//---Clock Options---
static option_item_t clock_options[] = {
{12, 0, 23}, //hours
{0, 0, 59}, //minutes
{0, 0, 59} //seconds
};
//---Date Options---
static option_item_t date_options[] = {
{1, 1, 31}, //day
{1, 1, 12}, //month
{25, 00, 99} //year
};
//---VIEW FUNCTIONS ---
static void ShowMenu(uint8_t index) {
printf("\033[H\033[J");//clear screen
printf("\033[?25l");//disable cursor
printf("\n=== MENU ===\n");
static const menu_item_t *menu_ptr = menu;
for (uint8_t i = 0; i < MENU_SIZE; i++) {
if (i == index) {
printf(" > %s\n", menu_ptr[i].text);
} else {
printf(" %s\n", menu_ptr[i].text);
}
}
}
static void ShowOptions(uint8_t index) {
printf("\033[H\033[J");
printf("\n=== OPTIONS ===\n");
printf("%s", active_options.text);
for (uint8_t i = 0; i < active_options.option_items; i++) {
printf("%02hhu", active_options.option_tbl_ptr[i].var);
if (i < active_options.option_items - 1) printf("%c", active_options.separator);
}
printf("\n%*s--\n", (int) index * 3 + 6, "");
}
static void EditOptions(uint8_t index) {
printf("\033[H\033[J");//clear screen
printf("\n=== OPTIONS ===\n");
printf("%s", active_options.text);
for (uint8_t i = 0; i < active_options.option_items; i++) {
printf("%02hhu", active_options.option_tbl_ptr[i].var);
if (i < active_options.option_items - 1) printf("%c", active_options.separator);
}
printf("\n%*s[%02hhu]\n", (int) index * 3 + 5, "", active_options.option_tbl_ptr[index].var);
}
static void Clock(void) {
active_options.option_tbl_ptr = clock_options;
active_options.option_items = sizeof (clock_options) / sizeof (clock_options[0]),
active_options.text = "Time: ";
active_options.separator = ':';
ShowOptions(0);
}
static void Date(void) {
active_options.option_tbl_ptr = date_options;
active_options.option_items = sizeof (date_options) / sizeof (date_options[0]),
active_options.text = "Date: ";
active_options.separator = '/';
ShowOptions(0);
}
static void ExitMenu(void) {
printf("Exiting...\n");
exit(EXIT_SUCCESS);
}
//--- INPUT HANDLER ---
static button_t GetButton(void) {
char ch = getchar(); // blocking read
ch = toupper(ch);
switch (ch) {
case 'Q': return BTN_UP; //Up
case 'W': return BTN_DOWN; //Down
case 'E': return BTN_SELECT;//Select
case 'R': return BTN_ESC; //Esc
default: return BTN_NONE;
}
}
void main(void) {
ShowMenu(v_index);
while (1) {
button_t btn = GetButton();
if (btn == BTN_NONE) continue;
switch (state) {
// --- MENU NAVIGATION ---
case STATE_MENU_NAV:
switch (btn) {
case BTN_UP:
if (v_index > 0) v_index--;
ShowMenu(v_index);
break;
case BTN_DOWN:
if (v_index < MENU_SIZE - 1) v_index++;
ShowMenu(v_index);
break;
case BTN_SELECT:
menu[v_index].menu_handler();
state = STATE_OPTION_NAV;
break;
case BTN_ESC:
printf("Exiting...\n");
exit(EXIT_SUCCESS);
default:
break;
}
break;
// --- OPTION NAVIGATION ---
case STATE_OPTION_NAV:
switch (btn) {
case BTN_UP:
if (h_index > 0) h_index--;
ShowOptions(h_index);
break;
case BTN_DOWN:
if (h_index < active_options.option_items - 1) h_index++;
ShowOptions(h_index);
break;
case BTN_SELECT:
state = STATE_OPTION_EDIT;
EditOptions(h_index);
break;
case BTN_ESC:
state = STATE_MENU_NAV;
ShowMenu(v_index);
break;
default:
break;
}
break;
// --- OPTION EDIT ---
case STATE_OPTION_EDIT:
switch (btn) {
case BTN_UP:
if (active_options.option_tbl_ptr[h_index].var < active_options.option_tbl_ptr[h_index].max) {
active_options.option_tbl_ptr[h_index].var++;
}
EditOptions(h_index);
break;
case BTN_DOWN:
if (active_options.option_tbl_ptr[h_index].var > active_options.option_tbl_ptr[h_index].min) {
active_options.option_tbl_ptr[h_index].var--;
}
EditOptions(h_index);
break;
case BTN_SELECT: // Do nothing
state = STATE_OPTION_NAV;
ShowOptions(h_index);
break;
case BTN_ESC: // Return
state = STATE_OPTION_NAV;
ShowOptions(h_index);
break;
default:
break;
}
break;
}
}
}
While the code is straighforward and probably adequate for very small FSMs, it violates almost every SOLID principle.
State logic and state transitioning is mixed in one block of code. The nested switch though being easy to understand, it introduces severe scalability problems. Adding new states requires editing the main switch statement risking corrupting the existing code and introducing new bugs. A better approach would be to keep separated switch statements for each state thus promoting single responsibility and avoiding nesting.
The independent Handler FSM
In an effort to eliminate the giant switch statement and promote signle reponsibility, we will create separate logic handlers for each state. The main loop will only have one responisbility: to dispatch the appropriate handler based on the current state.
This approach while still being very simple, offers several advantages:
- State logic is isolated — each function is responsible for only one state’s behavior.
- Improved Extensibility — adding a new state translates to adding a new handler.
- Reduces the risk of breaking existing functionality when extending the FSM.
While still using conditional statements inside each handler, the structure is much cleaner and far more modular compared to the nested switch design.
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <ctype.h>
//--Custom Types--
typedef enum {
STATE_MENU_NAV,
STATE_OPTION_NAV,
STATE_OPTION_EDIT,
STATE_COUNT
} menu_state_t;
typedef enum {
BTN_UP,
BTN_DOWN,
BTN_SELECT,
BTN_ESC,
BTN_NONE
} button_t;
typedef struct {
const unsigned char* text;
void (*menu_handler)(void);
} menu_item_t;
#define MENU_SIZE (sizeof(menu)/sizeof(menu[0]))
typedef struct {
uint8_t var;
const uint8_t min;
const uint8_t max;
} option_item_t;
typedef struct {
option_item_t *option_tbl_ptr;
uint8_t option_items;
const char* text;
char separator;
} option_t;
//---Globals ---
static menu_state_t State = STATE_MENU_NAV;
static uint8_t v_index = 0;
static uint8_t h_index = 0;
static option_t active_options;
//--Function Prototypes--
static void Clock(void);
static void Date(void);
static void ExitMenu(void);
static void ShowMenu(uint8_t index);
static void ShowOptions(uint8_t index);
static void EditOptions(uint8_t index);
//--Menu--
static const menu_item_t menu[] = {
{"Set Clock", &Clock},
{"Set Date", &Date},
{"Exit", &ExitMenu}
};
//--Clock Options--
static option_item_t clock_options[] = {
{12, 0, 23}, //hours
{0, 0, 59}, //minutes
{0, 0, 59} //seconds
};
//--Date Options--
static option_item_t date_options[] = {
{1, 1, 31}, //day
{1, 1, 12}, //month
{25, 0, 99} //year
};
//--- STATE HANDLERS ---
static void HandleMenuNav(button_t btn) {
switch (btn) {
case BTN_UP:
if (v_index > 0) v_index--;
ShowMenu(v_index);
break;
case BTN_DOWN:
if (v_index < MENU_SIZE - 1) v_index++;
ShowMenu(v_index);
break;
case BTN_SELECT:
menu[v_index].menu_handler();
State = STATE_OPTION_NAV;
break;
case BTN_ESC:
printf("Exiting...\n");
exit(EXIT_SUCCESS);
default:
break;
}
}
static void HandleOptionNav(button_t btn) {
switch (btn) {
case BTN_UP:
if (h_index > 0) h_index--;
ShowOptions(h_index);
break;
case BTN_DOWN:
if (h_index < active_options.option_items - 1) h_index++;
ShowOptions(h_index);
break;
case BTN_SELECT:
State = STATE_OPTION_EDIT;
EditOptions(h_index);
break;
case BTN_ESC:
State = STATE_MENU_NAV;
ShowMenu(v_index);
break;
default:
break;
}
}
static void HandleOptionEdit(button_t btn) {
switch (btn) {
case BTN_UP:
if (active_options.option_tbl_ptr[h_index].var < active_options.option_tbl_ptr[h_index].max) {
active_options.option_tbl_ptr[h_index].var++;
}
EditOptions(h_index);
break;
case BTN_DOWN:
if (active_options.option_tbl_ptr[h_index].var > active_options.option_tbl_ptr[h_index].min) {
active_options.option_tbl_ptr[h_index].var--;
}
EditOptions(h_index);
break;
case BTN_SELECT: // Do nothing
State = STATE_OPTION_NAV;
ShowOptions(h_index);
break;
case BTN_ESC: // Return
State = STATE_OPTION_NAV;
ShowOptions(h_index);
break;
default:
break;
}
}
//---VIEW FUNCTIONS ---
static void ShowMenu(uint8_t index) {
printf("\033[H\033[J");//clear screen
printf("\033[?25l");//disable cursor
printf("\n=== MENU ===\n");
static const menu_item_t *menu_ptr = menu;
for (uint8_t i = 0; i < MENU_SIZE; i++) {
if (i == index) {
printf(" > %s\n", menu_ptr[i].text);
} else {
printf(" %s\n", menu_ptr[i].text);
}
}
}
static void ShowOptions(uint8_t index) {
printf("\033[H\033[J");
printf("\n=== OPTIONS ===\n");
printf("%s", active_options.text);
for (uint8_t i = 0; i < active_options.option_items; i++) {
printf("%02hhu", active_options.option_tbl_ptr[i].var);
if (i < active_options.option_items - 1) printf("%c", active_options.separator);
}
printf("\n%*s--\n", (int) index * 3 + 6, "");
}
static void EditOptions(uint8_t index) {
printf("\033[H\033[J");//clear screen
printf("\n=== OPTIONS ===\n");
printf("%s", active_options.text);
for (uint8_t i = 0; i < active_options.option_items; i++) {
printf("%02hhu", active_options.option_tbl_ptr[i].var);
if (i < active_options.option_items - 1) printf("%c", active_options.separator);
}
printf("\n%*s[%02hhu]\n", (int) index * 3 + 5, "", active_options.option_tbl_ptr[index].var);
}
//--- MENU HANDLERS ---//
static void Clock(void) {
active_options.option_tbl_ptr = clock_options;
active_options.option_items = sizeof (clock_options) / sizeof (clock_options[0]);
active_options.text = "Time: ";
active_options.separator = ':';
h_index = 0;
ShowOptions(h_index);
}
static void Date(void) {
active_options.option_tbl_ptr = date_options;
active_options.option_items = sizeof (date_options) / sizeof (date_options[0]);
active_options.text = "Date: ";
active_options.separator = '/';
h_index = 0;
ShowOptions(h_index);
}
static void ExitMenu(void) {
printf("Exiting...\n");
exit(EXIT_SUCCESS);
}
//--- INPUT HANDLER ---
static button_t GetButton(void) {
char ch = getchar();
ch = toupper(ch);
switch (ch) {
case 'Q': return BTN_UP; //Up
case 'W': return BTN_DOWN; //Down
case 'E': return BTN_SELECT;//Select
case 'R': return BTN_ESC; //Esc
default: return BTN_NONE;
}
}
//---FSM---
void main(void) {
ShowMenu(v_index);
while (1) {
button_t btn = GetButton();
if (btn == BTN_NONE) continue;
switch (State) {
case STATE_MENU_NAV:
HandleMenuNav(btn);
break;
case STATE_OPTION_NAV:
HandleOptionNav(btn);
break;
case STATE_OPTION_EDIT:
HandleOptionEdit(btn);
break;
}
}
}
This is the expected output:
=== MAIN MENU ===
> Set Clock
Set Date
Exit
Select Key Pressed...
Time: 12:00:00
--
Right Key Pressed...
Time: 12:00:00
--
Select Key Pressed...
Time: 12:00:00
[00]
Up Key Pressed...
Time: 12:01:00
[01]
Up Key Pressed...
Time: 12:02:00
[02]
Up Key Pressed...
Time: 12:03:00
[03]
Select Key Pressed...
Time: 12:03:00
--
Exit Key Pressed...
=== MAIN MENU ===
> Set Clock
Set Date
Exit
I should also mention that in both implementations, the finite state machine is inherently a Mealy FSM, since the outputs depend not only on the current state but as well as on the input events. This was not an explicit design choice but rather a consequence of the scenario’s requirements.
Full code is also available on my GitHub repository.