Xiao SAMD21 cannot sleep and wake from external interrupt

I’m prototyping a closet light to operate for >3 months on 1000mAh 3.3v, built around Xiao SAMD21 (green led removed). Want deep sleep (0.1mA), woken by motion sensor. I’ve tried ArduinoLowPower, SleepyDog, RTC, samd21lpe, Stuarts. Nothing works. I’d settle for 500ms deep sleep woken by timer. Has anyone else hit the same major snag? I’d be eternally grateful to anyone who could provide a complete working code example that proves sleep/interrupt works on this platform.

/**
 * @file xiao_sleep_fail_example.ino
 * @brief Demonstrates the failure of a Seeed Studio Xiao SAMD21 to reliably
 * wake from deep sleep (STANDBY) using an external interrupt.
 *
 * @version 1.0
 * @date 2025-06-18
 *
 * HARDWARE:
 * - Seeed Studio Xiao SAMD21
 * - LED connected to the built-in LED pin (D13)
 * - A switch or wire connecting Pin D9 to GND to trigger the interrupt.
 *
 * GOAL:
 * - Enter STANDBY deep sleep mode (~µA consumption).
 * - Wake up instantly when Pin D9 is pulled LOW.
 * - Toggle an LED.
 * - Successfully re-enter sleep and repeat the cycle indefinitely.
 *
 * OBSERVED BEHAVIOR:
 * - On reset, the board sleeps correctly.
 * - On the FIRST interrupt, the board wakes, toggles the LED, and then hangs.
 * - It becomes unresponsive and requires a reset.
 * - This behavior has been confirmed on multiple, separate Xiao SAMD21 units.
 *
 * NOTES:
 * - This code incorporates all known best practices and workarounds, including
 * software debouncing, system stabilization delays, and the specific
 * USB/SysTick workaround discussed on the Seeed Studio forum.
 * - The failure persists even with these measures in place.
 */

// Required for access to the USBDevice object for the forum's workaround
#include "USB/USBAPI.h"

// --- Configuration ---
#define EXT_INT_PIN 9           // The pin used for the external wake-up interrupt
#define LED_PIN     LED_BUILTIN   // The built-in orange LED
#define DEBOUNCE_DELAY_MS 250   // Cooldown period to prevent switch bounce

// --- Global Variables ---
// Flag to communicate from the ISR to the main loop
volatile bool interruptFired = false;
// Timer for software debouncing
volatile unsigned long lastInterruptTime = 0;

/**
 * @brief Interrupt Service Routine (ISR) called by the hardware.
 * Implements a software debounce to ignore noise.
 */
void interruptHandler() {
  if (millis() - lastInterruptTime > DEBOUNCE_DELAY_MS) {
    interruptFired = true;
    lastInterruptTime = millis();
  }
}

/**
 * @brief Standard Arduino setup function.
 */
void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  // Configure the interrupt pin with an internal pull-up.
  // The interrupt will trigger when the pin is connected to GND.
  pinMode(EXT_INT_PIN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(EXT_INT_PIN), interruptHandler, FALLING);

  // Configure the system for DEEP sleep (STANDBY mode) using a direct
  // write to the System Control Block register.
  SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;

  // Prime the debounce timer to ignore any false triggers during startup.
  lastInterruptTime = millis();
}

/**
 * @brief Standard Arduino loop function.
 */
void loop() {
  // Check if the ISR has signaled that a valid interrupt occurred.
  if (interruptFired) {
    digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Perform action (toggle LED)
    interruptFired = false;                      // Reset the flag
    
    // A brief delay to allow internal systems to stabilize after waking.
    delay(100);
  }

  // --- Seeed Studio Forum Workaround: PRE-SLEEP ---
  // These steps are intended to prevent a hang-on-wake.
  SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk; // Disable SysTick interrupt
  USBDevice.detach();                      // Detach the native USB port

  // --- Enter Sleep ---
  __DSB(); // Data Synchronization Barrier
  __WFI(); // Wait For Interrupt (CPU halts here)

  // --- Seeed Studio Forum Workaround: POST-WAKE ---
  // Execution resumes here after the interrupt wakes the CPU.
  USBDevice.attach();                      // Re-attach USB port
  SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;  // Re-enable SysTick interrupt
}

