AVR Interrupt Routinen mit C++: Unterschied zwischen den Versionen

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche
(im Großen und Ganzen fertig)
(Typo)
 
(17 dazwischenliegende Versionen von 4 Benutzern werden nicht angezeigt)
Zeile 3: Zeile 3:
{{Wettbewerb Header}}
{{Wettbewerb Header}}


Einen Mikrocontroller mit C++ zu programmieren scheitert oft daran die Interrupt Routinen in die Klassenhierarchie zu integrieren. Ohne saubere Objektorientierung verliert C++ jedoch schnell an Wert. Zum Beispiel Frameworks um die Hardware zu abstrahieren sind mit den Interrupt Makros der avr-libc zwar machbar, jedoch mit reinem C++ weit flexibler.  
Einen Mikrocontroller mit [[C++]] zu programmieren, scheitert oft daran, die [[Interrupt]]-Routinen in die Klassenhierarchie zu integrieren. Ohne saubere Objektorientierung verliert C++ jedoch schnell an Wert. Zum Beispiel Frameworks um die Hardware zu abstrahieren, sind mit den Interrupt Makros der avr-libc zwar machbar, jedoch mit reinem C++ weit flexibler. Deshalb soll dieser Artikel einen Einblick in die Möglichkeiten des GNU C++ Compilers geben, der einige Stellschrauben besitzt, mit denen sich Interrupts trotz aller Probleme in Klassen, mit Zugriff auf nicht statische Membervariablen, integrieren lassen.
 
Deshalb soll dieser Artikel einen Einblick in die Möglichkeiten des g++ Compilers geben, der einige Stellschrauben besitzt mit denen sich Interrupts trotz aller Probleme in Klassen, mit Zugriff auf nicht statische Membervariablen, integrieren lassen.


== Probleme ==
== Probleme ==
=== name mangling ===
=== name mangling ===
In C++ gibt es Namensräume, daher werden nicht nur die Methodennamen für die Symbolnamen benutzt, sondern unter anderem auch die Klassennamen mit eingebracht. Die Schattenseite dieses Features ist allerdings, dass die Interrupt Makros der avr-libc nicht mehr genutzt werden können. ISR(...) ist eines davon; es wird vom Präprozessor über einige weitere Makros zur Funktion void __vector_n(void) (n = Interruptnummer) aufgelöst.
In C++ gibt es Namensräume, daher werden nicht nur die Methodennamen für die Symbolnamen benutzt, sondern unter anderem auch die Klassennamen mit eingebracht. Die Schattenseite dieses Features ist allerdings, dass die Interrupt-Makros der avr-libc nicht mehr genutzt werden können. Das Makro <code enclose="none" lang="cpp">ISR()</code> ist eines davon. Es wird vom Präprozessor über einige weitere Makros zur mit <code enclose="none" lang="cpp">extern "C"</code> deklarierten Funktion <code enclose="none" lang="cpp">__vector_n()</code> aufgelöst; n steht für die Interruptnummer. Da <code enclose="none" lang="cpp">extern "C"</code> in Klassen nicht zulässig ist, kann der daraus resultierende Quellcode nicht compiliert werden.


=== Zugriff auf nicht statische Membervariablen ===
=== Zugriff auf nicht statische Membervariablen ===
Da Methoden als ersten impliziten Parameter den this Zeiger ihres Objekts bekommen,<ref name="Thinking in C++">Eckel, Bruce: Thinking in C++. Second Edition. Upper Saddle River: Prentice Hall Inc. 2000. S. 429.</ref> Interrupt Handler allerdings keine Parameter haben, müssen gezwungermaßen statische Methoden verwendet werden. Statische Methoden haben allerdings nur auf statische Variablen Zugriff. Dies ermöglicht allerdings nur eine suboptimale Objektorientierung. Beispielsweise soll eine Timer Klasse benutzt werden. Wollte man je ein Objekt für zwei verschiedene Timer erstellen, bräuchte man für jedes eine statische Variable.
Da Methoden als ersten impliziten Parameter den <code enclose="none" lang="cpp">this</code>-Zeiger ihres Objekts erhalten,<ref>Eckel, Bruce: Thinking in C++. Second Edition. Upper Saddle River: Prentice Hall Inc. 2000. S. 429.</ref> Interrupt-Handler allerdings keine Parameter haben können - ohne Aufruf können keine Parameter übergeben werden - müssen gezwungermaßen statische Methoden verwendet werden. Statische Methoden haben allerdings nur auf statische Variablen Zugriff. Dies ermöglicht jedoch nur eine suboptimale Objektorientierung. Als Beispiel soll eine Timer-Klasse dienen: Wollte man je ein Objekt für zwei verschiedene Timer erstellen, bräuchte man für jedes eine statische Variable.


== Lösungen ==
== Lösungen ==
=== das asm Schlüsselwort ===
=== das asm Schlüsselwort ===
Das Schlüsselwort '''asm''' ist nicht nur dazu da inline Assembler zu programmieren, sondern kann auch dazu verwendet werden Symbolnamen zu bestimmen.<ref>http://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Asm-Labels.html</ref> Somit ergibt sich die Möglichkeit das name mangling zu steuern. Nennt man eine statische Methode zum Beispiel __vector_1 gehört sie weiterhin zur Klasse, bekommt aber diesen Symbolnamen. Da die avr-libc eine Interruptvektortabelle anlegt in der ein Sprungbefehl zu __vector_1 für den ersten Interrupt steht wird die Methode auch im Interruptfall aufgerufen.  
Mit dem Schlüsselwort asm kann nicht nur inline [[Assembler]] programmiert werden, es kann auch dazu verwendet werden, Symbolnamen zu bestimmen.<ref>http://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Asm-Labels.html</ref> Somit ergibt sich die Möglichkeit, das name mangling zu steuern. Weist man zum Beispiel einer statischen Methode den Symbolnamen <code enclose="none" lang="cpp">__vector_1</code> zu, so gehört sie weiterhin zur Klasse; es wird nur der Symbolname geändert. Da die avr-libc eine Interruptvektortabelle anlegt, in der ein Sprungbefehl zu <code enclose="none" lang="cpp">__vector_1</code> für den ersten Interrupt steht, wird die Methode auch im Interruptfall aufgerufen.  


