1.4 Interrupts

Eine LED soll bei Betätigung von Boardtaster an PC13 immer langsamer blinken. Der Taster ist entprellt und Low aktiv.

#define LED_B PA5 // D13 Led auf dem Board
#define T_B PC13  // Entpreller UserButton auf dem Board

int blinkZeit = 100; // Startzeit 100 ms
void setup() {
  pinMode(LED_B,OUTPUT);
  pinMode(T_B,INPUT);
}

void loop() {
  digitalWrite(LED_B,!digitalRead(LED_B)); // LED invertieren
  delay(blinkZeit);
  if(!digitalRead(T_B)){  // wenn Taster gedrückt
    blinkZeit*=2;
    while(!digitalRead(T_B));
  }
}

Wenn die Zeit länger wird, bewirkt ein kurzer Tastendruck oft keine Veränderung mehr. Warum?

Lösung: Taster öfter überprüfen

#define LED_B PA5 // D13 Led auf dem Board
#define T_B PC13  // Entpreller UserButton auf dem Board

int blinkZeit = 10;
void setup() {
  pinMode(LED_B,OUTPUT);
  pinMode(T_B,INPUT);
}

void loop() {
  digitalWrite(LED_B,!digitalRead(LED_B)); // LED invertieren
  for (int i=0; i< blinkZeit; i++){  // alle 10ms den Taster abfragen
    delay(10);
    if(!digitalRead(T_B)){
      blinkZeit*=2;
      while(!digitalRead(T_B));
    }
  }
}

Immer wieder vorbeischauen ob der Taster gedrückt wurde nennt man Polling auf den Taster. Das ist wie wenn man immer wieder zur Tür rennt und schaut ob jemand gekommen ist. Wäre es nicht geschickt eine “Klingel” zu haben die einen auf einen Gast aufmerksam macht und dann erst zur Tür zu gehen?

Tastendruck mit Externem Interrupt verarbeiten

Ein Interrupt ist ein meistens durch ein Hardwareereignis ausgelöstes Aufrufen eines Unterprogramms, benannt als Interrupt Service Routine (ISR). Dabei wird das Hauptprogramm unterbrochen und nach Ende der ISR wieder fortgesetzt. Ein externer Interrupt ist ein Ereignis, dass von aussen an den µC herangetragen wird: Taster, Sensorsignal usw. Wir werden noch interne Interrupts kennen lernen, z.B. Timer-Interrupt..
Taster T_B soll bei fallender Flanke ein Aufruf des Unterprogramms ISR_T_B() als ISR auslösen.
Im Setup wird mit attachInterrupt (digitalPinToInterrupt (T_B), ISR_T_B, FALLING); genau dies erreicht.

#define LED_B PA5 // D13 Led auf dem Board
#define T_B PC13  // Entpreller UserButton auf dem Board
int blinkZeit = 100;

void ISR_T_B(){ // Interrupt Service Routine
  blinkZeit*=2;
}
void setup() {
  pinMode(LED_B,OUTPUT);
  pinMode(T_B,INPUT);
  attachInterrupt (digitalPinToInterrupt (T_B), ISR_T_B, FALLING);
}
void loop() {
  digitalWrite(LED_B,!digitalRead(LED_B));
  delay(blinkZeit);
}

Zustandsdiagramm für ISR Ereignis

Zustandsdiagramm für ISR Ereignis
Zustandsdiagramm mit ISR Ereignis als Transition

Wird das ISR Ereignis als Transition dargestellt, wird der Zustand verlassen und wieder betreten, ggfs. vorhandene enter/ und exit/ werden ausgeführt.

Zustandsdiagramm Internes Ereignis
Zustandsdiagramm mit Internem Ereignis

Kompakt ist die mögliche Darstellung als Internes Ereignis.

Denkfehler? ISRs sind keine Events im Sinne der UML

