🔧 Turn Your Logic Analyzer into a Real-Time Firmware Debugger

Further to post:




Logic Analyzer Interface – Real-Time Firmware Debugging

Overview

This Logic Analyzer Interface allows you to observe firmware behavior in real time without using Serial.print() and without disturbing execution timing.

Key Capabilities

  • Observe state machine transitions
  • Track events and flags
  • Measure timing relationships
  • Correlate software with hardware signals
  • Debug without slowing down your code

Concept

  • Instead of printing debug text, the firmware writes to a 24-bit parallel debug bus implemented using SPI-driven shift registers.
    3 × 74HC595 shift registers
    Approximate update time: 3 µs

Hardware Interface

  • SPI-based 6-wire interface:
    • GND
    • SCK = D13
    • MISO = D12 (not used)
    • MOSI = D11
    • SS = D10 (must differ from user SPI latch)
    • MARK = D9 (~250 ns timing pulse)

MARK Pulse

  • Pin D9 generates a ~250 ns pulse marking precise execution timing events such as state transitions and critical code points.

24-bit Register Usage

  • Upper 16 bits:
    • Logic analyzer connections
    • Oscilloscope observation
    • Dedicated scope trigger
  • Lower 8 bits:
    • Diagnostic LEDs
    • Real-time activity indication
    • Heartbeat LED
    • Optional small load driving

Schematic Highlights

  • 74AHC125 buffer for signal integrity and level compatibility
  • Three 74HC595 shift registers for 24-bit output
  • ULN2803 driver for LEDs and higher current loads
  • Series resistors for protection and signal integrity

Software API

  • The API uses inline macros for high-speed instrumentation.
  • No function call overhead
  • Flash usage: ~1.8k – 2.5k

Enable / Disable Debugging

  • Use compile-time control:
    #define DEBUG_ANALYZER
    Commenting this out removes all debug code with zero runtime overhead.

Basic Usage

  • Connect analyzer to debug outputs
  • Run sketch
  • Observe timing relationships directly

What You Can Observe

  • State machine execution
  • ISR timing
  • Variable changes
  • Event sequencing
  • Execution timing

Timing Note

  • A small (~3 µs) delay from SPI updates may be visible. The MARK pulse represents true event timing.

Purpose

  • Designed for deterministic debugging with no timing distortion and full visibility into firmware behavior.

Demonstrate Interface and API Use Cases

  • Follow up posts, will cover examples in using the Interface and its API.
    If there is interest, we can take this further.






EDIT

API is at Version 2.6 now.

  • API code and example.
    MD5:
    14E1DD97C3467A1F91949E6919D8DA4F

LogicAnalyzerVersion2.6.zip (19.1 KB)

  • FYI
/*
  //================================================
  // Logic Analyzer Macros:
  //================================================

  DEBUG_PULSE_COUNT_START(bit, count)
  DEBUG_PULSE_COUNT_SERVICE()
  DEBUG_PULSE_COUNT_BUSY()
  DEBUG_PULSE_COUNT_CANCEL()

  DEBUG_TOGGLE(bit);
  DEBUG_HIGH(bit);
  DEBUG_LOW(bit);
  DEBUG_SET_REG(bit, value);
  DEBUG_FAST_PULSE(bit);

  DEBUG_EVENT_FAST(id);            
  DEBUG_EVENT(id);

  DEBUG_SCOPE_TRIGGER()       //Oscilloscope Trigger Reg16 only.

  //All DEBUG_DELAYED_*() macros use a shared timing engine. Only one delayed 
  //operation can be active at a time—subsequent calls overwrite any previous delayed action.
  DEBUG_DELAYED_MARK(bit, delay_us);
  DEBUG_DELAYED_HIGH(bit, delay_us);
  DEBUG_DELAYED_LOW(bit, delay_us);
  DEBUG_DELAYED_PULSE(bit, delay_us, width_us);
  DEBUG_DELAYED_SERVICE();

  DEBUG_LED_ON(bit);
  DEBUG_LED_OFF(bit);
  DEBUG_LED_PULSE(bit,duration);
  DEBUG_INTERFACE_HEARTBEAT(onTime_ms, offTime_ms)
  DEBUG_LEDsService();

*/
6 Likes

Nice job, looks professional. Thanks for sharing!

1 Like

Looking good. I hope to find some time later this year to dig into it.

1 Like

What is the function of D1?

1 Like
  • When unplugging then returning power to the interface, D1 provides a discharge path for C5.
    This ensures the timing of POR "Power On RESET" is kept relatively constant.
    A Schottky diode symbol should have been used.
    Since R26 is a small value (4.7k), the diode may not be needed.
2 Likes

Let's start the discussion.

Using a Logic Analyzer with an Arduino

When working with a logic analyzer on an Arduino-class microcontroller, there are two common approaches to observing firmware behavior.

Option 1: Direct GPIO Instrumentation

This is the most straightforward method:

  • Connect logic analyzer channels directly to GPIO pins
  • Use digitalWrite() (or direct port manipulation) to indicate events

Advantages:

  • Simple and familiar
  • No additional hardware required
    Limitations:
  • Consumes GPIO pins quickly
  • Timing skew between signals due to sequential execution
  • Intrusive code changes
  • Limited scalability

Option 2: SPI-Based Debug Interface

A more structured approach is to use a serial-to-parallel interface such as 74HC595 shift registers driven by SPI.

  • Debug data is sent over SPI
  • Shift registers present this data as a parallel bus
  • A logic analyzer monitors this bus

Advantages:

  • Very low pin usage
  • Simultaneous signal updates
  • Deterministic timing
  • Scales easily
  • Cleaner application code
  • Supports structured debugging
  • Logic Analyzer probes do not need to be connected to
    or moved while debugging an Arduino sketch

Debug API Overview

Instead of scattering digitalWrite() calls throughout your code, you use high-level macros:

  //================================================
  // Logic Analyzer Macros:
  //================================================

  DEBUG_PULSE_COUNT_START(bit, count)
  DEBUG_PULSE_COUNT_SERVICE()
  DEBUG_PULSE_COUNT_BUSY()
  DEBUG_PULSE_COUNT_CANCEL()

  DEBUG_TOGGLE(bit);
  DEBUG_HIGH(bit);
  DEBUG_LOW(bit);
  DEBUG_SET_REG(bit, value);
  DEBUG_FAST_PULSE(bit);

  DEBUG_EVENT_FAST(id);            
  DEBUG_EVENT(id);

  DEBUG_SCOPE_TRIGGER()       //Oscilloscope Trigger Reg16 only.

  //All DEBUG_DELAYED_*() macros use a shared timing engine. Only one delayed 
  //operation can be active at a time—subsequent calls overwrite any previous delayed action.
  DEBUG_DELAYED_MARK(bit, delay_us);
  DEBUG_DELAYED_HIGH(bit, delay_us);
  DEBUG_DELAYED_LOW(bit, delay_us);
  DEBUG_DELAYED_PULSE(bit, delay_us, width_us);
  DEBUG_DELAYED_SERVICE();

  DEBUG_LED_ON(bit);
  DEBUG_LED_OFF(bit);
  DEBUG_LED_PULSE(bit,duration);
  DEBUG_LEDsService();

  DEBUG_INTERFACE_HEARTBEAT(onTime_ms, offTime_ms)

Summary Comparison

Direct GPIO vs SPI Debug Interface:

  • Pin usage: High vs Low
  • Timing alignment: Poor vs Excellent
  • Maintainability: Low vs High
  • Scalability: Limited vs High
  • Debug capability: Basic vs Advanced
1 Like

Option 1: Direct GPIO Instrumentation

Here we have a sketch where the Logic Analyzer is connected directly to a GPIO.

//
//================================================^================================================
//                                   Basic Logic Analyzer Interface
//================================================^================================================
//
//  File:      Option_1.ino
//
//  URL
//
//  LarryD
//
//  Version    YY/MM/DD    Comments
//  =======    ========    ========================================================================
//  1.0        26/03/20    Running code
//
//
//
//

//================================================^================================================
//                                 A p p l i c a t i o n   c o d e
//================================================^================================================

// Macros
//========================
#define DEBUG_NOP() __asm__ __volatile__("nop\n\t")

//250ns MARK pulse.
#define DEBUG_PULSE_DELAY() \
  do { \
    DEBUG_NOP(); \
    DEBUG_NOP(); \
    DEBUG_NOP(); \
  } while(0)

byte pulse250_Pin                 = 9;
//
#define PULSE250 do { cli(); PINB = _BV(PB1); DEBUG_PULSE_DELAY(); PINB = _BV(PB1); sei(); } while(0)



//                                    S t a t e   M a c h i n e
//================================================^================================================
//

enum SystemState : byte
{
  // State      43210            Decimal      Change names as needed:
  STATE0    = 0b00000,        // 00      e.g. STATE0_POWER_UP
  STATE1    = 0b00001,        // 01           STATE1_WallSwitchPushedUp
  STATE2    = 0b00010,        // 02           STATE2_GarageDoorGoingUp
  STATE3    = 0b00011,        // 03           STATE3_WallSwitchPushedDown
  STATE4    = 0b00100,        // 04           STATE4_GarageDoorGoingDown
  STATE5    = 0b00101,        // 05           etc.
  STATE6    = 0b00110,        // 06
  STATE7    = 0b00111,        // 07
  STATE8    = 0b01000,        // 08
  STATE9    = 0b01001,        // 09
  STATE10   = 0b01010,        // 10
  STATE11   = 0b01011,        // 11
  STATE12   = 0b01100,        // 12
  STATE13   = 0b01101,        // 13
  STATE14   = 0b01110,        // 14
  STATE15   = 0b01111         // 15
};

//Let's start out in STATE0.
SystemState mState = STATE0;

//===============================
// Sketch Macros
//===============================

#define LEDon               HIGH          // [Pin]---[220R]---A[LED]K---GND
#define LEDoff              LOW

#define PRESSED             LOW           // +5V---[InternalPullup]---[Pin]---[Switch]---GND
#define RELEASED            HIGH

#define IMMEDIATE           0UL           // Used when we want the new State to immediately execute.
#define WAIT_FOREVER        0xFFFFFFFFul  // Used when we don't want to leave the State.


//===============================
// GPIOs
//===============================

const byte LEDs[]                = {2, 3, 4, 5};  // +5v---[220R]---A[LED]K---Pin

const byte ledArraySize          = sizeof(LEDs) / sizeof(LEDs[0]);

const byte mySwitch              = 6;             // +5V---[internal Pullup]---[Switch]---GND
const byte heartbeatLED          = 7;             // +5v---[220R]---A[LED]K---Pin

//===============================
// Variables
//===============================

byte lastMySwitchState           = LOW;

//========================
//State durations (us)
const unsigned long STATE0_Time  = 500ul;     // = 500ms
const unsigned long LED0_ON_Time = 250ul;     // = 250ms
const unsigned long LED1_ON_Time = 500ul;     // = 500ms
const unsigned long LED2_ON_Time = 750ul;     // = 750ms
const unsigned long LED3_ON_Time = 1000ul;    // = 1000ms

//===============================
// TIMERs
//===============================

unsigned long heartbeatTime;
unsigned long heartbeatInterval               = 500ul;   // 500ms

unsigned long switchesTime;
unsigned long switchesInterval                = 50ul;    // 50ms

unsigned long machineTime;
unsigned long machineInterval                 = 1000ul;  // 1000us or 1ms

unsigned long stateTimer;
unsigned long statePeriod;


//                                      N E X T _ S T A T E ( )
//================================================^================================================
//
void SET_STATE(SystemState next, unsigned long duration)
{
  mState = (next);

  statePeriod = (duration);

  //Restart this TIMER.
  stateTimer = millis();

} // END of   SET_STATE(next, duration)




