Hi Leute,
ich habe mir eine hübsche Klasse geschrieben, die es mir ermöglicht bei
OOP-Projekten komfortabler mit Interrupts umgehen zu können.
Zunächst mal die Klasse selbst. Sie ist aufgesplittet in eine .h- und
eine .cpp-Datei.
InterruptHelper.h
1
/*
2
* InterruptHelper.h
3
*
4
* Created on: 30.07.2014
5
* Author: nicolas
6
*/
7
8
#ifndef INTERRUPTHELPER_H_
9
#define INTERRUPTHELPER_H_
10
11
#include<avr/io.h>
12
#include<avr/interrupt.h>
13
#include<stdlib.h>
14
15
#if (_VECTORS_SIZE / _VECTOR_SIZE > 255)
16
typedefuint16_tINTERRUPT_NUM_t;
17
#else
18
typedefuint8_tINTERRUPT_NUM_t;
19
#endif
20
21
classInterrupt;
22
23
// Eine Art LinkedList, falls mehrere Instanzen auf einen Interrupt reagieren sollen.
interruptFunc_t* e = &interruptFunctions[name##_num]; \
14
if (e->interrupt) { \
15
e->interrupt->interrupt(); \
16
while (e->next) { \
17
e = e->next; \
18
if (e->interrupt) { \
19
e->interrupt->interrupt(); \
20
} \
21
} \
22
} \
23
} \
24
25
26
/*
27
* Im folgenden Abschnitt einfach alle Intrrupts deklarieren, die später mal
28
* genutzt werden sollen.
29
*/
30
declareISR(TCD1_OVF_vect)
31
declareISR(TCD1_ERR_vect)
32
33
// Nachfolgend ein Beispiel
34
#if false
35
classInterruptExample:Interrupt{
36
InterruptExample(){
37
addToInterrupt(TCD1_OVF_vect_num);
38
}
39
40
voidinterrupt(){
41
/* Diese Methode wird ausgeführt, sobald der
42
* entsprechende Interrupt ausgeführt wird.
43
*/
44
}
45
};
46
47
48
/* Nachfolgend ein Beispiel wie man innerhalb einer Klasse
49
* auf unterschiedliche Interrupts reagieren kann.
50
*/
51
classMyClass{
52
classInterrupt1:publicInterrupt{
53
MyClass*parent;
54
55
Interrupt1(MyClass*_parent):parent(_parent){
56
addToInterrupt(TCD1_OVF_vect_num);
57
}
58
59
voidinterrupt(){
60
parent->interrupt1();
61
}
62
};
63
64
classInterrupt2:publicInterrupt{
65
MyClass*parent;
66
67
Interrupt2(MyClass*_parent):parent(_parent){
68
addToInterrupt(TCD1_ERR_vect_num);
69
}
70
71
voidinterrupt(){
72
parent->interrupt2();
73
}
74
};
75
76
Interrupt1*int1;
77
Interrupt2*int2;
78
79
MyClass(){
80
int1=newInterrupt1(this);
81
int2=newInterrupt2(this);
82
}
83
84
voidinterrupt1(){
85
// Wird aufgerufen bei TCD1_OVF_vect
86
}
87
voidinterrupt2(){
88
// Wird aufgerufen bei TCD1_ERR_vect
89
}
90
};
91
#endif
In der .cpp-Datei befinden sich auch zwei ausgeklammerte Beispiele, die
verdeutlichen sollen wie die Klasse funktioniert.
Hat man also eine Klasse geschrieben, die auf genau einen Interrupt
reagieren soll, so erbt man einfach von der Klasse 'Interrupt',
definiert den Interrupt, auf den man reagieren will, im Konstruktor und
überschreibt dann die Methode 'void interrupt()'.
Möchte man innerhalb einer Klasse auf mehr als einen Interrupt reagieren
können, muss man Unterklassen erstellen, die wiederum die Methoden aus
der Hauptklasse aufrufen.
Es ist keine gute Idee einfach alle Interrupt zu deklarieren, die es
gibt. Also man sollte nicht einfach alle möglichen
'declareISR()'-Varianten einbauen, die man finden kann, da das zu viel
Flash-Speicher benötigt. Wenn man den Speicher natürlich hat, spricht
nicht unbedingt etwas dagegen.
Ich hoffe euch gefällt das Konstrukt. Über Kritik und
Verbesserungsvorschläge freue ich mich aber trotzdem. Ich hoffe hier
entfacht jetzt keine Diskussion darüber wie sinnvoll es ist mit OOP auf
einem AVR zu hantieren.
Nicolas G. schrieb:> ich habe mir eine hübsche Klasse geschrieben, die es mir ermöglicht bei> OOP-Projekten komfortabler mit Interrupts umgehen zu können.
Du hast einen Fehler gemacht...
> Es ist keine gute Idee einfach alle Interrupt zu deklarieren, die es> gibt. Also man sollte nicht einfach alle möglichen> 'declareISR()'-Varianten einbauen, die man finden kann, da das zu viel> Flash-Speicher benötigt.
Und das zeigt ihn überdeutlich...
Mehr ist dazu eigentlich nicht zu sagen.
OO ist nett (für den Programmierer) auf fetten Systemen mit viel
Reserven. Ja ich gebe zu, daß auch ich auf solchen Zielsystemen
größtenteils reinen OO-Code produziere (allerdings nur unter
fürchterlichem Zwang in C++, wenn schon gemütlich, dann richtig
gemütlich: also "managed" Code).
Aber auf Systemen mit eng begrenzten Resourcen knallt der Overhead
einfach viel zu stark rein. Und das nicht nur bezüglich des
Speicherbedarfs. Mach einfach mal eine Analyse bezüglich des
Rechenzeitbedarfs deines OO-Konstruktes. DEN Overhead braucht man echt
nicht, ganz sicher jedenfalls nicht in ISRs. Da ist oft genug schon der
Overhead der Runtime von plain C schon Show-Stopper.
Nicolas G. schrieb:> Ich hoffe hier> entfacht jetzt keine Diskussion darüber wie sinnvoll es ist mit OOP auf> einem AVR zu hantieren.
Oh ja, die wär aber bitter nötig... Natürlich aus Performance- und
Platzgründen. Und als Asm-Progger stell ich mir natürlich auch gleich
die Frage, warum es komplizierte Konstrukte wie diese hier für
Interrupts braucht, wo solche doch oft aus nur wenigen Assemblerzeilen
bestehen (können).
Ich kenne die Nachteile. Aber bei unserem Quadcopter sind die größten
Geschwindigkeitsfresser die Fließkommaberechnungen. Die Klassen werden
scheinbar gut umgesetzt. Wobei natürlich keine Lust hatte das mit einer
reinen C-Version zu vergleichen. Immerhin müsste ich dann alles
umschreiben.
Das einzige, was noch in C geschrieben ist, ist die main-Methode. Die
wird aber am Ende eh noch mal aufgeräumt.
Falls es dich interessiert: https://github.com/NicolasGoeddel/Quadcopter
Nicolas G. schrieb:> Aber bei unserem Quadcopter sind die größten> Geschwindigkeitsfresser die Fließkommaberechnungen. Die Klassen werden> scheinbar gut umgesetzt.
Na das zeigt ja wenigstens wieder mal die Leistungsfähigkeit der
AVR/Xmegas ;-)
Nicolas G. schrieb:> bei unserem Quadcopter sind die größten> Geschwindigkeitsfresser die Fließkommaberechnungen.
Dann wäre es doch sinnvoll, den Fließkommascheiß auszumerzen. Das ist in
aller Regel nur eine Krücke für Programmierer, die zu faul oder zu doof
sind, die Wertebereiche ihrer Algorithmen gut genug abschätzen zu
können.
Aber OK, es gibt Ausnahmen von dieser Regel. Wenn du mir nachweist, daß
du in deinem Copter zwingend einen solchen Algorithmus mit nicht
abschätzbarem Wertebereich verwenden mußt, dann darfst du ungestört
weiter mit dem Werkzeug der Dummen und Faulen arbeiten...
Um aber auf's Thema zurückzukommen: ISRs bringen bei hinreichend
häufigem Aufruf der ISR absolut *jedes* System zum Erliegen. Und
selbst wenn die Aufruffrequenz nicht derart hoch ist: Jeder in einer ISR
sinnlos verschwendete Takt fehlt genau mit der Häufigkeit ihres Aufrufs
an jeder anderen Stelle des Systems.
Also: Nur 10 Takte in einer ISR sinnlos verschwendet und diese ISR
100000mal pro Sekunde aufgerufen, mindert den effektiv nutzbaren
Systemtakt schon um ein ganzes 1MHz.
Das kann doch nicht so schwer zu verstehen sein, oder?
Nicolas G. schrieb:> Ich kenne die Nachteile.
ist ja nicht nur das C++. Schon das malloc zieht haufenweise Probleme
nach sich. So mal du nicht mal eine Fehlerbehandlung eingebaut hast.
Und wie wahrscheinlich ist es wohl das man Interrupts zur Laufzeit ein
oder aushängt?
Wenn überhaupt sollte man das als Template und alles static umsetzen,
dann dürfte der Overhead sehr gut zu vermeiden sein.
Auch die Stabilität vom removeFromInterrupt wage ich zu bezweifeln, wenn
gleichzeitig auch Interrupts auftreten.
Nicolas G. schrieb:> ich habe mir eine hübsche Klasse geschrieben, die es mir ermöglicht bei> OOP-Projekten komfortabler mit Interrupts umgehen zu können.
Kannst Du mal erläutern, worin der Komfort besteht.
Warum soll ich erst eine Zwischeninstanz schreiben müssen und nicht
einfach gleich:
1
ISR(PCINT1_vect)
2
{
3
// Code
4
}
Ich vermute mal stark, daß diese ganze Pointerei nicht zur Compilezeit
aufgelöst werden kann und daher ein erheblicher Overhead an Flash, RAM
und CPU-Belastung entsteht.
Nicolas G. schrieb:> while (e->next) {> e = e->next;> }> e->next = (interruptFunc_t*) malloc(sizeof(interruptFunc_t));> e->next->interrupt = this;> e->next->next = 0;
Füg doch am Anfang der Liste ein, das ist dann O(1) und nicht O(n).
Nicolas G. schrieb:> e->next = (interruptFunc_t*) malloc(sizeof(interruptFunc_t));
Warum "malloc" und nicht "new"?
Was auch interessant wäre, statt virtueller Funktionen std::function zu
verwenden. Dann könnte man auch Lambdas hinzufügen, zB so:
1
intmain(){
2
Interrupts::add(TCD1_ERR_vect_num,[](){/* do something */});
3
}
Dazu müsste diese add Funktion ein std::function<void()> entgegennehmen
und in die Linked List einfügen. Beim Interrupt dann die Liste
durchgehen und den operator() auf den std::function Objekten aufrufen.
c-hater schrieb:>> bei unserem Quadcopter sind die größten>> Geschwindigkeitsfresser die Fließkommaberechnungen.>> Dann wäre es doch sinnvoll, den Fließkommascheiß auszumerzen. Das ist in> aller Regel nur eine Krücke für Programmierer, die zu faul oder zu doof> sind, die Wertebereiche ihrer Algorithmen gut genug abschätzen zu> können.>> Aber OK, es gibt Ausnahmen von dieser Regel. Wenn du mir nachweist, daß> du in deinem Copter zwingend einen solchen Algorithmus mit nicht> abschätzbarem Wertebereich verwenden mußt, dann darfst du ungestört> weiter mit dem Werkzeug der Dummen und Faulen arbeiten...
Zeig mir schöne Routinen, mit denen mein atan schneller wird und genau
genug bleibt. Außerdem hätte ich dann gerne noch Routinen, die den
Komplementärfilter und die PID-Regelung sauber ohne Fließkommazahlen
berechnen.
Und da wir schon bei Beleidigungen angekommen sind: Du bist wohl selber
dumm, wenn du so eine Gülle schreibst.
Schau dir einfach mal andere Quadrocopter-Projekte an. Die nutzen auch
alle floats. Außerdem habe ich ja noch Rechenzeit übrig. Der Xmega darf
ruhig arbeiten. Der muss nicht idlen.
Peter II schrieb:> Nicolas G. schrieb:>> Ich kenne die Nachteile.>> ist ja nicht nur das C++. Schon das malloc zieht haufenweise Probleme> nach sich. So mal du nicht mal eine Fehlerbehandlung eingebaut hast.
Es sind noch nicht alle Dinge abgesichert. Aber ich bin hier ja auch
sozusagen noch in der Alpha-Phase.
Dr. Sommer schrieb:> Nicolas G. schrieb:>> while (e->next) {>> e = e->next;>> }>> e->next = (interruptFunc_t*) malloc(sizeof(interruptFunc_t));>> e->next->interrupt = this;>> e->next->next = 0;> Füg doch am Anfang der Liste ein, das ist dann O(1) und nicht O(n).
*Kopf->Tisch* Du hast natürlich Recht. Lag wohl an der späten Uhrzeit.
:D Hintergrund war wahrscheinlich, dass ich wollte, dass die Interrupts
auch in der Reihenfolge ausgeführt werden, in der sie sich eingeklingt
haben.
Und bezüglich der Lambdas: Das habe ich so noch nie gemacht. Klingt aber
interessant, wenn das mit avr-g++ geht.
Peter Dannegger schrieb:> Kannst Du mal erläutern, worin der Komfort besteht.>> Warum soll ich erst eine Zwischeninstanz schreiben müssen und nicht> einfach gleich:ISR( PCINT1_vect )> {> // Code> }> Ich vermute mal stark, daß diese ganze Pointerei nicht zur Compilezeit> aufgelöst werden kann und daher ein erheblicher Overhead an Flash, RAM> und CPU-Belastung entsteht.
Eine statische Funktion weiß nichts über die Datenstruktur, auf der sie
arbeiten soll. Man müsste diese also auch global machen. Von der
Interrupt-Klasse oben wird einem das abgenommen und man hat automatisch
Zugriff auf alle Klassen-Attribute und weitere Methoden.
Moby schrieb:> Nicolas G. schrieb:>> Aber bei unserem Quadcopter sind die größten>> Geschwindigkeitsfresser die Fließkommaberechnungen. Die Klassen werden>> scheinbar gut umgesetzt.>> Na das zeigt ja wenigstens wieder mal die Leistungsfähigkeit der> AVR/Xmegas ;-)
Naja, was willst du ohne FPU anders machen. ;) Mit reiner
Ganzzahlarithmetik komme ich nicht besonders weit. Das habe ich damals
schon mit dem PID-Regler versucht und die Wertebereiche waren einfach zu
groß. Das nimmt mir der float-Typ eben alles ab.
Falls es interessiert: Im Anhang ist der grobe Ablauf der Regelung vom
Quadcopter drin.
Nicolas G. schrieb:> Zeig mir schöne Routinen, mit denen mein atan schneller wird und genau> genug bleibt.
welche Winkel können denn vorkommen?
fixe Tabelle bietet sich doch an. Bis 1/10 Grad ist das doch kein
Problem.
Peter II schrieb:> Nicolas G. schrieb:>> Zeig mir schöne Routinen, mit denen mein atan schneller wird und genau>> genug bleibt.>> welche Winkel können denn vorkommen?>> fixe Tabelle bietet sich doch an. Bis 1/10 Grad ist das doch kein> Problem.
Für Tangens würde sich eine Lookup-Table anbieten, aber bei ArcusTangens
ist es problematisch, da der Definitionsbereich von -Unendlich bis
Unendlich geht. Bei Zwischenwerten kann man sehr schlecht auf eine
Tabelle mappen, da das keine lineare Funktion ist. Würde man jetzt die
Lookup-Tabelle mit gleichen Abständen zwischen den Werten erstellen,
dann wäre es im niedrigen Bereich sehr ungenau und im oberen sehr genau.
Und beim Quadcopter können durchaus alle Werte vorkommen.
Abgesehen davon ist die bereits vereinfachte Atan-Funktion sehr klein.
Natürlich wird hier auch nur das Polynom 3. Grades genommen, aber es ist
genauer als eine Lookup-Table.
1
floatmyAtan(floatx){
2
if(x>1.0){
3
returnM_PI_2-(x/(x*x+0.28));
4
}elseif(x<-1.0){
5
return-M_PI_2-(x/(x*x+0.28));
6
}
7
returnx/(1+0.28*x*x);
8
}
Oder wie würdest du es mit einer Lookup-Tabelle machen?
Nicolas G. schrieb:> Oder wie würdest du es mit einer Lookup-Tabelle machen?
ich hatte nur tan gelesen, für atan scheint deine Formel schon recht
kompakt zu sein.
Nicolas G. schrieb:> Von der> Interrupt-Klasse oben wird einem das abgenommen und man hat automatisch> Zugriff auf alle Klassen-Attribute und weitere Methoden.
Ich muß zugeben, von C++ verstehe ich nur Bahnhof.
Wenn ich das richtig sehe, soll dieses Klassendingens nur die Variablen
zusammen fassen, die der Interrupt und damit zusammen arbeitende
Funktionen benutzen. Ich kann mir nur schwer vorstellen, daß es in C++
keinen besseren Weg geben sollte ohne diesen riesen Overhead.
Vielleicht kannst Du ja mal ein konkretes Beispiel mit Variablen zeigen,
wie das mit den Klassen gedacht ist, z.B. UART Interrupt mit FIFO, init,
putchar und getchar.
Peter Dannegger schrieb:> Ich kann mir nur schwer vorstellen, daß es in C++> keinen besseren Weg geben sollte ohne diesen riesen Overhead.
Ja, indem man den "next" Pointer direkt in die "Interrupt" Klasse packt,
und sich somit das extra struct und den 2. malloc Aufruf spart.
Dr. Sommer schrieb:> Peter Dannegger schrieb:>> Ich kann mir nur schwer vorstellen, daß es in C++>> keinen besseren Weg geben sollte ohne diesen riesen Overhead.> Ja, indem man den "next" Pointer direkt in die "Interrupt" Klasse packt,> und sich somit das extra struct und den 2. malloc Aufruf spart.
Das Problem ist eben der einmalige Aufruf im globalen Scope von
'declareISR'. Der erstellt erst die ISR-Funktion. Wenn ich den
next-Pointer in die Interrupt-Klasse packe, dann weiß eine weitere
Instanz nichts mehr von der ersten und kann sich nicht dahinter hängen.
Oder wie hast du das gemeint?
Peter Dannegger schrieb:
> Ich kann mir nur schwer vorstellen, daß es in C++> keinen besseren Weg geben sollte ohne diesen riesen Overhead.
Naja, Templates sind schon erfunden. Und declareISR() ist, ähm, auch
weit entfernt vom Optimum. Die hier vorgestellte Lösung ist eben, sagen
wir mal, etwas infantil.
Die wichtigste Frage wurde weiter oben schon gestellt: Ist es wirklich
nötig, auf einem Mikrocontroller Interrupt-Handler dynamisch zur
Laufzeit zu ändern?
Die zweite Frage könnte lauten, muss das (Mini-) Framework wirklich
mehrere Handler pro Interrupt unterstützen?
Es gibt viele Überlegungen, die hier augenscheinlich nicht gemacht
wurden. Klar, es funktioniert für den Ersteller erstmal soweit. Elegant
und wiederverwendbar ist aber anders.
Nicolas G. schrieb:> Im Übrigen hat hier im Wiki schon einmal jemand etwas gemacht bezüglich> Interrupts und OOP. Allerdings erschien mir das viel zu kompliziert und> am Ziel vorbei.
NIH-Syndrom?
Ex-C++-Programmierer schrieb:> Peter Dannegger schrieb:>> Ich kann mir nur schwer vorstellen, daß es in C++>> keinen besseren Weg geben sollte ohne diesen riesen Overhead.>> Naja, Templates sind schon erfunden. Und declareISR() ist, ähm, auch> weit entfernt vom Optimum. Die hier vorgestellte Lösung ist eben, sagen> wir mal, etwas infantil.
Templates nutze ich an einigen anderen Stellen auch, also sind mir
bekannt. Aber mir ist so auf die Schnelle nicht eingefallen wie ich die
an dieser Stelle geschickt einsetzen könnte. Ich würde gerne den ISR
erst an der Stelle erstellen, an der er auch benutzt wird. Soll heißen,
wenn ich nirgendwo eine Klasse habe, die einen bestimmten Interrupt
benutzen möchte, dann soll auch nicht die entsprechende ISR-Funktion
erstellt werden. Momentan muss man eben mit 'declareISR()' jeden ISR
zunächst erstellen, die irgendwo anders mal benutzt werden soll.
> Die wichtigste Frage wurde weiter oben schon gestellt: Ist es wirklich> nötig, auf einem Mikrocontroller Interrupt-Handler dynamisch zur> Laufzeit zu ändern?
Vermutlich nicht. Zur Compile-Zeit reicht ja schon. Zur Laufzeit geht es
ja auch gar nicht. Man kann ihn nur während der Compile-Zeit erstellen
und dann währender Laufzeit entscheiden, ob man ihn benutzen möchte oder
nicht. Bei manchen Interrupts hat man dann eben das Problem, dass
bestimmte Bits gesetzt werden, wenn er ausgeführt wurde, auch wenn man
ihn nicht wirklich nutzt. Das heißt man muss schon genau wissen, ob man
'declareISR(x)' wirklich braucht oder nicht und das händisch festlegen.
> Die zweite Frage könnte lauten, muss das (Mini-) Framework wirklich> mehrere Handler pro Interrupt unterstützen?
Das ist recht unwahrscheinlich, aber ich wollte mir die Möglichkeit mal
offen halten. Und eigentlich spricht ja auch nicht viel dagegen, oder?
> Es gibt viele Überlegungen, die hier augenscheinlich nicht gemacht> wurden. Klar, es funktioniert für den Ersteller erstmal soweit. Elegant> und wiederverwendbar ist aber anders.
Ich lerne ja gerne dazu. Aber elegant im Sinne der Objektorientierung
ist es doch eigentlich schon, oder etwa nicht?
Ex-C++-Programmierer schrieb:>> Im Übrigen hat hier im Wiki schon einmal jemand etwas gemacht bezüglich>> Interrupts und OOP. Allerdings erschien mir das viel zu kompliziert und>> am Ziel vorbei.>> NIH-Syndrom?
Nicht wirklich. Ich erfinde zwar gerne mal ein Rad neu, aber die
Möglichkeit wie sie im Wiki beschrieben ist, hat mir wegen der vielen
Code-Dopplungen nicht so recht gefallen. Aber naja, im Grunde kann man
da wohl wieder weiter drüber streiten. ;)
Was passiert eigentlich, wenn der Interrupt freigegeben wird, bevor die
Interruptklasse aufgerufen wurde, bzw. nach Aufruf von
removeFromInterrupt.
Wird dann der BADISR_vect ausgeführt oder ein RETI?
Peter Dannegger schrieb:> Was passiert eigentlich, wenn der Interrupt freigegeben wird, bevor die> Interruptklasse aufgerufen wurde, bzw. nach Aufruf von> removeFromInterrupt.>> Wird dann der BADISR_vect ausgeführt oder ein RETI?
Ich verstehe nicht ganz. Der eigentliche ISR besteht ja immer. Ob sich
jetzt eine Interrupt-Instanz nun damit verbunden hat oder nicht, spielt
da keine Rolle.
Oder ging es einfach nur um die Frage, was passiert, wenn man
'removeFromInterrupt()' aufruft, ohne vorher 'addToInterrupt()'
aufzurufen? Das geht nämlich gar nicht erst, weil man die Methode
'removeFromInterrupt()' erst aufrufen kann, wenn eine Instanz von der
Interrupt-Klasse existiert.
In plain C werden alle Handler zur Compilezeit eingetragen und alle
nicht benutzten Vectoren rufen BADISR_vect auf
Mit Deiner Klasse wird aber erst zur Laufzeit der Zeiger initialisiert.
Wohin zeigt er vor dem "addToInterrupt" bzw. nach dem
"removeFromInterrupt"?
Peter Dannegger schrieb:> In plain C werden alle Handler zur Compilezeit eingetragen und alle> nicht benutzten Vectoren rufen BADISR_vect auf
Das ist hier genau so. Die Interrupthandler werden mit
1
declareISR(IRGENDEIN_vect)
festgelegt und zu Laufzeit auch nicht mehr geändert. Beim Eintreffen
eine Interrupts werden die interrupt()-Methoden der mit addToInterrupt()
registrierten Interrupt-Objekte aufgerufen. Sind keine solchen Objekte
registriert, tut der Interruphandler nicht viel mehr als diese Tatsache
festzustellen (das entsprechende Element in interruptFunctions[] ist
dann NULL) und wieder zurückzukehren.
Alle Vektoren, die nicht mit declareISR() definiert werden, werden zu
BADISR_vect.