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_tmain(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
voidDoComputation(void)
18
{
19
staticuint8_ti=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_tiFlag=false;
28
uint8_tmain(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
voidDoComputation(void)
42
{
43
staticuint8_ti=0;
44
if(iFlag)
45
{
46
i++;// This is the computation to be done every TIMER0 Overflow
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.
@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.
... 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.
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
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.^^
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...
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:
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.
@ 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.
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 ;-)
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.
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.
> 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.
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.
@ 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.
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?
> 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.
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!
@ 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.
@ 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.
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!
@ 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.
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.
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...
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.
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.
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.
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.
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.
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.
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.
> 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.
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
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 ;-)
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.
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.
@ 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.
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