Es wäre so schön gewesen, wenn man ISRs einfach als Events verwenden könnte. Das Tückische dabei ist, dass es zu 99,9?% fehlerfrei funktionieren wird. Betrachte Zustandsdiagramm, welcher Wert wird ausgegeben?
Wenn wir das mit echten Events umsetzen würde bei Tastendruck 2 ausgegeben werden, denn entry/ wird zu Ende ausgeführt.
Hier wird allerdings wenn während delay(500) der Taster gedrückt wird 1 ausgegeben denn: Die ISR unterbricht die entry-Operationsfolge, a wird 2 gesetzt und danach delay(500) bis zum Ende ausführt und a=1 gesetzt und dann in Zustand2 verzweigt. Es wird also trotz Tastendruck 1 ausgegeben.
Echte Events unterbrechen laufende Operationsfolgen nicht mittendrin!
Noch ein Problembeispiel: digitalWrite() ist keine “atomare” Operation, erst wird Portwert gelesen, dann geändert und geschrieben, drei Operationen. Diese Abfolge kann mit ISR unterbrochen werden, wenn die ISR dabei auf den selben Port zugreift gibt es Inkonsistenzen, siehe DigitalWrite during interrupts.
Statt digitalWrite() könnte auch z.B. GPIOA->BSRR = ..; verwendet werden, diese Operation ist atomar, allerdings funktionieren solche hardwarenahen “Hacks” immer nur für die spezifische Plattform, für einen AVM- oder ESP-Controller müssten andere Befehle verwendet werden.

Denkfehler: ISR als Event
Denkfehler: ISR als Event

ISRs unterbrechen sich nicht gegenseitig bei STM32-Arduino!

Das ist doch eine gute Nachricht, ist wie bisher bei den AVR-Arduinos. Wurde von mit in der STM32-Arduino Umgebung überprüft. (Wie sich das allerdings bei MBED verhält weis ich nicht.) Somit tritt das Problem der Inkonsistenten nur auf, wenn Hauptprogramm von ISR unterbrochen wird und bei auf die selben Ressourcen zugreifen. Somit sorgloses Leben, wenn die Main-Loop leer ist (keine Do-Bedingung). Wenn nicht, könnten wir das behandeln?

ISRs zeitweise blockieren?

Es gibt Funktion noInterrupts(). Allerdings nicht ohne Nebenwirkungen, wenn die Interrupts zu lange abgeschaltet werden.

Eine Event-Queue FirstIn-FirstOut in loop() einbauen?

Wie es bei Betriebssystemen üblich ist, allerdings für unsere Bedürfnisse mit Kanonen nach Spatzen geschossen.
Eine Event-Queue (Ereignis Schlange) ist eine Liste, die alle Ereignisse sammelt, die das Hauptprogramm verarbeiten soll. Praktisch würde vor der Switch-Anweisung nachgeschaut welche ISRs aufgelaufen sind und diese dann zuerst nacheinander abgearbeitet. Die ISRs kommen dem Hauptprogramm nicht in die Quere.

ISRs setzen “Flaggen” und diese werden in loop() abgefragt

Sobald Operationen in einer ISR mit Operationen im Hauptprogramm kollidieren können die Verarbeitung ins Hauptprogramm verlagern: Schlicht eine “Flagge” setzen und im Hauptprogramm abfragen.

Flaggen-Lösung für Blinken
Flaggen-Lösung für Blinken
#define LED_B PA5 // D13 Led auf dem Board
#define T_B PC13  // Entpreller UserButton auf dem Board
int blinkZeit = 100;
bool ISR_T_B_Flag=false;

void ISR_T_B(){ // Interrupt Service Routine
  gedruecktT_B_Flag=true;
}
void setup() {
  pinMode(LED_B,OUTPUT);
  pinMode(T_B,INPUT);
  attachInterrupt (digitalPinToInterrupt (T_B), ISR_T_B, FALLING);
}
void loop() {
  if(ISR_T_B_Flag){ // Taste gedrückt?
    ISR_T_B_Flag=false;
    blinkZeit*=2;
  }
  digitalWrite(LED_B,!digitalRead(LED_B));
  delay(blinkZeit);
}

Schön, wir verändern nicht mittendrin den Wert von blinkZeit (hätte hier wohl zu keiner Fehlfunktion geführt, das Beispiel soll das Prinzip verdeutlichen), ein Tastendruck wird sofort registriert und es muss nicht gewartet werden, bis die Taste losgelassen wurde 😁. Die Sache hat allerdings wieder einen Haken, die Delayzeit wird bei jedem Tastendruck immer länger, somit werden zwei Tastendrücke kurz hintereinander wieder nicht wahrgenommen es gibt ja nur ein Flag 😕.
Neues Problem, neue Lösungen:

  • In der ISR die Tastendrücke zählen statt nur ein Flag zu setzen und in der Loop entsprechend verarbeiten.
  • Wieder for-Schleife verwenden um das Delay nicht so lange unterbrechen zu lassen.
  • Doch die Kanone Event-Queue hervorholen?
  • Dieses elendige delay() beerdigen und endlich mit SystemTicks oder Millisekunden-Systemzeit arbeiten…

