Problem: Possible race condition using external interrupt and sleep

Setup: I have an Adafruit ADXL345 attached to an Adafruit feather 32u4 (LoRa), via I2C lines and an interrupt line.

Goal: The microcontroller should be in a sleep condition when acceleration values are below a certain value (threshold), and stay awake in all other cases. Interrupts make the microcontroller wake up.

Problem: Everything works perfectly most of the time. However when I start tapping a lot on the setup, it seems to get stuck after some time (sometimes after a few seconds, other times after a few minutes of intense tapping). Usually it gets stuck in the state where the LED is off. My suspicion is a race condition.

I have consulted stderr, Nick Gammon Notes.

Any help much appreciated. N.B. I reduced the code to the bare minimum which still reproduces the error, so the whole LoRa part is taken out of the code.

#include <Arduino.h>
#include <avr/sleep.h>
#include <SparkFun_ADXL345.h>                // SparkFun ADXL345 Library
#include <pins_arduino.h>

#define LED_BUILTIN               13
#define VBATPIN                   A9
#define ADXL345_MG2G_MULTIPLIER   (0.004)      // 4mg per lsb
#define SENSORS_GRAVITY_STANDARD  (9.80665F) 
volatile bool is_asleep = false;
volatile int  isr_counter;

ADXL345 adxl = ADXL345();             // USE FOR I2C COMMUNICATION

/// INTERRUPT
int     interruptPin	=   1;      // Setup pin X to be the interrupt pin 

float   ab_accY;

// Offset due to gravity/sensor
float gYoffset;

/* Declare functions */
void sleepNow(); 
void wake(); 
int  digPinToInterrupt(); 


void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(2000);
  Serial.begin(9600);

  // ***************** SETUP ADXL 345 ****************************
  adxl.powerOn();                     // Power on the ADXL345

  adxl.setRangeSetting(16);           // Give the range settings
                                      // Accepted values are 2g, 4g, 8g or 16g
                                      // Higher Values = Wider Measurement Range
                                      // Lower Values = Greater Sensitivity

  adxl.setRate(100);                  // Sampling rate to 100 Hz
  adxl.setActivityXYZ(0, 1, 0);       // Set to activate movement detection in the axes "adxl.setActivityXYZ(X, Y, Z);" (1 == ON, 0 == OFF)  
  adxl.setActivityThreshold(20);      // 62.5mg per increment   // Set activity   // Inactivity thresholds (0-255)// (default 75)
  adxl.setInactivityXYZ(0, 0, 0);     // Set to detect inactivity in all the axes "adxl.setInactivityXYZ(X, Y, Z);" (1 == ON, 0 == OFF)
  adxl.setTapDetectionOnXYZ(0, 0, 0); // Detect taps in the directions turned ON "adxl.setTapDetectionOnX(X, Y, Z);" (1 == ON, 0 == OFF)
  adxl.setInterruptMapping(ADXL345_INT_ACTIVITY_BIT,ADXL345_INT2_PIN);
  adxl.InactivityINT(0);
  adxl.ActivityINT(1);
  adxl.FreeFallINT(0);
  adxl.doubleTapINT(0); 
  adxl.singleTapINT(0);


  float offYsum = 0;

  // 10 seconds worth of measurements (assuming 100 Hz)
  for (int j = 0; j <= 1000; j++) 
  { 
    // Accelerometer Readings
    int x,y,z;   
    adxl.readAccel(&x, &y, &z);         // Read the accelerometer values and store them in variables declared above x,y,z
    float accY = 0.3065 * y;
    offYsum += accY;
  }
  
  gYoffset = offYsum/1000;
  pinMode(interruptPin,INPUT_PULLUP);//Set pin d2 to input using the builtin pullup resistor
  Serial.println("End of setup"); 

}

void loop()
{
  int x,y,z;   
  adxl.getInterruptSource();
  adxl.readAccel(&x, &y, &z);         // Read the accelerometer values and store them in variables declared above x,y,z
  
  float accY = 0.3065 * y;   
  ab_accY = abs(abs(accY) - abs(gYoffset));
  float measThresh = 3; 
  
  if ( ab_accY <= measThresh)
  { 
          sleepNow(); 
  }
  else 
  {
    /* Continue */
  }

}

/* --------------------------HELPER FUNCTION----------------------------- */

int digPinToInterrupt(int digPin) {
  int intPin; 
  if (digPin == 1){
      intPin = 3;
      // return intPin;
  }
  else if (digPin == 0)
  {
      intPin = 2;
      // return intPin; 
  }    
  else {     

  }
  return intPin; 
}
// ------------------------ INTERRUPT ROUTINES -------------------- //


