load prohibited on esp32 during ISR execution

Viewed 389

I have set up an interrupt that changes a boolean to true and in void loop(), I am constantly checking if the boolean is true like so:

TTGOClass *ttgo;
bool irq = false;

void setup()
{
    Serial.begin(115200);
    ttgo = TTGOClass::getWatch();
    ttgo->begin();
    ttgo->openBL();

    ttgo->tft->fillScreen(TFT_BLACK);
    ttgo->tft->drawString("T-Watch AXP202",  25, 50, 4);
    ttgo->tft->setTextFont(4);
    ttgo->tft->setTextColor(TFT_WHITE, TFT_BLACK);

    pinMode(AXP202_INT, INPUT_PULLUP);
    attachInterrupt(AXP202_INT, [] {
        irq = true;
    }, FALLING);

    //!Clear IRQ unprocessed  first
    ttgo->power->enableIRQ(AXP202_PEK_SHORTPRESS_IRQ | AXP202_VBUS_REMOVED_IRQ | AXP202_VBUS_CONNECT_IRQ | AXP202_CHARGING_IRQ, true);
    ttgo->power->clearIRQ();
}

void loop()
{
    if (irq) {
        irq = false;
        ttgo->power->readIRQ();
        if (ttgo->power->isVbusPlugInIRQ()) {
            ttgo->tft->fillRect(20, 100, 200, 85, TFT_BLACK);
            ttgo->tft->drawString("Power Plug In", 25, 100);
        }
        if (ttgo->power->isVbusRemoveIRQ()) {
            ttgo->tft->fillRect(20, 100, 200, 85, TFT_BLACK);
            ttgo->tft->drawString("Power Remove", 25, 100);
        }
        if (ttgo->power->isPEKShortPressIRQ()) {
            ttgo->tft->fillRect(20, 100, 200, 85, TFT_BLACK);
            ttgo->tft->drawString("PowerKey Press", 25, 100);
        }
        ttgo->power->clearIRQ();
    }
    delay(1000);
}