Mein Fazit: ISRs als Events denken geht aber nur mit Vorsicht!

Verrenkungen über Flags ISR-Arbeit pauschal ins Hauptprogramm zu verlegen um hat bei meinen Versuchen den Code nur verkompliziert.

  • Keine gemeinsamen Ressourcen (Variablen, Ports) von Hauptprogramm und ISR verändern lassen.
  • ISRs sollten nur kurz laufen, keine Delays usw. darin verwenden.

Ext. Interrupt und prellende Tasten?

Mit diesem Programm die Wirkung der prellenden Taste PA1 testen.

int aus = 1; // Ausgabe
void ISR_PA1(){ // Interrupt Service Routine
  aus=(aus<<1) +1; // Leuchtband
  GPIOC->ODR = aus;
}
void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PA1,INPUT_PULLDOWN);
  attachInterrupt (digitalPinToInterrupt (PA1), ISR_PA1, RISING);
  GPIOC->ODR = aus;
}
void loop() {
}

Der externe Interrupt nimmt uns zwar die Flankenerkennung ab, aber leider hilft er nicht gegen das Prellen. Ein Delay in der ISR könnte das Prellen beim Tastendruck abfangen, jedoch wie wir wissen prellt die Taste auch beim Loslassen, dies würde wieder einen Interrupt auslösen. In der ISR solange zu verharren bis die Taste wieder losgelassen wurde ist ausserdem nicht gut, denn solange wäre das Hauptprogramm wieder angehalten. Prellende Tasten mit Ext. Interrupt sind kein Spass (Flaggen setzen und im Hauptprogramm behandeln). Daher besser entweder entprellte Tasten verwenden oder im Hauptprogramm Polling mit buttonCheck() machen.

Aufgabe: Sekundenzähler mit Ext. Interrupt starten und stoppen

7 Segment Anzeige

Der Ext. Interrupt wird durch die entprellte Taste an PC13 ausgelöst. Nach dem Reset wird im Zustand Start auf Tastendruck gewartet. Beim ersten Druck auf die Taste soll die zunächst einstellige 7-Segmentanzeige laufend im Sekundentakt hoch zählen (Laufen), beim zweiten Druck stoppen (Stopp) und beim dritten Druck wieder auf 0 springen (Start): Start->Laufen->Stopp->Start->Laufen->…
Beim Druck auf die Taste wird ISR_taster() aufgerufen. Die Ausgabe der Zeit erfolgt mit dem UP ausgeben().
Erstellen Sie ein Zustandsdiagramm, verwenden Sie dabei ISR_taster() (spicken in der Forsa ist erlaubt).

Zustandsdiagramm
Zustandsdiagramm


Das Programm aus dem Zustandsdiagramm:

enum{START,LAUFEN,STOPP} zustand=START;
int bcd_7seg[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; // Umrechnung
int zeit=0;

void ausgeben(){
  GPIOC->ODR=bcd_7seg[zeit];
  digitalWrite(PC11,HIGH);  // Einer einschalten
}
void ISR_taster(){ // Interrupt Service Routine
  switch(zustand){
    case START:
      zustand = LAUFEN;
      break;
    case LAUFEN:
      zustand = STOPP;
      break;
    case STOPP:
      zeit=0;
      ausgeben();
      zustand = START;
  }
}
void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PC11,OUTPUT); // Einer
  pinMode(PC12,OUTPUT); // Zehner
  pinMode(PC13,INPUT);  // Notwendig??
  attachInterrupt (digitalPinToInterrupt (PC13), ISR_taster, FALLING);
  ausgeben();
}
void loop() {
  if (zustand==LAUFEN){
    zeit++;
    ausgeben();
    delay(1000);
  }
}

Was geschieht genau, wenn die ISR mitten in der loop zuschlägt? Diskutieren Sie die Auswirkungen bei den möglichen Einschlagsstellen.

Erweitern Sie das Programm auf eine zweistellige Anzeige mit Sekunden und Zehntelsekunden. Der Dezimalpunkt an PC7 von der Zehner-Anzeige sollte gesetzt sein. Die Zeit soll bei 9.9 Sekunden stehen bleiben. Vorgabe:

#define AZEIT 5 // 5ms Stelle anzeigen
int msZaehler=0;
void ausgeben(){
  GPIOC->ODR = bcd_7seg[zeit%10] | (1<<11); // Einer einschalten
  delay(AZEIT);
  GPIOC->ODR = bcd_7seg[zeit/10] | (1<<7) |(1<<12); // Dezimalpunkt und Zehner einschalten
  delay(AZEIT);
  msZaehler+=10;
}
enum{START,LAUFEN,STOPP} zustand=START;
int bcd_7seg[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; // Umrechnung
int zeit=0;

#define AZEIT 5 // 5ms Stelle anzeigen
int msZaehler=0;
void ausgeben(){
  GPIOC->ODR = bcd_7seg[zeit%10] | (1<<11); // Einer einschalten
  delay(AZEIT);
  GPIOC->ODR = bcd_7seg[zeit/10] | (1<<7) |(1<<12); // Dezimalpunkt und Zehner einschalten
  delay(AZEIT);
  msZaehler+=10;
}
void ISR_taster(){ // Interrupt Service Routine
  switch(zustand){
    case START:
      msZaehler=0;
      zustand = LAUFEN;
      break;
    case LAUFEN:
      zustand = STOPP;
      break;
    case STOPP:
      zeit=0;
      zustand = START;
  }
}
void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PC11,OUTPUT); // Einer
  pinMode(PC12,OUTPUT); // Zehner
  pinMode(PC13,INPUT);  // Notwendig??
  attachInterrupt (digitalPinToInterrupt (PC13), ISR_taster, FALLING);
}
void loop() {
  ausgeben();
  if (zustand==LAUFEN && msZaehler>=100 && zeit<99){
    zeit++;
    msZaehler=0;
  }
}

Erstellen Sie ein Zustandsdiagramm für Ihre Lösung.

Zustandsdiagramm
Zustandsdiagramm

Timer Interrupt verwenden statt delay()

Auch bei der “Besseren Lösung” ist die Zeitmessung nicht perfekt, weil zu den Zeiten des delay(5) noch die (unbekannten) Verarbeitungszeiten des restlichen Programmcodes dazu gerechnet werden muss. Ein Aufruf der ausgabe()-Funktion nach genau 10ms ist mit einem Hardware-Timer per ISR machbar siehe folgenden Sketch. Nun hoffte ich, dass es einen signifikanten Unterschied macht den Timer statt die delay() Funktion zu verwenden und ließ die SuS Messungen mit Digital-Oszilloskopen für beide Varianten durchführen. Leider konnte nur mit einem Frequenzzähler nach einigen Nachkommastellen ein Unterschied entdeckt werden: Der µC ist einfach zu schnell…

enum{START,LAUFEN,STOPP} zustand=START;
int bcd_7seg[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; // Umrechnung
int zeit=0;

static HardwareTimer mytimer = HardwareTimer(TIM2);  // Timerinstanz sowie Timeruaswahl

volatile int msZaehler=0; // volatile notwendig?
void ISR_ausgeben(){
  static bool einer = true;
  if (einer){
    GPIOC->ODR = bcd_7seg[zeit%10] | (1<<11); // Einer einschalten
  }
  else{
    GPIOC->ODR = bcd_7seg[zeit/10] | (1<<7) |(1<<12); // Dezimalpunkt und Zehner einschalten
  }
  einer = !einer;
  msZaehler+=10;
}
void ISR_taster(){ // Interrupt Service Routine
  switch(zustand){
    case START:
      msZaehler=0;
      zustand = LAUFEN;
      break;
    case LAUFEN:
      zustand = STOPP;
      break;
    case STOPP:
      zeit=0;
      zustand = START;
  }
}
void setup() {
  pinMode(PC0, OUTPUT);  // ohne diese Zeile klappts nicht
  GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
  pinMode(PC11,OUTPUT); // Einer
  pinMode(PC12,OUTPUT); // Zehner
  pinMode(PC13,INPUT);  // Notwendig??
  attachInterrupt (digitalPinToInterrupt (PC13), ISR_taster, FALLING);
  mytimer.setOverflow(10000, MICROSEC_FORMAT); // 100 Hz = 10 ms = 10000 µs
  mytimer.attachInterrupt(ISR_ausgeben); //Timer IR aktivieren und Sprung zur ISR
  mytimer.resume();   //Timer starten
}
void loop() {
  if (zustand==LAUFEN && msZaehler>=100 && zeit<99){
    zeit++;
    msZaehler=0;
  }
}