The switch...case statement in Arduino C++ is a control flow tool that tests a variable against a list of integer or character values. While beginners often rely on chained if/else statements, the switch structure is the backbone of robust embedded state machines. When your cases use contiguous integers, the GCC compiler optimizes the switch into a jump table in flash memory, executing in O(1) time compared to the O(N) evaluation of an if/else chain.
In this guide, we will build a practical Arduino switch case example: a 4-state hardware menu system driven by a rotary encoder and displayed on an I2C OLED. This project targets the Arduino Nano V3 (ATmega328P, 5V/16MHz logic), though the code is directly portable to the Uno R3 or Mega 2560.
State Machine Architecture & Transition Matrix
A menu system is fundamentally a Finite State Machine (FSM). Instead of writing blocking code that waits for user input, we define discrete states and let the loop() function poll for hardware events. Below is the data-dense transition matrix that dictates our switch logic.
| Current State | Input Event | Next State | Hardware Action Executed |
|---|---|---|---|
STATE_IDLE |
Encoder Switch Click | STATE_CONFIG_TEMP |
Highlight "Temp" row on OLED; reset encoder bounds |
STATE_CONFIG_TEMP |
Encoder Rotation | STATE_CONFIG_TEMP |
Increment/Decrement target temp (15-30°C); update OLED |
STATE_CONFIG_TEMP |
Encoder Switch Click | STATE_CONFIG_FAN |
Highlight "Fan" row on OLED; reset encoder bounds |
STATE_CONFIG_FAN |
Encoder Switch Click | STATE_SAVE |
Write variables to EEPROM; flash "Saved" on OLED |
STATE_SAVE |
Timeout (2000ms) | STATE_IDLE |
Clear highlight; return to main dashboard view |
Hardware Parts List & Pin Mapping
To replicate this build, you need components that operate natively at 5V to match the Nano V3's logic levels without requiring logic level shifters.
- Microcontroller: Arduino Nano V3 (ATmega328P) - ~$6.00
- Input: KY-040 Rotary Encoder Module (includes breakout board with pull-ups) - ~$2.50
- Display: 0.96" SSD1306 I2C OLED (128x64, 4-pin) - ~$5.00
- Wiring: 22 AWG solid core jumper wires, half-size breadboard
INPUT_PULLUP in your setup code, or the mechanical contacts will float and trigger phantom state changes.
Pin Mapping Table
| Component | Module Pin | Arduino Nano Pin | Wire Color | Notes |
|---|---|---|---|---|
| SSD1306 OLED | GND | GND | Black | Common ground |
| SSD1306 OLED | VCC | 5V | Red | Draws ~20mA max |
| SSD1306 OLED | SCL | A5 | Blue | I2C Clock (Hardware) |
| SSD1306 OLED | SDA | A4 | Green | I2C Data (Hardware) |
| KY-040 Encoder | CLK | D2 | Yellow | Interrupt 0 (Hardware) |
| KY-040 Encoder | DT | D3 | Orange | Interrupt 1 (Hardware) |
| KY-040 Encoder | SW | D4 | Purple | Active LOW on press |
| KY-040 Encoder | + / VCC | 5V | Red | Powers onboard pull-ups |
Complete Compilable Code
This code requires three libraries installed via the Arduino Library Manager: Adafruit GFX, Adafruit SSD1306, and Paul Stoffregen’s Encoder library. The Encoder library is critical here; it uses hardware interrupts to decode the quadrature signals, ensuring we never miss a rotational tick while the switch statement is executing display updates.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Encoder.h>
#include <EEPROM.h>
// --- Hardware Definitions ---
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C // 0x3D for some 128x64 variants
#define ENCODER_PIN_A 2 // Hardware Interrupt 0
#define ENCODER_PIN_B 3 // Hardware Interrupt 1
#define ENCODER_SW 4 // Switch pin
// --- Global Objects ---
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
Encoder myEnc(ENCODER_PIN_A, ENCODER_PIN_B);
// --- State Machine Enum ---
enum MenuState {
STATE_IDLE,
STATE_CONFIG_TEMP,
STATE_CONFIG_FAN,
STATE_SAVE
};
MenuState currentState = STATE_IDLE;
// --- Variables ---
int targetTemp = 22;
int fanSpeed = 50;
long oldPosition = -999;
unsigned long stateEnterTime = 0;
bool switchPressed = false;
void setup() {
Serial.begin(115200);
pinMode(ENCODER_SW, INPUT_PULLUP);
// Error Handling: Halt if OLED fails to initialize
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed. Check I2C wiring and address."));
for(;;); // Infinite loop to prevent erratic hardware behavior
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
// Load saved values from EEPROM
EEPROM.get(0, targetTemp);
EEPROM.get(sizeof(int), fanSpeed);
// Sanity check EEPROM data on first boot
if(targetTemp < 15 || targetTemp > 30) targetTemp = 22;
if(fanSpeed < 0 || fanSpeed > 100) fanSpeed = 50;
myEnc.write(0);
stateEnterTime = millis();
}
void loop() {
readHardwareInputs();
// --- THE SWITCH CASE STATE MACHINE ---
switch (currentState) {
case STATE_IDLE:
if (switchPressed) {
currentState = STATE_CONFIG_TEMP;
myEnc.write(targetTemp * 4); // Scale for encoder resolution
}
break;
case STATE_CONFIG_TEMP:
handleTempConfig();
if (switchPressed) {
currentState = STATE_CONFIG_FAN;
myEnc.write(fanSpeed * 4);
}
break;
case STATE_CONFIG_FAN:
handleFanConfig();
if (switchPressed) {
currentState = STATE_SAVE;
stateEnterTime = millis(); // Reset timer for save timeout
}
break;
case STATE_SAVE:
saveToEEPROM();
// Auto-return to idle after 2 seconds
if (millis() - stateEnterTime > 2000) {
currentState = STATE_IDLE;
}
break;
default:
// Failsafe: If memory corruption alters the enum, reset to IDLE
Serial.println("Error: Unknown state detected. Resetting.");
currentState = STATE_IDLE;
break;
}
updateDisplay();
switchPressed = false; // Debounce/reset flag
}
void readHardwareInputs() {
// Read switch (Active LOW)
if (digitalRead(ENCODER_SW) == LOW) {
delay(50); // Crude debounce, acceptable for slow menu navigation
if (digitalRead(ENCODER_SW) == LOW) {
switchPressed = true;
while(digitalRead(ENCODER_SW) == LOW); // Wait for release
}
}
}
void handleTempConfig() {
long newPosition = myEnc.read() / 4;
if (newPosition != oldPosition) {
oldPosition = newPosition;
targetTemp = constrain(newPosition, 15, 30);
myEnc.write(targetTemp * 4); // Prevent winding past bounds
}
}
void handleFanConfig() {
long newPosition = myEnc.read() / 4;
if (newPosition != oldPosition) {
oldPosition = newPosition;
fanSpeed = constrain(newPosition, 0, 100);
myEnc.write(fanSpeed * 4);
}
}
void saveToEEPROM() {
// Only write if values actually changed to save EEPROM write cycles
int storedTemp, storedFan;
EEPROM.get(0, storedTemp);
EEPROM.get(sizeof(int), storedFan);
if (storedTemp != targetTemp) EEPROM.put(0, targetTemp);
if (storedFan != fanSpeed) EEPROM.put(sizeof(int), fanSpeed);
}
void updateDisplay() {
display.clearDisplay();
display.setCursor(0, 0);
display.println("--- SYSTEM MENU ---");
if (currentState == STATE_CONFIG_TEMP || currentState == STATE_SAVE) {
display.print("> Temp: ");
} else {
display.print(" Temp: ");
}
display.print(targetTemp);
display.println(" C");
if (currentState == STATE_CONFIG_FAN) {
display.print("> Fan: ");
} else {
display.print(" Fan: ");
}
display.print(fanSpeed);
display.println(" %");
if (currentState == STATE_SAVE) {
display.setCursor(0, 50);
display.println("[SAVED TO EEPROM]");
}
display.display();
}
Debugging: First Three Things to Check When It Fails
When working with switch statements and hardware state machines, logical bugs often manifest as frozen menus or skipped states. If your build fails to transition correctly, check these three items in order.
1. The Missing break; Fall-Through Error
The most common syntax error in a switch block is omitting the break; statement at the end of a case. If you miss it, execution "falls through" to the next case, executing unintended code. Modern GCC compilers (used in Arduino IDE 2.x) will catch this and throw the following exact error string:
warning: this statement may fall through [-Wimplicit-fallthrough=]
Fix: Ensure every case ends with a break; or a return;. If fall-through is intentional (rare in FSMs), you must explicitly comment // fall through to suppress the warning.
2. Blocking Code Inside a Case
If your encoder feels unresponsive or the OLED flickers, you likely placed a delay() or a blocking while() loop inside one of your case blocks. A state machine relies on the loop() function executing hundreds of times per second to poll hardware inputs.
Fix: Replace blocking delays with non-blocking millis() timers, exactly as demonstrated in the STATE_SAVE timeout logic in the code above.
3. I2C Address Mismatch on the OLED
If the code compiles but the Serial Monitor prints SSD1306 allocation failed, your display isn't acknowledging on the I2C bus. While most 0.96" displays use address 0x3C, some 128x64 variants ship with 0x3D.
Fix: Run the standard Arduino I2CScanner example sketch to verify the exact hex address of your module, and update the SCREEN_ADDRESS macro accordingly.
Extending and Simplifying the Build
The switch...case structure is perfect for 3 to 10 states. However, as your project grows, you need to know when to pivot your architecture.
How to Extend (Adding More States)
To add a new configuration page (e.g., STATE_CONFIG_ALARM):
- Add the new identifier to the
MenuStateenum. - Add a new
case STATE_CONFIG_ALARM:block in theswitchstatement. - Update the transition logic in the preceding state (e.g., change
STATE_CONFIG_FANto transition toSTATE_CONFIG_ALARMinstead ofSTATE_SAVEon a button click). - Add the UI rendering logic inside the
updateDisplay()function.
How to Simplify (When Cases Exceed 15)
If your menu system expands beyond 15 states, the switch block becomes a massive, unreadable wall of text. At this point, simplify the build by using an array of function pointers (a lookup table).
Instead of a switch statement, you define an array where the index matches your enum:
void (*stateFunctions[])() = { handleIdle, handleTemp, handleFan, handleSave };
// In loop():
stateFunctions[currentState]();
This reduces the loop() function to a single line of execution, pushing the complexity into isolated, easily testable functions. For deeper reading on state machine design patterns in C++, refer to the official Arduino switch-case reference and advanced FSM frameworks like arduino-fsm.
By keeping your cases contiguous, handling hardware interrupts properly, and avoiding blocking code, the switch statement remains the most efficient and readable tool for embedded menu navigation.






