Using hardware interrupt to log lightning strikes - problem with SPI and I2C

Hello forum,

I've put together a weather station that measures a whole list of variables, and I'm having a hard time getting my AS3935 lightning sensor (CJMCU) to work within an interrupt. I'm using a MEGA and I use both pins 2 and 3. Pin 2 for the lightning detector and 3 for my rain guage. The rain guage works like a charm, making use of simple high / low state of the pin, dictated by the reed valve on the rain guage.

The lightning sensor however is proving a bit more tricky. This sensor lets you use either I2C or SPI - I couldn't get it to work within the interrupt when using I2C, I soon realised that I2C doesn't do well inside an ISR. Now I'm trying with SPI and having the same results, no joy.

As soon as I move the code that needs to execute with a detected strike to the loop it works great, but when I attach an interrupt to pin 2 and execute the code that way, nothing happens. I guess SPI doesn't like to run in ISR either, so what are my options?

The loop is jam packed with uploading data to my webserver through a SIM900 module, reading the other sensors, checking time from an RTC, so leaving the code for the lightning detector in the loop will not suffice, seeing that lightning happens without prior warning and if you miss one you may miss the only one. And I would like to record them all!

Here's the code that works, when left in loop. The code for I2C and SPI is identical, apart from initialising the sensor and setting an address for I2C.

void loop()
{
  if(digitalRead(LIGHTNINGINTERRUPT) == HIGH)
  {
    intVal = lightning.readInterruptReg();
    if(intVal == NOISE_INT){
      Serial.println("Noise."); 
    }
    else if(intVal == DISTURBER_INT){
      Serial.println("Disturber."); 
    }
    else if(intVal == LIGHTNING_INT)
    {
      Serial.println("Lightning Strike Detected!"); 
      byte distance = lightning.distanceToStorm(); 
      Serial.print("Approximately: "); 
      Serial.print(distance); 
      Serial.println("km away!"); 
      long lightEnergy = lightning.lightningEnergy(); 
      Serial.print("Lightning Energy: "); 
      Serial.println(lightEnergy); 
    }
  }
}

And this is how I call the same code using an interrupt, commenting out all Serial printing of course. The transfer between variables between noInterrupts() and interrupts() is to save the values so I can upload them with the SIM900.

void Setup()
{
    attachInterrupt(digitalPinToInterrupt(LIGHTNINGINTERRUPT), showLightingInterrupt, RISING);
 }


void showLightingInterrupt()
{

    intVal = lightning.readInterruptReg();
 //   Serial.println(intVal);
    
//    if(intVal == NOISE_INT)
//      Serial.println("Noise.");
       
//    else if(intVal == DISTURBER_INT)
//      Serial.println("Disturber."); 

  //  else 
     if(intVal == LIGHTNING_INT)
    {
  //    Serial.println("Lightning Strike Detected!"); 
      lightningVal++;
      distance = lightning.distanceToStorm(); 
  //    Serial.print("Approximately: "); 
  //    Serial.print(distance); 
  //    Serial.println("km away"); 

      noInterrupts();                     //  COPY VOLATILE VALUES TO VARIABLES FOR REST OF SKETCH
      intValOut = intVal;
      lightningValOut = lightningVal;
      distanceOut = distance;
      TXlightningNow = true;
      interrupts();     

      long lightEnergy = lightning.lightningEnergy(); 
//      Serial.print("Lightning Energy: "); 
//      Serial.println(lightEnergy); 
      
    }  
}

I guess I could add another board, maybe a pro-mini and connect this to the lightning sensor, and let it deal with the lightning strikes in a dedicated manner within loop. Once all the values around the strike has been extracted I can send these through the normal wire.write() from the pro-mini to the MEGA and use Wire.onReceive() on the MEGA to make use of the lightning strike values in an iterrupt sort of way. to send them on to my server.

But ideally I'd like to solve this problem by only using the MEGA, if possible.

What are the available options?

Please, post all the codes of your sketch along with the list of devices/sensors that you want to connect with MEGA.

For now I'm only trying to get this one sensor to work on its own, within an ISR. When I get this figured out I'll start bringing the other sensors in one by one.

It is one of the example sketches from the Sparkfun AS3935 sensors library. I just changed the pin numbers. The code below works 100%, as its in loop as I mentioned before.

#include <SPI.h>
#include <Wire.h>
#include "SparkFun_AS3935.h"

#define INDOOR 0x12 
#define OUTDOOR 0xE
#define LIGHTNING_INT 0x08
#define DISTURBER_INT 0x04
#define NOISE_INT 0x01

// SPI
SparkFun_AS3935 lightning;


// Interrupt pin for lightning detection 
const int lightningInt = 2; 
// Chip select pin 
int spiCS = 10; 

// Values for modifying the IC's settings. All of these values are set to their
// default values. 
byte noiseFloor = 2;
byte watchDogVal = 2;
byte spike = 2;
byte lightningThresh = 1; 


// This variable holds the number representing the lightning or non-lightning
// event issued by the lightning detector. 
byte intVal = 0; 

