ESP32 Interrupt in Arduino IDE problem

I am trying to get a switch press to trigger an interrupt using an ESP32 processor. The NO switch is pulled HIGH when idle, and LOW when pressed. I’m running Win 10 and IDE 1.8.10. I can duplicate the error with this code:

#include <TFT_eSPI.h>           // https://github.com/Bodmer/TFT_eSPI  Hardware-specific library
#include <rom/gpio.h>

//#include <Rotary.h>             // https://github.com/buxtronix/arduino/blob/master/libraries/Rotary/examples/interrupt/interrupt.ino

#define DISPLAYWIDTH            320
#define DISPLAYHEIGHT           240

#define MSGSWITCH1              22    // The message NO pushbutton switches
#define MSGSWITCH2              17
#define MSGSWITCH3              16

#define TFT_MISO                19    // From ESP32 TFT User_Setup.h
#define TFT_MOSI                23
#define TFT_SCLK                18
#define TFT_CS                  15    // Chip select control pin
#define TFT_DC                   2    // Data Command control pin
#define TFT_RST                  4    // Reset pin (could connect to RST pin)

char contestExchanges[5][31] = {    // Exchange Message for Contest or common info:
  "                              ",   // DO NOT CHANGE. 30 spaces to create 1-based array and used for erasing messages
  "ABCDEFGHIJKLMNOPQRST",                // General CQ. Change as needed
};

TFT_eSPI tft = TFT_eSPI();


void ReadMessageSwitch1()
{
  SendMessage(1);
}

void MyDelay(unsigned long millisWait)
{
  unsigned long now = millis();

  while (millis() - now < millisWait)
    ;     // Twiddle thumbs...
}

void SendMessage(int messageSwitch)
{
  char *msg = &contestExchanges[1][0];
  int letterCount = 0;
  int rowCount = 0;

  tft.fillRect(0, 0, DISPLAYWIDTH, DISPLAYWIDTH,0);      // Erase screen
  tft.setCursor(0, 20);
  Serial.print("In SendLetters(), msg = ");
  Serial.println(msg);
//  MyDelay(500L);
  while (*msg != '\0') {
    tft.print(*msg++);
  }
}


void setup() 
{
  Serial.begin(115200);
  pinMode(MSGSWITCH1,           INPUT_PULLUP);    // Message switches

  attachInterrupt(digitalPinToInterrupt(MSGSWITCH1), ReadMessageSwitch1, CHANGE);   // Message to send

  tft.setTextColor(TFT_WHITE, TFT_BLACK);
}

void loop() {

}

If I run the code as above, it appears that the interrupt fires properly and uses SendMessage() to display the message. It repeats the message 4 times. I figured this might be a bounce problem, so I tried adding MyDelay() to the code (commented out right now). Uncommenting MyDelay() causes me to get a “panicked guru” error message and a register dump. I was hoping to avoid a hardware debounce. I know a Serial.print() object in interrupt routines is a bad idea, but removing them doesn’t change the results. Any ideas?

You need to properly yield() in your delay().
Just use the built-in delay() if you want blocking code. It implemented the proper yielding.

arduino_new:
You need to properly yield() in your delay().
Just use the built-in delay() if you want blocking code. It implemented the proper yielding.

I tried using delay(100L) and it still send multiple messages. I increased the delay() call to 1 second and still get multiple calls.

First, you shouldn't be printing and delaying in an interrupt. Second, you can do all of this without an interrupt.

In loop call a debounce function that returns the stable state of the switch. Based on that state then perform your action. Also, the way you have the interrupt set up it will trigger on a button press AND release.

ToddL1962:
First, you shouldn't be printing and delaying in an interrupt. Second, you can do all of this without an interrupt.

In loop call a debounce function that returns the stable state of the switch. Based on that state then perform your action. Also, the way you have the interrupt set up it will trigger on a button press AND release.

As I said in my original post, I'm aware that using a print statement in an ISR is not a good idea. However, removing those statements makes no difference to what I"m seeing, so I left them in to make it easier to see the multiple calls.

I do not want to use polling if I can avoid it. I already have it working with polling. However, the "real" project's code is fairly large spread over 9 source code files. It uses a touch screen interface and, even when the screen is not being touched, there is a slight polling delay that I was hoping to avoid. I can live with it, but I was hoping not to use polling.

Not optimal but here’s a quick fix (pseudo code):

void SendMessage(int messageSwitch)
{
   static unsigned long lastTime = 0;
   unsigned long currentTime = millis();
   if (currentTime  - lastTime < SMALL_INTERVAL) {
      lastTime = currentTime;
      return;
   }
   //print message here
}

@arduino_new.

No joy using that code, either. No guru message, but still multiple print messages.

Starting to look like polling is the answer.

econjack:
@arduino_new.

No joy using that code, either. No guru message, but still multiple print messages.

Starting to look like polling is the answer.

Hmm, weird. How many messages are you seeing?

I tried this code:

void SendMessage(int messageSwitch)
{
  char *msg = &contestExchanges[1][0];
  int letterCount = 0;
  int rowCount = 0;

  static unsigned long lastTime = 0L;
   unsigned long currentTime = millis();
   if (currentTime  - lastTime < 1000L) {
      lastTime = currentTime;
      return;
   }
  //delay(1000L);
  tft.fillRect(0, 0, DISPLAYWIDTH, DISPLAYWIDTH,0);      // Erase screen
  tft.setCursor(0, 20);
  Serial.print("In SendLetters(), msg = ");
  Serial.println(msg);

  while (*msg != '\0') {
    tft.print(*msg++);
  }
}

