Forum: Mikrocontroller und Digitale Elektronik AVR Interrupt Processing - was ist besser ?


von Manfred L. (manni)


Lesenswert?

Hallo,

wenn zeitgeteuerte Aktionen oder Berechnungen durchgeführt werden 
sollen, wird typischerweise die TIMER Funktion verwendet, z.B. ISR 
(TIMER0_OVF_vect).

Dabei können zwei unterschiedliche Methoden angewendet werden:
1) die Aktion oder die Berechnung findet innerhalb der ISR 
(TIMER0_OVF_vect) statt.
2) innerhalb der ISR (TIMER0_OVF_vect) wird ein globales Flag gesetzt, 
welches in einer Endlosschleife in main() überprüft wird, ob die Aktion 
oder die Berechnung stattfinden soll.

Als Beispiel habe ich unten die beiden Methoden als C-Code dargestellt, 
die den Sachverhalt widergeben soll, auch wenn das Programm vollkommener 
Nonsens ist.

Jetzt meine Frage:
Welche Methode ist die "Richtige", was immer "Richtig" auch heisst ?

Nach meinem Verständnis und den Empfehlungen aus diesem und anderen 
Foren ist die Methode 2 die "Bessere", weil in den Interrupt Service 
Routinen so wenig wie möglich berechnet werden soll, d.h. wenn einmal 
drin, so schnell wie möglich wieder raus, um auch andere Interrupts 
bedienen zu können.

Was ist Eure Erfahrung oder habt ihr andere Empfehlungen ?

Grüße
Manni

Beispiel Programme für Methode 1 und 2:
1
// Method 1 for Interrupt Processing
2
// =================================
3
#include <stdio.h>
4
#include <avr/io.h>
5
uint8_t main (void)
6
   {
7
   ConfigureTimer0 ();  // ist unwichtig hier
8
   while (1)
9
      {
10
      DoSomethingStupid();
11
      }
12
   }
13
ISR (TIMER0_OVF_vect)
14
   {
15
   DoComputation ();
16
   }
17
void DoComputation (void)
18
   {
19
   static uint8_t i=0;
20
   i++;  // This is the computation to be done every TIMER0 Overflow
21
   }
22
23
// Method 2 for Interrupt Processing
24
// =================================
25
#include <stdio.h>
26
#include <avr/io.h>
27
uint8_t iFlag = false;
28
uint8_t main (void)
29
   {
30
   ConfigureTimer0 ();  // ist unwichtig hier
31
   while (1)
32
      {
33
      DoSomethingStupid ();
34
      DoComputation ();
35
      }
36
   }
37
ISR (TIMER0_OVF_vect)
38
   {
39
   iFlag = true;
40
   }
41
void DoComputation (void)
42
   {
43
   static uint8_t i=0;
44
   if (iFlag)
45
      {
46
      i++;  // This is the computation to be done every TIMER0 Overflow
47
      iFlag = false;
48
      }
49
   }

von Draco (Gast)


Lesenswert?

Wenn dann:
1
// Method 2 for Interrupt Processing
2
// =================================
3
#include <stdio.h>
4
#include <avr/io.h>
5
volatile uint8_t iFlag = 0;
6
uint8_t main (void)
7
   {
8
   ConfigureTimer0 ();  // ist unwichtig hier
9
   while (1)
10
      {
11
      DoSomethingStupid ();
12
      DoComputation ();
13
      }
14
   }
15
ISR (TIMER0_OVF_vect)
16
   {
17
   iFlag = 1;
18
   }
19
void DoComputation (void)
20
   {
21
   static uint8_t i=0;
22
   if (iFlag == 1)
23
      {
24
      i++;  // This is the computation to be done every TIMER0 Overflow
25
      iFlag = 0;
26
      }
27
   }


oder:
1
// Method 2 for Interrupt Processing
2
// =================================
3
#include <stdio.h>
4
#include <avr/io.h>
5
volatile bool iFlag = false;
6
uint8_t main (void)
7
   {
8
   ConfigureTimer0 ();  // ist unwichtig hier
9
   while (1)
10
      {
11
      DoSomethingStupid ();
12
      DoComputation ();
13
      }
14
   }
15
ISR (TIMER0_OVF_vect)
16
   {
17
   iFlag = true;
18
   }
19
void DoComputation (void)
20
   {
21
   static uint8_t i=0;
22
   if (iFlag)
23
      {
24
      i++;  // This is the computation to be done every TIMER0 Overflow
25
      iFlag = false;
26
      }
27
   }

von Mitlesa (Gast)


Lesenswert?

Bei eueren Scope-Klammersetzungen bekommt man Augenkrebs!

von Torsten C. (torsten_c) Benutzerseite


Lesenswert?

Manfred L. schrieb:
> Was ist Eure Erfahrung oder habt ihr andere Empfehlungen ?

Nein. Ich kann die Empfehlungen aus diesem und anderen Foren nur 
unterstützen. Es gibt andere CPUs mit "nested interrupts", aber Deine 
Frage war ja zum "AVR Interrupt Processing".

> so schnell wie möglich wieder raus, um auch andere Interrupts
> bedienen zu können.

Wenn man im Speziallfall weiß, dass es keine anderen gibt, kann man 
natürlich Ausnahmen machen.

: Bearbeitet durch User
von Falk B. (falk)


Lesenswert?

@Manfred Langemann (manni)

>Welche Methode ist die "Richtige", was immer "Richtig" auch heisst ?

Die, welche die Anforderungen des Programms bezüglich Reaktionszeit und 
sicherer Abarbeitung der Flags erfüllt.

>Nach meinem Verständnis und den Empfehlungen aus diesem und anderen
>Foren ist die Methode 2 die "Bessere",

Nö, das kann oft so sein, ist aber nicht allgemeingültig. Man kann auch 
ne ganze Menge in eine ISR packen, z.B. eine größere State machine.

Beide Methoden haben ihre Berechtigung, keine ist ein Patentrezept, 
beide kann man vermurksen.

von Rainer B. (katastrophenheinz)


Lesenswert?

... und wenn du es perfekt machen willst, dann packst du die Abfrage auf 
"iFlag" nicht in DoComputation(), sondern vor den Aufruf von 
DoComputation in die While-Schleife.
1
    while (1) {
2
      DoSomethingStupid ();
3
      if ( iFlag ) { iFlag=false; DoComputation ();}
4
    }
