Hallo zusammen
Ich versuche gerade zu verstehen, wie eine Interrupt Service Routine
codemässig funktioniert.
Mir ist ungefähr klar wie das ganze in Assembler funktioniert: Interrupt
wird ausgelöst und der Instructionpointer springt zur entsprechenden
Interrupt-source-adresse, wo dann der Sprung zur Interrupt Service
Routine hinterlegt ist usw.
Wie genau funktioniert das ganze aber in C? Wenn man zum Beispiel den
"USART Rx complete" Interrupt eines AtMegas betrachtet, dann wird
normalerweise das Makro ISR(USART_RXC_vect) aus der "interrupt.h"
Library von Atmel verwendet.
Leider verstehe ich diesen Codeabschnitt nicht wirklich. Nach meinem
Verständnis muss hier dem Compiler mitgeteilt werden, wohin der Sprung
bei einem "Rx complete" Interrupt gehen soll. Ich nehme an, dass das
irgendwie über die Compiler-Attribute funktioniert. Wo aber wird die
Information aus "USART_RXC_vect" in diesem Makro verwendet?
Gruss
Samuel K. schrieb:> Mir ist ungefähr klar wie das ganze in Assembler funktioniert: Interrupt> wird ausgelöst und der Instructionpointer springt zur entsprechenden> Interrupt-source-adresse, wo dann der Sprung zur Interrupt Service> Routine hinterlegt ist usw.
Das ist mit C genauso. Es gibt irgendwo eine Interrupt-Tabelle, und da
steht dann die Adresse der C-Funktion drin, die diesem Interrupt
zugeordnet ist.
> Wo aber wird die> Information aus "USART_RXC_vect" in diesem Makro verwendet?
#define ist eine Präprozessor-Anweisung, das ist reine Textersetzung.
Der Präprozessor, der wie der Nahme sagt vor dem Compiler durchläuft,
macht dann das hier daraus, alles in einer Zeile:
Er deklariert also erstmal die Funktion mit den nötigen
Compiler-Attributen, damit der Compiler mitbekommt, daß das hier eine
Interrupt-Routine darstellen soll. Das ist der Teil bis zum Semikolon.
Dann kommt die eigentliche Routine wie üblich als void-void-Funktion.
Daran anschließend kämen die geschweiften Klammern, die Du dann anfügst,
also mit dem Funktionsinhalt.
Samuel K. schrieb:> Wie genau funktioniert das ganze aber in C? Wenn man zum Beispiel den> "USART Rx complete" Interrupt eines AtMegas betrachtet, dann wird> normalerweise das Makro ISR(USART_RXC_vect) aus der "interrupt.h"> Library von Atmel verwendet.> //Auschnitt aus interrupt.h> # define ISR(vector, ...) \> void vector (void) _attribute_ ((signal,__INTR_ATTRS))> _VA_ARGS_; \> void vector (void)
Unter
https://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html
findet man ein Beispiel:
1
ISR(PCINT0_vect)
2
{
3
// code
4
}
Was könnte das bedeuten?
Das Macro setzt als Erstes einige Parameter als Prototypen Definition
und anschließend wird eine Funktion ohne Parameter (void) erzeugt.
Der anschließend kommen { .. } die den ISR-Code einschließen
Vorschlag:
Beispielprojekt mit Atmel Start anlegen. Da wird die Grundstruktur
automatisch erzeugt, man sieht sehr schön, wie das funktioniert (sofern
Atmel Start funktioniert, was nach meiner Erfahrung nicht immer der Fall
ist, aber meistens).
Im Prinzip funktioniert das nicht anders als in Assembler. Es mag sich
lohnen, sich noch einmal vor Augen zu führen, dass auch in C
geschriebene Programme letztlich in Maschinensprache umgesetzt werden.
Folgerichtig wird der Instruction Pointer auf die erste
Maschinenanweisung der Interrupt Service Routine gesetzt - AUCH und
TROTZ DEM diese ISR in C geschrieben ist.
Das "vor Augen führen" geht ganz real so, dass man den Compiler per
Option anweist ein Listing-File zu erzeugen. Diese (und eine andere
Compiler-Option, mit der nur die Präprozessor-Makros expandiert werden)
ist dafür nützlich,
Kurz gesagt ist "USART_RXC_vect" die Adresse in der
Interrupt-Vektor-Tabelle, an der die Adresse der ISR stehen muss - d.h.
die Adresse derjenigen Instruktion, welche die erste in der ISR ist. Das
Makro-Argument ist "vector".
Es gibt noch ein paar andere kleine Unterschiede zwischen ISRs und
"normalen" C-Funktionen aber das steht im Compilerhandbuch - hoffe ich.
:-)
Arduino Fanboy D. schrieb:> Gar nicht!>> Es wird eine Funktion mit dem Namen generiert.
Ja. Aber sicher ist ein Unterschied. Auf Grund des Attributes (ISR) wird
Code gneriert, der normalerweise ...
- alle verwendeten Register sichert
- beim Return in einem "reti" ein Restaurieren der Flags beinhaltet.
Das ist aber noch alles beeinflussbar.
https://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html
leo
Samuel K. schrieb:> Interrupt> wird ausgelöst und der Instructionpointer springt zur entsprechenden> Interrupt-source-adresse, wo dann der Sprung zur Interrupt Service> Routine hinterlegt ist usw.
Das siehst du aber ein bissel zu eng.
Prinzipiell ist es so, daß eine ISR in C mit einem speziellen Attribut
versehen wird, normalerweise __irq (nur der GCC tanzt da aus der Reihe),
damit der Compiler weiß, daß das keine void blabla(void) ist, sondern
ein Interrupt-Server, der ggf. spezielle Behandlung braucht (z.B.
Registerverwendung usw.).
So ist das auf der C-Seite.
Auf der HW-Seite mag das alles je nach Plattform ganz anders aussehen.
Entweder gibt es eine Tafel mit Einsprungadressen oder die
Einsprungadressen sind in einem Interrupt-Controller vermerkt (ARM7TDMI)
oder es geht ggf. noch ganz anders, z.B. PIC mit fester Adresse für alle
und anschließendem Abtesten, wer es gewesen war.
W.S.
Vielen Dank für die vielen und v.a. prompten Antworten.
Ich habe inzwischen noch etwas weiter in der avr-libc Library gegraben
und bin nun auf folgende Deklaration gekommen.
1
ISR(USART_RX_vect){...}
wird nach meiner Erkenntnis in folgenden Codeschnipsel übersetzt:
Wobei das "signal" Attribut die ISR deklariert. 24 ist die Vektornummer
bzw. im Fall Atmega328 auch die Programmadresse des Interrupts. Für mich
würde das heissen, dass der Compiler alles hat was er braucht: Er weiss,
dass es eine ISR ist (signal) und er weiss auch welcher (24).
Was ich noch nicht so ganz verstehe ist, dass die 24 im Namen der
Funktion steht. Da ich keine weiteren Referenzen im Code auf
"__vector_24" gefunden habe, gehe ich davon aus, dass das der wirkliche
Funktionsname ist. Das würde aber für mich heissen, dass der Compiler
bzw. Linker die Funktion "void __vector_24(void)" kennen muss? Habe aber
im GCC Manual auf die schnelle nichts gefunden was darauf hindeuten
würde.
Samuel K. schrieb:> Das würde aber für mich heissen, dass der Compiler> bzw. Linker die Funktion "void __vector_24(void)" kennen muss? Habe aber> im GCC Manual auf die schnelle nichts gefunden was darauf hindeuten> würde.
Im Manual findest du das nicht, aber in der crt.S (oder wie e für den
AVR heisst) der avr-libc. Da sind in der ISR-Tabelle alle __vector_* des
jeweiligen CPU-Typs aufgeführt, die dann erstmal intern als Dummy-ISR
definiert sind. Allerdings ist dort der Name "weak", d.h. der Linker
nimmt den nur, wenn er sonst nicht definiert ist. Damit passiert nichts
ganz Schlimmes, wenn der Interrupt aufgerufen wird, du aber keine eigene
ISR dafür hast.
Was der Präprozessor macht, sieht man auch mit -save-temps im .i File
(.ii für C++, .s für Assembler).
> Wobei das "signal" Attribut die ISR deklariert.
Da sich deine Fragen auf die GNU-Tools for AVR beziehen:
Oder "interrupt" für unterbrechbare ISRs mit SEI im Prolog.
> 24 ist die Vektornummer bzw. im Fall Atmega328 auch die Programmadresse> des Interrupts. Für mich würde das heissen, dass der Compiler alles hat> was er braucht: Er weiss, dass es eine ISR ist (signal) und er weiss> auch welcher (24).
Der Compiler "weiß" nur, dass die Funktion Attribut "signal" und / oder
"interrupt" hat, und erzeugt anhand dessen einen passenden Prolog und
Epilog(e) für die Funktion __vector_24.
Der Name der Funktion ist immer __vector_N, und GCC checkt das auch;
ansonsten macht er aber nix spezielles mit dem Funktionsname. Der wird
im Startup-Code der avr-libc (crt*.o) in der vector-Tabelle
referenziert:
http://svn.savannah.nongnu.org/viewvc/avr-libc/trunk/avr-libc/crt1/gcrt1.S?revision=2519&view=markup#l37
Die Symbole landen also in Section .vectors, welche wiederum im Standard
Linker-Skript an den Anfang der .text Section lokatiert wird:
http://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=blob;f=ld/scripttempl/avr.sc;h=05c0b890f050fe3cb2bc3db04178fef69cfbb02a;hb=HEAD#l113
(Das ist nur ein sh-Skript, aus dem der Zoo unterschiedlicher Skripte
generiert wird. In der Installation finden sich die leichter lesbaren
expandierten Formen.)
Das KEEP(*(.vectors)) bewirkt, dass die Section selbst dann nicht
entfernt wird (z.B. mit --gc-sections) wenn sie nicht (bzw. kein Symbol
daraus) referenziert wird.
Compilerseitig dient dazu Attribut "used", d.h. obwohl die Funktion
global nicht referenziert wird, darf sie vom Compiler nicht entsorgt
werden. Außerdem fehlt da noch Attribut "externally_visible".
> Was ich noch nicht so ganz verstehe ist, dass die 24 im Namen der> Funktion steht.
Das ist einzig die Naming-Convention der avr-libc:
XYZ_vect (Makro) -> __vector_N (Funktionsname, Symbol) -> Section
.vectors.
So ist die vector-Tabelle in gcrt1.S einfach durch .macro vector zu
generieren; alles was man wissen muss ist die Anzahl der Einträge
(_VECTORS_SIZE aus io*.h).
> Da ich keine weiteren Referenzen im Code auf "__vector_24" gefunden> habe, gehe ich davon aus, dass das der wirkliche Funktionsname ist.
Ja. In GNU-Assembler (.S oder .sx oder -x assembler-with-cpp) kann man
eine ISR z.B. einfach so definieren:
1
#include<avr/io.h>
2
3
.globalWDT_vect
4
WDT_vect:
5
reti
6
7
.global__vector_1
8
__vector_1:
9
reti
Weiters initialisiert die avr-libc EIND in Section .init1 aus Symbol
__vectors; das SFR wirkt auf EICALL und EIJMP. M.W. ist __vectors auch
speziell in avr-Binutils bei der Erzeugung der von gs() getriggerten
Linker-Stubs (aka. Trampolines). Siehe .trampolines im Linker-Skript.
> Das würde aber für mich heissen, dass der Compiler> bzw. Linker die Funktion "void __vector_24(void)" kennen muss?
Wie gesagt checkt der Compiler nur den Funktionsname, und dass die
Funktion Prototyp wie void func (void) hat. Der Linker weiß nichts
spezielles über die Symbole.
Prolog und Epilog werden anders generiert, i.d.R werden mehr Register
gesichert (z.B. auch __zero_reg__), und auch SFRs falls nötig wie SREG
oder RAMPZ. Ein Teil der Prolog- und Epilog-Generierung ist an den
Assembler ausgelagert:
1
#include<avr/interrupt.h>
2
3
ISR(WDT_vect)
4
{
5
PORTB=1;
6
}
wird compiliert zu
1
.global __vector_7
2
.type __vector_7, @function
3
__vector_7:
4
__gcc_isr 1
5
.L__stack_usage = 0 + __gcc_isr.n_pushed
6
ldi r24,lo8(1)
7
out 0x5,r24
8
__gcc_isr 2
9
reti
10
__gcc_isr 0,r24
und erst der Assembler expandert die Pseudo-Instruktion nach Analyse des
Codes zu
Johann hat das sehr detailliert erklärt - sehr schön!
Kurz gefasst bedeutet es:
Die Makro-Magie hinter dem Schlüsselwort ISR ist extrem Compiler
spezifisch. mit C hat das kaum etwas zu tun. Es geht hierbei schlicht
darum, dem Compiler mitzuteilen, dass diese Funktion eben keine normale
C Funktion ist, sondern Hardwarespezifisch als ISR dekoriert werden
muss.
Interessant finde ich dabei den Vergleich zu ARM Controllern, die
brauchen das nämlich nicht. Dort sind die ISR ganz normale C Funktionen.
Dafür kann allerdings der Compiler nichts, diese Vorgabe kommt vom CPU
Kern.
Vielen Dank für die hilfreichen Antworten. Es ist mir nun um einiges
klarer wie das ganze funktioniert. Ich werde heute Abend die Antwort von
Johann noch genau durchstudieren.
Gruss
Johann J. schrieb:> Tatsächlich? Muss man bei ARM-GCC z.B. nicht auch durch> void __attribute__((interrupt)) ...>> mitteilen, dass es sich um eine ISR handelt?
Kommt wie so oft darauf an, welchen ARM man sich anschauen möchte.
Die älteren ARM7TDMI haben nur 2 Interrupts: Einen (normalen) IRQ und
einen FIQ (Fast IRQ) Interrupt. Dann braucht es erst mal einen
Software-Handler, der an die richtige Adresse springt und sich ggf. um
das Sichern und Wiederherstellen der Register kümmern muss. Bei manchen
Controllern wird das durch einen Hardware Interrupt-Controller
unterstützt (AT91SAM7 z.B.), der die Adresse des Handlers ermittelt.
Wenn man es nicht möchte, braucht man das Attribut nicht unbedingt. Hab
es damals nicht benutzt.
Die neueren Cortex-M (R?, nicht A) haben den Interrupt-Controller gleich
dabei (NVIC). Zusätzlich unterstützen sie beim Eintritt in einen
Interrupt-Handler das Sichern der Register per Hardware nach dem AAPCS.
Daraus folgt, dass es für den Handler keinen Unterschied macht, ob er
als normale Funktion aufgerufen wird oder als Interrupt-Handler.
Dementsprechend braucht es auch keine Attribute, Flags oder sonstwas.
Der Compiler muss halt den AAPCS umsetzen, aber das tun sie eigentlich
alle.
>>>> mitteilen, dass es sich um eine ISR handelt?>> Nein, muß man nicht.
Und wozu sind Attribute "interrupt" und "isr" ?
http://gcc.gnu.org/onlinedocs/gcc/ARM-Function-Attributes.html
Oder sind diese Dinge alle in der libgloss oder im CRT versteckt, so
dass sich normale Anwender nicht darum kümmern müssen?
Anders gefragt: Wer oder was referenziert die "ganz normalen C
Funktionen"? Irgendo müssen die ja referenziert werden?
Johann J. schrieb:> Muss man bei ARM-GCC nicht auch mitteilen, dass es sich um eine ISR handelt?
Nein, jedenfalls nicht bei Cortex M0, M3 und M4. Mit anderen ARM Kernen
habe ich mich nicht beschäftigt.
> Und wozu sind Attribute "interrupt" und "isr" ?
Lies hier nach:
http://gcc.gnu.org/onlinedocs/gcc/ARM-Function-Attributes.html> Wer oder was referenziert die "ganz normalen C Funktionen"?
C oder Assembler Code, sowie die Interrupt Vektor Tabelle.
Was zeichnet Interupt-Handler (gegenüber "normalen" Funktionen)
üblicherweise aus?
1.) sie müssen alle verwendeten Register sichern (und nicht nur die, die
im ABI entsprechend ausgezeichnet sind)
2.) sie müssen (üblicherweise) mit einem speziellen Befehl verlassen
werden
1.) erledigen die Cortexe automatisch
2.) ist bei Cortexen nicht notwendig, auch das übernimmt der µC
3.) Ihre Adresse steht in einer Tabelle an definierter Speicherstelle /
wird dem Interrupt Controller oder dem entsprechenden Verzweigungs-Code
bekannt gegeben, damit sie auch angesprungen werden, wenn der Interrupt
eintritt
Keine Ahnung, wie das mit GCC für ARM Cortex üblicherweise funktioniert.
MfG, Arno
Arno schrieb:> Keine Ahnung, wie das mit GCC für ARM Cortex üblicherweise funktioniert.
Für Cortex-M gilt ebenfalls
Arno schrieb:> 3.) Ihre Adresse steht in einer Tabelle an definierter Speicherstelle
D.h. der NVIC ruft einfach die Funktion auf, deren Funktionszeiger an
der definierten Stelle im Flash liegt. Typischerweise findet man die
Namen der Funktionen im Startup-Code als Liste, so dass der Linker dann
die Adressen zu den Funktionen auflösen und an die entsprechende Stelle
im Flash packen kann.
PS:
Johann L. schrieb:> Und wozu sind Attribute "interrupt" und "isr" ?>> http://gcc.gnu.org/onlinedocs/gcc/ARM-Function-Attributes.html
Ich vermute mal das ist für legacy, d.h. ARM7TDMI o.Ä.
In der verlinkten Doku steht ja auch, dass das Attribut "isr" für ARMv7M
ignoriert wird. Das müsste eigentlich auch für ARMv6M, d.h. Cortex-M0
und M0+ gelten.
Christopher J. schrieb:> In der verlinkten Doku steht ja auch, dass das Attribut "isr" für ARMv7M> ignoriert wird.
Lies das nochmal bitte:
"On ARMv7-M the interrupt type is ignored, and the attribute means
the function may be called with a word-aligned stack pointer."
Christopher J. schrieb:> D.h. der NVIC ruft einfach die Funktion auf, deren Funktionszeiger an> der definierten Stelle im Flash liegt. Typischerweise findet man die> Namen der Funktionen im Startup-Code als Liste, so dass der Linker dann> die Adressen zu den Funktionen auflösen und an die entsprechende Stelle> im Flash packen kann
Also eigentlich wie überall:
Die Funktion, welche den Interrupt bearbeiten soll wird irgendwo bekannt
gemacht, damit der Compiler/Linker entsprechend arbeiten können.
Nix Magie und nix "kann beliebig heißen" - muss bekannt gemacht sein und
damit gut.
Markus F. schrieb:> Was zeichnet Interupt-Handler (gegenüber "normalen" Funktionen)> üblicherweise aus?>> 1.) sie müssen alle verwendeten Register sichern (und nicht nur die, die> im ABI entsprechend ausgezeichnet sind)> 2.) sie müssen (üblicherweise) mit einem speziellen Befehl verlassen> werden>> 1.) erledigen die Cortexe automatisch> 2.) ist bei Cortexen nicht notwendig, auch das übernimmt der µC
Irgendwie muß man dem Compiler und auch dem Linker mitteilen, daß eine
Funktion als ISR dient. Ganz ohne jedes Attribut klappt das nicht.
Oliver
Oliver S. schrieb:> Irgendwie muß man dem Compiler und auch dem Linker mitteilen, daß eine> Funktion als ISR dient. Ganz ohne jedes Attribut klappt das nicht.
Doch, tut es bei Cortex-M. ISRs sind dort ganz normale C-Routinen ohne
besonderen Prolog/Epilog.
void schrieb:> Lies das nochmal bitte:> "On ARMv7-M the interrupt type is ignored, and the attribute means> the function may be called with a word-aligned stack pointer."
Stimmt, das hatte ich glatt gelesen als "the interrupt attribute is
ignored". Mit dem Stack-Alignment habe ich mich ehrlich gesagt noch
nicht auseinandergesetzt. 8 Byte Alignment scheint standard zu sein und
ist bei manchen Cortexen (M0, M0+ und M7) wohl auch gar nicht
veränderlich:
https://community.arm.com/developer/ip-products/processors/f/cortex-m-forum/6344/what-is-the-meaning-of-a-64-bit-aligned-stack-pointer-addressJohann J. schrieb:> Nix Magie und nix "kann beliebig heißen" - muss bekannt gemacht sein und> damit gut.
Kann "beliebig" heißen im Sinne, dass der Name in der Vektortabelle im
Startup-Code mit dem Namen der Funktion übereinstimmen muss, damit der
Linker den Funktionszeiger an die entsprechende Stelle setzen kann.
Nimmt man den Startup-Code als gottgegeben an, dann kann man eben nur
den Namen verwenden, der darin vorgegeben ist. Ob es sinnvoll ist einen
anderen Namen als den vorgegebenen zu verwenden, darüber kann man sich
natürlich streiten. Ich meine aber, dass es eher nicht so sinnvoll ist.
Bei den Cortexen gibt es auch eine Konvention zu den IRQ-Bezeichnern an
die sich fast alle Hersteller halten (außer natürlich TI...). Konvention
ist PERIPHERIENAME_IRQHandler, d.h. wenn die Peripherie im CMSIS-Header
z.B. mit "USART3" bezeichnet wird, dann ist die Wahrscheinlichkeit sehr
hoch, dass der entsprechende Handler USART3_IRQHandler() heißt.
Außnahmen bestätigen natürlich die Regel, z.B. wenn sich
unterschiedliche Peripherie einen IRQ teilt oder eine Peripherie mehrere
IRQs hat.
Es geht gar nicht um Prolog und Epilog.
Es geht darum daß der Compiler jede Funktion, die nach seiner Kenntnis
niemals aufgerufen wird, einfach wegoptimieren darf, und das auch
gnadenlos tut, und es geht darum, daß die Adresse der ISR ja irgendwie
in die Interrupttabelle gelangen muß.
Beides erfordert etwas Automagie in der Funktionsdefinition und im
Linkerscript.
Ohne das gehts nicht.
Oliver
Oliver S. schrieb:> Beides erfordert etwas Automagie in der Funktionsdefinition und im> Linkerscript.
Nö. Wenn der Startup-Code Assembler ist, optimiert keiner die
Vektortabelle weg. Und weil jene eben die C-Handler referenziert, werden
die auch nicht wegoptimiert.
Keinerlei Magie notwendig.
Bloß ein bißchen: damit man nicht in jedem Pups-Programm alle ISRs
(aus)schreiben muß, kann man (z.B.) im Startupcode Dummy-Handler "weak"
definieren. Dann muß man nur noch die schreiben, die man wirklich
braucht.
Arno schrieb:> 3.) Ihre Adresse steht in einer Tabelle an definierter Speicherstelle /> wird dem Interrupt Controller oder dem entsprechenden Verzweigungs-Code> bekannt gegeben, damit sie auch angesprungen werden, wenn der Interrupt> eintritt>> Keine Ahnung, wie das mit GCC für ARM Cortex üblicherweise funktioniert.
Genauso wie beim AVR auch. Die ISR haben alle "magische" Namen und
werden über diesen Namen in der Vektortabelle referenziert. Zusätzlich
gibt es eine Dummy-ISR mit weak Aliases für alle magischen Namen. Wenn
der Linker die Vektortabelle zusammenstellt, tut er für die
existierenden ISR die Adressen der realen Funktionen rein und für alle
anderen den Alias auf die Dummy-ISR.