This sketch demonstrates a non-blocking state machine used 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 exported through a hardware debug bus and shifted out to a 74HC595 using hardware SPI at 8 MHz. A logic analyzer can monitor the shift register outputs to observe the internal behavior of the firmware in real time.
The goals of this sketch are:
- Demonstrate a clean, rollover-safe timing structure (no
delay()) - Show how multiple cooperative timers can run concurrently
- Provide hardware-level observability of state transitions using a logic analyzer.
- Explore minimum observable state widths
- Keep the design deterministic and easy to analyze
Three independent tasks run concurrently in the main loop:
- A heartbeat timer toggling an LED every 250 ms
- A switch monitor polling an input every 50 ms
- A state machine sequencing LEDs using microsecond-resolution timing
Each state transition is performed through a small macro that:
- Updates the current state
- Stores the next state duration
- Updates the debug bus
- Generates a very short ~62 ns transition marker pulse
- Captures the timing reference
This pulse appears on Arduino pin D9, allowing a logic analyzer to precisely correlate firmware state transitions with external events.
The state sequence is:
STATE0 → STATE1 → STATE2 → STATE3 → STATE4 → STATE5 → STATE0
The first four states sequence LEDs with different ON durations.
STATE4 turns all LEDs off, and STATE5 immediately restarts the sequence.
State durations range from 250 ms to 1000 ms, with the timing reference beginning at the moment the state change is committed.
The debug bus uses the outputs of the 74HC595 as follows:
- Bits 0–2 : Current state number
- Bit 3 : Heartbeat indicator
- Bit 4 : Switch activity indicator
- Bits 5–7 : Reserved
This is not meant to be production application code, but rather a structured example for studying:
- State machine design
- Microsecond-level non-blocking timing
- Cooperative multitasking with
millis()andmicros() - Hardware-observable firmware behavior
- Logic analyzer debugging techniques
- Below is the final version for this Discussion.
//
//================================================^================================================
// Non-Blocking State Machine with SPI Debug Bus and Transition Marker
//================================================^================================================
//
//
// URL
//
// LarryD
//
// Version YY/MM/DD Comments
// ======= ======== ========================================================================
// 1.00 26/03/01 Running code
// 1.01 26/03/05 Added comments
//
//
//
//
// Description
// ------------------------
// This sketch demonstrates a non-blocking state machine used to sequence
// four LEDs while maintaining responsive background tasks. The program
// avoids delay() and instead relies on millis() and micros() timers so
// multiple activities can run concurrently.
//
// The sketch also implements a hardware debug system using an SPI driven
// 74HC595 shift register so the internal state of the program can be
// monitored using a logic analyzer.
//
// The state machine turns on LEDs sequentially with different ON times,
// creating a simple time-based sequence. Up to seven states can be represented
// using the three debug bus bits.
//
// Who this tutorial is for
// ------------------------
// • This example is intended for Arduino users who already understand the basics
// of writing sketches and using digital inputs and outputs.
// • The goal is to demonstrate how to structure programs using non-blocking
// timing and state machines so multiple tasks can run at the same time
// without using delay().
// • Some familiarity with millis() and basic Arduino programming will be helpful.
// • The user should have access to a logic analyzer.
//
// Features
// ------------------------
// • Non-blocking state machine using micros() timing
// • Multiple cooperative timers running in parallel
// • SPI debug bus using a 74HC595 shift register
// • Heartbeat indicator showing the system is alive
// • Switch monitoring with edge detection
// • Ultra-short state transition marker pulse for timing analysis
//
// Debugging method
// ------------------------
// The internal state of the machine is exported through an SPI driven
// debug bus connected to a 74HC595. A logic analyzer can monitor the
// outputs to observe system behavior in real time.
// This allows internal firmware behavior to be observed without using
// Serial prints, which can disturb real-time timing.
//
// State Machine Overview
// ----------------------
// The following diagram shows the flow of the state machine along
// with the LED activity and timing duration of each state.
// State machines are easiest to understand visually.
//
// STATE0 → STATE1 → STATE2 → STATE3 → STATE4 → STATE5 → STATE0
//
// +---------+ +---------+ +---------+ +---------+
// | STATE0 | ---> | STATE1 | ---> | STATE2 | ---> | STATE3 |
// | LED0 ON | | LED1 ON | | LED2 ON | | LED3 ON |
// | 500 ms | | 500 ms | | 750 ms | | 1000 ms |
// +---------+ +---------+ +---------+ +---------+
// ^ |
// | v
// +---------+ +---------+
// | STATE5 | <------------------------------------ | STATE4 |
// | restart | | LEDs OFF|
// | ~16 µs | | |
// +---------+ +---------+
//
//
// Debug Bus Bit Map
// ------------------------
// Bit 0-2 : Current state number(A maximum of 7 States).
// If four bits (four 74HC595 outputs) are used, up to 16 states can be represented.
// Bit 3 : Heartbeat toggle
// Bit 4 : Switch activity indicator
// Bit 5-7 : Reserved
//
// Every time the state machine changes state, a ~62ns pulse is generated
// on Arduino pin D9. This pulse provides a precise timing marker allowing
// a logic analyzer to correlate state transitions with external events.
//
// System Tasks
// ------------------------
// The loop() function performs three independent tasks:
//
// 1) Heartbeat Timer
// Toggles LED on pin 8 every 250ms and updates the debug bus.
//
// 2) Switch Monitor
// Polls the input switch every 50ms and records activity on the
// debug bus when the switch changes state.
//
// 3) State Machine
// Sequences LEDs 0-3 with configurable ON times using a
// non-blocking timing method.
//
// Hardware Cconnections
// ------------------------
// LED outputs
// D2 -> LED0
// D3 -> LED1
// D4 -> LED2
// D5 -> LED3
//
// D6 -> Pushbutton (INPUT_PULLUP)
//
// Notes
// ------------------------
// The debug bus can represent any internal variable, not only physical inputs.
// For example the state of mySwitch is sent, but any boolean variable could
// also be exported for debugging.
//
// Logic Analyzer Note
// ------------------------
// The debug bus signals are intended to be viewed using a logic analyzer.
// This allows the state machine activity and timing relationships to be
// observed without affecting program execution.
//
// Signal Mapping
// --------------------------------------------------------------
// Pin Code Name Schematic Label Analyzer
// --------------------------------------------------------------
// Arduino D13 SCK SCK (not monitored)
// Arduino D11 MOSI MOSI (not monitored)
// Arduino D10 LATCH_PIN / SS RCLK (not monitored)
//
// Arduino D9 transitionPin transitionPin CH5 Clock Marker
//
// 74HC595 QA STATE0 Q0 CH0
// 74HC595 QB STATE1 Q1 CH1
// 74HC595 QC STATE2 Q2 CH2
// 74HC595 QD Heartbeat Q3 CH3
// 74HC595 QE mySwitch Q4 CH4
// 74HC595 QF Reserved Q5 CH5 (See above, Clock Marker)
// 74HC595 QG Reserved Q6 CH6
// 74HC595 QH Reserved Q7 CH7
//
//
// Click for schematic drawing
// https://europe1.discourse-cdn.com/arduino/original/4X/c/5/e/c5ef6fc033d869a8cad376668d1d6dcda698c9ec.jpeg
//
// Logic analyzer images
// https://europe1.discourse-cdn.com/arduino/original/4X/c/3/5/c3546d7b1bd16729a72821ec95837c0a75f291e3.jpeg
//
// https://europe1.discourse-cdn.com/arduino/original/4X/e/6/5/e65a5307748fd1e38e2680778b028e36b78587c7.jpeg
//
// Origin for this example
// https://forum.arduino.cc/t/making-arduino-state-machines-visible-on-a-logic-analyzer/1434280
//
//
//================================================^================================================
// P r o g r a m C o d e
//================================================^================================================
//
#include <SPI.h>
//Fast latch control (Pin 10 = PB2)
#define LATCH_LOW PORTB &= ~(1 << PB2)
#define LATCH_HIGH PORTB |= (1 << PB2)
//========================
//62ns pulse on Arduino pin 9
//Toggle twice for a very short analyzer marker pulse.
#define PULSE62D09 cli(); PINB = bit(PINB1); PINB = bit(PINB1); sei()
//========================
//State change macro
#define SET_STATE(next, duration) \
do { \
mState = (next); \
statePeriod = (duration); \
outputState(); \
PULSE62D09; \
stateTimer = micros(); \
} \
while(0) //No ; is needed. \
//========================
#define LEDon HIGH
#define LEDoff LOW
//================================================
// State Machine
//================================================
enum SystemState : byte
{
//8 States (maximum) can be represented with 3 bits.
STATE0 = 0b000, //0 - Each State is assigned a value that we will send to the 74HC595.
STATE1 = 0b001, //1 - We are using the lower 3 bits on the Logic Analyzer to display State.
STATE2 = 0b010, //2 -
STATE3 = 0b011, //3 -
STATE4 = 0b100, //4 -
STATE5 = 0b101 //5 -
};
//Let's start out in STATE0.
SystemState mState = STATE0;
//State Machine sequence:
//STATE0 -> LED0 ON
//STATE1 -> LED0 OFF, LED1 ON
//STATE2 -> LED1 OFF, LED2 ON
//STATE3 -> LED2 OFF, LED3 ON
//STATE4 -> LED3 OFF
//STATE5 -> Restart sequence
//================================================
// GPIOs and Variables
//================================================
//
const byte LEDs[] = {2, 3, 4, 5};
const byte ledArraySize = sizeof(LEDs) / sizeof(LEDs[0]);
const byte mySwitch = 6;
const byte heartbeatLED = 8;
const byte transitionPin = 9;
const byte LATCH_PIN = 10;
byte lastMySwitchState = LOW;
//State durations (µs)
const unsigned long STATE0time = 500000ul; //= 500ms
const unsigned long LED0_ON_Time = 250000ul; //= 250ms
const unsigned long LED1_ON_Time = 500000ul; //= 500ms
const unsigned long LED2_ON_Time = 750000ul; //= 750ms
const unsigned long LED3_ON_Time = 1000000ul; //= 1000ms
unsigned long heartbeatTime;
unsigned long switchesTime;
unsigned long stateTimer;
unsigned long statePeriod;
uint8_t debugBus = 0;
// s e t u p ( )
//================================================^================================================
//
void setup()
{
Serial.begin(115200);
for (byte x = 0; x < ledArraySize; x++)
{
pinMode(LEDs[x], OUTPUT);
digitalWrite(LEDs[x], LOW);
}
pinMode(heartbeatLED, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
digitalWrite(LATCH_PIN, HIGH);
pinMode(mySwitch, INPUT_PULLUP);
//Transition marker pin (D9 = PB1)
//This is used to display a State change marker pulse on the analyzer
//i.e. when we see a pulse we know the machine State has changed.
pinMode(transitionPin, OUTPUT);
digitalWrite(transitionPin, LOW);
//SPI
SPI.begin();
//8MHz
SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0));
//Reset 74HC595
LATCH_LOW;
SPI.transfer(0); //Clear the 74HC595 outputs.
LATCH_HIGH;
//Stay here for 20ms.
delay(20);
//Start the machine in STATE0
SET_STATE(STATE0, STATE0time);
} //END of setup()
// l o o p ( )
//================================================^================================================
//
void loop()
{
//======================================================================== TIMER heartbeat
//
if (millis() - heartbeatTime >= 250ul)
{
//restart this TIMER
heartbeatTime = millis();
//toggle the heartbeat LED
digitalWrite(heartbeatLED, !digitalRead(heartbeatLED));
//Toggle the 74HC595 debugBus heartbeat bit (i.e. bit3).
debugBus = debugBus ^ 0b00001000;
outputState();
}
//======================================================================== TIMER checkSwitches
//
if (millis() - switchesTime >= 50ul)
{
//restart this TIMER
switchesTime = millis();
checkSwitches();
}
//======================================================================== State machine
//
//Is it time to service the State Machine ?
if (micros() - stateTimer >= statePeriod)
{
switch (mState)
{
//========================
case STATE0:
{
digitalWrite(LEDs[0], LEDon);
SET_STATE(STATE1, LED0_ON_Time);
}
break;
//========================
case STATE1:
{
digitalWrite(LEDs[0], LEDoff);
digitalWrite(LEDs[1], LEDon);
SET_STATE(STATE2, LED1_ON_Time);
}
break;
//========================
case STATE2:
{
digitalWrite(LEDs[1], LEDoff);
digitalWrite(LEDs[2], LEDon);
SET_STATE(STATE3, LED2_ON_Time);
}
break;
//========================
case STATE3:
{
digitalWrite(LEDs[2], LEDoff);
digitalWrite(LEDs[3], LEDon);
SET_STATE(STATE4, LED3_ON_Time);
}
break;
//========================
case STATE4:
{
digitalWrite(LEDs[3], LEDoff);
//Immediate transition on the next loop iteration.
SET_STATE(STATE5, 0);
}
break;
//========================
case STATE5:
{
SET_STATE(STATE0, STATE0time);
}
break;
//========================
default:
{
}
break;
} //END of switch/case
}
} //END of loop()
// c h e c k S w i t c h e s ( )
//================================================^================================================
//
void checkSwitches()
{
byte switchState;
//======================================================================== mySwitch
switchState = digitalRead(mySwitch);
//Was there a change in this switch's state ?
if (lastMySwitchState != switchState)
{
//Update to the new state.
lastMySwitchState = switchState;
//Toggle the "mySwitch" bit (i.e. bit4) and update the 74HC595 debugBus.
debugBus = debugBus ^ 0b00010000;
outputState();
//========================
//Is this switch pressed ?
if (switchState == LOW)
{
}
else
{
}
} //END of mySwitch
} //END of checkSwitches()
// o u t p u t S t a t e ( )
//================================================^================================================
//
inline void outputState()
{
uint8_t value;
//Bit functions:
//bit 4 = mySwitch, bit 3 = heartbeat, bits 2-0 = State number.
//Combine debug bits with the lower 3 state bits
value = (debugBus & 0b00011000) | (mState & 0b00000111);
LATCH_LOW;
SPI.transfer(value);
LATCH_HIGH;
} //END of outputState()
//
//================================================^================================================
//