//                                           s e t u p ( )
//================================================^================================================
//
void setup()
{
  //Serial.begin(115200);

  //========================
  // Logic Analyzer connections.
  //========================
  pinMode(pulse250_Pin, OUTPUT);


  for (byte x = 0; x < ledArraySize; x++)
  {
    pinMode(LEDs[x], OUTPUT);
    digitalWrite(LEDs[x], LOW);
  }

  pinMode(heartbeatLED, OUTPUT);

  pinMode(mySwitch, INPUT_PULLUP);
  lastMySwitchState = digitalRead(mySwitch);

  //Initialize the state machine and its timer.
  //
  SET_STATE(STATE0, STATE0_Time);

} // END of   setup()


//                                            l o o p ( )
//================================================^================================================
//
void loop()
{
  //========================
  // Normal sketch code
  //========================

  checkHeartbeat();
  checkSwitches();
  serviceStateMachine();


  //================================================
  //       Other non blocking code goes here
  //================================================


} //END of   loop()


//                                  c h e c k H e a r t b e a t ( )
//================================================^================================================
//
void checkHeartbeat()
{
  //Is it time to toggle the heartbeat LED ?
  if (millis() - heartbeatTime < heartbeatInterval)
  {
    return;
  }

  //Restart this TIMER.
  heartbeatTime = millis();

  //Toggle the heartbeat LED.
  digitalWrite(heartbeatLED, !digitalRead(heartbeatLED));

} //END of   checkHeartbeat()


//                                    S t a t e   M a c h i n e
//================================================^================================================
//
void serviceStateMachine()
{
  //================================================
  //Is it time to service this State Machine ?
  //
  if (micros() - machineTime < machineInterval)
  {
    return;
  }

  //Restart the machineTime TIMER.
  machineTime = micros();

  //================================================
  //Is the State Machine's TIMER expired ?
  //
  //WAIT_FOREVER(0xFFFFFFFFUL), causes the current State to be executed forever.
  //IMMEDIATE (0), causes the State to be immediately executed (no delay).
  //
  if (statePeriod != WAIT_FOREVER &&
      (statePeriod == IMMEDIATE || millis() - stateTimer >= statePeriod))
  {
    //========================
    switch (mState)
    {
      //============
      case STATE0:
        {
          digitalWrite(LEDs[0], LEDon);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE1, LED0_ON_Time);
        }
        break;

      //============
      case STATE1:
        {
          digitalWrite(LEDs[0], LEDoff);
          digitalWrite(LEDs[1], LEDon);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE2, LED1_ON_Time);
        }
        break;

      //============
      case STATE2:
        {
          digitalWrite(LEDs[1], LEDoff);
          digitalWrite(LEDs[2], LEDon);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE3, LED2_ON_Time);
        }
        break;

      //============
      case STATE3:
        {
          digitalWrite(LEDs[2], !digitalRead(LEDs[2]));

          //digitalWrite(LEDs[2], LEDoff);
          //digitalWrite(LEDs[3], LEDon);

          //SET_STATE(STATE3, 200ul);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE4, LED3_ON_Time);
        }
        break;

      //============
      case STATE4:
        {
          digitalWrite(LEDs[3], LEDoff);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE5, IMMEDIATE);
        }
        break;

      //============
      case STATE5:
        {
          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE0, STATE0_Time);
        }
        break;

      //============
      default:
        {
          //Do something.
        }
        break;

    } //END of   switch/case

  } //END of   if(statePeriod != WAIT_FOREVER ... )

} //END of   serviceStateMachine()


//                                   c h e c k S w i t c h e s ( )
//================================================^================================================
//
void checkSwitches()
{
  //Is it time to check the switches ?
  if (millis() - switchesTime < switchesInterval)
  {
    return;
  }

  //Restart this TIMER.
  switchesTime = millis();

  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;

    //Generate a 250ns on GPIO D9
    PULSE250;

    //========================
    //Is this switch pressed ?
    //
    if (switchState == PRESSED)
    {
      static byte counter = 0;

      counter = counter + 1;

      //Every 10 switch presses.
      if (counter == 10)
      {
        //Counting starts over.
        counter = 0;
      }

    }

    //========================
    //The switch was released.
    //
    else
    {
    }

  } //END of   mySwitch

} //END of   checkSwitches()


//
//================================================^================================================
//

Option 1: Direct GPIO Instrumentation 4 Channels

  • We are now monitoring a total of 4 Channels.
    Our sketch is getting a bit contorted :thinking:

  • The Logic Analyzer can display timing relationships, pulse widths, edge counts and much more.

//
//================================================^================================================
//                                   Basic Logic Analyzer Interface
//================================================^================================================
//
//  File:      Option_1.ino
//
//  URL
//
//  LarryD
//
//  Version    YY/MM/DD    Comments
//  =======    ========    ========================================================================
//  1.0        26/03/20    Running code
//
//
//
//

//================================================^================================================
//                                 A p p l i c a t i o n   c o d e
//================================================^================================================

// Macros
//========================
#define DEBUG_NOP() __asm__ __volatile__("nop\n\t")

//250ns MARK pulse.
#define DEBUG_PULSE_DELAY() \
 do { \
   DEBUG_NOP(); \
   DEBUG_NOP(); \
   DEBUG_NOP(); \
 } while(0)

#define PULSE250 do { cli(); PINB = _BV(PB1); DEBUG_PULSE_DELAY(); PINB = _BV(PB1); sei(); } while(0)

//Analyzer pins
const byte pulse250Channel              = 9;
const byte switchChannel                = 8;
const byte FlagChannel                  = 14;
const byte loopTimingChannel            = 15;



//                                    S t a t e   M a c h i n e
//================================================^================================================
//

enum SystemState : byte
{
 // State      43210            Decimal      Change names as needed:
 STATE0    = 0b00000,        // 00      e.g. STATE0_POWER_UP
 STATE1    = 0b00001,        // 01           STATE1_WallSwitchPushedUp
 STATE2    = 0b00010,        // 02           STATE2_GarageDoorGoingUp
 STATE3    = 0b00011,        // 03           STATE3_WallSwitchPushedDown
 STATE4    = 0b00100,        // 04           STATE4_GarageDoorGoingDown
 STATE5    = 0b00101,        // 05           etc.
 STATE6    = 0b00110,        // 06
 STATE7    = 0b00111,        // 07
 STATE8    = 0b01000,        // 08
 STATE9    = 0b01001,        // 09
 STATE10   = 0b01010,        // 10
 STATE11   = 0b01011,        // 11
 STATE12   = 0b01100,        // 12
 STATE13   = 0b01101,        // 13
 STATE14   = 0b01110,        // 14
 STATE15   = 0b01111         // 15
};

//Let's start out in STATE0.
SystemState mState = STATE0;

//===============================
// Sketch Macros
//===============================

#define LEDon               HIGH          // [Pin]---[220R]---A[LED]K---GND
#define LEDoff              LOW

#define PRESSED             LOW           // +5V---[InternalPullup]---[Pin]---[Switch]---GND
#define RELEASED            HIGH

#define IMMEDIATE           0UL           // Used when we want the new State to immediately execute.
#define WAIT_FOREVER        0xFFFFFFFFul  // Used when we don't want to leave the State.


//===============================
// GPIOs
//===============================

const byte LEDs[]                = {2, 3, 4, 5};  // +5v---[220R]---A[LED]K---Pin

const byte ledArraySize          = sizeof(LEDs) / sizeof(LEDs[0]);

const byte mySwitch              = 6;             // +5V---[internal Pullup]---[Switch]---GND
const byte heartbeatLED          = 7;             // +5v---[220R]---A[LED]K---Pin

//===============================
// Variables
//===============================

byte lastMySwitchState           = LOW;

//========================
//State durations (us)
const unsigned long STATE0_Time  = 500ul;     // = 500ms
const unsigned long LED0_ON_Time = 250ul;     // = 250ms
const unsigned long LED1_ON_Time = 500ul;     // = 500ms
const unsigned long LED2_ON_Time = 750ul;     // = 750ms
const unsigned long LED3_ON_Time = 1000ul;    // = 1000ms

//===============================
// TIMERs
//===============================

unsigned long heartbeatTime;
unsigned long heartbeatInterval               = 500ul;   // 500ms

unsigned long switchesTime;
unsigned long switchesInterval                = 50ul;    // 50ms

unsigned long machineTime;
unsigned long machineInterval                 = 1000ul;  // 1000us or 1ms

unsigned long stateTimer;
unsigned long statePeriod;


//                                      N E X T _ S T A T E ( )
//================================================^================================================
//
void SET_STATE(SystemState next, unsigned long duration)
{
 mState = (next);

 statePeriod = (duration);

 //Restart this TIMER.
 stateTimer = millis();

} // END of   SET_STATE(next, duration)




//                                           s e t u p ( )
//================================================^================================================
//
void setup()
{
 //Serial.begin(115200);

 //========================
 // Logic Analyzer connections.
 //========================
 
 //This will mark the rising and falling edges of mySwitch state changes.
 pinMode(pulse250Channel, OUTPUT);
 digitalWrite(pulse250Channel, LOW);

 //This will follow the mySwitch state.
 pinMode(switchChannel, OUTPUT);
 digitalWrite(switchChannel, HIGH);

 //This will flag the number of switch presses 0-10
 pinMode(FlagChannel, OUTPUT);
 digitalWrite(FlagChannel, LOW);

 //This is used to check loop() execution time.
 pinMode(loopTimingChannel, OUTPUT);
 digitalWrite(loopTimingChannel, LOW);  

 //========================
 // LEDs
 //========================
 
 for (byte x = 0; x < ledArraySize; x++)
 {
   pinMode(LEDs[x], OUTPUT);
   digitalWrite(LEDs[x], LOW);
 }

 pinMode(heartbeatLED, OUTPUT);

 //========================
 pinMode(mySwitch, INPUT_PULLUP);
 lastMySwitchState = digitalRead(mySwitch);

 //========================
 //Initialize the state machine and its timer.
 //
 SET_STATE(STATE0, STATE0_Time);

} // END of   setup()


//                                            l o o p ( )
//================================================^================================================
//
void loop()
{
 //========================
 // Normal sketch code
 //========================

 //For loop() execution timing.
 digitalWrite(loopTimingChannel, HIGH);
 digitalWrite(loopTimingChannel, LOW);

 checkHeartbeat();
 checkSwitches();
 serviceStateMachine();


 //================================================
 //       Other non blocking code goes here
 //================================================


} //END of   loop()


//                                  c h e c k H e a r t b e a t ( )
//================================================^================================================
//
void checkHeartbeat()
{
 //Is it time to toggle the heartbeat LED ?
 if (millis() - heartbeatTime < heartbeatInterval)
 {
   return;
 }

 //Restart this TIMER.
 heartbeatTime = millis();

 //Toggle the heartbeat LED.
 digitalWrite(heartbeatLED, !digitalRead(heartbeatLED));

} //END of   checkHeartbeat()


