Datum:
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:
ISR (TIMER0_COMPA_vect)
{
counter++;
if (counter == BESTIMMTER_WERT)
{
counter = 0;
//tu etwas
}
}
|
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 :-)
Datum:
Und gleich noch ein Entschuldigung hinterher, natürlich das Wichtigste vergessen: ATmega64 @ 20MHz avr-gcc
Datum:
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.
Datum:
Bananen Joe schrieb: > Oder einfach mit AVR-Stduio simulieren. Ich arbeite unter Linux :/ Hier ist mal das Listing der ISR:
00000238 <__vector_13>: 238: 1f 92 push r1 23a: 0f 92 push r0 23c: 0f b6 in r0, 0x3f ; 63 23e: 0f 92 push r0 240: 0b b6 in r0, 0x3b ; 59 242: 0f 92 push r0 244: 11 24 eor r1, r1 246: 2f 93 push r18 248: 3f 93 push r19 24a: 4f 93 push r20 24c: 5f 93 push r21 24e: 6f 93 push r22 250: 7f 93 push r23 252: 8f 93 push r24 254: 9f 93 push r25 256: af 93 push r26 258: bf 93 push r27 25a: ef 93 push r30 25c: ff 93 push r31 25e: 80 91 12 01 lds r24, 0x0112 262: 8f 5f subi r24, 0xFF ; 255 264: 80 93 12 01 sts 0x0112, r24 268: 80 91 12 01 lds r24, 0x0112 26c: 89 31 cpi r24, 0x19 ; 25 26e: b0 f0 brcs .+44 ; 0x29c <__vector_13+0x64> 270: 10 92 12 01 sts 0x0112, r1 274: 80 91 13 01 lds r24, 0x0113 278: 82 30 cpi r24, 0x02 ; 2 27a: 69 f0 breq .+26 ; 0x296 <__vector_13+0x5e> 27c: 83 30 cpi r24, 0x03 ; 3 27e: 71 f4 brne .+28 ; 0x29c <__vector_13+0x64> 280: 80 91 6f 00 lds r24, 0x006F 284: 8d 7f andi r24, 0xFD ; 253 286: 80 93 6f 00 sts 0x006F, r24 28a: e0 91 28 01 lds r30, 0x0128 28e: f0 91 29 01 lds r31, 0x0129 292: 09 95 icall 294: 03 c0 rjmp .+6 ; 0x29c <__vector_13+0x64> 296: 84 e0 ldi r24, 0x04 ; 4 298: 80 93 13 01 sts 0x0113, r24 29c: ff 91 pop r31 29e: ef 91 pop r30 2a0: bf 91 pop r27 2a2: af 91 pop r26 2a4: 9f 91 pop r25 2a6: 8f 91 pop r24 2a8: 7f 91 pop r23 2aa: 6f 91 pop r22 2ac: 5f 91 pop r21 2ae: 4f 91 pop r20 2b0: 3f 91 pop r19 2b2: 2f 91 pop r18 2b4: 0f 90 pop r0 2b6: 0b be out 0x3b, r0 ; 59 2b8: 0f 90 pop r0 2ba: 0f be out 0x3f, r0 ; 63 2bc: 0f 90 pop r0 2be: 1f 90 pop r1 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?
Datum:
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?
Datum:
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.
Datum:
> 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.
Datum:
Der Teil hier
80 91 12 01 lds r24, 0x0112
262: 8f 5f subi r24, 0xFF ; 255
264: 80 93 12 01 sts 0x0112, r24
268: 80 91 12 01 lds r24, 0x0112
26c: 89 31 cpi r24, 0x19 ; 25
26e: b0 f0 brcs .+44 ; 0x29c <__vector_13+0x64>
270: 10 92 12 01 sts 0x0112, r1
|
ist das, was aus deinem
counter++;
if (counter == BESTIMMTER_WERT)
{
counter = 0;
...
|
geworden ist. Das heftige Speichern und Laden ist der volatile Variablen geschuldet. Würdest du es so schreiben
ISR (TIMER0_COMPA_vect)
{
uint8_t tmpCounter = counter;
tmpCounter ++;
if (tmpCounter == BESTIMMTER_WERT)
{
tmpCounter = 0;
...
}
counter = tmpCounter ;
|
würde auch das wahrscheinlich wegfallen.
Datum:
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.
Datum:
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.
Datum:
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
//tu was
|
-Teil findest wirklich ein Funktionsaufruf statt. Auskommentieren des Aufrufs verwandelt das Listing in ein hübsches
00000238 <__vector_13>: 238: 1f 92 push r1 23a: 0f 92 push r0 23c: 0f b6 in r0, 0x3f ; 63 23e: 0f 92 push r0 240: 11 24 eor r1, r1 242: 8f 93 push r24 244: 80 91 12 01 lds r24, 0x0112 248: 8f 5f subi r24, 0xFF ; 255 24a: 80 93 12 01 sts 0x0112, r24 24e: 80 91 12 01 lds r24, 0x0112 252: 89 31 cpi r24, 0x19 ; 25 254: 88 f0 brcs .+34 ; 0x278 <__vector_13+0x40> 256: 10 92 12 01 sts 0x0112, r1 25a: 80 91 13 01 lds r24, 0x0113 25e: 82 30 cpi r24, 0x02 ; 2 260: 41 f0 breq .+16 ; 0x272 <__vector_13+0x3a> 262: 83 30 cpi r24, 0x03 ; 3 264: 49 f4 brne .+18 ; 0x278 <__vector_13+0x40> 266: 80 91 6f 00 lds r24, 0x006F 26a: 8d 7f andi r24, 0xFD ; 253 26c: 80 93 6f 00 sts 0x006F, r24 270: 03 c0 rjmp .+6 ; 0x278 <__vector_13+0x40> 272: 84 e0 ldi r24, 0x04 ; 4 274: 80 93 13 01 sts 0x0113, r24 278: 8f 91 pop r24 27a: 0f 90 pop r0 27c: 0f be out 0x3f, r0 ; 63 27e: 0f 90 pop r0 280: 1f 90 pop r1 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!
Datum:
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.
Datum:
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
Datum:
@ 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
Datum:
Bringt zwar nicht viel: Zähler runter zu zählen und mit 0 zu vergleichen spart eine Instruktion. Sollte man sich zumindest angewöhnen.
Datum:
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.
Datum:
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.
Datum:
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
Datum:
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
Datum:
@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?
Datum:
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.
Datum:
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.
Datum:
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
Datum:
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
Datum:
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
Datum:
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.
Datum:
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