FALLING Interrupt triggers many times per event

Hello.
I recently got a new computer, and as a result, ended up upgrading all of the Arduino IDE components. IDE 2.3.4. BEFORE the upgrade, Using a three+ year old IDE, I had a falling-edge interrupt reading a signal from the output of a power meter, worked very reliably. Without changing ANY hardware, and only updating everything to work with the new IDE, I now find my falling edge interrupt triggering anywhere between 12 and 100 times per cycle. The power meter asserts its signal for ~20ms through an optocoupler output, so connecting directly to an Arduino input with pullup is no problem - and hasn't been a problem for a year and a half running on the same Arduino, two differ PCBs, but the latest for at least six months.
Then the IDE upgrade, and it feels like the Arduino is just triggering on level and not edge.
ESP32 Dev Module is the selected board.
to reiterate, this worked, exactly like this, before I upgraded the IDE and all the associated stuff.

I include relevant code below. The whole project is way too complicated to post in its entirety. Below are ALL of the relevant code snips.

Relevant code snippets:

#define pinInputZaehler     27 //*editted, was 25 in original post*  //has pull-up
volatile uint32_t zaehler = 0;  //volatile - can be changed in the interrupt
volatile unsigned long zaehlerMillis=0;  //volatile - can be changed in the interrupt
uint32_t zaehlerLast = 0;  //only changed in function
unsigned zaehlerMillisLast = 0;  //only changed in function

//in void setup():
  pinMode(pinInputZaehler, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pinInputZaehler), StromZaehler, FALLING);

// in void loop():
  ZaehlerStatus();

// interrupt function:
void StromZaehler() {
    zaehler = zaehler + 1;
    zaehlerMillis = millis();
}

//calculation function
void ZaehlerStatus(){
  if (zaehler != zaehlerLast) {    // pulse = 0.5kWh.  1p/h = 0.5kW
    if (zaehlerMillisLast != 0) {  //avoid calculation when there haven't been two good pulses.
      zaehlerPower_W = 3600.0 * 0.5 * float(zaehler - zaehlerLast) / float((zaehlerMillis - zaehlerMillisLast) / 1000.0);
      sprintf(a2dstr,"Z:%i/%i T:%i/%i",zaehler,zaehlerLast,zaehlerMillis,zaehlerMillisLast);
    }
    zaehlerMillisLast = zaehlerMillis;
    zaehlerLast = zaehler;
}

Welcome to the forum

Which Arduino board are you using ?
How are the components of your projected connected together ? For instance, are you using a breadboard ?

Please post a schematic of your project showing all components, how they are connected and powered

Can you reproduce the problem with a minimal sketch that you could post in its entirety ?

One more thing.

Which ESP32 core version have you been using before and which version one are you using now?

Software-wise, you could rate-limit the interrupt:

#define pinInputZaehler     25  //verified, has internal pull-up
volatile uint32_t zaehler = 0;  //volatile - can be changed in the interrupt
volatile unsigned long zaehlerMillis = 0; //volatile - can be changed in the interrupt
uint32_t zaehlerLast = 0;  //only changed in function
unsigned zaehlerMillisLast = 0;  //only changed in function
double zaehlerPower_W;
char a2dstr[128];

void setup() {
  //in void setup():
  pinMode(pinInputZaehler, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pinInputZaehler), StromZaehler, FALLING);
}

void loop() {
  // in void loop():
  ZaehlerStatus();

}

// interrupt function:
void StromZaehler() {
  uint32_t now = millis();
  static uint32_t lastInt = 0;
  if (now - lastInt < 10) return;
  lastInt = now;
  zaehler = zaehler + 1;
  zaehlerMillis = now;
}

//calculation function
void ZaehlerStatus() {
  if (zaehler != zaehlerLast) {    // pulse = 0.5kWh.  1p/h = 0.5kW
    if (zaehlerMillisLast != 0) {  //avoid calculation when there haven't been two good pulses.
      zaehlerPower_W = 3600.0 * 0.5 * float(zaehler - zaehlerLast) / float((zaehlerMillis - zaehlerMillisLast) / 1000.0);
      sprintf(a2dstr, "Z:%i/%i T:%i/%i", zaehler, zaehlerLast, zaehlerMillis, zaehlerMillisLast);
    }
    zaehlerMillisLast = zaehlerMillis;
    zaehlerLast = zaehler;
  }
}
1 Like