Hat den Vorteil, dass der Aufruf und vor allem das damit verbundene 
Sichern der Register auf dem Stack nur dann erfolgt, wenn notwendig.

: Bearbeitet durch User
von Klaus (Gast)


Lesenswert?

Manfred L. schrieb:
> 2) innerhalb der ISR (TIMER0_OVF_vect) wird ein globales Flag gesetzt,
> welches in einer Endlosschleife in main() überprüft wird, ob die Aktion
> oder die Berechnung stattfinden soll.

Da kann man sich die ISR schenken und das Interruptflag direkt pollen.

MfG Klaus

von Torsten C. (torsten_c) Benutzerseite


Lesenswert?

Klaus schrieb:
> Da kann man sich die ISR schenken und das Interruptflag direkt pollen.
Das stimmt. Wenn man z.B. auf einen UART-RX-Interrupt reagiert, sollte 
man natürlich auch das empfangede Byte innnerhalb der ISR in den 
Ringspeicher schreiben. Das Flag sagt dann nur: "&Head" wurde 
(mindestens einmal) incrementiert.

Also: So wenig wie möglich, aber so viel wie nötig, bis auf Ausnahmen.^^

: Bearbeitet durch User
von c-hater (Gast)


Lesenswert?

Falk B. schrieb:

> Man kann auch
> ne ganze Menge in eine ISR packen, z.B. eine größere State machine.
>
> Beide Methoden haben ihre Berechtigung, keine ist ein Patentrezept,
> beide kann man vermurksen.

So ist es. Der springende Punkt ist aber nicht, ob Code in einer ISR 
abgearbeitet wird, sondern ob er exklusiv abgearbeitet wird, denn die 
Exklusivität ist eigentlich die Sache, die andere Interrupts stört 
(verzögert oder gar ganz verhindert).

Die beste Methode ist deswegen oft: ISR wird in zwei Teile geteilt, 
einen exklusiv abgearbeiteten (der natürlich so kurz wie möglich sein 
sollte und deshalb nur das enthalten sollte, was wirklich unbedingt 
unter Interruptsperre abgearbeitet werden muss) und dann einen ggf. auch 
längeren Teil, der zwar immer noch ISR-Code ist, aber nicht mehr 
exklusiv läuft, also durch andere Interrupts unterbrechbar ist.

Das Konzept hat folgende Vorteile:
1) Die variable Interruptlatenz wird so gering wie möglich gehalten.
2) Wichtiger Code (eben der nichtexklusive Teil der ISR) wird trotzdem 
vorrangig (vor main()) abgearbeitet.
3) Unnötiger Rechenzeitverbrauch für das Polling der Flags in main() 
entfällt.
4) Es wird per Software das möglich, was Atmel der Hardware verweigert 
hat: Interrupts untereinander gezielt zu priorisieren.

Das Konzept hat aber auch einen schweren Nachteil:
Der nichtexklusive Teil der ISR kann auch durch eine neue Instanz 
desselben Interrupts unterbrochen werden->Stacküberlauf droht.

Das kann man durch geeignete Maßnahmen verhindern, allerdings sind diese 
wiederum rechenzeitaufwendiger als das Pollen eines Flags in main(). 
Allerdings muss nicht in jedem Fall mit diesen harten und teueren 
Maßnahmen hantiert werden, nämlich immer dann nicht, wenn die maximale 
Interruptfolgefrequenz gut vorhersehbar und der maximale 
Rechenzeitbedarf der ISR (und der sie unterbrechenden konkurrierenden 
ISRs) bekannt ist. Also weniger geeignet für extern ausgelöste 
Interrupts an irgendwelchen Pins (einschließlich Analogkomparator und 
ICP-Funktion der Timer), aber gut geeignet für sonstige Timer-Interrupts 
und den größten Teil des restlichen Peripheriekrams (I2C und SPI mit 
Einschränkungen für den Fall, dass man nicht alleiniger Master ist).

Wie auch immer, die allerblödeste ISR ist jedenfalls immer eine, die 
wirklich nichts anderes tut, als ein Flag im RAM zu setzen. Denn sie ist 
schlicht ÜBERFLÜSSIG. Man kann nämlich in main() genausogut gleich das 
Interruptflag der Hardware pollen...

von Rainer B. (katastrophenheinz)


Lesenswert?

c-hater schrieb:
> Wie auch immer, die allerblödeste ISR ist jedenfalls immer eine, die
> wirklich nichts anderes tut, als ein Flag im RAM zu setzen. Denn sie ist
> schlicht ÜBERFLÜSSIG. Man kann nämlich in main() genausogut gleich das
> Interruptflag der Hardware pollen...

Das halte ich für eine gewagte These. Mag so gehen, wenn 
Energieverbrauch kein Thema ist. Bei allen anderen Anwendungen, also 
platt gesagt, bei allem, was per Batterie versorgt ist, ist es keine 
gute Idee, Interrupts zu pollen, seien sie noch so primitiv. Da ist die 
Methode "tu's in der ISR, wenn zeitlich und von der Komplexität her 
passt oder setze Flag" die einzig vernünftige. Dazu braucht's nicht mal 
RAM, meistens gibt's ein GPIOR oder ein anderes ungenutztes Register im 
unteren Registerbereich, was man dafür nehmen und 8 Flag-Bits 
unterbringen kann. Hat zusätzlich den Vorteil dass man diese Flags mit 
nur einem Maschinenbefehl setzen/löschen/auswerten kann. In der 
Endlosschleife in main() steht dann nur sowas in der Art:
1
while(1) {
2
   TesteFlagsUndTuWas();
3
   SetzePassendenSleepMode();
4
   GehSchlafen();
5
}

von Peter D. (peda)


Lesenswert?

Der AVR stellt unter den MCs einen Sonderfall dar, da er keine 
Prioritäten zuweisen kann. Daher ist oft die Zweiteilung zu bevorzugen, 
d.h. nur das absolut Notwendige im Interrupt auszuführen und den Rest in 
der Mainloop.

Im Gegensatz dazu können z.B. die alten 80C51 bis zu 4 Prioritäten 
verteilen. D.h. ein wichtiger Interrupt kann andere Interrupts einfach 
unterbrechen ohne riesen Latenzen oder Softwareoverhead.

