Magnets and Hardware Interrupts

I’m working with a cheap hall effect sensor so that I will be able to put the buoy into sleep mode without the need for a mechanical on/off switch. The magnet sensor will be inside the lid of the buoy so there will be no moving parts for water to penetrate, keeping everything water tight (in theory).

IMG_1683.jpg

This first code example is written for the Arduino UNO or the Adafruit nRF52 Feather. It attaches a hardware interrupt to one of the pins and reads the status of the magnetic switch as it changes from HIGH or LOW. When the two magnets get close enough, the small switch inside closes and the signal goes LOW triggers a falling edge that is detected by the hardware interrupt. This jumps to the Interrupt Service Router (ISR) and runs a bit of code to toggle the LED on or off based on its previous state. The basic code was provided by the friendly folks at Sparkfun. Thanks for the awesome video!

Example #1 – Hardware Interrupt

The nRF52 Feather can use any of its pins for a hardware interrupt, but the Arduino UNO must use Pin2 for Int0 and Pin3 for Int1.

If using the UNO, there is a handy function to determine which interrupt you will be using based on the UNO pin you chose to connect to your switch or button. You can call

digitalPinToInterrupt(button_pin)

when setting up your interrupt to automatically determine the interrupt you are mapping to. But, this neat little trick does NOT work with the Feather. Be warned!

There is a 10K ohm resistor between +V and the input pin. This is to pull the pin HIGH so that it does not float while we wait for the magnet switch to activate.

// Filename: 01_HardwareInterrupt.ino
// Description: Basic external interrupt example for 
// Original code: https://www.youtube.com/watch?v=J61_PKyWjxU
// By: Sparkfun
// Modified: June 23,2018
// By: Nick Raymond

// Pins
 const uint8_t btn_pin = 2; // uncomment for UNO
//const uint8_t btn_pin = 7;    // uncomment for nRF52 Feather
const uint8_t led_pin = LED_BUILTIN;

// Globlas
uint8_t led_state = HIGH;

void setup() {
  Serial.begin(115200);
  pinMode(btn_pin, INPUT);
  pinMode(led_pin, OUTPUT);

  digitalWrite(led_pin, led_state);

  // Setup the interrupt
  attachInterrupt(digitalPinToInterrupt(btn_pin), toggle, FALLING); // uncomment for UNO
  //attachInterrupt(btn_pin, toggle, FALLING); // uncomment for nRF52 Feather, does not work with the "digitalPinToInterrupt" function!

}

void loop() {
  // Pretend we do something here
  Serial.println("do something");
  delay(500);
}


// Interrupt Service Routine
void toggle(void){
  led_state = !led_state;
  digitalWrite(led_pin, led_state);
  Serial.print("ISR   ");Serial.print("LED state = ");Serial.println(led_state);
}



With this implementation the LED will toggle ON and OFF very quickly and can be prone to accidental changes in state due to the sensitivity of the system. This is bad, since we do not want to accidentally have the buoy switch into sleep mode by mistake.

 

The following image is a plot showing the current usage during the on/off toggling for the Arduino UNO. We see that the UNO has a rather large idle current of approximately 33 mA, and draws about 35 milliamps with the onboard LED turned on. This is simply the sum total of the current needed for all the parts on the Arduino and  includes the current used for the microcontroller chip, the USB to serial convertor, the onboard voltage regulator, system clock and other bits and pieces. We can reduce this considerably by placing the UNO into a deep sleep mode which I will talk about in a later post. But for now,  just keep in mind that while the UNO is a great off-the-shelf general purpose prototyping board is may be a bit limiting for our lower power application inside the buoy.

Screen Shot 2018-06-24 at 11.07.24 AM

And this is a plot showing the current usage during the ON/OFF toggling for the nRF52 Feather. We see that the Feather has a fairly low idle current and draws approximately 6.9 mA at rest, and draws about 8.2 milliamps with the red onboard LED turned on. It is possible to get even lower values than this – with a few things to keep in mind.

