Moinsen,
ich hätte eine Frage, bzw eher die Bitte um eine kurze Abschätzung.
Ich habe einen Timerinterrupt, der alle 100us auftritt.
Dieser schaut ungefähr so aus:
1
ISR(TIMER0_COMPA_vect)
2
{
3
counter++;
4
if(counter==BESTIMMTER_WERT)
5
{
6
counter=0;
7
//tu etwas
8
}
9
}
D.h. die meißte Zeit hat die ISR nicht viel zu tun, lediglich der Sprung
und Rücksprung von und zur ISR findet statt sowie das inkrementieren
einer 8 Bit Variable.
Alle heilige Zeit muss die ISR bisl mehr machen, aber das interessiert
mich eher weniger.
Es ist eher das Kleinviech, welches hier wahrscheinlich Mist macht.
Kann jemand ungefähr abschätzen, wieviel Takte der Aufruf, der Vergleich
und das Inkrementieren kosten?
Ich kenne mich leider mit Assembler eher weniger aus. Mir wäre schon
geholfen wenn mir jemand erläutern könnte wie ich es selbst herausfinden
kann.
Nur als Beispiel um was es mir geht:
Angenommen die ISR bräuchte 30 Takte (dieser Wert ist nur ein Schuss ins
Blaue, ich wüsst nicht mal in welchen Größenordnungen ich mich bewege).
Dann hätte ich bei 20 MHz Takt und 100us ISR Intervall einen "Verlust"
von 1.5%. Damit könnte man wohl leben.
(Wobei bei niedriger Taktfrequenz der "Verlust" sehr schnell ansteigen
würde).
Kann jemand also ungefähr abschätzen, was diese ISR so braucht?
Danke und viele Grüße,
ein (noch) c-ler :-)
Der Compiler macht aus C erst mal Assembler, das Listing endet mit .lst
(glaube ich, kann gerade nicht nachsehen, da bei der Arbeit). Dann
schaust Du ins Datenblatt, wie lange die einzelnen Befehle dauern.
Bananen Joe schrieb:> Oder einfach mit AVR-Stduio simulieren.
Ich arbeite unter Linux :/
Hier ist mal das Listing der ISR:
1
00000238 <__vector_13>:
2
238: 1f 92 push r1
3
23a: 0f 92 push r0
4
23c: 0f b6 in r0, 0x3f ; 63
5
23e: 0f 92 push r0
6
240: 0b b6 in r0, 0x3b ; 59
7
242: 0f 92 push r0
8
244: 11 24 eor r1, r1
9
246: 2f 93 push r18
10
248: 3f 93 push r19
11
24a: 4f 93 push r20
12
24c: 5f 93 push r21
13
24e: 6f 93 push r22
14
250: 7f 93 push r23
15
252: 8f 93 push r24
16
254: 9f 93 push r25
17
256: af 93 push r26
18
258: bf 93 push r27
19
25a: ef 93 push r30
20
25c: ff 93 push r31
21
25e: 80 91 12 01 lds r24, 0x0112
22
262: 8f 5f subi r24, 0xFF ; 255
23
264: 80 93 12 01 sts 0x0112, r24
24
268: 80 91 12 01 lds r24, 0x0112
25
26c: 89 31 cpi r24, 0x19 ; 25
26
26e: b0 f0 brcs .+44 ; 0x29c <__vector_13+0x64>
27
270: 10 92 12 01 sts 0x0112, r1
28
274: 80 91 13 01 lds r24, 0x0113
29
278: 82 30 cpi r24, 0x02 ; 2
30
27a: 69 f0 breq .+26 ; 0x296 <__vector_13+0x5e>
31
27c: 83 30 cpi r24, 0x03 ; 3
32
27e: 71 f4 brne .+28 ; 0x29c <__vector_13+0x64>
33
280: 80 91 6f 00 lds r24, 0x006F
34
284: 8d 7f andi r24, 0xFD ; 253
35
286: 80 93 6f 00 sts 0x006F, r24
36
28a: e0 91 28 01 lds r30, 0x0128
37
28e: f0 91 29 01 lds r31, 0x0129
38
292: 09 95 icall
39
294: 03 c0 rjmp .+6 ; 0x29c <__vector_13+0x64>
40
296: 84 e0 ldi r24, 0x04 ; 4
41
298: 80 93 13 01 sts 0x0113, r24
42
29c: ff 91 pop r31
43
29e: ef 91 pop r30
44
2a0: bf 91 pop r27
45
2a2: af 91 pop r26
46
2a4: 9f 91 pop r25
47
2a6: 8f 91 pop r24
48
2a8: 7f 91 pop r23
49
2aa: 6f 91 pop r22
50
2ac: 5f 91 pop r21
51
2ae: 4f 91 pop r20
52
2b0: 3f 91 pop r19
53
2b2: 2f 91 pop r18
54
2b4: 0f 90 pop r0
55
2b6: 0b be out 0x3b, r0 ; 59
56
2b8: 0f 90 pop r0
57
2ba: 0f be out 0x3f, r0 ; 63
58
2bc: 0f 90 pop r0
59
2be: 1f 90 pop r1
60
2c0: 18 95 reti
Also wie gesagt, in Sachen asm bin ich eher unbewandert, aber auf den
ersten Blick schauts so aus als machen die PUSHs und POPs, die ja immer
notwendig sind, den größten Teil der ISR ausmachen (die laut Datenblatt
leider 2 Takte brauchen).
Was ab und zu nur ausgeführt wird ist der Teil zwischen 270: und 29c:
Ich muss also damit rechnen, dass meine ISR immer ca. 80 Takte braucht,
sehe ich das richtig?
c-ler schrieb:> schauts so aus als machen die PUSHs und POPs, die ja immer> notwendig sind, den größten Teil der ISR ausmachen (die laut Datenblatt> leider 2 Takte brauchen).
eigentlich sind sie nicht notwendig, ist der C quellcode genau das was
in der ISR steht oder nicht?
Am genauesten ist immer noch ein Blick ins Assembler-Listing bzw. im
Simulator einmal durchsteppen.
Aber so über den Daumen gepeilt:
Da der AVR eine 8 Bit Maschine ist und die 8-Bit Variablen benutzt.
counter++;
das ist ein Inkrement, den kann die CPU mit einem eigenen Befehl
abarbeiten. Also 1 Assembler Befehl
if (counter == BESTIMMTER_WERT)
Ein Vergleich. BESTIMMTER_WERT sieht jetzt so aus, als ob das eine
Konstante ist. Ein derartiger Vergleich ist eine Assembler-Instruktion
für den Vergleich mit einem nachfolgenden Sprung über den Programmteil
der vom Vergleich abhängig ist. Also 2 Assembler Instruktionen
counter = 0;
Also den Wert auf 0 setzen. Wieder 1 Assembler Instruktion.
Dazu kommt jetzt noch etwas Overhead für
den aktuellen Wert von der Variablen 'counter' im SRAM in ein Register
laden, ehe die ganzen Manipulationen beginnen und danach wieder
zurückschreiben.
Zusätzlich noch der Overhead, den man bei einer ISR immer hat.
Die ganzen 8-Bit Dinge in deinem Code brauchen 1 Takt pro Instruktion.
Der Sprung braucht (aus dem Gedächtnis) länger - 2 Takte. Es gibt keinen
Befehl, der mehr als 2 Takte braucht.
Ich würde sagen, deine 30 Takte sind da schon großzügig geschätzt. Ich
hätte mit allem drum und drann irgendwas um die 20 Takte angenommen.
> 292: 09 95 icall
Du machst da einen Funktionsaufruf in der ISR. Keine gute Idee. Das
zwingt den Compiler ausnahmslos alle Register zu sichern, was wiederrum
Zeit kostet.
Und der grössere Teil der Push/Pop-Orgie geht nicht auf den Counter
selbst, sondern dem nicht gezeigten Rest vom Code zurück. Ein
Funktionsaufruf bringt mit sich, dass alles gesichert werden muss, was
dort verwendet werden könnte. Das ist hier vmtl. kaum zu vermeiden,
erklärt aber das Ausmass.
Karl Heinz Buchegger schrieb:> Du machst da einen Funktionsaufruf in der ISR. Keine gute Idee.
So entstehen Gerüchte. Jene nicht so selten zu findenden, dass in einer
ISR keinesfalls eine Funktion aufgerufen werden darf. Klar, der Aufwand
steigt dadurch, aber obs anders wirklich einfacher und eleganter ist?
Abgesehen davon kriegst du einen generischen Timer-basierten Scheduler
beim besten Willen nicht ohne Funktionsaufruf in der ISR zustande. Der
ist darin der Kern der Sache.
Also bitte keine solchen pauschalen Aussagen ins Blaue schiessen.
Karl Heinz Buchegger schrieb:> Ich würde sagen, deine 30 Takte sind da schon großzügig geschätzt. Ich> hätte mit allem drum und drann irgendwas um die 20 Takte angenommen.
Das hast du aber schon geschrieben bevor ich mein Listing gepostet habe
oder? Die ganzen PUSHs und POPs haben ja schon ca. 40 Takte :-)
Ansonsten vielen Dank an dich, sehr interessante Lektüre, ich werde mich
wohl doch mal mehr mit asm beschäftigen, sehr interessant das ganze...
Und ja, du hast recht, im
1
//tu was
-Teil findest wirklich ein Funktionsaufruf statt.
Auskommentieren des Aufrufs verwandelt das Listing in ein hübsches
1
00000238 <__vector_13>:
2
238: 1f 92 push r1
3
23a: 0f 92 push r0
4
23c: 0f b6 in r0, 0x3f ; 63
5
23e: 0f 92 push r0
6
240: 11 24 eor r1, r1
7
242: 8f 93 push r24
8
244: 80 91 12 01 lds r24, 0x0112
9
248: 8f 5f subi r24, 0xFF ; 255
10
24a: 80 93 12 01 sts 0x0112, r24
11
24e: 80 91 12 01 lds r24, 0x0112
12
252: 89 31 cpi r24, 0x19 ; 25
13
254: 88 f0 brcs .+34 ; 0x278 <__vector_13+0x40>
14
256: 10 92 12 01 sts 0x0112, r1
15
25a: 80 91 13 01 lds r24, 0x0113
16
25e: 82 30 cpi r24, 0x02 ; 2
17
260: 41 f0 breq .+16 ; 0x272 <__vector_13+0x3a>
18
262: 83 30 cpi r24, 0x03 ; 3
19
264: 49 f4 brne .+18 ; 0x278 <__vector_13+0x40>
20
266: 80 91 6f 00 lds r24, 0x006F
21
26a: 8d 7f andi r24, 0xFD ; 253
22
26c: 80 93 6f 00 sts 0x006F, r24
23
270: 03 c0 rjmp .+6 ; 0x278 <__vector_13+0x40>
24
272: 84 e0 ldi r24, 0x04 ; 4
25
274: 80 93 13 01 sts 0x0113, r24
26
278: 8f 91 pop r24
27
27a: 0f 90 pop r0
28
27c: 0f be out 0x3f, r0 ; 63
29
27e: 0f 90 pop r0
30
280: 1f 90 pop r1
31
282: 18 95 reti
Das ist ja ne echt heftige Einsparung!
Werde wohl doch lieber ein Flag setzen und die Funktion danach aufrufen
:-)
Danke euch nochmal!
c-ler schrieb:> Werde wohl doch lieber ein Flag setzen und die Funktion danach aufrufen
Wenn das zum Programm passt, dann ist das sicherlich der effizientere
Weg. Jetzt musst du nur noch die erwähnte Sache mit dem volatile
angehen.
In dieseml Fall genügt ein Flag, ja.
Das zwischenspeichern der volatile Variable "counter" in einer lokalen
Variablen scheint leider nichts gebracht zu haben.
Wahrscheinlich sind diese 3 Zugriffe (Inkrementieren, Vergleichen, evtl.
auf 0 setzen) nicht genug um da was zu verschlechtern.
Viele Grüße,
c-ler
@ c-ler (Gast)
>Das zwischenspeichern der volatile Variable "counter" in einer lokalen>Variablen scheint leider nichts gebracht zu haben.
Logisch, LESEN muss die CPU immer.
>Wahrscheinlich sind diese 3 Zugriffe (Inkrementieren, Vergleichen, evtl.>auf 0 setzen) nicht genug um da was zu verschlechtern.
Jo.
Allgemein sollte man beim AVR-GCC keine Funktionsaufrufe in einer ISR
machen, wenn es schnell gehen soll. Der push/pop Overhead ist enorm. Der
Rest steht im Artikel Interrupt.
MfG
Falk
A. K. schrieb:> Abgesehen davon kriegst du einen generischen Timer-basierten Scheduler> beim besten Willen nicht ohne Funktionsaufruf in der ISR zustande.
Naja, etwas generisch zu machen, zieht meistens einen Overhead mit sich.
Aber warum soll ich dazu Funktionen in einer ISR aufrufen?
> Also bitte keine solchen pauschalen Aussagen ins Blaue schiessen.
Im Hinblick darauf, daß der Fragesteller möglichst nicht viel Zeit in
der ISR verbringen will und daß Funktionsauf, dem er quasi gar keine
Beachtung geschenkt hat, dann doch indirekt schon für mehr Laufzeit
verantwortlich ist, als der ganze Rest, finde ich die Aussage schon
gerechtfertigt.
Falk Brunner schrieb:> @ c-ler (Gast)>>>Das zwischenspeichern der volatile Variable "counter" in einer lokalen>>Variablen scheint leider nichts gebracht zu haben.>> Logisch, LESEN muss die CPU immer.
Naja, aber mit der temporären Variable muß genau einmal gelesen und
einmal geschrieben werden, unabhängig davon, was da alles noch
zwischendrin damit angestellt wird.
>Wahrscheinlich sind diese 3 Zugriffe (Inkrementieren, Vergleichen, evtl.>auf 0 setzen) nicht genug um da was zu verschlechtern.
Inkrementieren besteht aus zwei Zugriffen.
Rolf Magnus schrieb:> Aber warum soll ich dazu Funktionen in einer ISR aufrufen?
Beispielsweise wenn die erforderlichen Reaktionszeiten auf bestimmte
Timer-Events im Millisekundenbereich liegen, aber in der Mainloop Zeug
läuft, das sich nicht auf elegante Art in Häppchen garantiert unter 1ms
zerschnippeln lässt.
Man hat dann die Wahl, den Mainloop-Code entsprechend gewaltsam zu
zerlegen, unter dem Risiko, dass man irgendeinen Pfad übersieht, oder
die zeitgesteuerten Aktivitäten in der ISR aufzurufen.
Oft wird der Overhead von ein paar µs pro 1ms für gesicherte Reaktion
nicht weiter ins Gewicht fallen, während eine verpasste Reaktion fatal
ist.
Dass diese Aktivitäten dann eher preemptime als cooperative laufen und
man gemeinsame Resourcen in der Mainloop entsprechend beachten muss ist
klar. Einen Tod muss man sterben.
> Naja, aber mit der temporären Variable muß genau einmal gelesen und> einmal geschrieben werden, unabhängig davon, was da alles noch> zwischendrin damit angestellt wird.
Es kommt zwar unterschiedlicher Code raus, es sind aber beides Mal 6
Befehle in 8 Takten. Wird erst wirklich relevant, wenn man mit dem
Counter noch mehr macht, als ihn nur modulo N zu inkrementieren.
Falk Brunner schrieb:> Allgemein sollte man beim AVR-GCC keine Funktionsaufrufe in einer ISR> machen, wenn es schnell gehen soll. Der push/pop Overhead ist enorm. Der> Rest steht im Artikel Interrupt.
Das kann man so allgemein nicht sagen. Ist die aufgerufene Funktion
static, weiß der Compiler genau, ob und welche Register gesichert werden
müssen. Bei externen Funktionen hast Du natürlich recht.
Gruß,
Frank
Es werden alle 12 zerstörbaren Register gesichert, das sind allein schon
48 Zyklen. Ich würde sagen, insgesamt etwa 80 Zyklen.
Allerdings sind das nur Milchmädchenrechnungen, sobald Du mehrere
Interrupts hast.
Es kann ja sein, daß gerade ein anderer Interrupt mit 500 Zyklen
angefangen hat und der muß natürlich erst zuende ausgeführt worden sein.
Der längste mögliche Interrupt bestimmt also die maximale
Interruptlatenz.
Peter
@c-ler: blöde frage aber warum kann der Timerinterrupt nicht gleich bis
100µS * BESTIMMTER_WERT warten? wenn man alle 100µS eh erstmal nur eine
Zählervariable inkrementiert kann man doch gleich mit dem Timer Compare
register rumspielen oder?
Peter Dannegger schrieb:> Es kann ja sein, daß gerade ein anderer Interrupt mit 500 Zyklen> angefangen hat und der muß natürlich erst zuende ausgeführt worden sein.> Der längste mögliche Interrupt bestimmt also die maximale> Interruptlatenz.
wenn man aber weiß, das ein anderer Interrupt wichtiger ist, kann man in
solchen Fällen auch andere Interrupts während der Abarbeitung eines
solchen längeren Interrupts zulassen und somit die Latenz verbessern.
benwilliam schrieb:> @c-ler: blöde frage aber warum kann der Timerinterrupt nicht gleich bis> 100µS * BESTIMMTER_WERT warten? wenn man alle 100µS eh erstmal nur eine> Zählervariable inkrementiert kann man doch gleich mit dem Timer Compare> register rumspielen oder?
Bei einer einzelnen Wartezeit ja. Oft ist es freilich so, dass man
diverse zeitgesteuerte Abläufe hat, Uhrzeit inklusive, bei der man nicht
jeden einzelnen mit einem Hardware-Timer versehen will. Sondern mit
einem zentralen Timer-Tick arbeitet, von dem man alle nicht auf die
Mikrosekunde genauen Zeiten ableitet.
Zwar kann man einen solchen Timer-Tick auch adaptiv programmieren, also
statt in Software tote Ticks abzuzählen den Timer ad hoc
reprogrammieren, aber da siegt gern die Faulheit. In Software ist es
einfacher. Und wenn das nicht grad dank sehr kurzer Tick-Intervalle
exzessiv Background-Zeit frisst, dann spielt der Unterschied oft keine
Rolle.
A. K. schrieb:> Zwar kann man einen solchen Timer-Tick auch adaptiv programmieren, also> statt in Software tote Ticks abzuzählen den Timer ad hoc> reprogrammieren, aber da siegt gern die Faulheit.
und die Berechnung der nächsten Zykluszeit sowie die Auswertung, welche
Aktion beim aktuellen Tick fällig ist, gibt es auch nicht umsonst.
Oliver
benwilliam schrieb:> @c-ler: blöde frage aber warum kann der Timerinterrupt nicht gleich bis> 100µS * BESTIMMTER_WERT warten? wenn man alle 100µS eh erstmal nur eine> Zählervariable inkrementiert kann man doch gleich mit dem Timer Compare> register rumspielen oder?
Bei meiner ersten Version hab ichs so gemacht, ja.
Und dazu hab ich auch einen 16 Bit Timer verwendet.
Das ersschien mir aber als Verschwendung, mit dem kann man doch viel
schönere Sachen machen.
Durch Ändern des Compare Wertes kann man natürlich die Zeitbasis der
Software ändern.
Ich wollte also einen 8 Bit Timer verwenden. Dieser ist ja erstmal
weniger flexibel was die Einstellung der Zeitbasis betrifft. Und nur mit
dem Compare Register kommt man da auch nicht weit. Ohne Änderung des
Prescalers kriegt man kaum einen größeren Bereich an möglichen Zeitbasen
abgedeckt.
Deshalb verwende ich jetzt eben einen festen Prescaler (8) und einen
festen Comparewert (250) um die Kleinstmögliche Zeitbasis (100us) zu
generieren. Durch Ändern von BESTIMMTER_WERT kann man Vielfache bis
100us * 255 davon ableiten.
Ich halte das für die flexibelste Lösung und auch für die einfachste.
Außerdem wird nur ein 8 Bit Timer benötigt, und an dessen Einstellungen
muss auch nicht herumgespielt werden.
Eine Funktion die zB eine Stoppuhr für einen bestimmten Codeabschnitt
implementiert tut sich auch leichter (vor allem wenn diese
Festkommaarithmetik und Divisionsoptimierung durch Shiften von
Zweierpotenzen benutzt).
Und diese Vorteile überwieden imho den Nachteil, dass der
"Steuer"interrupt möglicherweise öfters ausgeführt wird.
Bei 30 Takten (mittlerweile gezählt anstatt geraten :-)) sind das 1.5%
Overhead, imho akzeptabel.
@ PeDa:
Danke für den Hinweiß mit der größtmöglichen Interruptlatenz.
In meinem Fall weniger tragisch, da der Timer ja trotzdem weiterläuft.
Solang also ein anderer IR nicht über 100us frisst geht mir nichts
verloren :-)
@ Allgemeinheit: Zwar bisl Offtopic:
In welchen Größenordnungen bewegt sich den üblicherweise der Overhead,
denn man bei Betriebssystem, Schedulern oder
was-auch-immer-für-Tasksteuerung in Kauf nimmt?
Grüße,
der c-ler
Volkmar Dierkes schrieb:> wenn man aber weiß, das ein anderer Interrupt wichtiger ist, kann man in> solchen Fällen auch andere Interrupts während der Abarbeitung eines> solchen längeren Interrupts zulassen und somit die Latenz verbessern.
Leider sind aber die langen Interrupts ausgerechnet immer solche, die
ihr Flag nicht beim Eintritt löschen, z.B. UART-, SPI- oder
I2C-Protokoll-Parser.
Das ISR_NOBLOCK beim AVR-GCC würde dann sofort den Stack fluten, geht
also nicht.
Also hilft nur, alles Lange ins Main auslagern. Den Luxus langer
Interrupts kann man sich nur bei MCs mit konfigurierbaren
Interruptleveln leisten.
Peter
c-ler schrieb:> In welchen Größenordnungen bewegt sich den üblicherweise der Overhead,> denn man bei Betriebssystem, Schedulern oder> was-auch-immer-für-Tasksteuerung in Kauf nimmt?
Ein "üblicherweise" gibt es nicht. Das hängt vom Betriebsystem (oder
RTOS, Realtime-Kernel, Scheduler), der eingesetzten Hardware und der
Länge des Timer-Ticks ab.
Ich habe mal einen 16MHz AVR mit einem Realtime-Kernel mit von mir recht
kurz definierten 100µs Ticks gefahren, weil der damit auch für manche
1-Wire Delays taugte. Da gingen zwar 10% für den Tick drauf, das störte
aber nicht weiter.
Peter Dannegger schrieb:> Volkmar Dierkes schrieb:>> wenn man aber weiß, das ein anderer Interrupt wichtiger ist, kann man in>> solchen Fällen auch andere Interrupts während der Abarbeitung eines>> solchen längeren Interrupts zulassen und somit die Latenz verbessern.>> Leider sind aber die langen Interrupts ausgerechnet immer solche, die> ihr Flag nicht beim Eintritt löschen, z.B. UART-, SPI- oder> I2C-Protokoll-Parser.> Das ISR_NOBLOCK beim AVR-GCC würde dann sofort den Stack fluten, geht> also nicht.
In diesem Fall würde ich auch nicht die ISR_NOBLOCK Option verwenden,
sondern es von Hand durchführen. Also erst den Interrupt der langen ISR
sperren und dann die Interrupts wieder freigeben. Somit wird die ISR
nicht mehrfach aufgerufen. Am Ende der ISR muß dann der zugehörige
Interrupt wieder erlaubt werden. Aber es ist nur ein letzter Notnagel,
wenn die HW in dieser Hinsicht eingeschränkt ist.
Volkmar