von Falk B. (falk)


Lesenswert?

@  Peter Dannegger (peda)


>Der AVR stellt unter den MCs einen Sonderfall dar, da er keine
>Prioritäten zuweisen kann.

Der AVR ist so schnell, der braucht das nicht. ;-)
Die Jungs von Atmel haben sich dabei schon was gedacht, wenn gleich so 
eine Entscheidung immer umstritten ist.
Die Praxis zeigt, daß die fehlenden, verschachtelten Interrupts der 
Verbreitung des AVRs nicht geschadet haben.

>Im Gegensatz dazu können z.B. die alten 80C51 bis zu 4 Prioritäten
>verteilen. D.h. ein wichtiger Interrupt kann andere Interrupts einfach
>unterbrechen ohne riesen Latenzen oder Softwareoverhead.

Geht beim AVR in Software auch, wenn gleich das einen Tick schlechter 
ist als echte Hardwareprioritäten.

Der TMS320 der PICCOLO-Serie von TI hat auch keine 
Hardwareprioritäten, und das ist ein ausgewachsener 32 Bit Controller!
Auch dort muss man die Prioritäten per Software nachrüsten.

von m.n. (Gast)


Lesenswert?

c-hater schrieb:
> Das Konzept hat aber auch einen schweren Nachteil:
> Der nichtexklusive Teil der ISR kann auch durch eine neue Instanz
> desselben Interrupts unterbrochen werden->Stacküberlauf droht.
>
> Das kann man durch geeignete Maßnahmen verhindern, allerdings sind diese
> wiederum rechenzeitaufwendiger als das Pollen eines Flags in main().

Man braucht keine Rechenzeit aufzuwenden, sondern sperrt lediglich das 
betreffende IE-Bit, löscht am Ende der ISR das dazugehörige Flag (was 
eigentlich nicht gesetzt sein darf) und gibt das IE wieder frei.
Wenn jetzt noch der Stack überläuft, ist entweder die ISR oder der µC zu 
langsam. Da hilft dann nur noch ein starker ARM ;-)

von Pandur S. (jetztnicht)


Lesenswert?

Das Beste ist das Hirn einzuschalten und sich alles unter den 
gewuenschten Anforderungen und Randbedingungen zu ueberlegen.

Mein TimerOverflow Interrupt macht einen Reload des Zaehlers, und setzt 
ein Flag fuer das Main.
Mein Kommunikationsinterrupt arbeitet eine Zustandsmaschine ab. zB 
Zeichen Speichern & neuer Zustand. zB Zeichen laden und neuer Zustand.

Im Main wird das Timer- und andere Flags, sowie die 
Kommunikationszustaende  gepollt und darauf geantwortet.
Ein Kommunikationszustand kann zB End-of-message sein. Dann arbeitet das 
Main damit weiter.

Man muss sich immer ueberlegen, was ist die Reaktionszeit, wo wird die 
Zeit verbraten und wo wird gewartet. Und gewartet wird generell immer 
nur im Main, beim Pollen der Flags und Zustaende. Es gibt auch keinen 
Delay. Nirgendwo anders darf gewartet werden. Durch Ueberwachen der 
Zustaende im Main kann so immer festgestellt werden wo sich das Programm 
befindet. Indem man zB den Zustand mit einer Nummer codiert und als 
serielles Packet auf einem Pin ausgibt. Dort haengt man ein Oszilloskop 
an und kann so alles verfolgen.

von m.n. (Gast)


Lesenswert?

Oh D. schrieb:
> Und gewartet wird generell immer
> nur im Main, beim Pollen der Flags und Zustaende.

Ich kenne Deine main() nicht, aber daß dort mal gewartet wird, ist doch 
eher die Ausnahme. Sobald das Programm mehrere Aufgaben zu erledigen 
hat, wird mal in "Abgleich", "Messung", oder sonstwo gewartet. Eine 
Rückkehr nach main() erfolgt immer nur nach Abschluß der Aufgabe.

Eine "Wartestelle" wäre zum Beispiel teste_taste(), die in der Regel aus 
allen Funktionen aufgerufen wird. Für eine Multiplex-Anzeige wäre es 
dennoch keine gute Idee, irgendwo, irgendwann auf ein gesetztes Flag zu 
reagieren.

von Pandur S. (jetztnicht)


Lesenswert?

> Ich kenne Deine main() nicht, aber daß dort mal gewartet wird, ist doch
eher die Ausnahme.

Nein, ganz sicher nicht, das ist so by Design. Alle Prozesse sind per 
Zustandsmaschine so gebaut, dass immer nur im Main gewartet wird. 
Strikt.
Der Prozess "Messung" ist eine Zustandsmaschine, die zB auch Daten vom 
ADC enthaelt, die auch im Main verarbeitet werden.
Ebenso der LCD. Der LCD wird im Main per timer, dh jeden Tick mit genau 
einem neuen Character oder command beschickt, sodass auch da nicht 
gewartet werden muss.

Main {
 if (UARTCame ==1) {
  ProcessUART ();
  UARTCame =0;
 }
 if (timercame==1) {
  LCD ();
  Messung ();
  Tastatur ();  // liest den Zustand der Schalter
  Ausgaben ();
  timercame=0;
  Sleep ();     // zentraler Powerdown hier moeglich
 }
}

Das erlaubt dann auch genau dort zB einen Sleep einzufuegen, wenn man 
Power spaen will.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Falk B. schrieb:
> Die Praxis zeigt, daß die fehlenden, verschachtelten Interrupts der
> Verbreitung des AVRs nicht geschadet haben.

Woher willst Du das wissen?
Er könnte durchaus noch mehr Verbreitung gefunden haben.
Ich hatte es öfters vermißt und teilweise haarsträubende Würg-Arounds 
programmieren müssen.

Der ARM Cortex-M3 hat sogar 256 Interruptprioritäten.

von Falk B. (falk)


Lesenswert?

@ m.n. (Gast)

>eher die Ausnahme. Sobald das Programm mehrere Aufgaben zu erledigen
>hat, wird mal in "Abgleich", "Messung", oder sonstwo gewartet.