Screen Shot 2018-06-24 at 10.16.53 AM

A. The nRF52 feather uses a schedule task manager to take care of scheduling events so that the BLE and MCU can both co-exist on one chip in harmony. By default this task scheduler will put the board into a low idle mode when nothing is happening in the loop. This is handled by a macro waitForEvent() but apparently you can also just add a delay in the loop and the board will automatically drop to this low idle state also. COOL!

B. Unfortunately, it was discovered that the USB to serial converter chip on the original version of the nRF52 Feather was sucking up a decent amount of power preventing the board from being able to get sub-milliamp current draws when in a deep sleep mode. This is all tracked on the Adafruit help forum and a new update version of the board is now available that addresses this issue. I have an older version of the board but will be ordering the new boards once they are back in stock. So for now, the best I can expect from my feather is about 6.9 – 7 mA when in “sleep mode”.

Example #2 – Interrupt + Debounce

The next example combines the hardware interrupt with a simple debounce example. With the debounce code we can control the sensitivity of the system by requiring that the switch stay closed for a specified amount of time as defined by

unsigned long debounceDelay = 3000;    // the debounce time

In this second example we want the user to hold the magnets together for at-least three seconds before the LED changes it state. In this implementation it also means that we need to WAIT at least three seconds before we try to change the state of the LED again. In this way we prevent an accidental ON/OFF situation and the system becomes more reliable for this buoy + magnet application.

The hardware doesn’t change for this example, just the code. The source code is based off off the example code  in the directory Arduino > Examples > 02. Digital > Debounce with the added hardware interrupt code chunk from Example #1:

/*
  Debounce

  Each time the input pin goes from LOW to HIGH (e.g. because of a push-button
  press), the output pin is toggled from LOW to HIGH or HIGH to LOW. There's a
  minimum delay between toggles to debounce the circuit (i.e. to ignore noise).

  The circuit:
  - LED attached from pin 13 to ground
  - pushbutton attached from pin 2 to +5V
  - 10 kilohm resistor attached from pin 2 to ground

  - Note: On most Arduino boards, there is already an LED on the board connected
    to pin 13, so you don't need any extra components for this example.

  created 21 Nov 2006
  by David A. Mellis
  modified 30 Aug 2011
  by Limor Fried
  modified 28 Dec 2012
  by Mike Walters
  modified 30 Aug 2016
  by Arturo Guadalupi
  modified 24 Jun 2016
  by Nick Raymond

  This example code is in the public domain.

  http://www.arduino.cc/en/Tutorial/Debounce
*/

// Pins
 const uint8_t btn_pin = 2; // uncomment for UNO
//const uint8_t btn_pin = 7;    // uncomment for nRF52 Feather
const uint8_t led_pin = LED_BUILTIN;

// Variables will change:
int led_state = HIGH;         // the current state of the output pin
int button_state;             // the current reading from the input pin
int lastbutton_state = LOW;   // the previous reading from the input pin
int flag = 0;                 // change if interrupt is activated
int sleep = 0;                // when == 1, go to sleep            

// the following variables are unsigned longs because the time, measured in
// milliseconds, will quickly become a bigger number than can be stored in an int.
unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 3000;    // the debounce time; increase if the output flickers

void setup() {
  Serial.begin(115200);
  pinMode(btn_pin, INPUT);
  pinMode(led_pin, OUTPUT);

  // set initial LED state
  digitalWrite(led_pin, led_state);
  
  // Setup the interrupt
  attachInterrupt(digitalPinToInterrupt(btn_pin), toggle, FALLING); // uncomment for UNO
  //attachInterrupt(btn_pin, toggle, FALLING); // uncomment for nRF52 Feather, does not work with the "digitalPinToInterrupt" function!

}

void loop(){

  if(sleep == 0){
    Serial.println("Awake    ");
    // turn on LED:
    digitalWrite(led_pin, HIGH);

  }
  else if(sleep == 1 && flag == 0){
    Serial.println("Sleep    ");
    // turn off LED:
    digitalWrite(led_pin, LOW);

  }

  // call the debounce() function, check if switch is being held down
  debounce();
  delay(500);
  Serial.println("\n\n");
}