=== befreundete Klassen ===
=== befreundete Klassen ===
Zu den pragmatischen Teilen der Sprache gehört das Schlüsselwort '''friend''', da es einer fremden Klasse Zugriff auf private Teile einer anderen Klasse gibt. Um mithilfe von '''friend''' einer Interrupt Routine Zugriff auf nicht statische Membervariablen zu geben kann eine geschachtelte befreundete Klasse angelegt werden, die eine statische Zeigervariable enthält.<ref>http://www.embedded.com/design/embedded/4023817/Interrupts-in-C-</ref> Wird mithilfe einer weiteren statischen Methode dieser Klasse der this Zeiger der äußeren Klasse gespeichert, kann durch die Interrupt Methode auf alle Member der äußeren Klasse zugegriffen werden.
Zu den pragmatischeren Teilen der Sprache C++ gehört das Schlüsselwort <code enclose="none" lang="cpp">friend</code>, da es einer fremden Klasse Zugriff auf private Teile einer anderen Klasse gibt. Um mithilfe von friend einer Interrupt Routine Zugriff auf nicht statische Membervariablen zu geben, kann beispielsweise eine geschachtelte befreundete Klasse angelegt werden, die neben der Interrupt Routine eine statische Zeigervariable auf ein Objekt der äußeren Klasse enthält.<ref>http://www.embedded.com/design/embedded/4023817/Interrupts-in-C-</ref> Wird mithilfe einer weiteren statischen Methode dieser Klasse der this Zeiger eines Objekts der äußeren Klasse gespeichert, kann durch die Interrupt Methode auf alle Member der äußeren Klasse zugegriffen werden.


== Implementierungen ==
== Implementierungen ==
=== ohne Elternklasse ===
Um die beiden folgenden Beispiele kompilieren zu können sollten die folgenden Headerdateien eingebunden werden; <code enclose="none" lang="cpp">stdint.h</code> wegen der Verwendung von <code enclose="none" lang="cpp">uint32_t</code>, <code enclose="none" lang="cpp">interrupt.h</code> wegen <code enclose="none" lang="cpp">sei()</code> und <code enclose="none" lang="cpp">io.h</code> wegen der Registermakros.
Eine einfache Implementierung am Beispiel des Timer 2 Overflow Interrupts auf einem AVR ATmega32 könnte wie folgt aussehen:
<syntaxhighlight lang="c">
#include <stdint.h>
#include <avr/interrupt.h>
#include <avr/io.h>
</syntaxhighlight>
 
Die Warnung die beim kompilieren des ersten Beispiels auftritt (beim zweiten in ähnlicher Form) kann ignoriert werden, denn es wird bereits nach Durchlauf des Präprozessors geprüft ob alle Funktionen mit dem <code enclose="none" lang="cpp">__signal__</code>-Attribut mit <code enclose="none" lang="cpp">__vector_</code> beginnen.<ref>http://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html</ref> Der <code enclose="none" lang="cpp">__asm__</code>-Befehl wird allerdings erst nach dem Kompilieren wirksam.
<pre>warning: 'serviceRoutine' appears to be a misspelled signal handler</pre>
 
=== Interrupt Routine in einer geschachtelten Klasse ===
Eine einfache Implementierung am Beispiel des [[Timer]]2 Overflow-Interrupts auf einem [[AVR]] ATmega32 könnte wie folgt aussehen:


Mit __asm__("__vector_5") wird der Symbolname von serviceRoutine() zum Namen des Interrupt Vektors für den Timer 2 Overflow Interrupt. Das Attribut __signal__ teilt dem Compiler mit, dass es sich hier um eine Interrupt Routine handelt, und alle Register gesichert und wiederhergestellt werden müssen. Mithilfe von __used__ wird verhindert, dass der Code wegoptimiert wird, denn es gibt nirgends im C++ Teil des Codes einen konkreten Aufruf der Methode. Damit die serviceRoutine() auch außerhalb der Timer Übersetzungseinheit sichtbar ist, sollte __externally_visible__ dem Quellcode hinzugefügt werden. Anstelle von __signal__ kann auch __interrupt__ verwendet werden um Interrupts sofort nach Eintritt in die Handler Methode wieder zu zulassen, anstatt erst nach deren Ende.<ref>http://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html</ref>
Mit <code enclose="none" lang="cpp">__asm__("__vector_5")</code> wird der Symbolname der Methode <code enclose="none" lang="cpp">serviceRoutine()</code> zum Namen des Interrupt Vektors für den Timer2 Overflow-Interrupt. Das Attribut <code enclose="none" lang="cpp">__signal__</code> teilt dem Compiler mit, dass es sich hier um eine Interrupt-Routine handelt, und alle Register gesichert und wiederhergestellt werden müssen. Mithilfe von <code enclose="none" lang="cpp">__used__</code> wird verhindert, dass der Code wegoptimiert wird, denn es gibt nirgends im C++-Teil des Codes einen konkreten Aufruf der Methode. Damit die <code enclose="none" lang="cpp">serviceRoutine()</code> auch außerhalb der Timer-Übersetzungseinheit sichtbar ist, sollte <code enclose="none" lang="cpp">__externally_visible__</code> dem Quellcode hinzugefügt werden. Anstelle von <code enclose="none" lang="cpp">__signal__</code> kann auch <code enclose="none" lang="cpp">__interrupt__</code> verwendet werden, um den Interrupt sofort nach Eintritt in die Handler-Methode wieder zu verlassen, anstatt erst nach deren Ende.<ref>http://gcc.gnu.org/onlinedocs/gcc-4.7.2/gcc/Function-Attributes.html</ref>
<c>
<syntaxhighlight lang="c">
class timer {
class timer {
volatile uint32_t i;
volatile uint32_t i;


class timerInterrupt {
class timerInterrupt {
static timer *t;
static timer *ownerTimer;
static void serviceRoutine(void) __asm__("__vector_5") __attribute__((__signal__, __used__, __externally_visible__));
static void serviceRoutine() __asm__("__vector_5") __attribute__((__signal__, __used__, __externally_visible__));


public:
public:
static void record(timer *timer);
static void record(timer *ownerTimer);
};
};


Zeile 41: Zeile 49:


public:
public:
timer(void);
timer();
};
};
</c>
</syntaxhighlight>