I'm using the Arduino board "NodeMCU DevKit C V4" from AZ-Delivery. It's based on WROOM-32 SOC. The "ESP32 Dev Module" is the board configuration I use in the IDE, and it has worked fine for the last years with this Arduino module.

I can't upload a minimal sketch to the board because it runs about ten other functions I don't want to be without.

I designed and had a PCB fabricated. Schematic below. Turns out the input is actual 27, but this doesn't change that it has a pull-up. The + output of the S0 signal from the power meter is connected to JE5 pin 1 and the - to JE5 pin 2. It's been this way for over a year on the hardware in the schematic below. The only change was recompiling the code in the updated IDE.

IDE Appplication version was 1.8.16, which was the latest when I downloaded. By core version, I think you mean the "Board" driver in the IDE, but unfortunately because of OneDrive's insistance on synchronizing absolutely everything... I can't get it anymore. But I pretty much didn't update any drivers until I was forced to when I got the new computer

Hmmm. that's a reasonable work-around. I would likely increase 10 to 20, since the pulse is supposed to be 20ms long. the pulses should be worst-case about half a second apart, but generally longer

You can change the board core file version from the IDE so I don't see what OneDrive has to do with it

Hi UKHeliBob, thought the files were autotragicly synchronized.
but you're absolutely right, the IDE settings stay with the machine.
The old IDE has esp32 from espressif Version 1.0.6
The new IDE has esp32 from espressif Version 3.1.1

https://docs.espressif.com/projects/arduino-esp32/en/latest/migration_guides/2.x_to_3.0.html

I don’t have a clue about the source of the problem but if the “true” falling signal shouldn’t happen faster than 500ms, you could use 200, 300, 400ms or more.

Hi @bmoerdyk2 ,

it might not be the cause in your case but I think this is interesting to read:

Slow Rise/Fall Times on GPIO inputs result in Spurious Interrupts

Briefly, in order for the rising- or falling-edge interrupts to function correctly on the ESP32 GPIO pins, both the rise and fall times of the input waveform must be short enough . My testing so far suggests that a 2 us (microsecond) or shorter transition between 10% and 90% logic levels avoids false triggering.

Just in case that you had any hardware changes that could interfere with the edges of the signal ...

Good luck!
ec2021

1 Like

Hi DaveX: 3.3kW is the practical limit of the circuit, even assuming an inrush current bringing the total load to 5kW, that is still 360ms between pulses. (2000 pulses per kWh) So, yeah... the pulses should be that far apart.

Hi @DaveX and @bmoerdyk2

I took Dave's modified sketch and created a Wokwi project here (with the original and the limited stromZaehler() function, to be chosen by #define).

https://wokwi.com/projects/423601736139637761

The interrupt source is a button with bouncing switched on, so you get a number of interrupts when it's pressed and released.

With bouncing switched off it does react only once per falling edge ...

I know it's just simulation so not necessarily representing the real thing but might be an indication for a different issue.

ec2021

Sketch
/*
  Forum: https://forum.arduino.cc/t/falling-interrupt-triggers-many-times-per-event/1356817/4
  Wokwi: https://wokwi.com/projects/423601736139637761

  Original: bmoerdyk2
  Modified: DaveX  -> Implemented Limited     StromZaehler()
  Modified: ec2021 -> Re-Implemented Original StromZaehler()

*/

//#define LIMITED

#define pinInputZaehler     27  //verified, has internal pull-up
volatile uint32_t zaehler = 0;  //volatile - can be changed in the interrupt
volatile unsigned long zaehlerMillis = 0; //volatile - can be changed in the interrupt
uint32_t zaehlerLast = 0;  //only changed in function
unsigned zaehlerMillisLast = 0;  //only changed in function
double zaehlerPower_W;
char a2dstr[128];

void setup() {
  //in void setup():
  Serial.begin(115200);
#ifdef LIMITED
  Serial.println("Limited");
#else
  Serial.println("Original");
#endif
  pinMode(pinInputZaehler, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(pinInputZaehler), StromZaehler, FALLING);
}
void loop() {
  // in void loop():
  ZaehlerStatus();

}