// Debounce function definition
void debounce() {
  // read the state of the switch into a local variable:
  int reading = digitalRead(btn_pin);

  // check to see if you just pressed the button
  // (i.e. the input went from LOW to HIGH), and you've waited long enough
  // since the last press to ignore any noise:

  // If the switch changed, due to noise or pressing:
  if (reading != lastbutton_state) {
    // reset the debouncing timer
    lastDebounceTime = millis();
  }

  Serial.print((millis() - lastDebounceTime)); Serial.print("  "); // debugging check the counter
  if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state:

    Serial.print("button_state "); Serial.print(button_state); Serial.print("reading "); Serial.println(reading);
    // if the button state has changed:
    if (reading != button_state) {
      button_state = reading;

      // only toggle the LED if the new button state is LOW
      if (button_state == LOW) {
        led_state = !led_state; // change state of LED
        sleep = !sleep;         // change state of sleep mode
        flag = 0;               // change state of the flag to 0
      }
    }
  }

  // save the reading. Next time through the loop, it'll be the lastbutton_state:
  lastbutton_state = reading;
}



// Interrupt Service Routine
void toggle(void){
  flag = 1;
  Serial.println("ISR");
  Serial.print("sleep "); Serial.print(sleep); Serial.print(" flag "); Serial.println(flag);
}

And this is the current draw from the Arduino running the above code. The current draw during idle and LED ON is about the same as last time. The signal is a bit noisy – I suspect this is because I am providing +5V to the Arduino UNO Vin pin when it should be more like 9-12V for stable operation. The orange line in the plot represents a simple low pass filter that was added by applying a moving average to the data using a window size of 5 data points. When transitioning from OFF to ON we see a slightly higher than normal current draw as the UNO powers on the LED. For both the On and OFF states we can also see a slight increase in current as the Arduino runs through the debounce code for three seconds before changing the state of OFF/ON.

Screen Shot 2018-06-24 at 11.38.27 AM

And here is what it looks like for the nRF52 Feather with a much cleaner signal. One reason I suspect we get a cleaner signal is because we are sending 5V into the USB pin which then regulates the voltage down to 3.3V for the nRF52 chip. So the nRF52 is operating in a more stable condition as compared to under powered UNO in the last example. I could switch power supplies when powering the UNO vs nRF52 but I damaged one of my Feathers when I accidentally sent 9V into the USB in pin and toasted the onboard USB to Serial converter.  So this is just me being lazy/cautious.

Here we are able to more clearly see the transitions in the code between OFF to ON and the debounce() routine.

Screen Shot 2018-06-24 at 10.15.12 AM

Below is a zoomed in view so show the transitions so we can discuss the details!

From 42 – 44 seconds the LED is OFF and the Feather is in idle mode pulling 6.9 mA. At second 44  I moved the magnets close together which trigger the hardware interrupt when there was a falling edge on pin2. This then called the Interrupt Service Routine (ISR) and changed the state of the variable flag = 1.

In the void loop the debounce() routine was being called and ran for about three seconds pulling 7.2 mA while it did the computations. At 47 seconds the debounce() routine has been satisfied and the LED is turned on. There is some transience in the signal as the LED is powered on with a max draw around 8.5 mA. After the LED has been on for a second it reaches a steady state pulling 8.2 mA. Nothing happens for several seconds, then I triggered the hardware interrupt again by moving the magnets back together.

When the program jumps back to the main loop the debounce() routine continues to run. The debounce routine runs for 3-4 seconds while I hold the magnets close together before finally transitioning to the LED OFF state with a current draw of about 6.9 mA again. And this all repeats over and over.

Screen Shot 2018-06-24 at 12.00.32 PM

BUT, there is a major flaw with this code!