and I still see 4 dups of the message, and that’s with a 1 second delay! I’m doing something very weird that I’m not aware of.

From documentation: https://lastminuteengineers.com/handling-esp32-gpio-interrupts-tutorial/
“Interrupt service routines should have the IRAM_ATTR attribute,”

void IRAM_ATTR ISR() {
    Statements;
}

ESP32, if one is to look at the code for delay(), as I was shown, one will see that delay() on an ESP32 invokes vTaskDelay(), to run vTaskDelay(), a non-blocking delay, freeRTOS will be loaded. Once freeRTOS is loaded, memory allocations need to be thread safe. Which means things like IRAM_ATTR need to be used in interrupt services on an ESP32, for one.

https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/mem_alloc.html

As a note, there is no need to yeild with delay()/vTaskDelay on an ESP32, once delay()/vTaskDelay() is called, freeRTOS will be loaded, which will take care of yielding.

Also, keeping things out of the loop() is a good idea on an ESP32 with freeRTOS loaded as freeRTOS places loop() at the lowest priority for running.

It might help to place

#include "esp_system.h" 
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/timers.h"

at the very top of your code.


This#define TFT_DC                  2    // Data Command control pin
Consider moving the TFT_DC pin to another pin as pin 2 is, also, the built in led pin.


You might consider using esp_timer_get_time() instead of using millis().

void fDo_AudioReadFreq( void *pvParameters )
{
  int64_t EndTime = esp_timer_get_time();
  int64_t StartTime = esp_timer_get_time(); //gets time in uSeconds like Arduino Micros
  for (;;)
  {
    xEventGroupWaitBits (eg, evtDo_AudioReadFreq, pdTRUE, pdTRUE, portMAX_DELAY);
    EndTime = esp_timer_get_time() - StartTime;
    // log_i( "TimeSpentOnTasks: %d", EndTime );
    Audio.ReadFreq(FreqVal);
    for (int i = 0; i < 7; i++)
    {
      FreqVal[i] = constrain( FreqVal[i], NOISE, A_D_ConversionBits );
      FreqVal[i] = map( FreqVal[i], NOISE, A_D_ConversionBits, 0, 255 );
      // log_i( "Freq %d Value: %d", i, FreqVal[i]);//used for debugging and Freq choosing
    }
    xQueueSend( xQ_LED_Info, ( void * ) &FreqVal, xTicksToWait0 );
    StartTime = esp_timer_get_time();
  }
  vTaskDelete( NULL );
} // fDo_ AudioReadFreq( void *pvParameters )

esp_timer_get_time() is a 64bit millis() counter that rolls over in about 147 or 247 years.


The ESP32 can handle a lot of code in an ISR, if its done correctly:

// timer ISR callback
void IRAM_ATTR onTimer()
{
  BaseType_t xHigherPriorityTaskWoken;
  iTicCount++;
  xEventGroupSetBitsFromISR(eg, OneMilliSecondGroupBits, &xHigherPriorityTaskWoken);
  if ( (iTicCount % 2) == 0 )
  {
    if ( (xSemaphoreTakeFromISR(sema_ReceiveSerial_LIDAR, &xHigherPriorityTaskWoken)) == pdTRUE ) // grab semaphore, no wait
    {
      xEventGroupSetBitsFromISR(eg, evtReceiveSerial_LIDAR, &xHigherPriorityTaskWoken); // trigger every 2mS, if not already processing
    }
  }
  if ( (iTicCount % 3) == 0 )
  {
    if ( (xSemaphoreTakeFromISR(sema_HexaPodAdjustment, &xHigherPriorityTaskWoken)) == pdTRUE ) // grab semaphore, no wait
    {
      xEventGroupSetBitsFromISR(eg1, evtWalk_Forward, &xHigherPriorityTaskWoken);
    }
  }
  if ( iTicCount == OneK )
  {
    xEventGroupSetBitsFromISR(eg, OneSecondGroupBits, &xHigherPriorityTaskWoken); // trigger every 1X a second
    // reset counter to start again
    iTicCount = 0;
  }
  if ( !bLIDAR_OK )
  {
    xSemaphoreTakeFromISR ( sema_iLIDAR_Power_Reset, &xHigherPriorityTaskWoken );
    ++iLIDAR_Power_Reset;
    xSemaphoreGiveFromISR ( sema_iLIDAR_Power_Reset,  &xHigherPriorityTaskWoken);
    if ( iLIDAR_Power_Reset >= 4000 )
    {
      xEventGroupSetBitsFromISR(eg, evtfLIDAR_Power_On, &xHigherPriorityTaskWoken);
    }
  }
} // void IRAM_ATTR onTimer()

Whiles I may get, multi triggers from time to time, those multi triggers are filtered out with the xSemaphoreTakeFromISR()'s


freeRTOS API: FreeRTOS API categories

ESP32 API: https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/index.html


You may also consider configuring the GPIO pin matrix for SPI:

I thought the Internal RAM attribute (IRAM_ATTR) was used to direct the compiler to place the ISR in the internal chip RAM, but that was for maximum performance. In fact, I did try that early on, but it changed nothing. Are you both saying it's required on any ISR?