Ich entwickle einen kleinen Logikanalysator mit 7 Eingängen. Mein Zielgerät ist ein ATmega168
mit einer Taktrate von 20 MHz. Um logische Änderungen zu erkennen, verwende ich Pinwechsel-Interrupts. Jetzt versuche ich herauszufinden, mit welcher niedrigsten Abtastrate ich diese Pin-Änderungen feststellen kann. Ich habe einen Wert von mindestens 5,6 µs (178,5 kHz) bestimmt. Jedes Signal unterhalb dieser Rate kann ich nicht richtig erfassen.
Mein Code ist in C (avr-gcc) geschrieben. Meine Routine sieht aus wie:
ISR()
{
pinc = PINC; // char
timestamp_ll = TCNT1L; // char
timestamp_lh = TCNT1H; // char
timestamp_h = timerh; // 2 byte integer
stack_counter++;
}
Meine erfasste Signaländerung befindet sich unter pinc
. Um es zu lokalisieren, habe ich einen 4 Byte langen Zeitstempelwert.
Im Datenblatt habe ich gelesen, dass die Interrupt-Serviceroutine 5 Takte benötigt, um einzuspringen, und 5 Takte, um zur Hauptprozedur zurückzukehren. Ich gehe davon aus, dass jeder Befehl in meinem ISR()
1 Takt benötigt, um ausgeführt zu werden. Insgesamt sollte es also einen Overhead von 5 + 5 + 5 = 15
Uhren geben. Die Dauer eines Takts sollte der Taktrate von 20 MHz entsprechen 1/20000000 = 0.00000005 = 50 ns
. Der Gesamtaufwand in Sekunden sollte dann sein : 15 * 50 ns = 750 ns = 0.75 µs
. Jetzt verstehe ich nicht, warum ich nichts unter 5,6 µs erfassen kann. Kann jemand erklären, was los ist?
Antworten:
Es gibt einige Probleme:
AND
ein Ein-Takt-Befehl,MUL
(Multiplizieren) dauert zwei Takte, währendLPM
(Programmierspeicher laden) drei undCALL
4 ist. In Bezug auf die Befehlsausführung hängt dies also wirklich vom Befehl ab.RETI
der Compiler zusätzlich zu den Sprüngen und Anweisungen alle möglichen anderen Codes hinzufügt, was ebenfalls einige Zeit in Anspruch nimmt. Beispielsweise benötigen Sie möglicherweise lokale Variablen, die auf dem Stapel erstellt und entfernt werden müssen usw. Um zu sehen, was tatsächlich vor sich geht, sollten Sie sich die Demontage ansehen.Wenn
x
es Zeit ist, Ihren Interrupt zu warten, wird Signal B niemals erfasst.Wenn wir Ihren ISR-Code in eine ISR-Routine (die ich verwendet habe
ISR(PCINT0_vect)
) einfügen, alle Variablen deklarierenvolatile
und für ATmega168P kompilieren, sieht der zerlegte Code wie folgt aus (weitere Informationen finden Sie in der Antwort von @ jipple), bevor wir zum Code gelangen das "macht etwas" ; Mit anderen Worten, der Prolog zu Ihrem ISR lautet wie folgt:Also ,
PUSH
x 5,in
x 1,clr
x 1. Nicht so schlecht wie die 32-Bit-Vars von Jipple, aber immer noch nichts.Einiges davon ist notwendig (erweitern Sie die Diskussion in den Kommentaren). Da die ISR-Routine jederzeit auftreten kann, muss sie natürlich die verwendeten Register vorab speichern, es sei denn, Sie wissen, dass kein Code, in dem ein Interrupt auftreten kann, dasselbe Register wie Ihre Interrupt-Routine verwendet. Zum Beispiel die folgende Zeile im zerlegten ISR:
Ist da, weil alles durchgeht
r24
: Ihrpinc
wird dort geladen, bevor es in den Speicher geht usw. Also müssen Sie das zuerst haben.__SREG__
wird geladenr0
und dann geschoben: Wenn dies durchgehenr24
könnte, könnten Sie sich selbst sparen aPUSH
Einige mögliche Lösungen:
ISR_NAKED
, generiert gcc keinen Prolog- / Epilog-Code, und Sie sind dafür verantwortlich, alle von Ihrem Code geänderten Register zu speichern und aufzurufenreti
(Rückkehr von einem Interrupt). Leider gibt es keine Möglichkeit, Register in avr-gcc C direkt zu verwenden (natürlich können Sie dies in Assembly tun). Sie können jedoch Variablen an bestimmte Register mit den Schlüsselwörternregister
+ bindenasm
, wie folgt :register uint8_t counter asm("r3");
. Wenn Sie dies tun, wissen Sie für den ISR, welche Register Sie im ISR verwenden. Das Problem ist dann, dass es keine Möglichkeit gibt,push
und zu generierenpop
zum Speichern der verwendeten Register ohne Inline-Assembly (vgl. Punkt 1). Um sicherzustellen, dass weniger Register gespeichert werden müssen, können Sie auch alle Nicht-ISR-Variablen an bestimmte Register binden. Es tritt jedoch kein Problem auf, dass gcc Register zum Mischen von Daten zum und vom Speicher verwendet. Dies bedeutet, dass Sie nicht wissen, welche Register Ihr Hauptcode verwendet, es sei denn, Sie sehen sich die Demontage an. Wenn Sie also überlegenISR_NAKED
, können Sie den ISR genauso gut in Assembly schreiben.quelle
Es gibt eine Menge PUSH'ing- und POP'ing-Register, die gestapelt werden müssen, bevor Ihr tatsächlicher ISR startet, dh zusätzlich zu den 5 Taktzyklen, die Sie erwähnen. Sehen Sie sich die Demontage des generierten Codes an.
Abhängig von der von Ihnen verwendeten Toolchain wird die Baugruppe, die uns auflistet, auf verschiedene Arten ausgegeben. Ich arbeite an der Linux-Befehlszeile und dies ist der Befehl, den ich verwende (es erfordert die .elf-Datei als Eingabe):
Schauen Sie sich ein Code-Sniplet an, das ich kürzlich für ein ATtiny verwendet habe. So sieht der C-Code aus:
Und das ist der generierte Assembler-Code dafür:
Um ehrlich zu sein, verwendet meine C-Routine einige weitere Variablen, die all diese Push'es und Pops verursachen, aber Sie haben die Idee.
Das Laden einer 32-Bit-Variablen sieht folgendermaßen aus:
Das Erhöhen einer 32-Bit-Variablen um 1 sieht folgendermaßen aus:
Das Speichern einer 32-Bit-Variablen sieht folgendermaßen aus:
Dann müssen Sie natürlich die alten Werte einfügen, sobald Sie den ISR verlassen:
Gemäß der Anweisungszusammenfassung im Datenblatt sind die meisten Anweisungen Einzelzyklen, aber PUSH und POP sind Doppelzyklen. Sie haben die Idee, woher die Verzögerung kommt?
quelle
avr-objdump -C -d $(src).elf
!avr-objdump
ausspuckenden Montageanweisungen zu verstehen. Sie werden im Datenblatt unter Anweisungszusammenfassung kurz erläutert. Meiner Meinung nach ist es empfehlenswert, sich mit den Mnemonics vertraut zu machen, da dies beim Debuggen Ihres C-Codes sehr hilfreich sein kann.Makefile
Wenn Sie also Ihr Projekt erstellen, wird es auch automatisch zerlegt, sodass Sie nicht darüber nachdenken oder sich daran erinnern müssen, wie es manuell durchgeführt wird.