Der Vollständigkeit halber soll die Initialisierung von t auf Null nicht fehlen.
Die Initialisierung der Zeigervariable <code enclose="none" lang="cpp">ownerTimer</code> auf Null sollte nicht vergessen werden, da es ansonsten zu Linkerfehlern kommen kann.
<c>
<syntaxhighlight lang="c">
timer *timer::timerInterrupt::t = 0;
timer *timer::timerInterrupt::ownerTimer = 0;
</c>
</syntaxhighlight>


Die Methode record() speichert lediglich ihren Parameter in die statische Zeigervariable t.
Die folgende Methode wird gebraucht um ein <code enclose="none" lang="cpp">timer</code>-Objekt für die Interrupt-Routine zu registrieren.
<c>
<syntaxhighlight lang="c">
void timer::timerInterrupt::record(timer *timer) {
void timer::timerInterrupt::record(timer *t) {
t = timer;
ownerTimer = t;
}
}
</c>
</syntaxhighlight>


Dies ist die Interrupt Routine. Um keinen Speicherzugriffsfehler zu erzeugen, falls der Interrupt auftritt bevor t auf ein Objekt der äußeren Klasse zeigt, wird t gegen Null gerüft. Zur Demonstration, dass der Zugriff möglich ist, wird i vom Objekt der timer Klasse um Eins erhöht.
Dies ist die Interrupt-Routine. Um keinen Speicherzugriffsfehler zu erzeugen, falls der Interrupt auftritt bevor <code enclose="none" lang="cpp">ownerTimer</code> auf ein Objekt der äußeren Klasse zeigt, wird der Zeiger gegen Null geprüft. Zur Demonstration, dass der Zugriff möglich ist, wird <code enclose="none" lang="cpp">i</code> vom Objekt der <code enclose="none" lang="cpp">timer</code>-Klasse erhöht.
<c>
<syntaxhighlight lang="c">
void timer::timerInterrupt::serviceRoutine(void) {
void timer::timerInterrupt::serviceRoutine() {
if(t != 0)
if(ownerTimer != 0)
++t->i;
++ownerTimer->i;
}
}
</c>
</syntaxhighlight>


Im Konstruktor der timer Klasse wird zuerst der this Zeiger der inneren Klasse bekannt gemacht. Dann wird der Prescaler von Timer 2 auf 1024 eingestellt, und somit der Timer gestartet. In der dritten Zeile wird der Overflow Interrupt aktiviert um danach Interrupts global zu aktivieren.
Im Konstruktor der <code enclose="none" lang="cpp">timer</code>-Klasse wird zuerst der <code enclose="none" lang="cpp">this</code>-Zeiger der inneren Klasse bekannt gemacht. Dann wird der Prescaler von Timer2 auf 1024 eingestellt, und somit der Timer gestartet. In der dritten Zeile wird der Overflow-Interrupt aktiviert um danach Interrupts global zu aktivieren.
<c>
<syntaxhighlight lang="c">
timer::timer(void) : i(0) {
timer::timer() : i(0) {
timerInterrupt::record(this);
timerInterrupt::record(this);
         TCCR2 |= (1 << CS20) + (1 << CS21) + (1 << CS22);
         TCCR2 |= (1 << CS20) | (1 << CS21) | (1 << CS22);
         TIMSK |= 1 << TOIE2;
         TIMSK |= 1 << TOIE2;
sei();
sei();
}
}
</c>
</syntaxhighlight>


Die folgende Warnung kann ignoriert werden, denn es wird bereits nach Durchlauf des Präprozessors geprüft ob alle Funktionen mit dem __signal__ Attribut mit __vector_ beginnen.<ref>http://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html</ref> Der __asm__ Befehl wird allerdings erst nach dem kompilieren wirksam.
=== mit Interrupt Elternklasse ===
<pre>warning: 'serviceRoutine' appears to be a misspelled signal handler</pre>
Eine [[Interrupt]]-Elternklasse erhöht die Flexibilität des Interrupt-Handlings enorm, allerdings auf Kosten des [[RAM]] und [[Flash]] Speichers.
 
Die abstrakte Klasse <code enclose="none" lang="cpp">interrupt</code> enthält ein statisches Array von Zeigern auf die <code enclose="none" lang="cpp">interrupt</code>-Kindklassen. Die Methode <code enclose="none" lang="cpp">serviceRoutine()</code> sollte in den Kindklassen überladen werden.<ref>http://www.embedded.com/design/embedded/4023817/2/Interrupts-in-C-</ref> Dann folgen 20 Interrupt-Handler jeweils einer für jeden Interrupt des [[AVR]] ATmega32. Die <code enclose="none" lang="cpp">record()</code>-Methode nimmt nun auch die Interrupt-Nummer als Parameter. Als Nummer können die Makros aus der Headerdatei <code enclose="none" lang="cpp">iom32.h</code> verwendet werden.
<syntaxhighlight lang="c">
class interrupt {
  static interrupt *owner[20];


=== mit Elternklasse ===
  virtual void serviceRoutine() = 0;
Eine Interrupt Elternklasse erhöht die Flexibilität des Interrupt handlings enorm, allerdings auf Kosten des Speichers.
  static void handler1() __asm__("__vector_1") __attribute__((__signal__, __used__, __externally_visible__));
  static void handler2() __asm__("__vector_2") __attribute__((__signal__, __used__, __externally_visible__));


Die pur virtuelle interrupt Klasse enthält ein statisches Array mit 20 Zeigern auf die interrupt Kindklassen. Die Methode serviceRoutine() sollte in den Kindklassen überladen werden. Dann folgen 20 Interrupt Handler jeweils einer für alle Interrupts auf dem AVR ATmega32. Die record() Methode nimmt nun auch noch die Interrupt Nummer. Als Nummer können die Makros aus der iom32.h verwendet werden.
  .
<c>
  .
class interrupt {
  .
        static interrupt *owner[20];