Nö, in einem vernünftigen Programm mit Multitasking wird nirgendwo 
gewartet, sondern die State machine dreht Ehrenrunden in 
Wartezuständen. Wenn es für eine Funktion im Moment nix zu tun gibt, 
geht der Programmablauf an andere Funktionen über.

> Eine
>Rückkehr nach main() erfolgt immer nur nach Abschluß der Aufgabe.

Nicht zwingend, of wird eine Aufgabe in kleinere Teile zerlegt und 
zwischendurch kehrt die Funktion zurück, siehe oben.

>Eine "Wartestelle" wäre zum Beispiel teste_taste(), die in der Regel aus
>allen Funktionen aufgerufen wird.

Das ist nur in einfachen Anfängerprogrammen der Fall.

von m.n. (Gast)


Lesenswert?

Falk B. schrieb:
> in einem vernünftigen Programm mit Multitasking

Von Multitasking war nirgends die Rede.

Falk B. schrieb:
> of wird eine Aufgabe in kleinere Teile zerlegt und
> zwischendurch kehrt die Funktion zurück

Dieses Zurückkehren ergibt keinen Sinn. Während einer Messung ist es 
völlig unnötig (oder sogar schädlich) noch Routinen wie "Abgleich" oder 
"Einrichten" aufzurufen.

Falk B. schrieb:
>>Eine "Wartestelle" wäre zum Beispiel teste_taste(), die in der Regel aus
>>allen Funktionen aufgerufen wird.
>
> Das ist nur in einfachen Anfängerprogrammen der Fall.

Und wie bekommen Profi-Programme einen Tastendruck mitgeteilt?

von Pandur S. (jetztnicht)


Lesenswert?

> Und wie bekommen Profi-Programme einen Tastendruck mitgeteilt?

indem man per timer im main die Tasten einliest und per differenz zu 
vorher, die Aenderung detektiert. Dann merk man sich das flag taste=3 
fuer das naechste Mal, wo eine Taste einen Sinn ergibt. Zb wenn man die 
Menu Zustandsmaschine abarbeitet.

Die Taste wirkt auf eine Zustandsmaschine, im Main.

: Bearbeitet durch User
von m.n. (Gast)


Lesenswert?

Tut mir Leid, aber mit dieser Vorgehensweise würde ich in meinen 
Programmen kein Land sehen.

In meinen main() wird die Hardware initialisiert und anschließend per 
Sprungliste die gewählte Funktion aufgerufen. Mehr nicht.

Die angewählte Funktion hat weitere Unterfunktionen, welche ebenfalls 
Unterfunktionen aufrufen, die Eingaben von Bedientasten oder einer 
Tastatur erwarten. All diese Funktionen wissen nichts von einer main() 
und haben folglich auch nichts damit zu tun.
Den Teufel werde ich tun, aus diesen Funktionen immer wieder nach main() 
zurückzukehren!

von Falk B. (falk)


Lesenswert?

@ m.n. (Gast)

>> in einem vernünftigen Programm mit Multitasking

>Von Multitasking war nirgends die Rede.

Nein? Aber der OP zerbricht sich vollkommen nutzlos seienn Kopf, wie er 
Aufgaben per Interrupt möglichst schnell verarbeiten kann?

>> of wird eine Aufgabe in kleinere Teile zerlegt und
>> zwischendurch kehrt die Funktion zurück

>Dieses Zurückkehren ergibt keinen Sinn. Während einer Messung ist es
>völlig unnötig (oder sogar schädlich) noch Routinen wie "Abgleich" oder
>"Einrichten" aufzurufen.

Das hängt von der konkreten Aufgabe ab. Natürlich kann man nicht JEDE 
Aufgabe x-beliebig zerlegen.

>Und wie bekommen Profi-Programme einen Tastendruck mitgeteilt?

Über globale Variablen oder, wer es richtig fett machen will, über 
RTOS-Container ala Semaphore/Mutex, whatever.

von Falk B. (falk)


Lesenswert?

@  m.n. (Gast)

>Tut mir Leid, aber mit dieser Vorgehensweise würde ich in meinen
>Programmen kein Land sehen.

Tja, dann kannst du vielleicht noch was lernen.

>In meinen main() wird die Hardware initialisiert und anschließend per
>Sprungliste die gewählte Funktion aufgerufen. Mehr nicht.

Das schließt Multitasking nicht aus. Lies den Artikel und denk 
drüber nach.

>Die angewählte Funktion hat weitere Unterfunktionen, welche ebenfalls
>Unterfunktionen aufrufen, die Eingaben von Bedientasten oder einer
>Tastatur erwarten.

Tja, und was machen die, wenn keiner eine Taste drückt? Oder die 
falsche?

> All diese Funktionen wissen nichts von einer main()

Darum geht es gar nicht.

>und haben folglich auch nichts damit zu tun.
>Den Teufel werde ich tun, aus diesen Funktionen immer wieder nach main()
>zurückzukehren!

Jaja, du hast den Durchblick. Du hast wahrscheinlich nicht mal 
ansatzweise das Konzept des (kooperativen) Multitasking verstanden, 
geschweige denn erfolgreich angewendet.

von m.n. (Gast)


Lesenswert?

Falk B. schrieb:
>>Und wie bekommen Profi-Programme einen Tastendruck mitgeteilt?
>
> Über globale Variablen

Dann bin ich wohl kein Profi. Bei mir gibt es eine Funktion, die testet, 
ob eine Eingabe vorliegt und eine Funktion, die die Eingabe aus dem 
Puffer holt.
Einlesen, Dekodieren und Ablage in den Puffer wird alles im Hintergrund 
in der ISR erledigt: main() ist dabei nie beteiligt!

Manfred L. schrieb:
> ISR (TIMER0_OVF_vect)
>    {
>      DoComputation ();
>    }
> void DoComputation (void)
>    {
>      static uint8_t i=0;
>      i++;  // This is the computation to be done every TIMER0 Overflow
>    }

Aus einer ISR heraus nach Möglichkeit keine Funktion aufrufen. Der 
Compiler weiß zumeist nicht, welche Register benötigt werden und rettet 
zur Sicherheit dann alle:
das braucht Stack und Rechenzeit -> nicht gut!

von Falk B. (falk)


Lesenswert?

@ m.n. (Gast)

>> Über globale Variablen

>Dann bin ich wohl kein Profi.

