I’d like to share a small state machine demonstration sketch that I’ve been using to explore precise timing and logic analyzer visibility on an Arduino Uno.
This example implements a fully non-blocking state machine using micros() for timing resolution. Each state is one-hot encoded and shifted out to a 74HC595 using hardware SPI at 8 MHz. The lower four outputs of the 595 represent the active state and are monitored with a logic analyzer.
The goals of this sketch are:
- Demonstrate a clean, rollover-safe timing structure (no
delay()) - Show how to align state timing with hardware output events
- Make state transitions clearly visible on a logic analyzer
- Explore minimum observable state widths (SPI transfer–limited)
- Keep the design deterministic and easy to analyze
Each state drives a corresponding LED for a defined duration, and a heartbeat LED runs independently to show the main loop remains non-blocking.
The state sequence is:
STARTUP → STATE0 → STATE1 → STATE2 → STATE3 → STARTUP
Timing ranges from 250 ms to 1250 ms, with the timing reference beginning at the SPI latch event.
This is not meant to be production application code, but rather a structured example for studying:
- One-hot state encoding
- Microsecond-level non-blocking timing
- Direct port manipulation for speed
- Hardware-observable firmware behavior
//
//================================================^================================================
// Non-Blocking Microsecond State Machine
// One-Hot Encoded with SPI Debug Output
//================================================^================================================
//
// URL
//
// LarryD
//
// Version YY/MM/DD Comments
// ======= ======== ========================================================================
// 1.00 24/10/23 Running code
//
//
//
//
// Description:
// ------------------
// This sketch demonstrates a structured, non-blocking state machine
// implemented using micros() timing on an Arduino Uno (16 MHz AVR).
//
// The state machine uses "one-hot encoding" and outputs the active state
// to a 74HC595 shift register via hardware SPI (8 MHz). The lower four
// bits of the shift register represent the current state and can be
// monitored with a logic analyzer for precise timing analysis.
//
// Each state controls a corresponding LED and runs for a defined
// duration without using delay(). Timing is rollover-safe and fully
// asynchronous.
//
// Key Features:
// ------------------
// • Non-blocking timing using micros()
// • One-hot state encoding for clear logic analyzer visibility
// • High-speed SPI (8 MHz) state output
// • Direct PORT manipulation for fast latch control
// • Deterministic state transitions
// • Rollover-safe timing arithmetic
// • Heartbeat LED running independently
//
// Hardware:
// ------------------
// • Arduino Uno (ATmega328P @ 16 MHz)
// • 74HC595 shift register
// • Logic analyzer connected to Q0–Q3 of 74HC595
// • LEDs on pins 2,3,4,5
// • Heartbeat LED on pin 8
// • Latch control on pin 10 (PB2)
//
// State Sequence:
// ------------------
// STARTUP → STATE0 → STATE1 → STATE2 → STATE3 → STARTUP
//
// Timing:
// ------------------
// STARTUP : 500 ms
// STATE0 : 250 ms
// STATE1 : 500 ms
// STATE2 : 1000 ms
// STATE3 : 1250 ms
//
// Notes:
// ------------------
// • State duration begins at the SPI latch event.
// • Minimum observable state width is limited by "SPI transfer time (~8–12 µs at 8 MHz)".
// • Designed for timing analysis and firmware architecture study.
// • If we have a state where no delay is required we can set statePeriod = 0.
//
//
//
//
//
//================================================^================================================
#include <SPI.h>
//Fast latch control (Pin 10 = PB2 on Uno)
#define LATCH_LOW PORTB &= ~(1 << PB2)
#define LATCH_HIGH PORTB |= (1 << PB2)
//This sets the new machine state, the the 74HC595 is then updated with the state's value.
#define SET_STATE(x) do { mState = (x); outputState(mState); stateTimer = micros(); } while(0)
#define LEDon HIGH
#define LEDoff LOW
// S t a t e M a c h i n e
//================================================^================================================
//
enum SystemState : byte
{
//Hot State Encoding
STARTUP = 0,
STATE0 = 0b0001,
STATE1 = 0b0010,
STATE2 = 0b0100,
STATE3 = 0b1000
};
byte mState = STARTUP;
// G P I O s A n d V a r i a b l e s
//================================================^================================================
//
const byte LEDs[] = {2, 3, 4, 5};
const byte heartbeatLED = 8;
const byte LATCH_PIN = 10;
//These are the times spend in each state.
const unsigned long STARTUPtime = 500000ul; // 500ms
const unsigned long LED0_ON_Time = 250000ul; // 250ms
const unsigned long LED1_ON_Time = 500000ul; // 500ms
const unsigned long LED2_ON_Time = 1000000ul; //1000ms
const unsigned long LED3_ON_Time = 1250000ul; //1250ms
//timing stuff
unsigned long heartbeatTime;
unsigned long stateTimer;
unsigned long statePeriod;
// s e t u p ( )
//================================================^================================================
//
void setup()
{
Serial.begin(115200);
//Initialize the LEDs.
for (byte x = 0; x < (sizeof(LEDs) / sizeof(LEDs[0])); x++)
{
pinMode(LEDs[x], OUTPUT);
digitalWrite(LEDs[x], LOW);
}
pinMode(heartbeatLED, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
digitalWrite(LATCH_PIN, HIGH);
//===============================
//Setup SPI.
SPI.begin();
//8MHz
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
//Reset the 74HC595
LATCH_LOW;
//We are using only the lower 4 bits.
SPI.transfer(0);
LATCH_HIGH;
delay(20);
//===============================
//Initialize the State Machine.
statePeriod = STARTUPtime;
SET_STATE(STARTUP);
stateTimer = micros();
} //END of setup()
// l o o p ( )
//================================================^================================================
//
void loop()
{
//======================================================================== T I M E R heartbeatLED
//Is it time to toggle the heartbeat LED ?
if (millis() - heartbeatTime >= 500ul)
{
//restart this TIMER
heartbeatTime = millis();
//toggle the heartbeat LED
digitalWrite(heartbeatLED, !digitalRead(heartbeatLED));
}
//======================================================================== State Machine
//Stay here until the state delay period has expired.
if (micros() - stateTimer >= statePeriod)
{
//Restart this TIMER.
stateTimer = micros();
//Service the State Machine.
switch (mState)
{
//================
case STARTUP:
{
SET_STATE(STATE0);
digitalWrite(LEDs[0], LEDon);
statePeriod = LED0_ON_Time;
}
break;
//================
case STATE0:
{
SET_STATE(STATE1);
digitalWrite(LEDs[0], LEDoff);
digitalWrite(LEDs[1], LEDon);
statePeriod = LED1_ON_Time;
}
break;
//================
case STATE1:
{
SET_STATE(STATE2);
digitalWrite(LEDs[1], LEDoff);
digitalWrite(LEDs[2], LEDon);
statePeriod = LED2_ON_Time;
//If we want, we can spend no time in STATE3.
//i.e. about 11us.
//statePeriod = 0ul;
}
break;
//================
case STATE2:
{
SET_STATE(STATE3);
digitalWrite(LEDs[2], LEDoff);
digitalWrite(LEDs[3], LEDon);
statePeriod = LED3_ON_Time;
}
break;
//================
case STATE3:
{
SET_STATE(STARTUP);
digitalWrite(LEDs[3], LEDoff);
statePeriod = STARTUPtime;
}
break;
} //END of switch/case
} //END of if (currentMillis - stateTimer >= statePeriod)
} //END of loop()
// o u t p u t S t a t e ( )
//================================================^================================================
//Updates the 74HC595 outputs to the state value.
//The Logic Analyzer's probes connected to the 74HC595 Q0-Q3 outputs.
//
inline void outputState(uint8_t newState)
{
//This takes about 8us to execute.
LATCH_LOW;
//We are using only the lower 4 bits.
SPI.transfer(newState & 0x0F);
LATCH_HIGH;
} //END of outputState()
// E N D
//================================================^================================================
//