        virtual void serviceRoutine() = 0;
  static void handler20() __asm__("__vector_20") __attribute__((__signal__, __used__, __externally_visible__));
static void handler1() __asm__("__vector_1") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler2() __asm__("__vector_2") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler3() __asm__("__vector_3") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler4() __asm__("__vector_4") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler5() __asm__("__vector_5") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler6() __asm__("__vector_6") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler7() __asm__("__vector_7") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler8() __asm__("__vector_8") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler9() __asm__("__vector_9") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler10() __asm__("__vector_10") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler11() __asm__("__vector_11") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler12() __asm__("__vector_12") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler13() __asm__("__vector_13") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler14() __asm__("__vector_14") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler15() __asm__("__vector_15") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler16() __asm__("__vector_16") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler17() __asm__("__vector_17") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler18() __asm__("__vector_18") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler19() __asm__("__vector_19") __attribute__((__signal__, __used__, __externally_visible__));
        static void handler20() __asm__("__vector_20") __attribute__((__signal__, __used__, __externally_visible__));


        public:
  public:
        static void record(int interruptNumber, interrupt *i);
    static void record(int interruptNumber, interrupt *i);
};
};
</c>
</syntaxhighlight>


Das owen Array wird auf Null initialisiert.
Das <code enclose="none" lang="cpp">owner</code>-Array wird - auch hier der Vollständigkeit des Codes halber - auf Null initialisiert.
<c>
<syntaxhighlight lang="c">
interrupt *interrupt::owner[] = {0};
interrupt *interrupt::owner[] = {0};
</c>
</syntaxhighlight>


Die Interrupt Handler rufen die überladene Service Routine der Kindklasse auf.
Die Interrupt-Handler rufen, nachdem überprüft wurde ob ein Objekt einer Kindklasse registriert worden ist, dessen Service-Routine auf.
<c>
<syntaxhighlight lang="c">
void interrupt::handler1() {
void interrupt::handler1() {
if(owner[0])
if(owner[0])
Zeile 130: Zeile 123:
}
}


void interrupt::handler3() {
.
if(owner[2])
.
owner[2]->serviceRoutine();
.
}
 
void interrupt::handler4() {
if(owner[3])
owner[3]->serviceRoutine();
}
 
void interrupt::handler5() {
if(owner[4])
owner[4]->serviceRoutine();
}
 
void interrupt::handler6() {
if(owner[5])
owner[5]->serviceRoutine();
}
 
void interrupt::handler7() {
if(owner[6])
owner[6]->serviceRoutine();
}
 
void interrupt::handler8() {
if(owner[7])
owner[7]->serviceRoutine();
}
 
void interrupt::handler9() {
if(owner[8])
owner[8]->serviceRoutine();
}
 
void interrupt::handler10()  {
if(owner[9])
owner[9]->serviceRoutine();
}
 
void interrupt::handler11()  {
if(owner[10])
owner[10]->serviceRoutine();
}
 
void interrupt::handler12()  {
if(owner[11])
owner[11]->serviceRoutine();
}
 
void interrupt::handler13()  {
if(owner[12])
owner[12]->serviceRoutine();
}
 
void interrupt::handler14()  {
if(owner[13])
owner[13]->serviceRoutine();
}
 
void interrupt::handler15()  {
if(owner[14])
owner[14]->serviceRoutine();
}
 
void interrupt::handler16()  {
if(owner[15])
owner[15]->serviceRoutine();
}
 
void interrupt::handler17()  {
if(owner[16])
owner[16]->serviceRoutine();
}
 
void interrupt::handler18()  {
if(owner[17])
owner[17]->serviceRoutine();
}
 
void interrupt::handler19()  {
if(owner[18])
owner[18]->serviceRoutine();
}


void interrupt::handler20()  {
void interrupt::handler20()  {
Zeile 219: Zeile 131:
owner[19]->serviceRoutine();
owner[19]->serviceRoutine();
}
}
</c>
</syntaxhighlight>


Die Methode record speichert den Zeiger auf die Kindklasse an die entsprechende Interruptnummer.
Die Methode <code enclose="none" lang="cpp">record()</code> speichert den Zeiger auf die Kindklasse an die entsprechende Interruptnummer im <code enclose="none" lang="cpp">owner</code>-Array.
<c>
<syntaxhighlight lang="c">
void interrupt::record(int interruptNumber, interrupt *i) {
void interrupt::record(int interruptNumber,
      interrupt *i) {
owner[interruptNumber - 1] = i;
owner[interruptNumber - 1] = i;
}
}
</c>
</syntaxhighlight>


Um einen Linker Fehler zu vermeiden benötigt man einen Handler für fehlgeleitete rein virtuelle Methoden.
Um einen Linker-Fehler zu vermeiden, sollte ein Handler für aufgerufene, nicht definierte, rein virtuelle Methoden von Hand erstellt werden. Dies, da es für AVR keine Standard-C++-Bibliothek gibt.
<c>
<syntaxhighlight lang="c">
extern "C" void __cxa_pure_virtual() {
extern "C" void __cxa_pure_virtual() {
for(;;)
for(;;)
;
;
}
}
</c>
</syntaxhighlight>