Hi there,

and Welcome Here…

So this is a known problem with , SAMD21 often fails to re-enter standby sleep after waking from an external interrupt. The behavior described — success on first wake, then permanent hang — is a known issue rooted in SAM D21’s low-level peripheral and clock system state during standby.

Root Causes (and Observations):

  1. USB + SysTick interaction
  • USB is active by default on the SAMD21.
  • The MCU sometimes hangs after deep sleep due to clocks not reinitializing cleanly, especially when USB is reattached.
  1. SysTick Interrupt (CoreTimer)
  • ArduinoCore uses SysTick for timing. But if SysTick is left enabled across standby, it can cause unintentional wakeups or hangs.
  1. Interrupt flag isn’t always cleared properly, especially if wake-up occurs just before __WFI().
  2. attachInterrupt() isn’t re-attached after wake, depending on deep power state transitions and core clock behavior.

You are doing things correctly though, :+1:

  • The initial sleep and first wake are handled properly.
  • __WFI() is correctly used with the SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk to enter standby mode (which should draw ~10–15 µA).
  • USB and SysTick are disabled before sleep and re-enabled after wake, per Seeed community guidance.

The second sleep cycle appears to hang after wake — likely due to USB clock domain instability, or SysTick reactivation glitch.

It’s also possible that NVIC-level or GCLK configurations get disturbed, and are not being reinitialized.

The ArduinoLowPower library does support standby with external wake on SAMD21 — but only if:

  • You avoid USB during sleep,
  • You don’t touch SysTick manually,
  • And your interrupt is properly configured for EIC wake-up domain.

You can test these and see if it helps… I asked AI for an Example with motion sensor to include the caveats.

first the basic , but looks like you got that covered.

#include <Arduino.h>
#include <ArduinoLowPower.h>

#define EXT_INT_PIN 9
#define LED_PIN     LED_BUILTIN

volatile bool wakeFlag = false;

void wakeISR() {
  wakeFlag = true;
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  pinMode(EXT_INT_PIN, INPUT_PULLUP);
  LowPower.attachInterruptWakeup(EXT_INT_PIN, wakeISR, FALLING);

  delay(1000); // Give USB time to print Serial debug, then detach it
  USBDevice.detach();
}

void loop() {
  if (wakeFlag) {
    digitalWrite(LED_PIN, !digitalRead(LED_PIN));
    wakeFlag = false;
  }

  // Sleep for as long as possible — wake on interrupt
  LowPower.deepSleep();
}

For 500ms periodic wake without external trigger:

void setup() {
  pinMode(LED_PIN, OUTPUT);
  USBDevice.detach();
}

void loop() {
  digitalWrite(LED_PIN, !digitalRead(LED_PIN)); // Toggle LED
  LowPower.deepSleep(500); // Wake every 500ms
}

this reliably stays in deep sleep and wakes periodically with µA-level draw on Xiao SAMD21

  • The original code manually disables SysTick and detaches USB — which is fragile.
  • The LowPower library handles the deeper SAMD21 internals (GCLKs, PM, EIC) better.
  • If deeper customization is needed (e.g., RTC instead of EIC)

if everything works, definitely try this one, there’s a thread here where they used the motion sensor to wake it…
Motion sensor (like PIR, radar, etc.) connected to D9 (active LOW).