//                                    S t a t e   M a c h i n e
//================================================^================================================
//
void serviceStateMachine()
{
 //================================================
 //Is it time to service this State Machine ?
 //
 if (micros() - machineTime < machineInterval)
 {
   return;
 }

 //Restart the machineTime TIMER.
 machineTime = micros();

 //================================================
 //Is the State Machine's TIMER expired ?
 //
 //WAIT_FOREVER(0xFFFFFFFFUL), causes the current State to be executed forever.
 //IMMEDIATE (0), causes the State to be immediately executed (no delay).
 //
 if (statePeriod != WAIT_FOREVER &&
     (statePeriod == IMMEDIATE || millis() - stateTimer >= statePeriod))
 {
   //========================
   switch (mState)
   {
     //============
     case STATE0:
       {
         digitalWrite(LEDs[0], LEDon);

         //Set the NEXT State and initialize the State Machine TIMER.
         SET_STATE(STATE1, LED0_ON_Time);
       }
       break;

     //============
     case STATE1:
       {
         digitalWrite(LEDs[0], LEDoff);
         digitalWrite(LEDs[1], LEDon);

         //Set the NEXT State and initialize the State Machine TIMER.
         SET_STATE(STATE2, LED1_ON_Time);
       }
       break;

     //============
     case STATE2:
       {
         digitalWrite(LEDs[1], LEDoff);
         digitalWrite(LEDs[2], LEDon);

         //Set the NEXT State and initialize the State Machine TIMER.
         SET_STATE(STATE3, LED2_ON_Time);
       }
       break;

     //============
     case STATE3:
       {
         digitalWrite(LEDs[2], !digitalRead(LEDs[2]));

         //digitalWrite(LEDs[2], LEDoff);
         //digitalWrite(LEDs[3], LEDon);

         //SET_STATE(STATE3, 200ul);

         //Set the NEXT State and initialize the State Machine TIMER.
         SET_STATE(STATE4, LED3_ON_Time);
       }
       break;

     //============
     case STATE4:
       {
         digitalWrite(LEDs[3], LEDoff);

         //Set the NEXT State and initialize the State Machine TIMER.
         SET_STATE(STATE5, IMMEDIATE);
       }
       break;

     //============
     case STATE5:
       {
         //Set the NEXT State and initialize the State Machine TIMER.
         SET_STATE(STATE0, STATE0_Time);
       }
       break;

     //============
     default:
       {
         //Do something.
       }
       break;

   } //END of   switch/case

 } //END of   if(statePeriod != WAIT_FOREVER ... )

} //END of   serviceStateMachine()


//                                   c h e c k S w i t c h e s ( )
//================================================^================================================
//
void checkSwitches()
{
 //Is it time to check the switches ?
 if (millis() - switchesTime < switchesInterval)
 {
   return;
 }

 //Restart this TIMER.
 switchesTime = millis();

 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;

   //Mark the edges, generate a 250ns on GPIO D9
   PULSE250;

   //Display the mySwitch state on D8.
   digitalWrite(switchChannel, !digitalRead(switchChannel));

   //========================
   //Is this switch pressed ?
   //
   if (switchState == PRESSED)
   {
     static byte counter = 0;

     counter = counter + 1;

     //Flag the start of the 1st press.
     digitalWrite(FlagChannel, HIGH);

     //Every 10 switch presses.
     if (counter == 10)
     {
       //Counting starts over.
       counter = 0;

       //Flag the end of the 10th press.
       digitalWrite(FlagChannel, LOW);
     }

   }

   //========================
   //The switch was released.
   //
   else
   {
   }

 } //END of   mySwitch

} //END of   checkSwitches()


//
//================================================^================================================
//



Option 1: Direct GPIO Instrumentation "State Machine"

  • We are now monitoring a total of 7 Channels.
    It is difficult to separate the Application's code from Analyzer's code.

  • The Logic Analyzer displays and decodes the States in our State Machine.
    D9 is used as a Pulse/Strobe to decode parallel 3 bits that define the States.

//
//================================================^================================================
//                                   Basic Logic Analyzer Interface
//================================================^================================================
//
//  File:      Option_1.ino
//
//  URL
//
//  LarryD
//
//  Version    YY/MM/DD    Comments
//  =======    ========    ========================================================================
//  1.0        26/03/20    Running code
//
//
//
//

//================================================^================================================
//                                 A p p l i c a t i o n   c o d e
//================================================^================================================

//========================
// Logic Analyzer Stuff.
//========================

#define DEBUG_NOP() __asm__ __volatile__("nop\n\t")

//250ns MARK pulse.
#define DEBUG_PULSE_DELAY() \
  do { \
    DEBUG_NOP(); \
    DEBUG_NOP(); \
    DEBUG_NOP(); \
  } while(0)

#define PULSE250 do { cli(); PINB = _BV(PB1); DEBUG_PULSE_DELAY(); PINB = _BV(PB1); sei(); } while(0)

//Analyzer pins
const byte pulse250Channel              = 9;
const byte switchChannel                = 8;
const byte FlagChannel                  = 14;
const byte loopTimingChannel            = 15;

const byte bit_0                        = 16;
const byte bit_1                        = 17;
const byte bit_2                        = 18;
const byte bit_3                        = 19;



//                                    S t a t e   M a c h i n e
//================================================^================================================
//

enum SystemState : byte
{
  // State      43210            Decimal      Change names as needed:
  STATE0    = 0b00000,        // 00      e.g. STATE0_POWER_UP
  STATE1    = 0b00001,        // 01           STATE1_WallSwitchPushedUp
  STATE2    = 0b00010,        // 02           STATE2_GarageDoorGoingUp
  STATE3    = 0b00011,        // 03           STATE3_WallSwitchPushedDown
  STATE4    = 0b00100,        // 04           STATE4_GarageDoorGoingDown
  STATE5    = 0b00101,        // 05           etc.
  STATE6    = 0b00110,        // 06
  STATE7    = 0b00111,        // 07
  STATE8    = 0b01000,        // 08
  STATE9    = 0b01001,        // 09
  STATE10   = 0b01010,        // 10
  STATE11   = 0b01011,        // 11
  STATE12   = 0b01100,        // 12
  STATE13   = 0b01101,        // 13
  STATE14   = 0b01110,        // 14
  STATE15   = 0b01111         // 15
};

//Let's start out in STATE0.
SystemState mState = STATE0;

//===============================
// Sketch Macros
//===============================

#define LEDon               HIGH          // [Pin]---[220R]---A[LED]K---GND
#define LEDoff              LOW

#define PRESSED             LOW           // +5V---[InternalPullup]---[Pin]---[Switch]---GND
#define RELEASED            HIGH

#define IMMEDIATE           0UL           // Used when we want the new State to immediately execute.
#define WAIT_FOREVER        0xFFFFFFFFul  // Used when we don't want to leave the State.


//===============================
// GPIOs
//===============================

const byte LEDs[]                = {2, 3, 4, 5};  // +5v---[220R]---A[LED]K---Pin

const byte ledArraySize          = sizeof(LEDs) / sizeof(LEDs[0]);

const byte mySwitch              = 6;             // +5V---[internal Pullup]---[Switch]---GND
const byte heartbeatLED          = 7;             // +5v---[220R]---A[LED]K---Pin

//===============================
// Variables
//===============================

byte lastMySwitchState           = LOW;

//========================
//State durations.
const unsigned long STATE0_Time  = 500ul;     // = 500ms
const unsigned long LED0_ON_Time = 250ul;     // = 250ms
const unsigned long LED1_ON_Time = 500ul;     // = 500ms
const unsigned long LED2_ON_Time = 750ul;     // = 750ms
const unsigned long LED3_ON_Time = 1000ul;    // = 1000ms

//===============================
// TIMERs
//===============================

unsigned long heartbeatTime;
unsigned long heartbeatInterval               = 500ul;   // 500ms

unsigned long switchesTime;
unsigned long switchesInterval                = 50ul;    // 50ms

unsigned long machineTime;
unsigned long machineInterval                 = 1000ul;  // 1000μs or 1ms

unsigned long stateTimer;
unsigned long statePeriod;


//                                      N E X T _ S T A T E ( )
//================================================^================================================
//
void SET_STATE(SystemState next, unsigned long duration)
{
  mState = (next);

  //Logic Analyzer binary value of the current State of our State Machine.
  digitalWrite(bit_0, mState & 0b00001);
  digitalWrite(bit_1, mState & 0b00010);
  digitalWrite(bit_2, mState & 0b00100);
  digitalWrite(bit_3, mState & 0b01000);

  //Generate a 250ns parallel decode pulse on GPIO D9
  PULSE250;

  statePeriod = (duration);

  //Restart this TIMER.
  stateTimer = millis();

} // END of   SET_STATE(next, duration)




//                                           s e t u p ( )
//================================================^================================================
//
void setup()
{
  //Serial.begin(115200);

  //========================
  // Logic Analyzer connections.
  //========================

  //This will mark the rising and falling edges of mySwitch state changes.
  pinMode(pulse250Channel, OUTPUT);
  digitalWrite(pulse250Channel, LOW);

  //This will follow the mySwitch state.
  pinMode(switchChannel, OUTPUT);
  digitalWrite(switchChannel, HIGH);

  //This will flag the number of switch presses 0-10
  pinMode(FlagChannel, OUTPUT);
  digitalWrite(FlagChannel, LOW);

  //This is used to check loop() execution time.
  pinMode(loopTimingChannel, OUTPUT);
  digitalWrite(loopTimingChannel, LOW);

  //State Machine bits.
  pinMode(bit_0, OUTPUT);
  pinMode(bit_1, OUTPUT);
  pinMode(bit_2, OUTPUT);
  pinMode(bit_3, OUTPUT);

  //========================
  // LEDs
  //========================

  for (byte x = 0; x < ledArraySize; x++)
  {
    pinMode(LEDs[x], OUTPUT);
    digitalWrite(LEDs[x], LOW);
  }

  pinMode(heartbeatLED, OUTPUT);

  //========================
  pinMode(mySwitch, INPUT_PULLUP);
  lastMySwitchState = digitalRead(mySwitch);

  //========================
  //Initialize the state machine and its timer.
  //
  SET_STATE(STATE0, STATE0_Time);

} // END of   setup()


//                                            l o o p ( )
//================================================^================================================
//
void loop()
{
  //========================
  // Normal sketch code
  //========================

  //For loop() execution timing.
  digitalWrite(loopTimingChannel, HIGH);
  digitalWrite(loopTimingChannel, LOW);

  checkHeartbeat();
  checkSwitches();
  serviceStateMachine();


  //================================================
  //       Other non blocking code goes here
  //================================================


} //END of   loop()


//                                  c h e c k H e a r t b e a t ( )
//================================================^================================================
//
void checkHeartbeat()
{
  //Is it time to toggle the heartbeat LED ?
  if (millis() - heartbeatTime < heartbeatInterval)
  {
    return;
  }

  //Restart this TIMER.
  heartbeatTime = millis();

  //Toggle the heartbeat LED.
  digitalWrite(heartbeatLED, !digitalRead(heartbeatLED));

} //END of   checkHeartbeat()


//                                    S t a t e   M a c h i n e
//================================================^================================================
//
void serviceStateMachine()
{
  //================================================
  //Is it time to service this State Machine ?
  //
  if (micros() - machineTime < machineInterval)
  {
    return;
  }

  //Restart the machineTime TIMER.
  machineTime = micros();

  //================================================
  //Is the State Machine's TIMER expired ?
  //
  //WAIT_FOREVER(0xFFFFFFFFUL), causes the current State to be executed forever.
  //IMMEDIATE (0), causes the State to be immediately executed (no delay).
  //
  if (statePeriod != WAIT_FOREVER &&
      (statePeriod == IMMEDIATE || millis() - stateTimer >= statePeriod))
  {
    //========================
    switch (mState)
    {
      //============
      case STATE0:
        {
          digitalWrite(LEDs[0], LEDon);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE1, LED0_ON_Time);
        }
        break;

      //============
      case STATE1:
        {
          digitalWrite(LEDs[0], LEDoff);
          digitalWrite(LEDs[1], LEDon);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE2, LED1_ON_Time);
        }
        break;

      //============
      case STATE2:
        {
          digitalWrite(LEDs[1], LEDoff);
          digitalWrite(LEDs[2], LEDon);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE3, LED2_ON_Time);
        }
        break;

      //============
      case STATE3:
        {
          digitalWrite(LEDs[2], !digitalRead(LEDs[2]));

          //digitalWrite(LEDs[2], LEDoff);
          //digitalWrite(LEDs[3], LEDon);

          //SET_STATE(STATE3, 200ul);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE4, LED3_ON_Time);
        }
        break;

      //============
      case STATE4:
        {
          digitalWrite(LEDs[3], LEDoff);

          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE5, IMMEDIATE);
        }
        break;

      //============
      case STATE5:
        {
          //Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE0, STATE0_Time);
        }
        break;

      //============
      default:
        {
          //Do something.
        }
        break;

    } //END of   switch/case

  } //END of   if(statePeriod != WAIT_FOREVER ... )

} //END of   serviceStateMachine()


//                                   c h e c k S w i t c h e s ( )
//================================================^================================================
//
void checkSwitches()
{
  //Is it time to check the switches ?
  //
  if (millis() - switchesTime < switchesInterval)
  {
    return;
  }

  //Restart this TIMER.
  switchesTime = millis();

  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;

    //Display the mySwitch state on D8.
    digitalWrite(switchChannel, !digitalRead(switchChannel));

    //========================
    //Is this switch pressed ?
    //
    if (switchState == PRESSED)
    {
      static byte counter = 0;

      counter = counter + 1;

      //Flag the start of the 1st press.
      digitalWrite(FlagChannel, HIGH);

      //Every 10 switch presses.
      if (counter == 10)
      {
        //Counting starts over.
        counter = 0;

        //Flag the end of the 10th press.
        digitalWrite(FlagChannel, LOW);
      }

    }

    //========================
    //The switch was released.
    //
    else
    {
      //Do something.
    }

  } //END of   mySwitch

} //END of   checkSwitches()


//
//================================================^================================================
//



Bring in the "Simple Parallel" protocol analyzer.




Notice the Blue area where the State is displayed.

  • Also note State 5 is not seen, however, it is identified in the bottom right corner.
    Clicking on a number in this area, zooms the display to fill that State on the screen.

Option 2: SPI-Based Debug Interface

  • Using a simple serial to parallel register we can communicate to the Logic Analyzer more effectively.

  • We of course need an Interface API which was include in the opening post ZIP file.













  • 10 presses on mySwitch.

Option 2: SPI-Based Debugging

  • Once we have the three 74HC595 shift registers wired, we connect the logic analyzer as seen in the previous post.

    • After a few years of assembling the circuit on a solderless breadboard, a prototyping board becomes more appropriate. :smiling_face_with_sunglasses:
  • To use the Interface's API, we need to make a few changes to our application (sketch) code.

    • In the same directory as our sketch, add the two .h Tabs:
      • DebugAnalyzerConfig.h
      • LogicAnalyzerDebug.h
    • We need to make a few changes to our .ino file as seen in the sketch below.
  • The original ZIP file in Post #1 has all the files below plus two documentation Tabs.




  • You do not need to define a State Machine if you are not using one.

  • When your application code includes a State Machine, use the enum format as seen in the example sketch below.

    • Use the SET_STATE(SystemState next, unsigned long duration) function to change State and to set the State TIMER duration.
      • SET_STATE(STATE1, LED0_ON_Time);
        The next State is STATE1 set TIMER to LED0_ON_Time.
  • If we upload the example sketch and start the logic analyzer, we should see a similar analyzer screen:




Application (Sketch) code:

//
//================================================^================================================
//                      Non-Blocking State Machine with Logic Analyzer Interface
//================================================^================================================
//
//  File:      LogicAnalyzerVersion2.5.ino
//
//  URL
//
//  LarryD
//
//  Version    YY/MM/DD    Comments
//  =======    ========    ========================================================================
//  2.4        26/04/04    Running code
//  2.5        26/04/10    Added an Interface "Pulse Generation Macro"
//
//
//
//  • Documentation is found in the ReadMe and Preamble Tabs.
//
// Sketch Overview:
// -------------------------------------
//
//  MyProject
//  |
//  +-- MyProject.ino            (Application / user sketch)
//  |      |
//  |      +-- StateMachine      (Application logic)
//  |
//  +-- Preamble                 (Project notes / overview)
//  |
//  +-- ReadMe                   (Documentation)
//  |
//  +-- DebugAnalyzerConfig.h    (Configuration for Debug API)
//  |      |
//  |      +-- EVENTs            (Event definitions)
//  |
//  +-- LogicAnalyzerDebug.h     (Public Debug API)
//
//
// Integration Notes:
// -------------------------------------
//
//  • Lines marked with ◄◄◄◄◄ must be added to an existing sketch.
//
//  • Sections between ▼▼▼▼▼ and ▲▲▲▲▲ contain required framework code.
//
//  • Lines marked  <---<<<<  D e b u g  are examples of debug usage.
//
//


//                                                                  START   ▼▼▼▼▼▼▼▼▼▼▼▼▼
//▼ When comment out disable debugging.
#define DEBUG_ANALYZER

//================================================
// State decoding:
// When defined: Reg1–Reg5 carry STATE/EVENT encoding.
// EVENT temporarily overrides STATE bits (Reg1–Reg5),
// then restores previous state after MARK.
// This allows precise event tagging without losing state context.
//
//
//▼ When commented out, Reg1–Reg5 act as a regular 5-bit data bus.
#define DEBUG_DECODE_STATE


//===============================
#ifdef DEBUG_ANALYZER

#include "DebugAnalyzerConfig.h"

//===============================
#else

// When we are not using Interface debug code macros, undefine them, i.e. they are not compiled.

// Pulse Generation.
#define DEBUG_PULSE_COUNT_START(bit, count)
#define DEBUG_PULSE_COUNT_SERVICE()
#define DEBUG_PULSE_COUNT_BUSY()
#define DEBUG_PULSE_COUNT_CANCEL()

// Bit manipulation.
#define DEBUG_MARK()
#define DEBUG_INIT()
#define DEBUG_TOGGLE(x)
#define DEBUG_HIGH(x)
#define DEBUG_LOW(x)
#define DEBUG_SET_REG(bit, value)
#define DEBUG_FAST_PULSE(x)

#define DEBUG_EVENT_FAST(EVENT)
#define DEBUG_EVENT(EVENT)

// Bit delayed manipulation.
#define DEBUG_DELAYED_MARK(bit, delay_us)
#define DEBUG_DELAYED_HIGH(bit, delay_us)
#define DEBUG_DELAYED_LOW(bit, delay_us)
#define DEBUG_DELAYED_PULSE(bit, delay_us, width_us)
#define DEBUG_DELAYED_SERVICE()

// Debug LEDs.
#define DEBUG_LED_ON(bit)
#define DEBUG_LED_OFF(bit)
#define DEBUG_LED_PULSE(bit,duration)
#define DEBUG_LEDsService()

#define DEBUG_INTERFACE_HEARTBEAT(rate)

#endif

//                                                                  END      ▲▲▲▲▲▲▲▲▲▲▲▲▲


//================================================^================================================
//                                 A p p l i c a t i o n   c o d e
//================================================^================================================


//===============================
// State Machine
//===============================

// NOTE: State Machine typically use 0x00–0x0F.
//       When using the logic analyzer, EVENTs use (0x10–0x1F).
//
// For clarity, it helps to keep "STATE##_" in the variable name.

enum SystemState : byte
{
  // State      43210            Decimal      Change names as needed:
  STATE0    = 0b00000,        // 00      e.g. STATE0_POWER_UP
  STATE1    = 0b00001,        // 01           STATE1_WallSwitchPushedUp
  STATE2    = 0b00010,        // 02           STATE2_GarageDoorGoingUp
  STATE3    = 0b00011,        // 03           STATE3_WallSwitchPushedDown
  STATE4    = 0b00100,        // 04           STATE4_GarageDoorGoingDown
  STATE5    = 0b00101,        // 05           etc.
  STATE6    = 0b00110,        // 06
  STATE7    = 0b00111,        // 07
  STATE8    = 0b01000,        // 08
  STATE9    = 0b01001,        // 09
  STATE10   = 0b01010,        // 10
  STATE11   = 0b01011,        // 11
  STATE12   = 0b01100,        // 12
  STATE13   = 0b01101,        // 13
  STATE14   = 0b01110,        // 14
  STATE15   = 0b01111         // 15
};

// Start out in STATE0.
SystemState mState = STATE0;

//===============================
// Sketch Macros
//===============================

#define LEDon               HIGH          // [Pin]---[220R]---A[LED]K---GND
#define LEDoff              LOW

#define PRESSED             LOW           // +5V---[InternalPullup]---[Pin]---[Switch]---GND
#define RELEASED            HIGH

#define IMMEDIATE           0UL           // Used when we want the new State to immediately execute.
#define WAIT_FOREVER        0xFFFFFFFFul  // Stay in the new State forever.


//===============================
// GPIOs
//===============================

const byte LEDs[]                = {2, 3, 4, 5};  // +5v---[220R]---A[LED]K---Pin

const byte ledArraySize          = sizeof(LEDs) / sizeof(LEDs[0]);

const byte mySwitch              = 6;             // +5V---[internal Pullup]---[Switch]---GND
const byte heartbeatLED          = 7;             // +5v---[220R]---A[LED]K---Pin

//===============================
// Variables
//===============================

byte lastMySwitchState           = LOW;

//========================
// State duration timing.
const unsigned long STATE0_Time  = 500ul;     // 500ms
const unsigned long LED0_ON_Time = 250ul;     // 250ms
const unsigned long LED1_ON_Time = 500ul;     // 500ms
const unsigned long LED2_ON_Time = 750ul;     // 750ms
const unsigned long LED3_ON_Time = 1000ul;    // 1000ms

//===============================
// TIMERs
//===============================

unsigned long heartbeatTime;
unsigned long heartbeatInterval               = 500ul;   // 500ms

unsigned long switchesTime;
unsigned long switchesInterval                = 50ul;    // 50ms

unsigned long machineTime;
unsigned long machineInterval                 = 1000ul;  // 1000us or 1ms

unsigned long stateTimer;
unsigned long statePeriod;


//========================
// When we are not using a logic analyzer, we use the function below
// to change the State of the State Machine.

#ifndef DEBUG_ANALYZER                                                          // ◄◄◄◄◄◄◄◄◄◄◄

//                                      N E X T _ S T A T E ( )
//================================================^================================================
//
void SET_STATE(SystemState next, unsigned long duration)
{
  mState = (next);

  statePeriod = (duration);

  //Restart this TIMER.
  stateTimer = millis();

} // END of   SET_STATE()

#endif
//========================



//                                           s e t u p ( )
//================================================^================================================
//
void setup()
{
  //Serial.begin(115200);

  // We need to "initialize" the analyzer's code before we use it.  ✅ REQUIRED      ◄◄◄◄◄◄◄◄◄◄◄
  DEBUG_INIT();

  // Initialyze the LEDs
  for (byte x = 0; x < ledArraySize; x++)
  {
    pinMode(LEDs[x], OUTPUT);
    digitalWrite(LEDs[x], LOW);
  }

  pinMode(heartbeatLED, OUTPUT);

  pinMode(mySwitch, INPUT_PULLUP);
  lastMySwitchState = digitalRead(mySwitch);

  // We will use the analyzer's Reg7 bit to display the mySwitch state.  // <---<<<<  D e b u g
  DEBUG_SET_REG(Reg7, lastMySwitchState );                               // <---<<<<  D e b u g

  // Initialize the state machine and its timer.
  //
  SET_STATE(STATE0, STATE0_Time);

} // END of   setup()


//                                            l o o p ( )
//================================================^================================================
//
void loop()
{
  //                                                             START        ▼▼▼▼▼▼▼▼▼▼▼▼▼
  //========================
  //Logic Analyzer stuff:
  //========================

  // Toggle the last LED on the 3rd 595 (Analyzer's heartbeat).  // <---<<<<  D e b u g
  DEBUG_INTERFACE_HEARTBEAT(1000ul);                             // <---<<<<  D e b u g  ✅ REQUIRED

  // Check for delayed bit TIMER expirations.                    // <---<<<<  D e b u g
  DEBUG_DELAYED_SERVICE();                                       // <---<<<<  D e b u g  ✅ REQUIRED

  // Check for LED TIMER expirations.                            // <---<<<<  D e b u g
  DEBUG_LEDsService();                                           // <---<<<<  D e b u g  ✅ REQUIRED

  // Pulse Generator.                                            // <---<<<<  D e b u g
  DEBUG_PULSE_COUNT_SERVICE();                                   // <---<<<<  D e b u g  ✅ REQUIRED


  // We could check loop() execution time.                       // <---<<<<  D e b u g
  //DEBUG_FAST_PULSE(Reg11);                                     // <---<<<<  D e b u g

  //                                                            END          ▲▲▲▲▲▲▲▲▲▲▲▲▲


  //========================
  // Normal sketch code
  //========================

  checkHeartbeat();
  checkSwitches();
  serviceStateMachine();


  //================================================
  //       Other non blocking code goes here
  //================================================


} // END of   loop()


//                                  c h e c k H e a r t b e a t ( )
//================================================^================================================
//
void checkHeartbeat()
{
  // Is it time to toggle the heartbeat LED ?
  if (millis() - heartbeatTime < heartbeatInterval)
  {
    return;
  }

  // Restart this TIMER.
  heartbeatTime = millis();

  // Toggle the heartbeat LED.
  digitalWrite(heartbeatLED, !digitalRead(heartbeatLED));

  // We will use Reg6 to monitor this sketches heartbeat.        // <---<<<<  D e b u g
  DEBUG_TOGGLE(Reg6);                                            // <---<<<<  D e b u g

} //END of   checkHeartbeat()


//                                    S t a t e   M a c h i n e
//================================================^================================================
//
void serviceStateMachine()
{
  //================================================
  // Is it time to service this State Machine ?
  //
  if (micros() - machineTime < machineInterval)
  {
    return;
  }

  // Restart the machineTime TIMER.
  machineTime = micros();

  //================================================
  // WAIT_FOREVER(0xFFFFFFFFUL), stay in the current State forever.
  // IMMEDIATE (0), causes the State to be immediately executed (no delay).
  //
  // Is the State Machine's TIMER expired ?
  if (statePeriod != WAIT_FOREVER &&
      (statePeriod == IMMEDIATE || millis() - stateTimer >= statePeriod))
  {
    //========================
    switch (mState)
    {
      //============
      case STATE0:
        {
          digitalWrite(LEDs[0], LEDon);

          // Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE1, LED0_ON_Time);
        }
        break;

      //============
      case STATE1:
        {
          digitalWrite(LEDs[0], LEDoff);
          digitalWrite(LEDs[1], LEDon);

          // Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE2, LED1_ON_Time);
        }
        break;

      //============
      case STATE2:
        {
          digitalWrite(LEDs[1], LEDoff);
          digitalWrite(LEDs[2], LEDon);

          // Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE3, LED2_ON_Time);
        }
        break;

      //============
      case STATE3:
        {
          digitalWrite(LEDs[2], LEDoff);
          digitalWrite(LEDs[3], LEDon);

          // Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE4, LED3_ON_Time);
        }
        break;

      //============
      case STATE4:
        {
          digitalWrite(LEDs[3], LEDoff);

          // Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE5, IMMEDIATE);
        }
        break;

      //============
      case STATE5:
        {
          // Set the NEXT State and initialize the State Machine TIMER.
          SET_STATE(STATE0, STATE0_Time);
        }
        break;

      //============
      default:
        {
          // Do something.
        }
        break;

    } // END of   switch/case

  } // END of   if(...)

} // END of   serviceStateMachine()


//                                   c h e c k S w i t c h e s ( )
//================================================^================================================
//
void checkSwitches()
{
  // Is it time to check the switches ?
  if (millis() - switchesTime < switchesInterval)
  {
    return;
  }

  // Restart this TIMER.
  switchesTime = millis();

  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;

    //========================
    // Is this switch pressed ?
    if (switchState == PRESSED)
    {
      // Do something.
      // If we change the State Machine's State, "always" use SET_STATE(x,y)
      // example: make State Machine go to STATE8 set the TIMER to 200ms.
      // SET_STATE(STATE8, 200000ul);

      static byte counter = 0;
      counter = counter + 1;

      //================================================
      // Some analyzer interface examples.
      //================================================

      // Reg7 is used to display the mySwitch state.               // <---<<<<  D e b u g
      DEBUG_SET_REG(Reg7, lastMySwitchState );                     // <---<<<<  D e b u g
      
      // Reg1-Reg5 can be used in DATA Mode                        // <---<<<<  D e b u g
      //DEBUG_SET_REG(Reg1, lastMySwitchState);                    // <---<<<<  D e b u g

      // Reg10 Flags/marks the first of 10 presses.                // <---<<<<  D e b u g
      DEBUG_SET_REG(Reg10, HIGH );                                 // <---<<<<  D e b u g

      // Generate a pulse train equal to an integer variable.      // <---<<<<  D e b u g
      //DEBUG_PULSE_COUNT_START(Reg8, counter);                    // <---<<<<  D e b u g

      // We could display a variable's value. (0 to 32)            // <---<<<<  D e b u g
      // To be useful, DEBUG_DECODE_STATE is commented out.        // <---<<<<  D e b u g
      //DEBUG_EVENT_FAST(counter);                                 // <---<<<<  D e b u g

      // Every 10 switch presses.
      if (counter == 10)
      {
        // Pulse the "6th DEBUG LED" (Reg20LED6) for 2 seconds.
        // Pulse the 6th Debug LED.                                // <---<<<<  D e b u g
        DEBUG_LED_PULSE(Reg22LED6, 3000ul);                        // <---<<<<  D e b u g

        // Mark this with event with EVENT1F (decimal 31)          // <---<<<<  D e b u g
        DEBUG_EVENT(EVENT31);                                      // <---<<<<  D e b u g

        // Reg10 flags/marks the end of 10 presses.                // <---<<<<  D e b u g
        DEBUG_SET_REG(Reg10, LOW );                                // <---<<<<  D e b u g

        // Counting starts over.
        counter = 0;
      }

      //More examples:
      //============

      // Use Reg11 to make a measurable 25ms  pulse.               // <---<<<<  D e b u g
      DEBUG_DELAYED_PULSE(Reg11, 0, 25000ul);                      // <---<<<<  D e b u g
 
      // Pulse the Debug LED1 for 1 second.                        // <---<<<<  D e b u g
      DEBUG_LED_PULSE(Reg17LED1, 1000ul);                          // <---<<<<  D e b u g
    }

    //========================
    // The switch was released:
    else
    {
      // Reg7 is used to display the mySwitch state.               // <---<<<<  D e b u g
      DEBUG_SET_REG(Reg7, lastMySwitchState );                     // <---<<<<  D e b u g
    }

  } // END of   mySwitch

} // END of   checkSwitches()


//
//================================================^================================================
//

DebugAnalyzerConfig.h

//================================================
// DebugAnalyzerConfig.h
// Configuration for the Logic Analyzer Debug API
//================================================
//

#define ANALYZER_LATCH_PIN   10  // Pin 10 is the UNO's SS pin, other pins can be used.
#define ANALYZER_MARK_PIN     9  // ~240ns pulses will be seen on this pin, other pins can be used.

// SPI Pins (fixed by hardware):
// UNO:  MOSI=11, SCK=13, SS=10
// Mega: MOSI=51, SCK=52, SS=53
// If the your sketch uses SPI, make sure the latch pin is different from the analyzer's.
// A L L  your sketch SPI code must use SPI.beginTransaction() / SPI.endTransaction().
//
// You must wire 74HC595 accordingly, see the interface schematic.

// https://europe1.discourse-cdn.com/arduino/original/4X/f/8/5/f8575b22873ae0259a6e47ad715b762f82df5146.jpeg

//===============================
// Requires about 1.8k to 2.5k of flash memory.
//===============================

#include "LogicAnalyzerDebug.h"


//===============================
// EVENT Bus definitions
//===============================

// Three 74HC595 shift registers are used for the analyzer's register (3 X 8 = 24 bits).
//
// NOTE:
// Register numbers (Reg1–Reg24) are 1-based for readability.
// Internally, 595 bits are 0-23, i.e. ZERO-based:
// Reg1  = bit 0
// Reg2  = bit 1
// . . .
// Reg24 = bit 23  (last register bit)
//
// UNO D9   Connects to logic analyzer CH0,  this is a MARK pulse (~240ns) NOT a periodic clock.
//          MARK pulse indicates:
//           • exact moment of state transition
//           • or event boundary (depending on usage)
//             
//
// Analyzer's first 5 bits are typically used for the State / EVENT field.
// (They may also be used as general purpose bits in DATA mode.)
//
// By definition:
// Reg5 is the MSB of a 5-bit field:
//   • when 0, we use the: enum SystemState (0x00–0x0F)
//   • when 1, we use the: enum EVENT       (0x10–0x1F)
//
// For clarity, it helps to keep "RegX" in the variable name.
//
//                                                 N O T E S : 
//                                            CH0  D9 MARK 
//                 75HC595     Function
#define Reg1             0  // Logic analyzer CH1  State0, D0 of the parallel bits -+
#define Reg2             1  // Logic analyzer CH2  State1, D1 of the parallel bits  |  
#define Reg3             2  // Logic analyzer CH3  State2, D2 of the parallel bits   > State/EVENT Mode
#define Reg4             3  // Logic analyzer CH4  State3, D3 of the parallel bits  |
#define Reg5             4  // Logic analyzer CH5  State4, D4 of the parallel bits -+
//
#define Reg6             5  // Logic analyzer CH6  e.g. Reg6_heartbeat      
#define Reg7             6  // Logic analyzer CH7       Reg7_Timing   
#define Reg8             7  // Logic analyzer CH8
#define Reg9             8  // Logic analyzer CH9
#define Reg10            9  // Logic analyzer CH10
#define Reg11            10 // Logic analyzer CH11
#define Reg12            11 // Logic analyzer CH12
#define Reg13            12 // Logic analyzer CH13
#define Reg14            13 // Logic analyzer CH14
#define Reg15            14 // Logic analyzer CH15
//
#define Reg16_Scope      15 // Scope trigger       Oscilloscope Trigger
//
#define Reg17LED1        16 // Debug LED1          First interface debug LED
#define Reg18LED2        17 // Debug LED2  
#define Reg19LED3        18 // Debug LED3  
#define Reg20LED4        19 // Debug LED4  
#define Reg21LED5        20 // Debug LED5  
#define Reg22LED6        21 // Debug LED6  
#define Reg23LED7        22 // Debug LED7          Last interface debug LED    
//
#define Reg24LED8        23 // Debug LED8          Interface's debug heartbeat LED

//===============================
//EVENT bits
//===============================

// Example usage:
// DEBUG_EVENT(EVENT1F);  //When we are in the State/EVENT mode, make a narrow event mark, #31.
//
// NOTE: Event IDs typically use 0x10–0x1F to distinguish from SystemState (0x00–0x0F).
//
// For clarity, it helps to keep "EVENT##_" in the variable name.

enum EVENT : uint8_t
{
  // ID          43210       Decimal      Change names as needed:
  EVENT16    = 0b10000,   // 16      e.g. EVENT10_ISR_ENTRY
  EVENT17    = 0b10001,   // 17           EVENT11_ISR_EXIT
  EVENT18    = 0b10010,   // 18           EVENT12_ERROR1
  EVENT19    = 0b10011,   // 19           EVENT13_ERROR2
  EVENT20    = 0b10100,   // 20           EVENT14_SWITCH_PRESS
  EVENT21    = 0b10101,   // 21           EVENT15_SWITCH_RELEASE
  EVENT22    = 0b10110,   // 22           etc.
  EVENT23    = 0b10111,   // 23
  EVENT24    = 0b11000,   // 24
  EVENT25    = 0b11001,   // 25
  EVENT26    = 0b11010,   // 26
  EVENT27    = 0b11011,   // 27
  EVENT28    = 0b11100,   // 28
  EVENT29    = 0b11101,   // 29
  EVENT30    = 0b11110,   // 30
  EVENT31    = 0b11111,   // 31
};

//===============================
// Analyzer bus
//===============================

uint32_t debugBus = 0;
uint8_t  debugBus_event5 = 0;
bool     debugEventActive = false;

//================================================                 
// State transition macro       
//================================================
// Bits 0–4 of debugBus are reserved for SystemState (5-bit state).
// To change the State Machine's State "always" use SET_STATE(x,y).
// Where x is the new State and Y is the State TIMER interval.

// Debug version
#define SET_STATE(next, duration) \
  do { \
    mState = (next); \
    STATE_DEBUG_UPDATE(next); \
    STATE_DEBUG_MARK(); \
    statePeriod = (duration); \
    stateTimer = millis(); \
  } while(0)


//========================
// Should we be decoding the State/EVENT and make a MARK on ANALYZER_MARK_PIN (D9)?
#ifdef  DEBUG_DECODE_STATE

// Yes we do.
#define STATE_DEBUG_UPDATE(next)  DEBUG_SET_STATE5(next)
#define STATE_DEBUG_MARK()        DEBUG_MARK()
//
//========================
#else
//
// No we don't.
#define STATE_DEBUG_UPDATE(next)  do {} while(0)
#define STATE_DEBUG_MARK()        do {} while(0)

//========================
#endif


/*
  //================================================
  // Logic Analyzer Macros:
  //================================================

  DEBUG_PULSE_COUNT_START(bit, count)
  DEBUG_PULSE_COUNT_SERVICE()
  DEBUG_PULSE_COUNT_BUSY()
  DEBUG_PULSE_COUNT_CANCEL()

  DEBUG_TOGGLE(bit);
  DEBUG_HIGH(bit);
  DEBUG_LOW(bit);
  DEBUG_SET_REG(bit, value);
  DEBUG_FAST_PULSE(bit);

  DEBUG_EVENT_FAST(id);            
  DEBUG_EVENT(id);

  DEBUG_TRIGGER();        //Oscilloscope Trigger Reg16 only.

  //All DEBUG_DELAYED_*() macros use a shared timing engine. Only one delayed 
  //operation can be active at a time—subsequent calls overwrite any previous delayed action.
  DEBUG_DELAYED_MARK(bit, delay_us);
  DEBUG_DELAYED_HIGH(bit, delay_us);
  DEBUG_DELAYED_LOW(bit, delay_us);
  DEBUG_DELAYED_PULSE(bit, delay_us, width_us);
  DEBUG_DELAYED_SERVICE();

  DEBUG_LED_ON(bit);
  DEBUG_LED_OFF(bit);
  DEBUG_LED_PULSE(bit,duration);
  DEBUG_INTERFACE_HEARTBEAT(period);
  DEBUG_LEDsService();

*/

LogicAnalyzerDebug.h

//================================================
// LogicAnalyzerDebug.h
// Public API: SPI debug bus driver and debug LED utilities
//================================================

#include <Arduino.h>
#include <SPI.h>

#define DEBUG_NOP() __asm__ __volatile__("nop\n\t")

// About 240ns Mark pulse.
#define DEBUG_PULSE_DELAY() \
  do { \
    DEBUG_NOP(); \
    DEBUG_NOP(); \
    DEBUG_NOP(); \
  } while(0)

#ifndef DEBUG_SPI_SETTINGS
#define DEBUG_SPI_SETTINGS SPISettings(8000000, MSBFIRST, SPI_MODE0)

#endif

#ifndef LOGIC_ANALYZER_DEBUG_H
#define LOGIC_ANALYZER_DEBUG_H

//================================================
// Logic analyzer hardware configuration
//================================================

#if ANALYZER_MARK_PIN == 10 || \
    ANALYZER_MARK_PIN == 11 || \
    ANALYZER_MARK_PIN == 12 || \
    ANALYZER_MARK_PIN == 13
#error "ANALYZER_MARK_PIN cannot be an SPI pin (10,11,12,13)"
#endif

#if (ANALYZER_LATCH_PIN < 2) || (ANALYZER_LATCH_PIN > 17)
#error "ANALYZER_LATCH_PIN must be a digital pin between 2 and A3"
#endif

#if (ANALYZER_MARK_PIN < 2) || (ANALYZER_MARK_PIN > 17)
#error "ANALYZER_MARK_PIN must be a digital pin between 2 and A3"
#endif

#if ANALYZER_LATCH_PIN == ANALYZER_MARK_PIN
#error "ANALYZER_LATCH_PIN and ANALYZER_MARK_PIN must be different pins"
#endif

#define LATCH_LOW  *portOutputRegister(digitalPinToPort(ANALYZER_LATCH_PIN)) &= ~digitalPinToBitMask(ANALYZER_LATCH_PIN)
#define LATCH_HIGH *portOutputRegister(digitalPinToPort(ANALYZER_LATCH_PIN)) |=  digitalPinToBitMask(ANALYZER_LATCH_PIN)

//================================================
// Fast analyzer pulse on: pins 2–9, A0–A3. (~200–250ns)
//================================================

#if ANALYZER_MARK_PIN == 2
#define PULSE240 do { cli(); PIND = _BV(PD2); DEBUG_PULSE_DELAY(); PIND = _BV(PD2); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 3
#define PULSE240 do { cli(); PIND = _BV(PD3); DEBUG_PULSE_DELAY(); PIND = _BV(PD3); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 4
#define PULSE240 do { cli(); PIND = _BV(PD4); DEBUG_PULSE_DELAY(); PIND = _BV(PD4); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 5
#define PULSE240 do { cli(); PIND = _BV(PD5); DEBUG_PULSE_DELAY(); PIND = _BV(PD5); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 6
#define PULSE240 do { cli(); PIND = _BV(PD6); DEBUG_PULSE_DELAY(); PIND = _BV(PD6); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 7
#define PULSE240 do { cli(); PIND = _BV(PD7); DEBUG_PULSE_DELAY(); PIND = _BV(PD7); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 8
#define PULSE240 do { cli(); PINB = _BV(PB0); DEBUG_PULSE_DELAY(); PINB = _BV(PB0); sei(); } while(0)

// Perferred pin.
//
#elif ANALYZER_MARK_PIN == 9
#define PULSE240 do { cli(); PINB = _BV(PB1); DEBUG_PULSE_DELAY(); PINB = _BV(PB1); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 14
#define PULSE240 do { cli(); PINC = _BV(PC0); DEBUG_PULSE_DELAY(); PINC = _BV(PC0); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 15
#define PULSE240 do { cli(); PINC = _BV(PC1); DEBUG_PULSE_DELAY(); PINC = _BV(PC1); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 16
#define PULSE240 do { cli(); PINC = _BV(PC2); DEBUG_PULSE_DELAY(); PINC = _BV(PC2); sei(); } while(0)

#elif ANALYZER_MARK_PIN == 17
#define PULSE240 do { cli(); PINC = _BV(PC3); DEBUG_PULSE_DELAY(); PINC = _BV(PC3); sei(); } while(0)

#else
#error "Unsupported ANALYZER_MARK_PIN"

#endif


//================================================
// Update the debug bus.
//================================================

#define DEBUG_SET_STATE5(n) \
  do { \
    uint8_t _n = (uint8_t)(n); \
    DEBUG_SAFE_UPDATE( \
                       debugBus = (debugBus & 0xFFFFFFE0UL) | (_n & 0x1F) \
                     ); \
  } while(0)

//================================================
// Public timing marker, the user can use this any time,
// when in debug mode.
//================================================

#define DEBUG_MARK()  PULSE240

//================================================
// External variables supplied by sketch
//================================================

extern uint32_t       debugBus;
extern uint8_t        debugBus_event5;
extern bool           debugEventActive;

extern unsigned long  stateTimer;
extern unsigned long  statePeriod;

//================================================
// Initialize analyzer hardware
//================================================

static inline void DEBUG_INIT()
{
  // Power-up settling
  delay(20);

  // Ensure MCU stays SPI master (required on AVR)
  pinMode(SS, OUTPUT);

  // Latch pin (74HC595 RCLK)
  pinMode(ANALYZER_LATCH_PIN, OUTPUT);
  digitalWrite(ANALYZER_LATCH_PIN, HIGH);

  // Marker pin (timing pulse)
  pinMode(ANALYZER_MARK_PIN, OUTPUT);
  digitalWrite(ANALYZER_MARK_PIN, LOW);

  // Initialize SPI hardware
  SPI.begin();

  // Known startup state
  debugBus = 0;
  debugEventActive = false;

  uint32_t value = debugBus;

  // Send initial cleared frame (use transaction!)
  SPI.beginTransaction(DEBUG_SPI_SETTINGS);

  LATCH_LOW;
  SPI.transfer(value >> 16);  // 3rd 74HC595
  SPI.transfer(value >> 8);   // 2nd 74HC595
  SPI.transfer(value);        // 1st 74HC595
  LATCH_HIGH;

  SPI.endTransaction();
}

//================================================
// Send debug state to shift registers
//================================================

static inline void DEBUG_UPDATE()
{
  uint32_t value = debugBus;

  if (debugEventActive)
  {
    value = (value & 0xFFFFFFE0UL) | (debugBus_event5 & 0x1F);
  }

  // Begin SPI transaction (protect bus)
  SPI.beginTransaction(DEBUG_SPI_SETTINGS);

  LATCH_LOW;

  uint8_t b2 = value >> 16;   // 3rd 595
  uint8_t b1 = value >> 8;    // 2nd 595
  uint8_t b0 = value;         // 1st 595

  SPDR = b2;

  while (!(SPSR & _BV(SPIF)));
  SPDR = b1;

  while (!(SPSR & _BV(SPIF)));
  SPDR = b0;

  while (!(SPSR & _BV(SPIF)));

  LATCH_HIGH;

  // End SPI transaction
  SPI.endTransaction();
}


//                                        Debug bit helpers
//================================================^================================================
//

//------------------------------------------------
// NON-BLOCKING EDGE-COUNT PULSE GENERATOR
//------------------------------------------------
//
// Generates N pulses (N falling edges) at loop() speed.
//
// Each loop() = 1 pulse (via DEBUG_FAST_PULSE)
//
// Analyzer counts FALLING edges = exact value
//
// Usage:
//   DEBUG_PULSE_COUNT_START(Reg8, value);
//   DEBUG_PULSE_COUNT_SERVICE();   // call every loop()
//
//------------------------------------------------

static uint16_t _pc_remaining = 0;
static uint8_t  _pc_bit = 0;
static bool     _pc_active = false;

// Start pulse generation
//========================
#define DEBUG_PULSE_COUNT_START(bit, count) \
  do { \
    _pc_bit = (bit); \
    _pc_remaining = (count); \
    _pc_active = true; \
  } while(0)

// Service routine (call in loop)
//========================
#define DEBUG_PULSE_COUNT_SERVICE() \
  do { \
    if (_pc_active) { \
      if (_pc_remaining > 0) { \
        DEBUG_FAST_PULSE(_pc_bit); \
        _pc_remaining--; \
      } else { \
        _pc_active = false; \
      } \
    } \
  } while(0)

// Status
//========================
#define DEBUG_PULSE_COUNT_BUSY() (_pc_active)

// Cancel
//========================
#define DEBUG_PULSE_COUNT_CANCEL() \
  do { \
    _pc_active = false; \
    _pc_remaining = 0; \
  } while(0)

//------------------------------------------------


//========================
// Output a Reg16 trigger pulse to the oscilloscope.
#define DEBUG_TRIGGER() \
  DEBUG_FAST_PULSE(Reg16_Scope)

//========================
#define DEBUG_EVENT_FAST(id) \
  do { \
    uint8_t _id = (id) & 0x1F; \
    \
    uint8_t sreg = SREG; \
    cli(); \
    \
    debugBus_event5 = _id; \
    debugEventActive = true; \
    DEBUG_UPDATE(); \
    \
    DEBUG_MARK(); \
    \
    debugEventActive = false; \
    DEBUG_UPDATE(); \
    \
    SREG = sreg; \
  } while(0)

//========================
#define DEBUG_EVENT(id) DEBUG_EVENT_WINDOW(id)

//========================
#define DEBUG_EVENT_WINDOW(id) \
  do { \
    uint8_t _id = (uint8_t)(id); \
    uint32_t _prevBus; \
    \
    uint8_t sreg = SREG; \
    cli(); \
    _prevBus = debugBus; \
    SREG = sreg; \
    \
    DEBUG_SET_STATE5(_id); \
    DEBUG_PULSE_DELAY(); \
    DEBUG_MARK(); \
    \
    DEBUG_SAFE_UPDATE(debugBus = _prevBus); \
    DEBUG_MARK(); \
  } while(0)

//========================
// ATOMIC
#define DEBUG_SAFE_UPDATE(code) \
  do { \
    uint8_t sreg = SREG; \
    cli(); \
    code; \
    DEBUG_UPDATE(); \
    SREG = sreg; \
  } while(0)

//========================
#define DEBUG_TOGGLE(bit) \
  DEBUG_SAFE_UPDATE(debugBus ^= (1UL << bit))

//========================
#define DEBUG_HIGH(bit) \
  DEBUG_SAFE_UPDATE(debugBus |= (1UL << bit))

//========================
#define DEBUG_LOW(bit) \
  DEBUG_SAFE_UPDATE(debugBus &= ~(1UL << bit))

//========================
// Set a debugBus bit from a variable (0 = LOW, non-zero = HIGH)
#define DEBUG_SET_REG(bit, value) \
  do { \
    uint8_t _v = (value); \
    if (_v) \
      DEBUG_HIGH(bit); \
    else \
      DEBUG_LOW(bit); \
  } while(0)

//========================
// ATOMIC
#define DEBUG_FAST_PULSE(bit) \
  do { \
    uint8_t sreg = SREG; \
    cli(); \
    \
    debugBus ^= (1UL << (bit)); \
    DEBUG_UPDATE(); \
    \
    debugBus ^= (1UL << (bit)); \
    DEBUG_UPDATE(); \
    \
    SREG = sreg; \
  } while(0)

//========================
// Delayed event engine
//========================

// OFF event
static unsigned long delayedOffTime = 0;
static bool delayedOffPending = false;
static byte delayedOffBit = 0;

#define DELAYED_LOW    0
#define DELAYED_HIGH   1
#define DELAYED_PULSE  2

// ON / marker event
static unsigned long delayedMarkerTime = 0;
static bool delayedMarkerPending = false;
static byte delayedMarkerBit = 0;
static byte delayedMarkerLevel = 0;   // 0=LOW, 1=HIGH, 2=PULSE

// All DEBUG_DELAYED_*() macros use a shared timing engine. Only one delayed
// operation can be active at a time—subsequent calls overwrite any previous delayed action.

//========================
#define DEBUG_DELAYED_MARK(bit, delay_us) \
  do { \
    uint32_t _delay = (delay_us); \
    delayedMarkerTime = micros() + _delay; \
    delayedMarkerPending = true; \
    delayedMarkerBit = (bit); \
    delayedMarkerLevel = DELAYED_PULSE; \
    delayedOffPending = false; \
  } while(0)

//========================
#define DEBUG_DELAYED_HIGH(bit, delay_us) \
  do { \
    delayedMarkerTime = micros() + (delay_us); \
    delayedMarkerPending = true; \
    delayedMarkerBit = (bit); \
    delayedMarkerLevel = DELAYED_HIGH; \
    delayedOffPending = false; \
  } while(0)

//========================
#define DEBUG_DELAYED_LOW(bit, delay_us) \
  do { \
    delayedMarkerTime = micros() + (delay_us); \
    delayedMarkerPending = true; \
    delayedMarkerBit = (bit); \
    delayedMarkerLevel = DELAYED_LOW; \
    delayedOffPending = false; \
  } while(0)

//========================
#define DEBUG_DELAYED_PULSE(bit, delay_us, width_us) \
  do { \
    uint32_t _delay = (delay_us); \
    uint32_t _width = (width_us); \
    uint32_t _start = micros() + _delay; \
    \
    delayedMarkerTime = _start; \
    delayedMarkerPending = true; \
    delayedMarkerBit = (bit); \
    delayedMarkerLevel = DELAYED_HIGH; \
    \
    delayedOffTime = _start + _width; \
    delayedOffPending = true; \
    delayedOffBit = (bit); \
  } while(0)

//======================== (call in loop)
//
#define DEBUG_DELAYED_SERVICE() \
  do { \
    unsigned long now = micros(); \
    \
    /* Handle ON / pulse */ \
    if (delayedMarkerPending && \
        (long)(now - delayedMarkerTime) >= 0) \
    { \
      if (delayedMarkerLevel == DELAYED_PULSE) \
      { \
        DEBUG_FAST_PULSE(delayedMarkerBit); \
      } \
      else if (delayedMarkerLevel == DELAYED_HIGH) \
      { \
        DEBUG_HIGH(delayedMarkerBit); \
        \
        /* Adjust OFF timing to actual execution time */ \
        if (delayedOffPending) \
        { \
          delayedOffTime = now + (delayedOffTime - delayedMarkerTime); \
        } \
      } \
      else \
      { \
        DEBUG_LOW(delayedMarkerBit); \
      } \
      \
      delayedMarkerPending = false; \
    } \
    \
    /* Handle OFF */ \
    if (delayedOffPending && \
        (long)(now - delayedOffTime) >= 0) \
    { \
      DEBUG_LOW(delayedOffBit); \
      delayedOffPending = false; \
    } \
  } while(0)

//================================================
// Debug LED system
//================================================

//========================
#define DEBUG_LED_ON(bit) \
  do{ \
    if((bit) >= DEBUG_LED_FIRST && (bit) <= DEBUG_LED_LAST){ \
      DEBUG_SAFE_UPDATE \
      ( \
        byte idx = (bit) - DEBUG_LED_FIRST; \
        debugLedManual[idx] = true; \
        debugBus |= (1UL << (bit)); \
      ); \
    } \
  } \
  while(0)

//========================
#define DEBUG_LED_OFF(bit) \
  do{ \
    if((bit) >= DEBUG_LED_FIRST && (bit) <= DEBUG_LED_LAST){ \
      DEBUG_SAFE_UPDATE \
      ( \
        byte idx = (bit) - DEBUG_LED_FIRST; \
        debugLedManual[idx] = false; \
        debugBus &= ~(1UL << (bit)); \
      ); \
    } \
  } \
  while(0)

//========================
#define DEBUG_LED_PULSE(bit,duration) \
  do{ \
    if((bit) >= DEBUG_LED_FIRST && (bit) <= DEBUG_LED_LAST){ \
      DEBUG_SAFE_UPDATE \
      ( \
        byte idx = (bit) - DEBUG_LED_FIRST; \
        debugBus |= (1UL << (bit)); \
        debugLedTimer[idx] = millis(); \
        debugLedDuration[idx] = (duration); \
        debugLedActive[idx] = true; \
      ); \
    } \
  } \
  while(0)

//======================== (call in loop)
//
#define DEBUG_LED_FIRST 16    //First debug LED register number, ZERO Indexed.
#define DEBUG_LED_COUNT 7     //7 LEDs can be timed.  
#define DEBUG_LED_LAST  22    //Last LED that can be a timed.
//                              Note: 23 is used exclusively for interface heartbeat LED.

static unsigned long debugLedTimer[DEBUG_LED_COUNT];
static unsigned long debugLedDuration[DEBUG_LED_COUNT];

//
static bool debugLedActive[DEBUG_LED_COUNT];
static bool debugLedManual[DEBUG_LED_COUNT];

// Iterate through the debug LEDs. 
// Check if they are used as a timed LED. 
//
static inline void DEBUG_LEDsService()
{
  unsigned long now = millis();

  for (byte i = 0; i < DEBUG_LED_COUNT; i++)
  {
    // When this LED is being timed, has its TIMER expired ?
    if (debugLedActive[i])
    {
      if (now - debugLedTimer[i] >= debugLedDuration[i])
      {
        // Disable this TIMER
        debugLedActive[i] = false;

        DEBUG_SAFE_UPDATE(
        {
          if (debugLedManual[i])
          {
            debugBus |= (1UL << (i + DEBUG_LED_FIRST));
          }
          else
          {
            debugBus &= ~(1UL << (i + DEBUG_LED_FIRST));
          }
        });

      }
    }
  }
}

//========================
// This LED is the analyzer's heartbeat LED.
// If it toggles, the interface is wired properly.
//
const byte DEBUG_HEARTBEAT_LED          = 23;  //Zero-Indexed
static unsigned long debugHeartbeatTime = 0;

// Is it time to toggle the interface heartbeat LED ?
#define DEBUG_INTERFACE_HEARTBEAT(period) \
  do { \
    uint32_t _p = (period); \
    unsigned long now = millis(); \
    if(now - debugHeartbeatTime >= _p) \
    { \
      debugHeartbeatTime = now; \
      DEBUG_SAFE_UPDATE(debugBus ^= (1UL << DEBUG_HEARTBEAT_LED)); \
    } \
  } \
  while(0)

#endif


//================================================^================================================

Option 2: SPI-Based Debugging

Running Interface API version 2.5

  • There are 22 Interface API Macros available to the user.

  • Macro code becomes inline when the sketch is compiled.

  • If a Macro is not used, it is not included in the compiled sketch.

  • If the #define DEBUG_DECODE_STATE line of code is commented, no debug Interface code is included in the sketch.




  • See the Preamble and ReadMe Tabs in the ZIP file (Post #1) for Macro usage.
/*
  //================================================
  // Logic Analyzer Macros:
  //================================================

  DEBUG_PULSE_COUNT_START(bit, count)
  DEBUG_PULSE_COUNT_SERVICE()
  DEBUG_PULSE_COUNT_BUSY()
  DEBUG_PULSE_COUNT_CANCEL()

  DEBUG_TOGGLE(bit);
  DEBUG_HIGH(bit);
  DEBUG_LOW(bit);
  DEBUG_SET_REG(bit, value);
  DEBUG_FAST_PULSE(bit);

  DEBUG_EVENT_FAST(id);
  DEBUG_EVENT(id);

  DEBUG_TRIGGER();        //Oscilloscope Trigger Reg16 only.

  //All DEBUG_DELAYED_*() macros use a shared timing engine. Only one delayed
  //operation can be active at a time—subsequent calls overwrite any previous delayed action.
  DEBUG_DELAYED_MARK(bit, delay_us);
  DEBUG_DELAYED_HIGH(bit, delay_us);
  DEBUG_DELAYED_LOW(bit, delay_us);
  DEBUG_DELAYED_PULSE(bit, delay_us, width_us);
  DEBUG_DELAYED_SERVICE();

  DEBUG_LED_ON(bit);
  DEBUG_LED_OFF(bit);
  DEBUG_LED_PULSE(bit,duration);
  DEBUG_INTERFACE_HEARTBEAT(period);
  DEBUG_LEDsService();

*/
  • Let's examine the DEBUG_PULSE_COUNT_START(bit, count).
    This Macro will generate n-Pulses on bit RegX.
//    Examples:
      DEBUG_PULSE_COUNT_START(Reg8, 500);
      DEBUG_PULSE_COUNT_START(Reg8, myVariable);
      DEBUG_PULSE_COUNT_START(Reg8, analogRead(A0));
  • In example below, GPIO A0 is tied to 2.5V.
    Every time mySwitch is pressed, the integer value of A0 generates that many pulses on bit Reg8.

We add an Analyzer Measurement with Rising Edges selected.

Option 2: SPI-Based Debugging

Running Interface API version 2.5

  • State Machine versus Event Display.
    • As we have seen, debug Reg1 - Reg5 are use by by the Interface API to decode the current State the State Machine is sitting at.

      • When Reg5 is LOW, we decode the 16 States in the sketch’s State Machine
    • An event is:
      “At THIS exact time, THIS thing happened”

      • By definition, when Reg5 is HIGH, we will display a very short EVENT
      • The Event Codes are defined in DebugAnalyzerConfig.h
      • EVENT codes are decoded similarly to States
      • Reg1-Reg5 (Reg5 being HIGH) = event ID
      • We can use an EVENT to flag: errors, changes, evaluations etc.
      • On the analyzer:
        You instantly see which path is taken.
        No Serial prints needed.
//======================
//Mark code paths
if (temperature > 80)
{
    DEBUG_EVENT(EVENT16);   // Overheat path
}
else
{
    DEBUG_EVENT(EVENT17);   // Normal path
}

//======================
// Flag an error 
if (bufferOverflow)
{
    DEBUG_EVENT(EVENT30);   // critical error
}

//======================
// Timing between two EVENTs
DEBUG_EVENT(EVENT20);   // start

doSomething();

DEBUG_EVENT(EVENT21);   // end

//======================
// Detect Unexpected Conditions
if (state == STATE2 && input == INVALID)
{
    DEBUG_EVENT(EVENT22);   // illegal condition
}

//======================
// Track Function Entry Points
void readSensor()
{
    DEBUG_EVENT(EVENT23);
    . . .
}

//======================
// Combine Event + Value
DEBUG_EVENT(EVENT26);                  // “ADC sample taken”
DEBUG_PULSE_COUNT_START(Reg8, adc);    // value

//======================
// Mark External Inputs
if (switchPressed)
{
    DEBUG_EVENT(EVENT27);
}

//======================
// Display Event Sequencing
DEBUG_EVENT(EVENT10);   // start
. . .
DEBUG_EVENT(EVENT11);   // step A
. . . 
DEBUG_EVENT(EVENT12);   // step B
. . . 
DEBUG_EVENT(EVENT13);   // done

  • It is always best to use meaningful naming:
    EVENT12_ERROR1
    EVENT13_SENSOR_READ

  • Example:

 if (counter == 10)
{
    DEBUG_EVENT(EVENT31);              // milestone Reached
    DEBUG_PULSE_COUNT_START(Reg8, counter);
}
  • Use with:

    • DEBUG_EVENT()what happened
    • DEBUG_MARK()when it happened
    • DEBUG_PULSE_COUNT_START()what the value was
  • When debugging code, we can befit from adding a good amount of Pre-trigger time:
    e.g. 5+ seconds
    You get: full history BEFORE the bug.




Logic Analyzer Debug Cheat Sheet


Core Debug Model
WHAT happened → DEBUG_EVENT()
WHEN it happened → DEBUG_MARK() ← Included in DEBUG_EVENT()
VALUE at time → DEBUG_PULSE_COUNT_START()


Required loop() Services
DEBUG_INTERFACE_HEARTBEAT(...)
DEBUG_DELAYED_SERVICE()
DEBUG_LEDsService()
DEBUG_PULSE_COUNT_SERVICE() ← REQUIRED for pulse counts


Essential Macros
Event (WHAT)
DEBUG_EVENT(id)
DEBUG_EVENT_FAST(id)
Time Anchor (WHEN)
DEBUG_MARK() ← Included in DEBUG_EVENT()
Value Encoding (VALUE)
DEBUG_PULSE_COUNT_START(bit, value)


Standard Debug Pattern
DEBUG_EVENT(EVENT_x);
DEBUG_MARK(); ← Included in DEBUG_EVENT()
DEBUG_PULSE_COUNT_START(Reg8, value);
:check_mark: Produces:
• Event label
• Exact timestamp
• Variable value (pulse count)


Error Capture Pattern
if (errorCondition)
{
DEBUG_EVENT(EVENT_ERROR);
DEBUG_MARK(); ← Included in DEBUG_EVENT()
DEBUG_PULSE_COUNT_START(Reg8, errorCode);
}
:check_mark: Captures full context at failure


State Machine Debugging
case STATE2:
DEBUG_EVENT(EVENT_STATE2);
break;
:check_mark: Shows execution flow and transitions


Timing Measurement
DEBUG_EVENT(EVENT_START);

doSomething();

DEBUG_EVENT(EVENT_END);
:check_mark: Measure duration between events


Illegal Condition Detection
if (state == STATE2 && nextState == STATE5)
{
DEBUG_MARK(); ← Included in DEBUG_EVENT()
DEBUG_EVENT(EVENT_ILLEGAL);
}
:check_mark: Catches impossible transitions


Pulse Count (Value Encoding)
DEBUG_PULSE_COUNT_START(Reg8, value);
Analyzer:
• Count falling edges (Σ)
• Result = value
:check_mark: Verified accurate (e.g., 1023 → Σ = 1023)


Analyzer Setup
• Trigger: DEBUG_MARK();
• Pre-trigger: 5+ seconds
• Measurement: Falling/Rising Edge Count (Σ)
• When: DEBUG_EVENT value :
e.g. EVENT22 = 0b10110, // 22


Best Practices
:check_mark: Use meaningful EVENT names
:check_mark: Keep events sparse
:check_mark: Always include MARK for precision ← Included in DEBUG_EVENT()
:check_mark: Use pulse count for numeric values
:check_mark: Avoid debug in ISRs
:check_mark: Prefer FAST events in tight loops


Debug Checklist

  1. Define failure clearly
  2. Add trigger (capture point)
  3. Instrument states
  4. Capture key variables
  5. Measure timing
  6. Correlate inputs
  7. Narrow scope iteratively

Think of signals, not prints.

//▼ When commented out, Reg1–Reg5 act as a regular 5-bit data bus.
//#define DEBUG_DECODE_STATE      //  <--------<<<<<<<<<  

. . .


    //========================
    // Is this switch pressed ?
    if (switchState == PRESSED)
    {
      counter = counter + 1;

      // Reg7 is used to display the mySwitch state.               // <---<<<<  D e b u g
      DEBUG_SET_REG(Reg7, lastMySwitchState );                     // <---<<<<  D e b u g

      // Pulse the Debug LED1 for 1/2 second.                      // <---<<<<  D e b u g
      DEBUG_LED_PULSE(Reg17LED1, 500ul);                           // <---<<<<  D e b u g

      // Mark this Pressed event with EVENT1D (decimal 29)         // <---<<<<  D e b u g
      DEBUG_EVENT_FAST(EVENT29);                                   // <---<<<<  D e b u g

      // Every 10 switch presses.
      if (counter == 10)
      {
        // Pulse the "6th DEBUG LED" (Reg20LED6) for 3 seconds.
        // Pulse the 6th Debug LED.                                // <---<<<<  D e b u g
        DEBUG_LED_PULSE(Reg22LED6, 3000ul);                        // <---<<<<  D e b u g

        // Mark this with event EVENT1F (decimal 31)               // <---<<<<  D e b u g
        DEBUG_EVENT_FAST(EVENT31);                                 // <---<<<<  D e b u g

        // Generate n_pulses on "bit" Reg8 (integer)               // <---<<<<  D e b u g
        DEBUG_PULSE_COUNT_START(Reg8, counter);                    // <---<<<<  D e b u g

        // Counting starts over.
        counter = 0;
      }
    }

    //========================
    // The switch was released
    else
    {
      // Reg7 is used to display the mySwitch state.               // <---<<<<  D e b u g
      DEBUG_SET_REG(Reg7, lastMySwitchState );                     // <---<<<<  D e b u g

      // Mark this Released event with EVENT1E (decimal 30)        // <---<<<<  D e b u g
      DEBUG_EVENT_FAST(EVENT30);                                   // <---<<<<  D e b u g
    }

1 Like

Option 2: SPI-Based Debugging

Diagnostic LEDs

Think of Diagnostic LEDs as:

“Slow-motion, non-blocking, no logic analyzer needed, debug signals for humans”

  • Remember:
    • LEDs are time-stretched
    • Analyzer signals are real-time

Use cases:

  • LED flashes every time State changes
  • visual confirmation (LED)
  • Warning indicators
  • Limit indicators

Mental Model

Tool Purpose
Analyzer precision
Events meaning
Pulse count values
LEDs visibility
if (fault)
{
   DEBUG_EVENT(EVENT_FAULT);
   DEBUG_PULSE_COUNT_START(Reg8, faultCode);

   DEBUG_LED_ON(Reg20LED4); // persistent visual alarm
}

We have 8 Diagnostic LEDs available for troubleshooting code.

  • LEDs are controlled by their own API Macros.

  • Using the Interface Heartbeat LED, we can show the user if there is SPI communications.
    When the application sketch does not have a heartbeat LED, the Interface Heartbeat LED can be quite valuable.
    We can control the flashing rate to indicated different conditions. e.g. normal flashing, Error condition (Fast), stuttering (blocking code), solid ON/OFF (communications).

  • A ULN2803 gives current drive capability for active buzzer control, turning on external relays and for LOW side switching of devices.

    • Internal kickback diodes are located in the 2803.
  • LEDs are used to give visual feedback on events, errors etc.
    Retriggerable One-Shot operation can show us when fast events occur.

  • You can visually see:

    • sequence of operations
    • flow through code
DEBUG_LED_PULSE(Reg17LED1, 100);  // step A
. . .
DEBUG_LED_PULSE(Reg18LED2, 100);  // step B
. . .
DEBUG_LED_PULSE(Reg19LED3, 100);  // step C
  • Use a diagnostic LED to mirror a sensor (Boolean )
DEBUG_SET_REG(Reg18LED2, lastMySwitchState );
  • Use LEDs as a 3-bit status display
DEBUG_SET_REG(Reg17LED1, value & 0x01);
DEBUG_SET_REG(Reg18LED2, value & 0x02);
DEBUG_SET_REG(Reg19LED3, value & 0x04);
  • We can Latch LEDs to Flag an event.
    When we are not in the vicinity, this LED will tell us something has happened when we return.

Diagnostic LED Macros

  DEBUG_LED_ON(bit);
  DEBUG_LED_OFF(bit);
  DEBUG_LED_PULSE(bit,duration);
  DEBUG_INTERFACE_HEARTBEAT(period);

  DEBUG_LEDsService()   //Must be added to loop() for Interface LED servicing.



It should be emphasized the Logic Analyzer Interface needs to only be connected to the SPI bus to let us use its 24 bits of diagnostic capabilities.
There is NO need to move the logic analyzer probe wires.

1 Like

Option 2: SPI-Based Debugging

Analyzer Triggering

  • Setting trigger conditions on our Logic Analyzer and Oscilloscope is a skill we must master.

  • Without proper triggering, debugging becomes a hit and miss operation.

  • Our Interface has a dedicated scope trigger, Reg16.
    Complicated software conditions can pulse Reg16 to trigger an oscilloscope.

    • For example, we are having problems with a servo.
    • We can send a trigger pulse to the scope when software is just about to make the servo move to a new position.
    • The scope uses this trigger pulse to help monitor the servo power supply.
    • We see a momentary dip when the servo first moves.




Trigger Setting for Saleae Logic16

See Post #1 for Interface API "ZIP File" Version 2.6

1 Like