Die geschachtelte Klasse timerInterrupt ist eine Kindklasse von interrupt. Ihre Zeigervariable t speichert wieder den this Zeiger der äußeren Klasse. Die serviceRoutine() wird im Interruptfall vom Interrupt Handler in der interrupt Elternklasse aufgerufen. Da von der timerInterrupt Klasse diesmal ein Objekt angelegt wird, gibt es einen Konstruktor. Als Parameter wird die Interruptnummer sowie ein Zeiger auf die äußere Klasse als Parameter erwartet. Aufgrund der hinzugewonnenen Flexibilität soll im timer Konstruktor gezeigt werden, dass es nun möglich ist Referenzen auf die Timer Register zu übergeben und somit mit der selben Klasse Objekte für mehrere Timer zu erzeugen.
In der <code enclose="none" lang="cpp">timer</code>-Klasse ist die Klasse <code enclose="none" lang="cpp">timerInterrupt</code> enthalten, welche von der obigen <code enclose="none" lang="cpp">interrupt</code>-Klasse abgeleitet ist. Ihre Zeigervariable <code enclose="none" lang="cpp">ownerTimer</code> soll auf ein Objekt der <code enclose="none" lang="cpp">timer</code>-Klasse zeigen. Die Methode <code enclose="none" lang="cpp">serviceRoutine()</code> wird im Interrupt-Fall vom Interrupt-Handler der Elternklasse aufgerufen. Da von der <code enclose="none" lang="cpp">timerInterrupt</code>-Klasse ein Objekt erstellt wird, gibt es einen Konstruktor der als Parameter die Interrupt-Nummer sowie einen Zeiger auf die äußere Klasse erwartet. Der Konstruktor der <code enclose="none" lang="cpp">timer</code>-Klasse erwartet als ersten Parameter das Timer-Counter-Control-Register des zu konfigurierenden Timers. Als zweites sollte der in dieses Register einzutragende Wert angegeben werden. Die nächsten beiden Parameter widmen sich in der selben Art und Weise dem Timer-Interrupt-Mask Register. Zuletzt ist die Nummer des Timer-Overflow-Interrupts anzugeben.
<c>
<syntaxhighlight lang="c">
class timer {
class timer {
volatile uint32_t i;
volatile uint32_t i;


class timerInterrupt : interrupt {
class timerInterrupt : public interrupt {
static timer *t;
timer *ownerTimer;
void serviceRoutine(void);
void serviceRoutine();


public:
public:
timerInterrupt(int interruptNumber, timer *t);
timerInterrupt(int interruptNumber,
} tI;
      timer *ownerTimer);
} nestedTimerInterrupt;


friend timerInterrupt;
friend timerInterrupt;


public:
public:
timer(volatile uint8_t &timerCounterControlRegister, volatile uint8_t &timerInterruptMaskRegister, int timerOverflowInterruptEnable, int interruptNumber);
timer(volatile uint8_t &timerCounterControlRegister,
      uint8_t tccrNewState,
      volatile uint8_t &timerInterruptMaskRegister,
      uint8_t timskNewState,
      int interruptNumber);
};
};
</c>
</syntaxhighlight>


Der timerInterrupt Konstruktor sichert den this Zeiger der äußeren Klasse. Beim aufruf der record() Methode seiner Elternklasse gibt er allerdings seinen eigenen this Zeiger weiter.
Der Konstruktor der Klasse <code enclose="none" lang="cpp">timerInterrupt</code> sichert einen Zeiger auf das Objekt mit dem die Interrupt-Service-Routine arbeiten soll. Beim Aufruf der <code enclose="none" lang="cpp">record()</code>-Methode seiner Elternklasse gibt er den <code enclose="none" lang="cpp">this</code>-Zeiger seines Objekts weiter.
<c>
<syntaxhighlight lang="c">
timer::timerInterrupt::timerInterrupt(int interruptNumber, timer *timer) {
timer::timerInterrupt::timerInterrupt(int interruptNumber,
t = timer;
      timer *ownerTimer) : ownerTimer(ownerTimer) {
record(interruptNumber, this);
record(interruptNumber, this);
}
}
</c>
</syntaxhighlight>


In der serviceRoutine() wird wieder t gegen Null geprüft, und i um Eins erhöht.
Die Methode <code enclose="none" lang="cpp">serviceRoutine()</code> wird vom eigentlichen Interrupt-Handler der <code enclose="none" lang="cpp">interrupt</code>-Klasse aufgerufen. In sie sollten die Aufgaben des Interrupts geschrieben werden. Im Beispiel wird die Membervariable <code enclose="none" lang="cpp">i</code> des im Konstruktor bestimmten <code enclose="none" lang="cpp">timer</code>-Objekts erhöht.
<c>
<syntaxhighlight lang="c">
void timer::timerInterrupt::serviceRoutine(void) {
void timer::timerInterrupt::serviceRoutine() {
if(t != 0)
++ownerTimer->i;
++t->i;
}
}
</c>
</syntaxhighlight>


Der Konstruktor der timer Klasse bekommt als ersten Parameter das Timer Counter Compare Register des Timers mit dem das zu erstellende Objekt arbeiten soll. Als zweiten Parameter das Timer Interrupt Mask Register. Der dritte Parameter soll die Bitnummer zum aktivieren des Timer Overflow Interrupts in TIMSK sein. Der letzte Parameter ist die Interrupt Nummer.
Der Konstruktor der <code enclose="none" lang="cpp">timer</code>-Klasse initialisiert <code enclose="none" lang="cpp">i</code> auf Null und gibt die ihm mitgeteilte Interrupt-Nummer sowie den <code enclose="none" lang="cpp">this</code>-Zeiger seines Objekts an den Konstruktor der <code enclose="none" lang="cpp">timerInterrupt</code>-Klasse weiter. Er beschreibt zudem die beiden Timer-Register mit den ihm übergebenen Werten. Zudem aktiviert er Interrupts global.
<c>
<syntaxhighlight lang="c">
timer::timer(volatile uint8_t &tccr, volatile uint8_t &timsk, int toie, int interruptNumber) : i(0), tI(interruptNumber, this) {
timer::timer(volatile uint8_t &tccr,
         tccr |= (1 << CS20) + (1 << CS21) + (1 << CS22);
    uint8_t tccrNewState,
         timsk |= 1 << toie;
    volatile uint8_t &timsk,
    uint8_t timskNewState,
    int interruptNumber) : i(0),
    nestedTimerInterrupt(interruptNumber,
this) {
         tccr = tccrNewState;
         timsk = timskNewState;
         sei();
         sei();
}
}
</c>
</syntaxhighlight>