Schon möglich.

> Bei mir gibt es eine Funktion, die testet,
>ob eine Eingabe vorliegt und eine Funktion, die die Eingabe aus dem
>Puffer holt.

Kein Einspruch, das hat Oh Doch genau so formuliert.

>Einlesen, Dekodieren und Ablage in den Puffer wird alles im Hintergrund
>in der ISR erledigt: main() ist dabei nie beteiligt!

Das sind Details, die man so oder so handhaben kann.

>Aus einer ISR heraus nach Möglichkeit keine Funktion aufrufen.

Das ist nur dein AVR-Tunnelblick. Auf anderen, vor allem größeren CPUs 
ist das deutlich unkritischer, vor allem weil die sowieso einen viel 
höheren Takt haben.

> Der
>Compiler weiß zumeist nicht, welche Register benötigt werden und rettet
>zur Sicherheit dann alle:

Mehr als ohne Funktionsaufruf, aber nicht alle.

>das braucht Stack und Rechenzeit -> nicht gut!

Auch das ist relativ. In einer 50 kHz ISR ist das sicher tödlich, in 
einer gemächlichen 10ms ISR kein Thema.

von m.n. (Gast)


Lesenswert?

Falk B. schrieb:
>>Aus einer ISR heraus nach Möglichkeit keine Funktion aufrufen.
>
> Das ist nur dein AVR-Tunnelblick.

Dann lies doch mal die Überschrift; ist wahrscheinlich auch eine 
Tunnelfrage.

von c-hater (Gast)


Lesenswert?

m.n. schrieb:

> Man braucht keine Rechenzeit aufzuwenden, sondern sperrt lediglich das
> betreffende IE-Bit, löscht am Ende der ISR das dazugehörige Flag (was
> eigentlich nicht gesetzt sein darf) und gibt das IE wieder frei.

Und das kostet keine Rechenzeit?

Nehmen wir doch einfach mal den ungünstigsten Fall, das entsprechende 
Maskenregister liegt im MMIO-Bereich und enthält mehrere benutzte 
Interruptflags. Dann brauchst du schon 5 Takte allein für die Operation 
zum Sperren des IRQ und nochmal 5 Takte, um ihn wieder freizugeben.

Aber damit ist es ja noch nicht getan, du brauchst auch noch ein 
Register, um diese Operation auszuführen, dieses musst du (ggf. 
zusätzlich) sichern und wiederherstellen. Macht im "schlimmsten" Fall 
vier weitere Takte. (Die ganz schlimmen Fälle mit externem RAM oder so 
seien hier mal noch außen vor)

Aber das ist immer noch nicht alles, bei der Operation Sperren/Freigeben 
des IRQ veränderst du zwangsläufig die Flags. Folglich mußt du auch 
diese sichern und wiederherstellen. Was wiederum sechs weitere Takte 
kostet (wenn man das Register aus dem vorigen Absatz dazu nutzt, um sie 
auf den Stack zu kriegen, was praktisch immer möglich sein dürfte).

Rechnen wir mal zusammen: 5+5+4+6 = 20 Takte. Wenn das "keine" 
Rechenzeit ist, was denn dann? Viele meiner ISRs dauern insgesamt 
nichtmal so lange. Und in noch sehr viel mehr wäre schlicht nicht die 
Zeit, das zusätzlich zu der tatsächlichen Nutzfunktion zu leisten...

Fazit: Takte-Rechnen lohnt. Wenn man dabei herausbekommt, das kein 
Stacküberlauf droht, ist es absolut kontraproduktiv, Maßnahmen dagegen 
zu treffen, denn es besteht die absolut reale Gefahr, dass genau diese 
Maßnahmen erst das Verlassen der Echtzeit für die Gesamtanwendung 
bewirken, was zwar dann nicht zum Stacküberlauf führt, aber trotzdem 
dazu, das die Sache nicht wie gewünscht funktioniert. Was im Endergebnis 
praktisch das gleiche ist: Die Anwendung funktioniert schlicht nicht...

Und selbst wenn die unnötigen Maßnahmen nicht dazu führen, das garnix 
mehr geht: sie schlucken auf jeden Fall unnötig Zeit, die man entweder 
anderswo gebrauchen könnte oder während der man schlafend Energie sparen 
könnte...

von m.n. (Gast)


Lesenswert?

c-hater schrieb:
> Rechnen wir mal zusammen: 5+5+4+6 = 20 Takte. Wenn das "keine"
> Rechenzeit ist, was denn dann?

Bei 20 MHz Takt ist das 1 µs, was in der Regel nicht so dramatisch ist. 
Ein zu langsam getakteter µC oder eine ungeschickte ISR-Programmierung 
brauchen mehr Zeit (s.o.).
Es kommt letztlich auf's konkrete Problem/Programm an.

In einigen Fällen hilft es auch, bei erneut gesetzem I-Flag an den 
Anfang der ISR zu springen, um erneutes Sichern/Restaurieren der 
Register zu sparen.

von Peter D. (peda)


Angehängte Dateien:

Lesenswert?

m.n. schrieb:
> Eine
> Rückkehr nach main() erfolgt immer nur nach Abschluß der Aufgabe.

Das geht aber nur für sehr einfache Aufgaben.
In der Regel möchte man aber mehrere Aufgaben quasi parallel abarbeiten. 
Z.B. wäre es unschön, wenn eine Temperaturregelung gegen den Baum fährt, 
nur weil man gerade die Uhrzeit stellt.
Auch kommt es oft vor, daß ein Programm noch erweitert werden soll und 
dann dürfen die neuen Aufgaben die alten nicht behindern.

Daher ist Dein Ansatz in keinem einzigen meiner Programme zu finden.
Ich mache es so, wie Oh Doch, mit beliebige vielen Zustandsmaschinen, 
die von sämtlichen Wartestellen zum Main zurück kehren.

Die Mainloop besteht oft auch aus 2 Schleifen, eine die sofort auf 
Ereignisse reagiert und eine, die in einem konstanten Zeitraster 
aufgerufen wird, z.B. alle 10ms. Damit kann man dann sehr schön 
Zeitabläufe realisieren.
Und die Sofortschleife reagiert z.B. auf ein empfangenes Paket im 
UART-FIFO oder vom CAN-Bus, Ethernet usw.