The problem is that the debounce() function is called once every time in the void loop() regardless of the status of interrupt. So even when we should be in SLEEP mode, we are still running the debounce() function instead of waiting for the hardware interrupt to determine when we check the status of the switch. While we see from the plots that the ISR is being activated by the magnets, it is not controlling when the debounce() function gets called. This is not good – we want a way to have the ISR prompt the debounce() function so that we only read the pin state once the hardware interrupt has been called. This way we can set the MCU to go to sleep for long period of time to save power and only be woken up when the magnet switch is held together for at least three.

Example #3 – Interrupt + Debounce + Sleep

Still a work in progress… but the functionality is all there. The code can be simplified and optimized but that is for another day. This will at least get me up and running with something to test in the field. All the serial.print() calls are to help with debugging and are not needed during actual implementation.

// Filename: 03_ToggleSleepMode.ino
// Description: External interrupt with debounce code to trigger sleep or awake mode
// Created: June 23,2018
// By: Nick Raymond
// Notes: needs to be cleaned up / simplified 

// Pins
// const unint8_t btn_pin = 2; // pull pin high with 10k resistor to +V, uncommnet for UNO
const uint8_t btn_pin = 7;    // pull pin high with 10k resistor to +V, uncomment for nRF52 Feather
const uint8_t led_pin = LED_BUILTIN;

// Variables will change:
int led_state = HIGH;         // the current state of the output pin
int button_state;             // the current reading from the input pin
int lastbutton_state = LOW;   // the previous reading from the input pin
int flag = 0;                 // change if interrupt is activated
int sleep = 0;                // when == 1, go to sleep
int counter = 1;              // counter to track through debounce looping


// the following variables are unsigned longs because the time, measured in
// milliseconds, will quickly become a bigger number than can be stored in an int.
unsigned long lastDebounceTime = 0;  // the last time the output pin was toggled
unsigned long debounceDelay = 3000;    // the debounce time; increase if the output flickers


void setup() {
  Serial.begin(115200);
  pinMode(btn_pin, INPUT);
  pinMode(led_pin, OUTPUT);

  digitalWrite(led_pin, led_state);

  // Setup the interrupt
  //attachInterrupt(digitalPinToInterrupt(btn_pin), pinInterrupt, FALLING); // uncomment for UNO
  attachInterrupt(btn_pin, toggle, FALLING); // uncomment for nRF52 Feather

}