The t-watch 2020 (an esp32 smartwatch) runs on a battery and this weird method found in its example sketches (the code above is an example not the actual code because it is quite chaotic) wastes quite a lot of precious battery power. So I tried throwing the code from the if inside void loop() right into attachInterrupt(AXP202_INT, [] but... I needed to execute lengthy commands (I think that's the cause of the problem)such as Serial.print() and delay() (they are absolutely necesarry), the esp32 crashes returning the following error:

16:18:32.486 -> Guru Meditation Error: Core  0 panic'ed (LoadProhibited). Exception was unhandled.
16:18:32.486 -> Core 0 register dump:
16:18:32.486 -> PC      : 0x4009947b  PS      : 0x00060233  A0      : 0x8009894f  A1      : 0x3ffbd150  
16:18:32.486 -> A2      : 0x3ffba5a8  A3      : 0x3ffbd2dc  A4      : 0x00000001  A5      : 0x00000001  
16:18:32.486 -> A6      : 0x00060223  A7      : 0x00000000  A8      : 0x00000000  A9      : 0x3ffba5a8  
16:18:32.486 -> A10     : 0x3ffba5a8  A11     : 0x00060023  A12     : 0x00060021  A13     : 0x3ffc0910  
16:18:32.486 -> A14     : 0x00000003  A15     : 0x00060023  SAR     : 0x00000000  EXCCAUSE: 0x0000001c  
16:18:32.519 -> EXCVADDR: 0x00000004  LBEG    : 0x40093948  LEND    : 0x40093964  LCOUNT  : 0x00000000  
16:18:32.519 -> 
16:18:32.519 -> ELF file SHA256: 0000000000000000
16:18:32.519 -> 
16:18:32.519 -> Backtrace: 0x4009947b:0x3ffbd150 0x4009894c:0x3ffbd170 0x4009726f:0x3ffbd190 0x400972fd:0x3ffbd1b0 0x40098fbe:0x3ffbd1d0 0x4009909f:0x3ffbd210 0x40096b06:0x3ffbd240
16:18:32.519 -> 
16:18:32.519 -> Rebooting...

the code is like this:

TTGOClass *ttgo;

bool axpIrq = false;  //axpIrq for button press and power plug in/remove events.
bool lenergy = false;
bool BLaudio = false;
bool keepAwake = false;

//some initializations
ttgo = TTGOClass::getWatch();
ttgo->begin();
ttgo->lvgl_begin();

  pinMode(AXP202_INT, INPUT_PULLUP);
  attachInterrupt(AXP202_INT, [] {
    axpIrq = true;
        ttgo->power->readIRQ();
    if (ttgo->power->isPEKShortPressIRQ()) {
      //ttgo->power->clearIRQ();

      Serial.println("button pressed");
      low_energy();
    }
    ttgo->power->clearIRQ();
  }, FALLING);

low energy is defined in another file:

void low_energy() {
  //portENTER_CRITICAL(&synch);

  if (!keepAwake) {
    if (ttgo->bl->isOn()) {
      //Serial.println("backlight on, turning off");

      ttgo->closeBL();
      ttgo->stopLvglTick();
      ttgo->bma->enableStepCountInterrupt(false);
      ttgo->displaySleep();

      //lenergy = true;
      gpio_wakeup_enable ((gpio_num_t)AXP202_INT, GPIO_INTR_LOW_LEVEL);
      gpio_wakeup_enable ((gpio_num_t)BMA423_INT1, GPIO_INTR_HIGH_LEVEL);
      esp_sleep_enable_gpio_wakeup ();

      if (!BLaudio) {
        setCpuFrequencyMhz(20);
        esp_light_sleep_start();
        //Serial.println("BLaudio is off, light sleep starts");
      } else {
        //Serial.println("BLaudio is on, light sleep won't start");
      }
      //Serial.println("screen off");

    } else {
      //Serial.println("Waking up");
      setCpuFrequencyMhz(160);
      ttgo->startLvglTick();
      ttgo->displayWakeup();
      ttgo->rtc->syncToSystem();
      lv_disp_trig_activity(NULL);
      ttgo->openBL();
      ttgo->bma->enableStepCountInterrupt();
      displayTime(true);
    }
  }
  //portEXIT_CRITICAL(&synch);
}

I've tried replacing bool axpIrq = false; with volatile bool axpIrq = false;, uncommenting portENTER_CRITICAL(&synch); and portEXIT_CRITICAL(&synch); and still no results. If I am right when I think that lengthy commands are the problem, how can I execute the commands while the CPU continues in void loop normally(make a callback executed on the second core? I don't know)? If I am not right, what's the actual problem and how can I solve it?

1 Answers

Two problems here.

First, interrupt handlers need to be defined using the IRAM_ATTR attribute in order to ensure that they're already loaded into instruction memory (IRAM). The ESP32 understandably doesn't like having to load code from flash to RAM in order to service an interrupt. You need to make sure it's already there. If you don't, you see exactly the error you're seeing. Instruction RAM is also an extremely limited resource; you don't want to occupy more of it than is absolutely necessary.

You're specifying a lambda expression as the interrupt handler. I'm not sure whether you can use IRAM_ATTR with a lambda. To be sure, break out the interrupt handler into a function that's properly defined using IRAM_ATTR:

void IRAM_ATTR handle_interrupt() {
    axpIrq = true;
        ttgo->power->readIRQ();
    if (ttgo->power->isPEKShortPressIRQ()) {
      //ttgo->power->clearIRQ();

      Serial.println("button pressed");
      low_energy();
    }
    ttgo->power->clearIRQ();
}

...

  attachInterrupt(AXP202_INT, handle_interrupt, FALLING);
...

Second, you're doing WAY too much in your interrupt handler. I quoted your existing code above, but there's no way that's going to work. Unless you really know what you're doing, your handler shouldn't do much more than set a volatile boolean flag variable and return. In this case, you're calling this TTGO library, Serial, possibly Bluetooth it looks like?

You'd need to ensure that every single function you call, and every single function those functions call, etc, are defined using IRAM_ATTR. You're not going to want to do that because you'd need to modify lots of third party code, and it would take up a lot - possibly more than there is - of instruction RAM.

You also don't know that any of these functions you're calling from the interrupt handler are re-entrant. You don't know if they lock out interrupts or not. It's simply not safe to call them unless you do know this - their internal data structures may be in an inconsistent state when they're interrupted and re-entered. The best thing to assume with any high level libraries is that they're not intended to be called from interrupt handlers.

Finally, interrupts are intended to be handled quickly so that the normal flow of code can be resumed.

Your first chunk of code isn't a "weird method" - it's the normal way that interrupts are handled in Arduino code on the ESP32, and it's how you write interrupt driven code that does much more work than is safe to do from inside an interrupt service routine. This is the "right" way to do it in an ESP32 Arduino program, which is why the examples showed this kind of code.

Your real problem seems to be how to do this and not kill your battery. This is a much bigger set of issues; you'll need to learn about ESP32 sleep modes and ESP-IDF/FreeRTOS tasks, how to create one and wake it up from an interrupt handler. This all depends on the rest of the watch code as well and how it's designed to save power. If you try to go further with this the best thing to do is to come back and post questions about specific problems you encounter while trying to make that work.

Related