ESP32 und Interrupts - ein Erfahrungsbericht

Hallo,
derzeit bin ich ja dabei, die MobaTools für den ESP32 zu ertüchtigen. Da die MobaTools sehr viel mit Interrupts arbeiten, musste ich mich also auch mit den Interrupts auf ESP32 näher beschäftigen. Einige dieser Erfahrungen will ich hier mal teilen - vielleicht kennt die ja doch der eine oder andere noch nicht alle. Und alles was ich erfahren musste, steht auch nicht in der Doku :wink:

Dass man Interruptfunktionen ( und allen in ihnen aufgerufenen Unterfunktionen ) das Attribut IRAM_ATTR mitgeben muss, dürfte sich inzwischen rumgesprochen haben ( das es u.U. auch ohne geht vielleicht noch nicht so, aber dazu unten mehr ).
Vielleicht nicht ganz so bekannt ist die Tatsache, dass konstante Tabellen/Arrays die in Interrupts genutzt werden, das DRAM_ATTR benötigen. Und zwar auch dann, wenn sie innerhalb einer ISR als lokales Array definiert werden. Der Compiler wertet hier das IRAM_ATTR der Funktion nicht aus. Das ‘const’ weglassen hilft nicht, da der Compiler selbsständig erkennt, dass die Tabelle nicht verändert wird, und sie deshalb als Konstante im Programmspeicher ablegt. Aber das steht ja noch alles in der Doku ;).

Was da nicht steht, dass eine switch-Anweisung in einer ISR nicht mehr als 4 case-Blöcke haben darf. Bei mehr raucht’s auch . Ich vermute, dass der Compiler das bis zu 4 cases in Art einer if-Abfrage realisiert, während er bei mehr case-Blöcken eine Sprungtabelle einrichtet, und die dann wieder an der falschen Stelle (im Programm-Cache) ablegt. Hier habe ich aber keinen Weg gefunden, ihm das DRAM_ATTR unterzujubeln. In der Esp-IDF kan man das wohl dem Linker mit speziellen Linkerfiles mitteilen, aber wie und ob das in der Arduino-IDE geht … ???

Das tückische an der Sache ist, dass der Absturz nicht sofort passiert, wenn der Interrupt auftritt. Das kann u.U. lange dauern oder gar nicht passieren. Ursache ist ja, dass der ESP32 einen externen Flash-Speicher hat. Die Programme werden aber aus einem Cache ausgeführt, der vom Flash versorgt wird. Solange alles im Cache abläuft, und kein Flash-Zugriff notwendig ist, passiert nichts. Wann da ein Cache-Miss auftritt ist nicht vorhersehbar.
Eine Möglichkeit diese Situation zu provozieren (und damit zu testen ob die ISR ‘sauber’ ist) ist aber die EEPROM-Lib. Da der ESP32 kein echtes Eeprom hat, werden die Werte im Flash gespeichert. Zwar nicht sofort, denn die Schreibaufrufe der Lib legen die Werte erst in einem Pufferbereich ab. Erst mit EEPROM.commit() (Was es auf den AVR’s gar nicht gibt) werden die Wert im Puffer in den Flash geschrieben. Bei diesem Flashzugriff ist der Programmcache deaktiviert. Wenn also nun ein Interrupt auftritt der nicht ‘sauber’ ist, kommt es zur Exception:

Guru Meditation Error: Core 1 panic’ed (Cache disabled but cached memory region accessed)

Wen’s interessiert, der kann das an dem kleinen angehängten Sketch nachvollziehen. Der macht nicht viel sinnvolles sondern dient nur zur Demonstation. An einem Pin wird ein 5kHz Signal erzeugt, und an dessen Flanke wird ein Interrupt erzeugt. In der ISR gibt es Zugriff auf eine Tabelle und eine switch-Anweisung - also beste Voraussetzungen für einen Absturz ;).

// TestSketch für das Interrruptverhalten des ESP32

#include <EEPROM.h>
// Einige Ausgangssignale, wenn man den Ablauf mit einem LA nachvollziehen will
const byte tonePin = 13;        // Hier wird das 5kHz Signal erzeugt
const byte irqPin = 14;         // = HIGH während die ISR läuft
const byte commitPin = 15;      // = HIGH während der commit läuft
volatile byte switchVar;
volatile byte switchResult;

//void testISR() {
void IRAM_ATTR testISR() {
    //static volatile byte bitTab[] = { 5,4,3,2,1,0 };  //<<- ohne volatile schmiert die ISR ab
    //static const byte bitTab[] = { 5,4,3,2,1,0 };   //<<- Auch wenn man dieses Zeile aktiviert
    static const DRAM_ATTR byte bitTab[] = { 5,4,3,2,1,0 };   //<<- So geht's
    digitalWrite( irqPin, HIGH );
     switch ( switchVar ) {
      case 0:
        switchResult = bitTab[switchVar];
        break;
      case 1:
        switchResult = bitTab[switchVar];
        break;
      case 2:
        switchResult = bitTab[switchVar];
        break;
      case 3:
        switchResult = bitTab[switchVar];
        break;
      /*case 4: // wenn case 4 aktiv ist, gibt es auch eine exeption 
        switchResult = bitTab[switchVar];
        break; */
      default:
        switchResult = -1;
        ;
    }
    delayMicroseconds(15);
    digitalWrite( irqPin, LOW );
}
void setup() {
    Serial.begin(115200);
    EEPROM.begin(512);
    pinMode( irqPin, OUTPUT );
    pinMode( commitPin, OUTPUT );
    ledcWriteTone( 0, 5000 );
    ledcAttachPin( tonePin, 0 );
    attachInterrupt( tonePin, testISR, RISING );
 }

void loop() {
    static int loopcnt = 0;
    loopcnt++;
    if ( ++switchVar > 5 ) switchVar = 0;
    Serial.printf( "Switch=%d, Result=%d, loopCnt=%d\n\r", switchVar, switchResult, loopcnt );
    EEPROM.write( switchVar, loopcnt );
    digitalWrite( commitPin, HIGH);
    EEPROM.commit();
    digitalWrite( commitPin, LOW );
    delay(1000);
}

Grndsätzlich erlaubt es der ESP auch, Interrupts ohne IRAM_ATTR zu realiseren. Bei der Registrierung eines Interrupts können Flags mitgegeben werden, die das Verhalten des IRQ bestimmen. Eines dieser Flags ist das IRAM-ATTR Flag, mit dem man dem System sagt, dass die ISR im IRAM liegt. Ist das Flag nicht gesetzt, so wird die ISR während eines Flash-Zugriffs nicht aufgerufen. Bei ISR, die auch mal eine Verzögerung vertragen können, könnte man das machen. Der Arduino-Core setzt diese Flag aber grundsätzlich bei den attachInterrupt Aufrufen. Da müsste man dann schon tieferliegende API-Aufrufe nutzen, die es erlauben dieses Flag eben nicht zu setzen.

Im Anhang gibt’s noch ein .c File. Wenn man das in den Sketchordner von obigem Sketch reinkopiert, wird der attachInterrupt nicht mehr als ‘im IRAM’ gekennzeichnet. Dann funktioniert der Sketch immer, auch ganz ohne IRAM_ATTR. Der Interrupt kann dann aber durchaus mal um einen mittleren 2-stelligen ms Zeitraum unterdrückt sein.

ESPgpio.c (9.98 KB)

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.