Feedback on "Making Arduino State Machines Visible on a Logic Analyzer" tutorial

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
//================================================^================================================
//

Arduino UNO R3 uses two types of MCU packages -- 28-pin DIP and 32-pin TQFP. Your schematic has shown the TQFP package which you have not even mentioned. So, this schematic for the MCU serves to those users who have 32-pin TQFP processors on their boards.

I think the following DPin-based shematic (Fig-1) for the Arduino UNO R3 would serve all users regardless of the MCU package onboard.


Figure-1:

  • I Like to have pin 10 do a hardware reset on powering. R1 and C1 in my circuit.

  • My UNO schematic uses a 328 DIP.

Then you may remove the physical pin numbers (1 to 32) and put DPin- numbers instead near the dots of the wires.

  • My old schematic program wants to add the numbers, but I can hide the #s.
    Now I should shorten the leads :thinking: Done

:+1:

1.
This is good. If you have made it by schematic capture, then you can put texts like A5, A4, ....., 1, 0 near the dots of the wires.

2.
You may write:

On/Off Control
in Input Mode

3.
20-50K
==> 20K - 50K (20 might mean 20 ohm)

4.
You could show the onboard LED, L.

  • Is it possible I have met my match ? :wink:

You have put a"rectifying diode". It should a "Light Emitting Diode".

  • I knew you were going to say that, tomorrow . . .
    It does say L LED

Done

I mean to say that it is not a "rectifying diode" but a "Light Emitting Diode".

it's hard for others to exercise your code without your hardware. Another approach is to periodically (100ms) output the state and its new value when it changes and display on the plotter. (sequential state value, no need for bit-mask)

Beautiful except that the LED in #9 is missing the current limiting resistor.

:scream:

  • It's on the UNO board.

:smiling_face_with_sunglasses:

That very internal pull-up is also on the UNO board and yet it has come into the schematic?

  • That's because we are talking about the pullup. :nerd_face:

  • With the L LED we are talking about the LED.
    And there's no way the LED driver will enter the discussion . . . :wink:

Here, we are also talking about onboard L which should have some kind of over-current protection?

:roll_eyes:

  • Point taken, but this is just a representative UNO symbol.
  • You might be bored.
    Here is something else you can review too.