void setup()
{
  // When lightning is detected the interrupt pin goes HIGH.
  pinMode(lightningInt, INPUT); 

  Serial.begin(19200); 
  Serial.println("AS3935 Franklin Lightning Detector"); 

   SPI.begin(); // For SPI
  if( !lightning.beginSPI(spiCS, 2000000) ) { 
    Serial.println ("Lightning Detector did not start up, freezing!"); 
    while(1); 
  }
  else
    Serial.println("Schmow-ZoW, Lightning Detector Ready!\n");

  // "Disturbers" are events that are false lightning events. If you find
  // yourself seeing a lot of disturbers you can have the chip not report those
  // events on the interrupt lines. 

  lightning.maskDisturber(true); 

  int maskVal = lightning.readMaskDisturber();
  Serial.print("Are disturbers being masked: "); 
  if (maskVal == 1)
    Serial.println("YES"); 
  else if (maskVal == 0)
    Serial.println("NO"); 

  // The lightning detector defaults to an indoor setting (less
  // gain/sensitivity), if you plan on using this outdoors 
  // uncomment the following line:

  lightning.setIndoorOutdoor(OUTDOOR); 

  int enviVal = lightning.readIndoorOutdoor();
  Serial.print("Are we set for indoor or outdoor: ");  
  if( enviVal == INDOOR )
    Serial.println("Indoor.");  
  else if( enviVal == OUTDOOR )
    Serial.println("Outdoor.");  
  else 
    Serial.println(enviVal, BIN); 

  // Noise floor setting from 1-7, one being the lowest. Default setting is
  // two. If you need to check the setting, the corresponding function for
  // reading the function follows.    

  lightning.setNoiseLevel(noiseFloor);  

  int noiseVal = lightning.readNoiseLevel();
  Serial.print("Noise Level is set at: ");
  Serial.println(noiseVal);

  // Watchdog threshold setting can be from 1-10, one being the lowest. Default setting is
  // two. If you need to check the setting, the corresponding function for
  // reading the function follows.    

  lightning.watchdogThreshold(watchDogVal); 

  int watchVal = lightning.readWatchdogThreshold();
  Serial.print("Watchdog Threshold is set to: ");
  Serial.println(watchVal);

  // Spike Rejection setting from 1-11, one being the lowest. Default setting is
  // two. If you need to check the setting, the corresponding function for
  // reading the function follows.    
  // The shape of the spike is analyzed during the chip's
  // validation routine. You can round this spike at the cost of sensitivity to
  // distant events. 

  lightning.spikeRejection(spike); 

  int spikeVal = lightning.readSpikeRejection();
  Serial.print("Spike Rejection is set to: ");
  Serial.println(spikeVal);


  // This setting will change when the lightning detector issues an interrupt.
  // For example you will only get an interrupt after five lightning strikes
  // instead of one. Default is one, and it takes settings of 1, 5, 9 and 16.   
  // Followed by its corresponding read function. Default is zero. 

  lightning.lightningThreshold(lightningThresh); 

  uint8_t lightVal = lightning.readLightningThreshold();
  Serial.print("The number of strikes before interrupt is triggerd: "); 
  Serial.println(lightVal); 

  // When the distance to the storm is estimated, it takes into account other
  // lightning that was sensed in the past 15 minutes. If you want to reset
  // time, then you can call this function. 

  //lightning.clearStatistics(); 

  // The power down function has a BIG "gotcha". When you wake up the board
  // after power down, the internal oscillators will be recalibrated. They are
  // recalibrated according to the resonance frequency of the antenna - which
  // should be around 500kHz. It's highly recommended that you calibrate your
  // antenna before using these two functions, or you run the risk of schewing
  // the timing of the chip. 

  //lightning.powerDown(); 
  //delay(1000);
  //if( lightning.wakeUp() ) 
   // Serial.println("Successfully woken up!");  
  //else 
    //Serial.println("Error recalibrating internal osciallator on wake up."); 
  
  // Set too many features? Reset them all with the following function.
  lightning.resetSettings();

}

void loop()
{
  if(digitalRead(lightningInt) == HIGH){
    // Hardware has alerted us to an event, now we read the interrupt register
    // to see exactly what it is. 
    intVal = lightning.readInterruptReg();
    if(intVal == NOISE_INT){
      Serial.println("Noise."); 
    }
    else if(intVal == DISTURBER_INT){
      Serial.println("Disturber."); 
    }
    else if(intVal == LIGHTNING_INT){
      Serial.println("Lightning Strike Detected!"); 
      // Lightning! Now how far away is it? Distance estimation takes into
      // account previously seen events. 
      byte distance = lightning.distanceToStorm(); 
      Serial.print("Approximately: "); 
      Serial.print(distance); 
      Serial.println("km away!"); 

      // "Lightning Energy" and I do place into quotes intentionally, is a pure
      // number that does not have any physical meaning. 
      long lightEnergy = lightning.lightningEnergy(); 
      Serial.print("Lightning Energy: "); 
      Serial.println(lightEnergy); 

    }
  }
}