Ein beispielhafter Aufruf in main() könnte etwa so aussehen. Den beiden Timern werden die Register mit auf den Weg gegeben, sowie die Interruptnummern die als Makros definiert sind.
Ein beispielhafter Aufruf in <code enclose="none" lang="cpp">main()</code> könnte etwa so aussehen. Hier werden Timer0 und Timer2 mit Prescaler-Werten von 1024 initialisiert.
<c>
<syntaxhighlight lang="c">
int main(void) {
int main() {
timer t0(TCCR0, TIMSK, TIMER0_OVF_vect_num)
timer timer0(TCCR0,
timer t2(TCCR2, TIMSK, TIMER2_OVF_vect_num)
    TCCR0 | (1 << CS00) & ~(1 << CS01) | (1 << CS02),
    TIMSK,
    TIMSK | (1 << TOIE0),
    TIMER0_OVF_vect_num);
 
timer timer2(TCCR2,
    TCCR2 | (1 << CS20) | (1 << CS21) | (1 << CS22),
    TIMSK,
    TIMSK | (1 << TOIE2),
    TIMER2_OVF_vect_num);


for(;;)
for(;;)
Zeile 292: Zeile 224:
return 0;
return 0;
}
}
</c>
</syntaxhighlight>
 
=== ein pragmatischer Ansatz im Hinblick auf den Flash-Speicherverbrauch ===
Die Größe des produzierten Binärcodes beträgt bei der zuerst vorgestellten Methode um die 0,3kB. In der Implementierung mit Elternklasse steigt diese --- aufgrund der vielen ungenutzten, aber als <code enclose="none" lang="cpp">__used__</code> gekennzeichneten Interrupt Handler --- auf ca. 2,2kB an. Um den Speicherverbrauch im Flash-Speicher händisch zu senken, bietet es sich daher an nicht benötigte Interrupt-Handler auszukommentieren. Mit dieser zwar etwas unschönen, jedoch pragmatischen, Methode lässt sich die Codegröße wieder auf ca. 0,5kB reduzieren.


=== pragmatischer Ansatz ===
Die Angaben der Codegrößen beziehen sich auf ein Testprogramm in welchem nur ein <code enclose="none" lang="cpp">timer</code>-Objekt erzeugt wurde und dann eine Endlosschleife folgte. Kompiliert wurde mit Optimierung auf Codegröße.
Eine Elternklasse zu ertellen und die nicht benötigten Interrupt Handler auszukommentieren kann auf einem Mikrocontroller keine Sünde sein. Man behält einen Großteil der Flexibilität, kann jedoch einiges an Speicher sparen.


== Einzelnachweise ==
== Einzelnachweise ==
<references/>
<references/>


[[Kategorie:AVR| ]]
[[Kategorie:AVR]]
[[Kategorie:C++ ]]
[[Kategorie:C++]]

Aktuelle Version vom 26. November 2014, 07:30 Uhr

von Christian M.

Dieser Artikel nimmt am Artikelwettbewerb 2012/2013 teil.

Einen Mikrocontroller mit C++ zu programmieren, scheitert oft daran, die Interrupt-Routinen in die Klassenhierarchie zu integrieren. Ohne saubere Objektorientierung verliert C++ jedoch schnell an Wert. Zum Beispiel Frameworks um die Hardware zu abstrahieren, sind mit den Interrupt Makros der avr-libc zwar machbar, jedoch mit reinem C++ weit flexibler. Deshalb soll dieser Artikel einen Einblick in die Möglichkeiten des GNU C++ Compilers geben, der einige Stellschrauben besitzt, mit denen sich Interrupts trotz aller Probleme in Klassen, mit Zugriff auf nicht statische Membervariablen, integrieren lassen.

Probleme

name mangling

In C++ gibt es Namensräume, daher werden nicht nur die Methodennamen für die Symbolnamen benutzt, sondern unter anderem auch die Klassennamen mit eingebracht. Die Schattenseite dieses Features ist allerdings, dass die Interrupt-Makros der avr-libc nicht mehr genutzt werden können. Das Makro ISR() ist eines davon. Es wird vom Präprozessor über einige weitere Makros zur mit extern "C" deklarierten Funktion __vector_n() aufgelöst; n steht für die Interruptnummer. Da extern "C" in Klassen nicht zulässig ist, kann der daraus resultierende Quellcode nicht compiliert werden.

Zugriff auf nicht statische Membervariablen

Da Methoden als ersten impliziten Parameter den this-Zeiger ihres Objekts erhalten,[1] Interrupt-Handler allerdings keine Parameter haben können - ohne Aufruf können keine Parameter übergeben werden - müssen gezwungermaßen statische Methoden verwendet werden. Statische Methoden haben allerdings nur auf statische Variablen Zugriff. Dies ermöglicht jedoch nur eine suboptimale Objektorientierung. Als Beispiel soll eine Timer-Klasse dienen: Wollte man je ein Objekt für zwei verschiedene Timer erstellen, bräuchte man für jedes eine statische Variable.

Lösungen

das asm Schlüsselwort