void loop() {

  // Put MCU into awake mode
  if (sleep == 0) {
    Serial.println("---- Awake ---- ");
    digitalWrite(led_pin, HIGH); // turn LED ON
    debounce();

    //******************************************************************//
    //*********** Put code in here to run during awake mode ************//
    //******************************************************************//
    
     delay(500); // added here to simulate polling sensors and logging data

    //******************************************************************//
    //******************************************************************//

  }
  
  // Put MCU into sleep mode
  if (sleep == 1){
    
    // Prepare for sleep mode
    if(flag == 0){
      Serial.println(" ");
      Serial.println("---- Remove the magnet!----");
      Serial.println(" ");

      // flash the red LED 10 times, warn user to remove the magnet
      for (int i = 1; i <= 10; i++) {
        digitalWrite(led_pin, HIGH);
        delay(250);
        digitalWrite(led_pin, LOW);
        delay(250);
      }
      flag = 1;
    }
    // Check button status before going to sleep
    else if (flag == 1) {
      Serial.println(" ");
      Serial.println("---- CHECK - should I wake up?----");
      Serial.println(" ");
      Serial.print("counter ");Serial.print(counter);Serial.print("flag ");Serial.println(flag);
      
      // need to check if we should wake up
      for (int i = 1; i <= 10; i++) {         debounce();         delay(500);       }       if(flag == 1){         Serial.println(" ");         Serial.println("---- Go to Sleep!----");         Serial.println(" ");         flag = !flag;         while (!flag) {           //__WFI(); // put into low power mode           delay(300); // wait in low power mode         }       }     }     else{       Serial.print("*****else --- flag ");Serial.print(flag);Serial.print("Sleep ");Serial.print(sleep);       while(counter > 10){
        debounce();
        delay(500);
        counter++; // keep track of number of times we call debounce outside of AWAKE mode 
      }
      flag = 0;
      counter = 0;
   }
  }
}


// Interrupt Service Routine
void toggle(void) {
  flag = 1;
  Serial.println("ISR");
  Serial.print("sleep "); Serial.print(sleep); Serial.print(" flag "); Serial.println(flag);
}


// Debounce function definition
void debounce() {
  // read the state of the switch into a local variable:
  int reading = digitalRead(btn_pin);

  // check to see if you just pressed the button
  // (i.e. the input went from HIGH to LOW), and you've waited long enough
  // since the last press to ignore any noise:

  // If the switch changed, due to noise or pressing:
  if (reading != lastbutton_state) {
    // reset the debouncing timer
    lastDebounceTime = millis();
  }

  Serial.println(millis() - lastDebounceTime); // for debugging ONLY
  if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there for longer than the debounce
    // delay, so take it as the actual current state:
    Serial.print("button_state "); Serial.print(button_state); Serial.print("reading "); Serial.println(reading);

    // if the button state has changed:
    if (reading != button_state) {
      button_state = reading;

      // only toggle the LED if the new button state is LOW
      if (button_state == LOW) {
        led_state = !led_state;
        sleep = !sleep;
        flag = 0;
        Serial.println("Debounce function");
        Serial.print("sleep changed:"); Serial.println(sleep);
        Serial.print("flag changed:"); Serial.println(flag);
      }
    }
  }

  // set the LED:
  //digitalWrite(led_pin, led_state);

  // save the reading. Next time through the loop, it'll be the lastbutton_state:
  lastbutton_state = reading;
}

And this is what the current draw looks like. Very similar to example #2 – mainly because the nRF52 Feather has such a low idle current draw thanks to the onboard task manager. The power saving for the UNO would be expected to be fairly significant if you setup a deep sleep mode. Running out of time today…but hopefully can show an example with the UNO later on.

The major benefit of running this code as compared to Example #2 is that now the debounce() function should only get called when the hardware interrupt detects a falling edge. The Example #3 code has the MCU go to “sleep” and will remain asleep until the hardware interrupt runs the ISR. These two major differences make it possible to use the magnet switch to place the MCU in a long sleep mode where it will wait for an input from the user before waking up. If it detects a change then the MCU will check that the switch has been closed for more than three seconds before waking back up. If it detects a change on the hardware interrupt pin BUT the switch was NOT closed for three seconds then the MCU will go back to sleep.

Below of the current usage from the demo in the video.

Screen Shot 2018-06-24 at 2.39.59 PM.png

Next I’ll update the CAD model of the lid to be able to mount the magnetic sensor to the underside near the solar panel. Once the magnet sensor is mounted to the bottom of the lid I’ll need to use a stronger magnet to penetrate through and activate the switch. Based on some preliminary testing this should be ok.

Also on the to-do list is setup a RPi to read the power monitor sensor and create log files while running headless. This way I can monitor both the solar panel output and the MCU power draw over a period of a few days to get a better idea of the overall energy budget with the GPS and LoRa and SD card. Oh – and water testing soon to verify that the 3D prints + epoxy will make a water tight container.

2 thoughts on “Magnets and Hardware Interrupts

  1. Hi,
    Great project! Currently I’m developing a buoy to measure pH and temperature in a photobiorreactor (is for my master thesis) and is quite similar to what you’re doing. Thanks for sharing the CAD files for the buoy.
    Cheers

    • Hi Anna,

      Hope the CAD files are helpful. What are you looking to study with the photobioreactor? I did my masters at UC Davis and there was some neat work being done with the Aquatic Biology and Aquaculture center – any overlap in work or subject matter?

      Would love to see some photos or hear more about your own build. Please let me know if you have any other questions.

      Nick-

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s