Sehr gerne benutze ich auch meinen Scheduler. Das entlastet dann die 
einzelnen Tasks davon, die Wartezeiten nicht mehr selber mitzählen zu 
müssen.

Anbei mal eine Mainloop als Beispiel.

von Lurchi (Gast)


Lesenswert?

Es hängt sehr vom Konkreten Programm ab.

Auch die 3. Möglichkeit mit freigeben von verschachtelten Interrupts 
sollte man in Betracht ziehen, vor allem bei einem Timer Interrupt, wo 
man weiß wann der Nächste kommt und man daher in der Regel genügend Zeit 
hat. Wenn nicht produziert die Methode mit dem Flag in der Regel auch 
Probleme / Fehler.

Der Funktionsaufruf in der ISR ist wie gesagt keine so gute Idee, vor 
allem nicht beim AVR mit GCC. Aber auch andere µC / Compiler können da 
leicht schlechten Code erzeugen. Selbst wenn die HW die Register rettet 
brauchen zusätzliche Register Platz auf dem Stack und ggf. Rechenzeit.
Bei dem einfachen Fall mit einer Funktion die nur für die ISR benutzt 
wird, wird es auch nicht einmal übersichtlicher.


Es macht schon Sinn die Rechenroutine in der ISR kurz zu halten. Eine 
schlechtes Beispiel, dass man öfter mal sieht, ist es in der ISR die 
Zeit gleich in Einheiten wie Stunden  Minuten  Sekunden hoch zu 
zählen. Besser ist es oft in der ISR einfach mit Taktzyklen oder 
ähnlichen Einheiten zu rechnen und erst bei der Ausgang / Eingabe die 
Umrechnung in andere Einheiten zu machen.

von Manfred L. (manni)


Lesenswert?

Erst mal vielen Dank an alle, die hier ihr Know-how, ihre Erfahrung, 
Kommentare und Empfehlungen zum Besten gegeben haben.

Ich fasse mal so zusammen:
1) In die ISR so wenig wie möglich, aber so viel wie nötig reinpacken - 
Ausnahmen bestätigen die Regel
2) Dies ist in sofern zu realisieren, dass der ISR Prozess in zwei Teile 
aufzuteilen ist: einen exklusiv abzuarbeitenden Teil in der ISR, den 
Rest in einem ausgelagerten nichtexklusiven Teil, der durch 
entsprechende globale Zustands-Flags abzuarbeiten ist.
3) Sicher stellen, dass die Rechenzeit des ausgelagerten nichtexklusiven 
Teils wesentlich kürzer ist als die Zeitspanne zwischen zwei Interrupts.
4) In der main() mit einer Loop beliebig viele Zustandsmaschinen 
aufrufen und zum main() zurückkehren. Innerhalb der Zustandsmaschinen 
werden dann die Tasks abgearbeitet, je nachdem, ob in einer 
Zustandsmaschine durch externe oder interne Interrupt ein Task 
durchzuführen ist.
5) Möglichst keine Funktionsaufrufe in der ISR wegen 
Register-Rettungs-Overhead.
6) Code-Optimierungen durch AVR Register-Checks in main() bzgl. 
Interrupt Stati sind schön und gut, aber führen nicht zwangsläufig zu 
einem übersichtlichen Code, der in 5 Jahren noch auf Anhieb verstanden 
wird.
7) Delay Routinen in ISR sind verboten.

Ich will doch hoffen, dass ich diese Zusammenfassung jetzt nicht um die 
Ohren gehauen bekomme. Trotzdem vielen Dank an alle, ihr seid super !

Beste Grüße
Manni

Zu guter Letzt hier noch meine Kommentare zu einigen Beiträgen:

@ Draco (Gast), 12.09.2016 21:55
OK, volatile bei globalen Variable ist wohl selbstverständlich, wenn 
jeder beliebige Task darauf zugreifen wird !
----------------------------
@ Rainer B. (katastrophenheinz), 12.09.2016 23:00
Die von Dir vorgeschlagene while(1) Variante ist prinzipiell richtig und 
gut, so spart man sich den Einsprung mit Stack Ops in DoComputation(), 
wenn ehe nix zu tun ist.
Wenn aber die DoComputation eine PID Berechnung in float ist, machen die 
paar Stack Ops den Braten auch nicht mehr fett.
Fazit: Zwecks Code Übersichtlichkeit sehe ich die Variante
1
while (1)
2
   {
3
   DoSomethingStupid ();
4
   DoComputation ();
5
   }
als gelungener an, wenn DoComputation() nicht gerade aus zwei 
Anweisungen besteht.
----------------------------
@ Klaus (Gast), 12.09.2016 23:03
>Da kann man sich die ISR schenken und das Interruptflag direkt pollen.
Ja, richtig, dann wird's aber in main() ordentlich unübersichtlich, wenn 
ich darin nach Herzenslust auf alle möglichen Register zugreifen muß, um 
zu checken, ob's was zu tun gibt. Es ist halt immer ein Balanceakt 
zwischen 100% optimiertem Code und einer übersichtlichen Struktur im 
Code, speziell, wenn man den nach 5 Jahren noch mal anpacken muß.
----------------------------
@ c-hater (Gast), 13.09.2016 06:36, der Frühaufsteher :-)
>Die beste Methode ist deswegen oft: ISR wird in zwei Teile geteilt,
>einen exklusiv abgearbeiteten (der natürlich so kurz wie möglich sein
>sollte und deshalb nur das enthalten sollte, was wirklich unbedingt
>unter Interruptsperre abgearbeitet werden muss) und dann einen ggf.
>auch längeren Teil, der zwar immer noch ISR-Code ist, aber nicht mehr
>exklusiv läuft, also durch andere Interrupts unterbrechbar ist.

Das sehe ich auch so. Den von Dir angesprochenen schweren Nachteil des 
Stacküberlaufes durch eine neue Instanz dessselben Interrupts sehe ich 
nicht, denn der ausgelagerte nichtexklusive Teils des Interrupts kann 
nach meinem Wissen so of unterbrochen werden wie er will, er kommt 
dennoch zu Ende ohne Stacküberlauf. Dieser Fall sollte jedoch nie 
passieren, wenn sicher gestellt ist, dass das Intervall zwischen den 
Interrupts vieeeeeeeel größer ist als die Prozessdauer des ausgelagerten 
nichtexklusiven Teils.