Mit dem Schlüsselwort asm kann nicht nur inline Assembler programmiert werden, es kann auch dazu verwendet werden, Symbolnamen zu bestimmen.[2] Somit ergibt sich die Möglichkeit, das name mangling zu steuern. Weist man zum Beispiel einer statischen Methode den Symbolnamen __vector_1 zu, so gehört sie weiterhin zur Klasse; es wird nur der Symbolname geändert. Da die avr-libc eine Interruptvektortabelle anlegt, in der ein Sprungbefehl zu __vector_1 für den ersten Interrupt steht, wird die Methode auch im Interruptfall aufgerufen.

befreundete Klassen

Zu den pragmatischeren Teilen der Sprache C++ gehört das Schlüsselwort friend, da es einer fremden Klasse Zugriff auf private Teile einer anderen Klasse gibt. Um mithilfe von friend einer Interrupt Routine Zugriff auf nicht statische Membervariablen zu geben, kann beispielsweise eine geschachtelte befreundete Klasse angelegt werden, die neben der Interrupt Routine eine statische Zeigervariable auf ein Objekt der äußeren Klasse enthält.[3] Wird mithilfe einer weiteren statischen Methode dieser Klasse der this Zeiger eines Objekts der äußeren Klasse gespeichert, kann durch die Interrupt Methode auf alle Member der äußeren Klasse zugegriffen werden.

Implementierungen

Um die beiden folgenden Beispiele kompilieren zu können sollten die folgenden Headerdateien eingebunden werden; stdint.h wegen der Verwendung von uint32_t, interrupt.h wegen sei() und io.h wegen der Registermakros.

#include <stdint.h>
#include <avr/interrupt.h>
#include <avr/io.h>

Die Warnung die beim kompilieren des ersten Beispiels auftritt (beim zweiten in ähnlicher Form) kann ignoriert werden, denn es wird bereits nach Durchlauf des Präprozessors geprüft ob alle Funktionen mit dem __signal__-Attribut mit __vector_ beginnen.[4] Der __asm__-Befehl wird allerdings erst nach dem Kompilieren wirksam.

warning: 'serviceRoutine' appears to be a misspelled signal handler

Interrupt Routine in einer geschachtelten Klasse

Eine einfache Implementierung am Beispiel des Timer2 Overflow-Interrupts auf einem AVR ATmega32 könnte wie folgt aussehen:

Mit __asm__("__vector_5") wird der Symbolname der Methode serviceRoutine() zum Namen des Interrupt Vektors für den Timer2 Overflow-Interrupt. Das Attribut __signal__ teilt dem Compiler mit, dass es sich hier um eine Interrupt-Routine handelt, und alle Register gesichert und wiederhergestellt werden müssen. Mithilfe von __used__ wird verhindert, dass der Code wegoptimiert wird, denn es gibt nirgends im C++-Teil des Codes einen konkreten Aufruf der Methode. Damit die serviceRoutine() auch außerhalb der Timer-Übersetzungseinheit sichtbar ist, sollte __externally_visible__ dem Quellcode hinzugefügt werden. Anstelle von __signal__ kann auch __interrupt__ verwendet werden, um den Interrupt sofort nach Eintritt in die Handler-Methode wieder zu verlassen, anstatt erst nach deren Ende.[5]

class timer {
	volatile uint32_t i;

	class timerInterrupt {
		static timer *ownerTimer;
		static void serviceRoutine() __asm__("__vector_5") __attribute__((__signal__, __used__, __externally_visible__));

		public:
			static void record(timer *ownerTimer);
	};

	friend timerInterrupt;

	public:
		timer();
};

Die Initialisierung der Zeigervariable ownerTimer auf Null sollte nicht vergessen werden, da es ansonsten zu Linkerfehlern kommen kann.

timer *timer::timerInterrupt::ownerTimer = 0;

Die folgende Methode wird gebraucht um ein timer-Objekt für die Interrupt-Routine zu registrieren.

void timer::timerInterrupt::record(timer *t) {
	ownerTimer = t;
}

Dies ist die Interrupt-Routine. Um keinen Speicherzugriffsfehler zu erzeugen, falls der Interrupt auftritt bevor ownerTimer auf ein Objekt der äußeren Klasse zeigt, wird der Zeiger gegen Null geprüft. Zur Demonstration, dass der Zugriff möglich ist, wird i vom Objekt der timer-Klasse erhöht.

void timer::timerInterrupt::serviceRoutine() {
	if(ownerTimer != 0)
		++ownerTimer->i;
}

Im Konstruktor der timer-Klasse wird zuerst der this-Zeiger der inneren Klasse bekannt gemacht. Dann wird der Prescaler von Timer2 auf 1024 eingestellt, und somit der Timer gestartet. In der dritten Zeile wird der Overflow-Interrupt aktiviert um danach Interrupts global zu aktivieren.

timer::timer() : i(0) {
	timerInterrupt::record(this);
        TCCR2 |= (1 << CS20) | (1 << CS21) | (1 << CS22);
        TIMSK |= 1 << TOIE2;
	sei();
}

mit Interrupt Elternklasse

Eine Interrupt-Elternklasse erhöht die Flexibilität des Interrupt-Handlings enorm, allerdings auf Kosten des RAM und Flash Speichers.

Die abstrakte Klasse interrupt enthält ein statisches Array von Zeigern auf die interrupt-Kindklassen. Die Methode serviceRoutine() sollte in den Kindklassen überladen werden.[6] Dann folgen 20 Interrupt-Handler jeweils einer für jeden Interrupt des AVR ATmega32. Die record()-Methode nimmt nun auch die Interrupt-Nummer als Parameter. Als Nummer können die Makros aus der Headerdatei iom32.h verwendet werden.

class interrupt {
  static interrupt *owner[20];

  virtual void serviceRoutine() = 0;
  static void handler1() __asm__("__vector_1") __attribute__((__signal__, __used__, __externally_visible__));
  static void handler2() __asm__("__vector_2") __attribute__((__signal__, __used__, __externally_visible__));

  .
  .
  .

  static void handler20() __asm__("__vector_20") __attribute__((__signal__, __used__, __externally_visible__));

  public:
    static void record(int interruptNumber, interrupt *i);
};