#ifdef LIMITED
// interrupt function Limited:
void StromZaehler() {
  uint32_t now = millis();
  static uint32_t lastInt = 0;
  if (now - lastInt < 10) return;
  lastInt = now;
  zaehler = zaehler + 1;
  zaehlerMillis = now;
}
#else
// interrupt function Original:
void StromZaehler() {
  zaehler = zaehler + 1;
  zaehlerMillis = millis();
}
#endif

//calculation function
void ZaehlerStatus() {
  if (zaehler != zaehlerLast) {    // pulse = 0.5kWh.  1p/h = 0.5kW
    if (zaehlerMillisLast != 0) {  //avoid calculation when there haven't been two good pulses.
      zaehlerPower_W = 3600.0 * 0.5 * float(zaehler - zaehlerLast) / float((zaehlerMillis - zaehlerMillisLast) / 1000.0);
      sprintf(a2dstr, "Z:%i/%i T:%i/%i", zaehler, zaehlerLast, zaehlerMillis, zaehlerMillisLast);
      Serial.println(a2dstr);
    }
    zaehlerMillisLast = zaehlerMillis;
    zaehlerLast = zaehler;
  }
}

[Edit]: @bmoerdyk2 , did you consider to test the interrupt routine by using a different source like a separate ESP32 or the like? Would be interesting to see how the device reacts if you connnect that source directly to the GPIO pin ...

ec2021. the source is the "S0" output of a power meter. Driven via an optocoupler with 100 ohm maximum on resistance, which is then pulling down through the 4.7k resistor against the ~45k pullup in the microcontroller.
This configuration has been working just fine for over a year with this hardware. the problems started when I reprogrammed it using the new IDE and new Board configuration (v3.1.1 instead of V1.0.6). I didn't change anything else.

That reminded me that lots of ESP32s do not have Schmitt triggers on inputs as the AVRs do.

Some ESP32s do have optional hysteresis:

https://docs.espressif.com/projects/esp-idf/en/stable/esp32h2/api-reference/peripherals/gpio.html#gpio-hysteresis-filter

1 Like

Thank you to everyone for all the answers.
Following DaveX's suggestion, I implemented the "rate-limit" on the interrupt.
If the interrupt is triggered within 40ms of the "last" trigger, it's ignored.
This turns out to give a very stable result.
Even though I thought I was getting good results before, it's very clear now that even though it got infiinitely worse after reprogramming, that the problem existed before and that the changes in the core driver have impacted how the pins function, making the debouncing / rate limit necessary. So, ec2021 is correct in suspecting problems with the slew rate. The ~45k internal pullups are insufficient for this function.
I'll be upgrading the hardware to reduce the pullup resistance (reduce slew rate) the next time I need to take the system offline for maintenance.

If I could mark two answers as the solution... I would...

1 Like

The 4.7k resistor in a voltage divider with the ~45k GPIO pin pullup resistor prevents the pin being pulled lower than 0.3 volts.

I found this for ESP32 input logic voltage levels:

Input logic level low VIL -0.3 - 0.25VDD V
Input logic level high VIH 0.75VDD - VDD+0.3 V

at Voltage Ranges for Low & High - ESP32 Forum

so you may be in violation of this because the pin appears never to be pulled into a defined LOW state.

Also, as already pointed out, a prolonged period in the window where the logic level is undefined for the ESP32 could cause spurious interrupts.

I did a quick/rough simulation of this using LTspice showing 20mS pulses at two pulses per second.

zooming in, showing the pulse in the undefined range between 0.25v and 0.75v for more than 20mS.

You've already got a solution with a lockout type debouncing routine where you accept the first interrupt as valid and refuse to accept further interrutps until a timer has expired.

Another option, still using interrupts, would be to use a hardware timer called routine to check the status of the pin every say 15 milliseconds to see if there is a pulse there.

V_IL_MAX = 0.25VDD not 0.25V
0.25
VDD = 0.825V.
4.7k / (4.7k+45k) * 3.3V = 0.312V < 0.825V
my input divider is fine.

Sorry, yes of course. I misinterpreted that. Then it appears that the period the curve is in the undefined logic level area during the falling edge appears just under 1mS . Until I read the quoted article, I did not realise that there was evidence that the ESP32 was so sensitive to this.

1 Like