Ich danke Dir für die vielen Hinweise und Tips !
----------------------------
@ Peter Dannegger (peda), 13.09.2016 10:29
Danke Dir für die klare Empfehlung mit der Zweiteilung, sehe ich auch so 
!
----------------------------
@ Oh Doch (jetztnicht), 13.09.2016 13:20
>Er schrieb:
1
Main {
2
 if (UARTCame ==1) {
3
  ProcessUART ();
4
  UARTCame =0;
5
 }
6
 if (timercame==1) {
7
  LCD ();
8
  Messung ();
9
  Tastatur ();  // liest den Zustand der Schalter
10
  Ausgaben ();
11
  timercame=0;
12
  Sleep ();     // zentraler Powerdown hier moeglich
13
 }
14
}

Genau so sehen auch meine main()s aus, einach nur checken ob was in 
irgend einem Task was zu tun ist, so kommt jeder Task beizeiten ran, 
denn normalerweise gibt's ja nix zu tun.

Danke für das Beispiel !
----------------------------
@ m.n. (Gast), 13.09.2016 17:08
>Aus einer ISR heraus nach Möglichkeit keine Funktion aufrufen.

Danke Dir für den Hinweis, denn das mit dem 'Register retten' hatte ich 
noch nicht so bedacht. Werde wohl einiges umschreiben müssen :-(
----------------------------
@ c-hater (Gast), 13.09.2016 17:53
>Rechnen wir mal zusammen: 5+5+4+6 = 20 Takte. Wenn das "keine" Rechenzeit ist...

Wie oben schon angedeutet, ist DoComputation() einen PID Berechnung in 
float, da kommt es auf 20 Takte nicht an. Ansonsten gebe ich Dir recht, 
wenn in DoComputation() nur 1 oder 2 bits zu setzen sind. Es kommt halt 
immer auf den Einzelfall an.
----------------------------
@ Peter Dannegger (peda), 13.09.2016 20:50
>Daher ist Dein Ansatz in keinem einzigen meiner Programme zu finden.
>Ich mache es so, wie Oh Doch, mit beliebige vielen Zustandsmaschinen,
>die von sämtlichen Wartestellen zum Main zurück kehren.

Klare Ansage von Dir, Peter. Auch das mitgelieferte main() macht einen 
sehr aufgeräumten Eindruck und die Detailarbeit ist in den einzelnen 
Funktionen untergebracht.

von Pandur S. (jetztnicht)


Lesenswert?

Genau den PID Regler : DoComputation() macht man im Main. Angestossen 
durch den Timer. Sollte diese Routine mangels Zeit einmal nicht 
angestossen werden, ist das auch nicht weiter schlimm, sofern die 
Updaterate ein stueck oberhalb der Reglerbandbreite ist.

Und Float fuer einen PID Regler ist meist voellig unnoetig. Ich rechne 
in ADC Koordinaten, oder transformierten ADC Koordinaten fuer eine 
Regelung, mit der Berechung in 32 bit integer.
Allenfalls muss ein extrem nichtlinearer Sensor per float linearisiert 
werden. Trotzdem laeuft der Regler in 32bit integer.

Weshalb mit 32 bit integer ?

- 32bit integer hat 9 signifikante stellen, waehrend 32bit float
  nur 5 signifikante Stellen hat.
- Das Stellglied hat sowieso einen sehr kleinen Integer bereich.
  Dh das Resulat des Regelalgorithmuses muss nur noch per
  Schieben herunterskaliert werden.

von M. K. (sylaina)


Lesenswert?

Manfred L. schrieb:
> Jetzt meine Frage:
> Welche Methode ist die "Richtige", was immer "Richtig" auch heisst ?

Ist von der Situation abhängig. Die ISR sollte immer so kurz wie möglich 
sein. Nehmen wir z.B. die UART-RX-ISR: Die muss mindestens das 
empfangene Zeichen abspeichern. Je nach Programmkomplexibilität kann man 
hier nun ein Flag setzen und das Abspeichern des Zeichens im Mainloop 
machen oder man speichert das Zeichen gleich in der ISR korrekt ab und 
erst in der Mainloop wertet man aus oder man macht schlicht alles in der 
ISR. Es kommt hierbei schlicht darauf an was es noch so zu tun gibt. 
(ich mache idR immer die 2. Option hierbei: Zeichen in der ISR 
abspeichern, auswerten in der Mainloop).
In der Regel wird es wohl so sein: Je zeitkritischer das Programm wird 
desto kürzer wird man die ISRs gestalten und desto mehr Arbeit 
verschiebt man in die Mainloop.

von m.n. (Gast)


Lesenswert?

Manfred L. schrieb:
> wenn sicher gestellt ist, dass das Intervall zwischen den
> Interrupts vieeeeeeeel größer ist als die Prozessdauer des ausgelagerten
> nichtexklusiven Teils.

Nein, das Intervall muß nur hinreichend größer sein.
Es ist kein Problem, in einer Timer-ISR, die im 10 ms Abstand aufgerufen 
wird, 5 ms für die Abarbeitung der zeitkritischen Aufgaben zu nutzen. 
Wichtig ist nur, daß man in der ISR andere Interruptquellen gleich 
wieder zuläßt, um kurze Unterbrechungen z.B. der USART zu ermöglichen. 
Damit können alle zeitkritischen Aufgaben eines Programmes im 
Hintergrund laufen.

Auch habe ich kein Problem damit, in einer ISR float zu rechnen, sofern 
dies notwenig ist. Andere sind mit ISRs wohl deutlich zu ängstlich.

Oh D. schrieb:
> waehrend 32bit float nur 5 signifikante Stellen hat.

Ach nee :-(
Jetzt geht das wieder los. Neben den signifikanten Stellen (<=7) haben 
float-Zahlen auch einen erheblich höheren Dynamikbereich.

von Pandur S. (jetztnicht)


Lesenswert?

> Ach nee :-( Jetzt geht das wieder los. Neben den signifikanten Stellen (<=7) 
haben float-Zahlen auch einen erheblich höheren Dynamikbereich.

Es geht nicht wieder los ... wenn es eben nicht verstanden wurde.
Und du braucht einen groesseren Bereich wie 10^9 ? Um mit Mega und Pico 
zu rechnen ? Muss man das ? Zeig mal.

Bei mit ist eine Temperatur vielleicht 24 bit, weil der wandler soviel 
bringt. Brauchen tue ich davon vielleicht 16bit. Ich brauche keine 
Milikelvin, rechne nicht in miliKelvin.
Ich brauche auch keine miliWatt oder Kilowatt, sondern 10bit fuers 
Stellglied, und berechne die auch direkt.

: Bearbeitet durch User
von Klaus (Gast)


Lesenswert?

m.n. schrieb:
> Nein, das Intervall muß nur hinreichend größer sein.
> Es ist kein Problem, in einer Timer-ISR, die im 10 ms Abstand aufgerufen
> wird, 5 ms für die Abarbeitung der zeitkritischen Aufgaben zu nutzen.

Im Einzelfall darf man sogar mal 15ms verbrauchen, das verschiebt den 
Interrupt nur, verloren geht er nicht.

Meine Lieblings-Mainloop ist leer, enthält höchstens ein sleep(). Alles 
andere läuft im Interrupt, bevorzugt im Timer. Auch von da kann man die 
Interruptflags der übrigen Hardware passend abfragen.

MfG Klaus

von m.n. (Gast)


Lesenswert?

Oh D. schrieb:
> Ich brauche auch keine miliWatt oder Kilowatt, sondern 10bit fuers
> Stellglied.

Watt auch immer Du willst, mache es so.
Ich bin nicht so bescheiden ;-)

von Axel S. (a-za-z0-9)


Lesenswert?

M. K. schrieb:
> Manfred L. schrieb:
>> Jetzt meine Frage:
>> Welche Methode ist die "Richtige", was immer "Richtig" auch heisst ?
>
> Die ISR sollte immer so kurz wie möglich sein.

Nein. Sie sollte so kurz wie nötig sein. Und um das beurteilen zu 
können, muß man die Gesamtsituation kennen. Die wichtigsten Punkte 
wurden ja schon angesprochen:

- während die ISR läuft, kann (auf dem AVR, ohne Tricks) keine andere 
ISR ausgelöst werden. Zwischenzeitlich eintreffende Interrupts werden 
verzögert barbeitet

- wenn ein anderer Interrupt öfter als einmal auslöst, während unsere 
ISR läuft, werden Interrupts übersehen

- in jedem Fall sollte die ISR fertig sein, bevor "ihr" Interrupt das 
nächste Mal auslöst

> In der Regel wird es wohl so sein: Je zeitkritischer das Programm wird
> desto kürzer wird man die ISRs gestalten und desto mehr Arbeit
> verschiebt man in die Mainloop.

Würde ich so nicht unterschreiben. Denn auch die Synchronisierung 
zwischen ISR und Mainloop kriegt man ja nicht zum Nulltarif. Man braucht 
extra RAM für die volatile Variablen, diese Variablen müssen behandelt 
(gesetzt, abgefragt) werden. Oft erzeugt der Compiler sehr umständlichen 
Code, wenn volatile Variablen gelesen werden etc.

Unter dem Strich braucht man mehr Taktzyklen, wenn man die Arbeit 
zwischen ISR und Mainloop aufteilt, als wenn man sie nur an einer Stelle 
macht. Der Extremfall wurde schon von einem Vorposter genannt: wenn die 
ISR wirklich nur ein Flag setzt, das dann in der Mainloop ausgewertet 
wird, dann braucht man auch gar keine ISR. Dann kann die Mainloop 
genauso gut auch das Pending-Bit des Interrupts direkt abfragen. Es 
sollte unmittelbar klar sein, daß man damit Taktzyklen spart.

Ich gehe das Problem üblicherweise von der anderen Seite her an: alles, 
was spezifisch zu diesem Interrupt gemacht werden muß, kommt in die ISR. 
Und nur, wenn das (unter Berücksichtigung der o.g. Punkte) zu einem 
Timing-Problem führt, wird Code in die Mainloop ausgelagert.

von Peter D. (peda)


Lesenswert?

m.n. schrieb:
> Neben den signifikanten Stellen (<=7) haben
> float-Zahlen auch einen erheblich höheren Dynamikbereich.

Ja, mit float regelt es sich deutlich besser und genauer. Man muß nicht 
ständig rumskalieren und kann in einem weiten Bereich regeln ohne 
Rundungsfehler oder Überlauf. Damit regeln sich auch sehr kleine 
I-Anteile aus ohne bleibende Regelabweichung.
Z.B. bei einer Elektronenquelle muß ich den Emissionsstrom von 10nA .. 
250µA regeln können. Den Meß-ADC mußte ich in 3 Bereiche aufteilen, der 
Regler selber schafft den gesamten Bereich.
Da float nur mit 24Bit Mantisse rechnet, ist es auch nicht langsamer als 
32Bit int.

: Bearbeitet durch User
von Falk B. (falk)


Lesenswert?

@ Peter Dannegger (peda)

>Ja, mit float regelt es sich deutlich besser und genauer.

Wenn man es WIRKLICH braucht.

>Z.B. bei einer Elektronenquelle muß ich den Emissionsstrom von 10nA ..
>250µA regeln können. Den Meß-ADC mußte ich in 3 Bereiche aufteilen, der
>Regler selber schafft den gesamten Bereich.

Das ist mal sicher nicht der Normalfall sondern eher exotisch.

>Da float nur mit 24Bit Mantisse rechnet, ist es auch nicht langsamer als
>32Bit int.

Man muss Fließkommazahlen auch nicht verteufeln, sie haben in vielen 
Fällen ihre Berechtigung und Vorteile. Aber man sollte schon mal ein 
paar Minuten "verschwenden" und darüber nachdenken, ob man ein 
bestimmtes Problem besser mit Fest- oder Fließkommazahlen zu lösen ist.

von Klaus (Gast)


Lesenswert?

Falk B. schrieb:
> Aber man sollte schon mal ein
> paar Minuten "verschwenden" und darüber nachdenken, ob man ein
> bestimmtes Problem besser mit Fest- oder Fließkommazahlen zu lösen ist.

Insbesondere da die meisten Sensoren (ADC, Timer, ...) Integerwerte 
liefern.

MfG Klaus

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.