Das owner-Array wird - auch hier der Vollständigkeit des Codes halber - auf Null initialisiert.

interrupt *interrupt::owner[] = {0};

Die Interrupt-Handler rufen, nachdem überprüft wurde ob ein Objekt einer Kindklasse registriert worden ist, dessen Service-Routine auf.

void interrupt::handler1() {
	if(owner[0])
		owner[0]->serviceRoutine();
}

void interrupt::handler2() {
	if(owner[1])
		owner[1]->serviceRoutine();
}

.
.
.

void interrupt::handler20()  {
	if(owner[19])
		owner[19]->serviceRoutine();
}

Die Methode record() speichert den Zeiger auf die Kindklasse an die entsprechende Interruptnummer im owner-Array.

void interrupt::record(int interruptNumber,
		       interrupt *i) {
	owner[interruptNumber - 1] = i;
}

Um einen Linker-Fehler zu vermeiden, sollte ein Handler für aufgerufene, nicht definierte, rein virtuelle Methoden von Hand erstellt werden. Dies, da es für AVR keine Standard-C++-Bibliothek gibt.

extern "C" void __cxa_pure_virtual() {
	for(;;)
		;
}

In der timer-Klasse ist die Klasse timerInterrupt enthalten, welche von der obigen interrupt-Klasse abgeleitet ist. Ihre Zeigervariable ownerTimer soll auf ein Objekt der timer-Klasse zeigen. Die Methode serviceRoutine() wird im Interrupt-Fall vom Interrupt-Handler der Elternklasse aufgerufen. Da von der timerInterrupt-Klasse ein Objekt erstellt wird, gibt es einen Konstruktor der als Parameter die Interrupt-Nummer sowie einen Zeiger auf die äußere Klasse erwartet. Der Konstruktor der timer-Klasse erwartet als ersten Parameter das Timer-Counter-Control-Register des zu konfigurierenden Timers. Als zweites sollte der in dieses Register einzutragende Wert angegeben werden. Die nächsten beiden Parameter widmen sich in der selben Art und Weise dem Timer-Interrupt-Mask Register. Zuletzt ist die Nummer des Timer-Overflow-Interrupts anzugeben.

class timer {
	volatile uint32_t i;

	class timerInterrupt : public interrupt {
		timer *ownerTimer;
		void serviceRoutine();

		public:
			timerInterrupt(int interruptNumber,
				       timer *ownerTimer);
	} nestedTimerInterrupt;

	friend timerInterrupt;

	public:
		timer(volatile uint8_t &timerCounterControlRegister,
		      uint8_t tccrNewState,
		      volatile uint8_t &timerInterruptMaskRegister,
		      uint8_t timskNewState,
		      int interruptNumber);
};

Der Konstruktor der Klasse timerInterrupt sichert einen Zeiger auf das Objekt mit dem die Interrupt-Service-Routine arbeiten soll. Beim Aufruf der record()-Methode seiner Elternklasse gibt er den this-Zeiger seines Objekts weiter.

timer::timerInterrupt::timerInterrupt(int interruptNumber,
				      timer *ownerTimer) : ownerTimer(ownerTimer) {
	record(interruptNumber, this);
}

Die Methode serviceRoutine() wird vom eigentlichen Interrupt-Handler der interrupt-Klasse aufgerufen. In sie sollten die Aufgaben des Interrupts geschrieben werden. Im Beispiel wird die Membervariable i des im Konstruktor bestimmten timer-Objekts erhöht.

void timer::timerInterrupt::serviceRoutine() {
	++ownerTimer->i;
}

Der Konstruktor der timer-Klasse initialisiert i auf Null und gibt die ihm mitgeteilte Interrupt-Nummer sowie den this-Zeiger seines Objekts an den Konstruktor der timerInterrupt-Klasse weiter. Er beschreibt zudem die beiden Timer-Register mit den ihm übergebenen Werten. Zudem aktiviert er Interrupts global.

timer::timer(volatile uint8_t &tccr,
	     uint8_t tccrNewState,
	     volatile uint8_t &timsk,
	     uint8_t timskNewState,
	     int interruptNumber) : i(0),
				    nestedTimerInterrupt(interruptNumber,
							 this) {
        tccr = tccrNewState;
        timsk = timskNewState;
        sei();
}

Ein beispielhafter Aufruf in main() könnte etwa so aussehen. Hier werden Timer0 und Timer2 mit Prescaler-Werten von 1024 initialisiert.

int main() {
	timer timer0(TCCR0,
		     TCCR0 | (1 << CS00) & ~(1 << CS01) | (1 << CS02),
		     TIMSK,
		     TIMSK | (1 << TOIE0),
		     TIMER0_OVF_vect_num);

	timer timer2(TCCR2,
		     TCCR2 | (1 << CS20) | (1 << CS21) | (1 << CS22),
		     TIMSK,
		     TIMSK | (1 << TOIE2),
		     TIMER2_OVF_vect_num);

	for(;;)
		;

	return 0;
}

ein pragmatischer Ansatz im Hinblick auf den Flash-Speicherverbrauch

Die Größe des produzierten Binärcodes beträgt bei der zuerst vorgestellten Methode um die 0,3kB. In der Implementierung mit Elternklasse steigt diese --- aufgrund der vielen ungenutzten, aber als __used__ gekennzeichneten Interrupt Handler --- auf ca. 2,2kB an. Um den Speicherverbrauch im Flash-Speicher händisch zu senken, bietet es sich daher an nicht benötigte Interrupt-Handler auszukommentieren. Mit dieser zwar etwas unschönen, jedoch pragmatischen, Methode lässt sich die Codegröße wieder auf ca. 0,5kB reduzieren.

Die Angaben der Codegrößen beziehen sich auf ein Testprogramm in welchem nur ein timer-Objekt erzeugt wurde und dann eine Endlosschleife folgte. Kompiliert wurde mit Optimierung auf Codegröße.

Einzelnachweise