I don't understand why you want to use an ISR for the lightning detector. Wouldn't it be sufficient to poll the device regularly? I don't imagine there would be so many lightning strikes that you might miss one.

Both I2C and SPI rely on interrupts for their operation.

...R

Sending data to my server using at commands takes a very long time - when signal is bad (often the case with remote stations) it may take a couple minutes to complete updates. And I like to update no less than once every five minutes anyway, to have solid data, so on average I guess the MEGA would have roughly 50% of the time to poll. This is not enough, you may miss half of all the strikes during a storm.

Using an interrupt is the only way to catch them all. And I really do want to catch them all.

If this is not possible with an ISR I'll use a second arduino that's dedicated to the lightning sensor, but before I do this I wanted to know from the clever guys if I'm missing a good alternative.

volatile bool YupTheLighteningThingDidAThing = 0;

void Setup()
{
    attachInterrupt(digitalPinToInterrupt(LIGHTNINGINTERRUPT), showLightingInterrupt, RISING);
 }

void showLightingInterrupt()
{
YupTheLighteningThingDidAThing = true;
}
void DoTheLighteningThing()
{
The lightningcode goes here;
}
void loop()
{
if YupTheLighteningThingDidAThing 
{ 
 DoTheLighteningThing();
YupTheLighteningThingDidAThing = false;
}

}

Some contorted thing like that should work.

Thanks, that's certainly an improvement, but it will still mean that after the loop finishes with whatever it has been doing for a couple minutes, all the strikes that occurred during that time will be lost, apart from the last one.

I've hooked the sensor up to a pro-mini, and sending the info over I2C to the MEGA. Problem solved for now, but if someone comes across this thread and knows of a way to catch any possible event and process it using only one arduino, please post your thoughts here.

Thanks,
Hein

heinburgh:
Sending data to my server using at commands takes a very long time

I suspect the code could be written so it initiates a communication with the server and then can go back to doing other things rather than sitting twiddling its thumbs waiting for the server.

...R

If there's a strike during the 10 seconds it takes to initiate, I'll miss that strike. I can definitely clean the code up to free more time in loop, but the whole idea is to catch 100% of all strikes and no matter how well I do the code there will always be that couple seconds / minutes that I can miss that whopper.

So I tried this morning with a pro-mini 3.3V but for some reason it shows far too much disturbance. Swapped it with an UNO and things are looking much better. To sacrifice an UNO to ensure I get 100% of the strikes is a small price to pay. That's if this sensor works like its meant to, will have to see when the electrical storms come.

We're a couple weeks or so away from our first rains and in our area the storms tend to put up quite a show. I'll post my results here.

heinburgh:
If there's a strike during the 10 seconds it takes to initiate,

Ten seconds for an Arduino is like 10 years for a human. I would be very surprised if it needs to wast 10 seconds waiting for something to initiate. Just start the initializing and check back from time to time to see if it has completed.

You don't sit watching the oven for 90 minutes while your chicken cooks - you put the chicken in and then check the clock at intervals to see when it is time to take the chicken out.

...R

heinburgh:
Thanks, that's certainly an improvement, but it will still mean that after the loop finishes with whatever it has been doing for a couple minutes, all the strikes that occurred during that time will be lost, apart from the last one.

I've hooked the sensor up to a pro-mini, and sending the info over I2C to the MEGA. Problem solved for now, but if someone comes across this thread and knows of a way to catch any possible event and process it using only one arduino, please post your thoughts here.

Thanks,
Hein

How fast does the code in loop() run?

Using an Uno one can determine that, using millis() or micros() by catching the time of millis or micros at the top of the loop, then at the bottom of the loop, do the timestart-timeend math, and with the difference you get the time it took to do the thing. So let us know how long the loops are taking to run and you might get a few suggestions to eek out a few more microseconds.

Have you guys ever used a SIM module to upload data to a server like Ubidots, using at commands? There are several processes that necessitates a waiting period - when you connect to the server and wait for the repsponse, sending the data and waiting for the "received" repsonse, waiting for the "data is ok" response.

Even setting up the GPRS connection has several steps where you sometimes have to wait for many seconds to connect. The unit goes though these steps every five minutes.

Added challenge is that the lightning sensor only keeps a lightning strike data in its register for a second after the strike. So if you miss this second you miss the strike.

I'm sure if I took a couple months to fine tune this I could get a few more seconds out of it, but in my inexperienced mind there's no way to to keep the Arduino's ears open 100% of the time. For me, for now, I'll be happy with adding another arduino.

heinburgh:
Have you guys ever used a SIM module to upload data to a server like Ubidots, using at commands?

Please post a link to the datasheet that describes the process.

AFAIK an AT command is just a message sent using Serial. Sure it takes some time for the response to come back. But there is no need to wait idly until it does. Just keep checking for a new message. Have a look at the examples in Serial Input Basics - simple reliable non-blocking ways to receive data.

...R

I will make this a free-time research project. I know my serial comms are very inefficient as it is. Thanks for the tips