void wakeUp(){
  if (is_asleep)
  {
    sleep_disable();
    detachInterrupt(digPinToInterrupt(interruptPin));
    digitalWrite(LED_BUILTIN, HIGH);
    isr_counter++;
    is_asleep = false; 
  } 
} 

void sleepNow()
{
  delay(10);
  if (!is_asleep) 
  {
    set_sleep_mode(SLEEP_MODE_PWR_DOWN); 
    noInterrupts(); 
    sleep_enable();  
    digitalWrite(LED_BUILTIN, LOW);
    is_asleep = true;
    attachInterrupt(digPinToInterrupt(interruptPin),wakeUp,HIGH);
    interrupts(); 
    sleep_cpu();
  }
}

This doesn’t really seem to be a TTN question but rather a generic Arduino question.

You might however move all the wakeup stuff out of the ISR to the main loop, that could simplify things quite a bit. Is there some reason why the ISR actually needs to do anything at all, vs. just have the processor resume the main thread of execution after waking from sleep?

Also make sure your I2C bus isn’t getting stuck independent of the sleep mode, some access functions for some platforms in the Arduino realm fail to implement a timeout, so if they ever fail, they never recover.

Are you sure that the Serial class is compatible with sleep on a 32u4 where it would be USB-based? Perhaps you should be using the actual UART for debug output.

1 Like

Yes @cslorabox you’re right, the reason I posted it here anyway is because I posted it on a number of other Arduino forums and no-one has been able to answer it. And I thought it might be relevant for other TTN users because sleep is quite an important asset…

Since posting this I have found the answer I think (because the problem has not popped up since).
What I did was to move the LED commands out of the sleep and wakeup functions, which is pretty much the same as you said.

How do I make sure of this?

I am pretty sure that the Serial class is completely incompatible, because I have not been able to use it because of the sleep :frowning: The UART suggestion is a good one thanks, I am not familiar with it. I will look into it.

Thanks a lot for your help and advice!

It seems quite unlikely that the LED operations would be the problem.

You’d have to examine the source of the accelerometer library and whatever I2C routines it uses to see if they implement a timeout and start over.

A USB-based logic analyzer can be very useful for I2C issues, though you’d need to connect it with flexible wires (silicone insulated is great for this!) or maybe improvise some kind of shaker table (sacrificial speaker playing an audio track? phone vibrator motor? dollar store electric toothbrush?) to trigger the accelerometer a lot, without actually creating large physical movements.

1 Like

A Google search on ‘Arduino I2C hangs’ is informative.

Probably best to post a question on that topic over in the Arduino forums, lots more expertise on the Arduino environment in the Arduino forums than in here.

If you want long term continued operation from a node, an independant watchdog is essential really. That will rescue the situation of a locked up I2C as well as coping with what I am sure your programs dont have, bugs.

1 Like

Hi @LoRaTracker and @cslorabox,
Thanks for both your contributions. I have looked up the things you suggested.
Just one final question:

Any tips/examples on using this? I have been googling for this, but everything seems to point back to using the serial monitor.

You’d probably use Serial1 and a logic level USB serial board, as would be used with a bare ATmega328p or minimalist Arduino not having native USB or its own USB-serial onboard.

I see a few things:

  1. You should interrupt on an edge (RISING/FALLING/CHANGE). HIGH isn’t supported by 32U4. I presume you want RISING but I haven’t bothered to look at the sensor datasheet.

  2. As cslorabox pointed out you simply need to put any wakeup stuff immediately after cpu_sleep() since that is where the MCU resumes on every wakeup.

  3. It’s possible for the USB to wake the device from SLEEP_MODE_PWR_DOWN (another reason to implement point 2).

  4. Why aren’t you using the provided digitalPinToInterrupt()? Is your helper doing the correct mapping?

Finally, if you implement point 2, remember to still attach an interrupt handler to enable the interrupt - it can just be an empty function. Also, you only need to attach it once in setup() - there is no need to detach.

That would make sense. However it would practically mean that the sleep mode can’t really be tried while connected to USB, because it would be waking up 1000 times a second, regardless if there is any serial-over-USB traffic to move or not.

The reason I’m suggesting an ordinary UART is that this (and electrical or logic disconnect from USB without wakeup by it) would allow debugging the more typical battery powered node case of sleeping 99.9% of the time for power savings