// ===================================================================================
// Xiao SAMD21 - Motion Sensor Wake from Deep Sleep (Ultra Low Power)
//  Authors  : Pj Glasso, & Embedded AI Assistant (ESP32 Expert Mode), https://wiki.seeedstudio.com/ (REV 1.0a - 2025-06-18)
// - Sleep current: ~10-15 µA (Standby)
// - Wake from external pin (motion sensor on D9)
// - Toggle LED on wake, then return to sleep
// ===================================================================================

#include <Arduino.h>
#include <ArduinoLowPower.h>

#define MOTION_PIN 9        // Pin connected to motion sensor (active LOW)
#define LED_PIN    LED_BUILTIN

volatile bool motionDetected = false;

void motionWakeISR() {
  motionDetected = true;
}

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  pinMode(MOTION_PIN, INPUT_PULLUP);  // Assumes active LOW from sensor
  LowPower.attachInterruptWakeup(MOTION_PIN, motionWakeISR, FALLING);

  delay(1000);       // Give time for USB serial or reprogramming (optional)
  USBDevice.detach(); // Disable USB to save power
}

void loop() {
  if (motionDetected) {
    digitalWrite(LED_PIN, HIGH);   // Toggle LED on motion
    delay(200);                    // Visible flash
    digitalWrite(LED_PIN, LOW);
    motionDetected = false;
  }

  LowPower.deepSleep();  // Go to deep sleep until next motion
}

I have found when testing with Arduino LowPower how you re-attach the interrupt after wake is key to a good next cycle. Also in the end I don’t use a USB at ALL. :crossed_fingers:

HTH
GL :slight_smile: PJ :v:

You are in your way and close, stay with it…:+1:

1 Like

Thank you, PJ! “LowPower.deepSleep(500)” works plenty good enough for my requirements. During sleep, total system draw on battery is under 0.4mA, and about half of that is loss from the step-up to 3.3v converter (85% efficient). The surprising and cool thing is that Serial1 works fine across sleeps with no detach/begin after setup.

Originally I figured sleep would be woken by external interrupt, but that’s difficult and unnecessary. When it wakes, Xiao can check whatever it needs to see if it has a reason not to sleep, i.e. user is adjusting a pot knob.

Thank you!

1 Like

Hi there,

Way to go… that is respectful Low power too. It’s not the lowest power Xiao but AOK. Good for sticking with it… With all this stuff it’s a process, not rocket science. You ask good questions , you get good answers.:grin:

Do us a solid and mark the post as the solution so other may find it fast. :+1:

GL :slight_smile: PJ :v:

Check out the Nrf52840 , it’s like a camel in the desert sipping power. :dromedary_camel:

Can Xiao SAMD21 idle while maintaining steady pwm output? The reason I asks is because that appears not to be the case with LowPower.sleep(500). The pwm output shuts off during sleep.

Hi there,

So I don’t believe so but , I know there is such a thing with other ESP32’s you set it and let the Hardware handle it. The ESP32 uses a subsystem called LEDC (LED Control) for PWM output. It’s hardware-driven and very flexible.

You configure:

  • PWM frequency (e.g., 25 kHz for motor)
  • PWM resolution (e.g., 10 bits for 0–1023)
  • Duty cycle (e.g., 500 for ~50%)
    Then the LEDC peripheral maintains the output without needing CPU cycles.

Sleep Modes and PWM

Sleep Mode LEDC PWM Output? Notes
Light Sleep :white_check_mark: Yes Timer and LEDC stay active, CPU paused
Deep Sleep :x: No Only RTC and ULP stay active — LEDC halts

Something like this is implemented.

ledcSetup(0, 25000, 10);     // Channel 0, 25kHz, 10-bit resolution
ledcAttachPin(5, 0);         // Pin 5 to channel 0
ledcWrite(0, 512);           // 50% duty

// Optionally go to light sleep
esp_sleep_enable_timer_wakeup(1000000); // wake after 1s
esp_light_sleep_start();

HTH
GL :slight_smile: PJ :v:

Samd21 is the light weight Xiao, check the Others too :+1: