1.4 Interrupts
Die BOARD_LED an PA5 (D13) soll bei Betätigung vom Board-Taster USER_BUTTON an PC13 immer langsamer blinken. Der Taster ist entprellt und Low aktiv.
#define BOARD_LED PA5 // D13 Led auf dem Board
#define USER_BUTTON PC13 // Entpreller blauer UserButton auf dem Board
int blinkZeit = 100; // Startzeit 100 ms
void setup() {
pinMode(BOARD_LED,OUTPUT);
pinMode(USER_BUTTON,INPUT);
}
void loop() {
digitalWrite(BOARD_LED,!digitalRead(BOARD_LED)); // LED invertieren
delay(blinkZeit);
if(!digitalRead(USER_BUTTON)){ // wenn Taster gedrückt
blinkZeit*=2;
while(!digitalRead(USER_BUTTON));
}
}
❓ Wenn die Zeit länger wird, bewirkt ein kurzer Tastendruck oft keine Veränderung mehr. Warum?
Lösung: Taster öfter überprüfen
#define BOARD_LED PA5 // D13 Led auf dem Board
#define USER_BUTTON PC13 // Entpreller blauer UserButton auf dem Board
int blinkZeit = 10;
void setup() {
pinMode(BOARD_LED,OUTPUT);
pinMode(USER_BUTTON,INPUT);
}
void loop() {
digitalWrite(BOARD_LED,!digitalRead(BOARD_LED)); // LED invertieren
for (int i=0; i< blinkZeit; i++){ // alle 10ms den Taster abfragen
delay(10);
if(!digitalRead(USER_BUTTON)){
blinkZeit*=2;
while(!digitalRead(USER_BUTTON));
}
}
}
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 USER_BUTTON soll bei fallender Flanke ein Aufruf des Unterprogramms isr_tasterUB() als ISR auslösen.
Im Setup wird mit attachInterrupt (digitalPinToInterrupt (USER_BUTTON), isr_tasterUB, FALLING); genau dies erreicht.
#define BOARD_LED PA5 // D13 Led auf dem Board
#define USER_BUTTON PC13 // Entpreller blauer UserButton auf dem Board
int blinkZeit = 100;
void isr_tasterUB(){ // Interrupt Service Routine
blinkZeit*=2;
}
void setup() {
pinMode(BOARD_LED,OUTPUT);
pinMode(USER_BUTTON,INPUT);
attachInterrupt (digitalPinToInterrupt (USER_BUTTON), isr_tasterUB, FALLING); // Interrupt für USER_BUTTON einstellen
}
void loop() {
digitalWrite(BOARD_LED,!digitalRead(BOARD_LED));
delay(blinkZeit);
}
Zustandsdiagramm für ISR Ereignis
Wird das ISR Ereignis als Transition dargestellt, wird der Zustand verlassen und wieder betreten, ggfs. vorhandene enter/ und exit/ werden ausgeführt.
Kompakt ist die mögliche Darstellung als Internes Ereignis.
Zustand wird bei ISR nicht verlassen, keine Ausführung von enter/ und /exit
Aufgabe: Sekundenzähler mit Ext. Interrupt starten und stoppen
Der Ext. Interrupt wird durch Druck auf die entprellte Taste USER_BUTTON an PC13 (Low aktiv, FALLING) ausgelöst und dabei die ISR isr_taster() aufgerufen.
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->…
Die Ausgabe der Zeit erfolgt mit dem UP ausgeben().
🖌 Erstellen Sie ein Zustandsdiagramm, verwenden Sie dabei isr_taster() (spicken in der Forsa ist erlaubt).
Lösungsvorschlag
Implementieren eines Ereignisses
Die Zustandsübergänge werden nun auch in der ISR getätigt.
🖥 Ergänzen Sie den vorgegebenen Code:
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:
break;
case LAUFEN:
break;
case STOPP:
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); // ISR einrichten
ausgeben();
}
void loop() {
if (zustand==LAUFEN){
delay(1000);
}
}
Lösung
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); // ISR einrichten
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. Den Dezimalpunkt an PC7 von der Zehner-Anzeige nicht vergessen. Die Zeit bleibt bei 9.9 Sekunden stehen. Beachten Sie den msZaehler in der Vorgabe zur Zeitmessung:
#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;
}
Lösungsvorschlag
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.
Lösungsvorschlag
Skigondel
Die Skigondel hat einen Fassungsvermögen von 6 Personen. Die Personen gehen durch ein Drehkreuz in einen Einstiegsbereich und steigen alle in die Gondel ein. Eine Anzeige zeigt an, wie viele das Drehkreuz zum Einstiegsbereich passiert haben, im Bild sind es 3 Personen (L3 leuchtet). Das Drehkreuz wird bei 6 Personen blockiert (HALT-Signal PC7). Nach Start der Gondel ist der Einstiegsbereich wieder leer und die Anzeige L0 leuchtet. Die Gondel kann auch mit weniger als 6 Personen losfahren.
- Die Anzeige ist an PC6..PC0 angeschlossen.
- Das HALT-Signal wird an PC7 ausgegeben (PC7 und PC6 sind 1).
- Das Drehkreuz gibt ein kurzes 0-Signal (DK an PC13 weil entprellt) wenn eine Person durchgegangen ist.
- Wenn die Gondel ablegt gibt sie ein kurzes 1-Signal (RESET an PA1).
- Die Signale werden durch externe Interrupts verarbeitet, siehe vorgegebener Code.
void setup(){ // Einmalige Ausführung => Initialisierungen...
pinMode(PC0, OUTPUT); // ohne diese Zeile klappts nicht
GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
pinMode(PA1,INPUT_PULLDOWN); // RESET
pinMode(PC13,INPUT); // DK
attachInterrupt (digitalPinToInterrupt (PA1), isr_RESET, RISING); // Interrupt für RESET einstellen
attachInterrupt (digitalPinToInterrupt (PC13),isr_DK, FALLING); // Interrupt für DK einstellen
GPIOC->ODR=1;
}
typedef enum {ZAEHLEN,HALT} zustandstyp;
zustandstyp zustand=ZAEHLEN;
void isr_RESET(){ // wird bei steigender Flanke von PA1 ausgelöst
}
void isr_DK(){ // wird bei fallender Flanke von PC13 ausgelöst
}
void loop(){
}
- 🖌 Zeichen Sie ein Zustandsdiagramm.
- 🖥 Vervollständigen Sie den Code.
Lösungsvorschlag Zustandsdiagramm
Lösungsvorschlag Code
void setup(){ // Einmalige Ausführung => Initialisierungen...
pinMode(PC0, OUTPUT); // ohne diese Zeile klappts nicht
GPIOC->MODER = 0x5555; // PC0..PC7 als Ausgang
pinMode(PA1,INPUT_PULLDOWN); // RESET
pinMode(PC13,INPUT); // DK
attachInterrupt (digitalPinToInterrupt (PA1), isr_RESET, RISING); // Interrupt für RESET einstellen
attachInterrupt (digitalPinToInterrupt (PC13),isr_DK, FALLING); // Interrupt für DK einstellen
GPIOC->ODR=1;
}
typedef enum {ZAEHLEN,HALT} zustandstyp;
zustandstyp zustand=ZAEHLEN;
void isr_RESET(){ // wird bei steigender Flanke von PA1 ausgelöst
GPIOC->ODR=1;
zustand=ZAEHLEN;
}
void isr_DK(){ // wird bei fallender Flanke von P13 ausgelöst
switch (zustand){
case ZAEHLEN:
GPIOC->ODR=GPIOC->ODR<<1;
if(GPIOC->ODR>=0x40){
GPIOC->ODR=0xC0;
zustand=HALT;
}
break;
}
}
void loop(){
}
Ausblick: 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 ISR aktivieren und Sprung zur ISR
mytimer.resume(); //Timer starten
}
void loop() {
if (zustand==LAUFEN && msZaehler>=100 && zeit<99){
zeit++;
msZaehler=0;
}
}
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.
ISRs unterbrechen sich nicht gegenseitig bei STM32-Arduino?
Leider doch! Hier Testprogramm für Arduino, Taster-ISR unterbricht Timer-ISR:
#define LED_B PA5 // D13 Led auf dem Board
#define T_B PC13 // Entpreller UserButton auf dem Board
#define LED2 PC0 // LED an PC0
#define TEST 1 // 1: Test ob Taster-ISR Timer-ISR unterbricht 0: Test ob Timer Taster ISR unterbricht
static HardwareTimer mytimer = HardwareTimer(TIM2); // Timerinstanz sowie Timeruaswahl
void isr_blinken() {
#if TEST == 1
digitalWrite(LED_B,!digitalRead(LED_B)); // LED umschalten
digitalWrite(LED2,HIGH);
delay(2000);
digitalWrite(LED2,LOW);
#else
digitalWrite(LED_B, LOW); // wenn Timer-ISR aufgerufen dann BoardLed
digitalWrite(LED2,HIGH);
delay(500); // halbe Sekunde aus!
digitalWrite(LED2,LOW);
#endif
}
void isr_taste() { // Interrupt Service Routine
#if TEST == 1
digitalWrite(LED_B,!digitalRead(LED_B)); // LED umschalten
#else
for (int i = 0; i < 300; i++) { // wenn ISR, dann ein wenig beschäftigen
digitalWrite(LED_B, !digitalRead(LED_B));
delay(20);
}
#endif
}
void setup() {
pinMode(LED_B, OUTPUT); // BoardLED
pinMode(LED2, OUTPUT); // TestLED
mytimer.setOverflow(3000000, MICROSEC_FORMAT); // ISR alle 3 Sekunden
mytimer.attachInterrupt(isr_blinken); //Timer IR aktivieren und Sprung zur ISR// put your setup code here, to run once:
mytimer.resume();
pinMode(T_B, INPUT);
attachInterrupt(digitalPinToInterrupt(T_B), isr_taste, FALLING);
}
void loop() {
}
Das ist eine schlechte Nachricht, ist nicht wie bisher bei den AVR-Arduinos. Somit kann das Problem der Inkonsistenten neben Unterbrechung des Hauptprogramms auch auftreten wenn eine Timer-ISR von einer Taster-ISR unterbrochen wird, wenn auf die selben Ressourcen zugegriffen wird. Wie 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.
#define LED_B PA5 // D13 Led auf dem Board
#define T_B PC13 // Entpreller UserButton auf dem Board
int blinkZeit = 100;
bool isr_tasterUB_Flag=false;
void isr_tasterUB(){ // Interrupt Service Routine
gedruecktT_B_Flag=true;
}
void setup() {
pinMode(LED_B,OUTPUT);
pinMode(T_B,INPUT);
attachInterrupt (digitalPinToInterrupt (T_B), isr_tasterUB, FALLING);
}
void loop() {
if(isr_tasterUB_Flag){ // Taste gedrückt?
isr_tasterUB_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.