Ich möchte eine LED mit einer Frequenz von 0,5 Hz blinken lassen. Also verwende ich einen Atmega328p, getaktet mit 16 MHz, und den Timer1 im CTC-Modus, der jede Millisekunde einen Interrupt auslöst. Mein Programm in Atmel Assembly sieht jedoch so aus:
.include "m328pdef.inc"
.def WR1 = R16 ; the working registers
.def WR2 = R17
.def WR3 = R18
.def T1 = r13 ; A 24 bits value used to hold the
.def T2 = r14 ; number of milliseconds
.def T3 = r15 ; In T1 is the LSB, and in T3 the MSB
.cseg
.org $0000
RJMP Start
.org OC1Aaddr ; The address of the Timer1A compare match interrupt
JMP Timer1comp
NOP
.org INT_VECTORS_SIZE
;---------------;
; Settings ;
;---------------;
Start:
; Initialize the Stack Pointer
LDI WR1, HIGH(RAMEND)
OUT SPH, WR1
LDI WR1, LOW(RAMEND)
OUT SPH, WR1
; Timer1A setup
CLR T1 ; Reset the ms counter
CLR T2
CLR T3
LDI WR1, (1<<WGM12)|(1<<CS10) ; Timer in CTC mode, TOP = OCR1A
STS TCCR1B, WR1
LDI WR1, HIGH(15999) ; Must wait 16000 clock cycles in order to
LDI WR2, LOW(15999) ; have a frequency of interrupt of 1 kHz.
STS OCR1AH, WR1 ; ISR doesn't interrupt timer, so have
STS OCR1AL, WR2 ; to set OCR1A to 16000 - 1
LDI WR1, 1 << OCIE1A ; Enable Output Compare A match interrupt
STS TIMSK1, WR1
; I/O settings
SER WR1 ; The LED is plugged on any pin of port D
OUT DDRD, WR1
OUT PORTD, WR1
; Enable interrupts
SEI
;---------------;
; Main Loop ;
;---------------;
Loop:
CLI ; Reset the ms counter
CLR T1
CLR T2
CLR T3
SEI
IN WR1, PORTD ; Toggle port D
SER WR2
EOR WR1, WR2
OUT PORTD, WR1
; HERE WAS MY ERROR
;LDI WR1, BYTE1(988) ; 12 clock cycles are passed
;LDI WR2, BYTE2(988) ; We want to wait 1000 ms
;LDI WR3, BYTE3(988)
; CORRECT WAY
LDI WR1, BYTE1(1000) ; We have already spent 12 clock cycles (750 ns)
LDI WR2, BYTE2(1000) ; which is totally negligible against
LDI WR3, BYTE3(1000) ; our delay of 1000 ms
Wait:
CLI
CP T1, WR1 ; Compare the time (ms) since start of loop
CPC T2, WR2 ; with 1000
CPC T3, WR3
SEI
BRLO Wait ; Go to Wait if time < 1000 ms
RJMP Loop
;-----------------;
; Timer1A ISR ;
;-----------------;
Timer1comp:
; Save the current state on the stack
PUSH WR1
IN WR1, SREG
PUSH WR1
INC T1 ; Increment the ms counter
BRNE Timer1compEnd
INC T2
BRNE Timer1compEnd
INC T3
Timer1compEnd:
; Reset the state
POP WR1
OUT SREG, WR1
POP WR1
RETI
Das Problem ist, wenn ich die Blinkfrequenz der LED messe, sehe ich statt 0,500 Hz 0,506 Hz. Ich frage mich, woher dieser Fehler kommt.
Außerdem bin ich wirklich neu in der Assembler-Programmierung, daher freue ich mich über jede Art von Beratung.
Endlich habe ich den Ursprung meines Problems gefunden: In der Hauptschleife lade ich WR1:WR2:WR3 mit 988 ms , weil die Hauptschleife 12 Taktzyklen benötigt . Aber selbst wenn die Hauptschleife 12 Taktzyklen benötigt, dauert sie nur 12 / 16000000 = 0,75 µs, was gegenüber der 1-s-Periode völlig vernachlässigbar ist. Wir können davon ausgehen, dass diese Schleife 0s dauert: Dann warte ich 12 ms weniger als erwartet, was einem Fehler von 1,2 % entspricht, wie Michael Karas sagte. Um dies zu korrigieren, sollte ich WR1:WR2:WR3 auf (1000 - 0,00075) setzen, also 1000.
Bei meinem vorherigen Code gab es ein Problem: In der Wait-Schleife habe ich getestet, ob die gewünschte Zeit größer oder gleich der verstrichenen Zeit war. Wenn also die verstrichene Zeit 1000 ist, dann würde der Mikrocontroller bis zur 1001-ten Millisekunde warten, um zur Hauptschleife zu verzweigen. Um dies zu korrigieren, muss ich also testen, ob die verstrichene Zeit kleiner als die gewünschte Zeit ist oder nicht.
Versuchen Sie, einen Wert von 15999 für TOP in OCR1 zu verwenden.
Sie müssen die Zeit, die für die Ausführung des ISR benötigt wird, nicht berücksichtigen. Der Timer ist freilaufend. Im CTC-Modus beginnt es bei 0 und zählt bis zum TOP-Wert hoch, löscht dann auf Null zurück und beginnt erneut. Es kann optional jedes Mal, wenn es gelöscht wird, einen Interrupt generieren, aber das ist ein Nebeneffekt - es zählt so ganz alleine mit oder ohne Interrupt.
Der Timer zählt weiter, während Ihr ISR ausgeführt wird. Ihre ISR wird beim nächsten Löschen erneut aufgerufen, unabhängig davon, wie lange die Ausführung dauert, solange die Ausführung vor dem nächsten Löschen und dem nächsten Interrupt abgeschlossen ist.
Daher kann man die Linien auch eliminieren...
LDI WR1, BYTE1(988) ; 12 clock cycles are passed
LDI WR2, BYTE2(988) ; We want to wait 1000 ms
LDI WR3, BYTE3(988)
...da sie keine Wirkung haben.
Beachten Sie, dass Ihre LED mit dem obigen Code aufgrund von Jitter nicht genau alle 2 Sekunden umschaltet, da die Vordergrundaufgabe zu zufälligen Zeiten durch die ISR unterbrochen werden kann. Wenn es nach dem Vergleich unterbrochen wird, schaltet die LED bis zum nächsten Durchgang nicht um. Sie werden immer noch eine sehr solide Ausgangsfrequenz von 0,5 Hz haben, es wird nur ein paar Dutzend Zyklen von Jitter bei jedem gegebenen Übergang geben. Wenn Sie möchten, dass der Ausgang genau +/- 1 Zyklus jitterfrei ist, können Sie einen der Ausgangsvergleichsstifte in den Umschaltmodus versetzen und die Hardware die LED für Sie blinken lassen.
Sinn ergeben?
Der endgültige ASM-Code ist sehr schön, aber hier sind ein paar Vorschläge ...
-Ich glaube nicht, dass du den NOP
nach dem Timer1comp-Vektor brauchst. Dies NOP
landet tatsächlich im nächsten Vektor. Es schadet hier zwar nicht, ist aber zumindest überflüssig.
-Das CS01
Bit in der Zeile LDI WR1, (1<<WGM12)|(1<<CS10) ; Timer in CTC mode, TOP = OCR1A
aktiviert tatsächlich den Timer und beginnt mit dem Zählen. Da Sie die OCR-Register noch nicht zugewiesen haben, werden sie auf ihre Anfangswerte von 0 gesetzt, was bedeutet, dass eine Übereinstimmung sofort erfolgt. Auch dies verursacht keine Probleme im aktuellen Programm, aber sobald Sie anfangen, Funktionalität hinzuzufügen, kann es einige schwer zu findende Fehler machen. Es ist besser, alle Timer-Register einzurichten und es dann als letzten Schritt zu aktivieren, wenn alles bereit und in einem bekannten Zustand ist. Dies würde bedeuten, die obige Zeile zu ändern und dann am Ende des Setups LDI WR1, (1<<WGM12)
eine zusätzliche Zeile hinzuzufügen .LDI WR1, (1<<WGM12)|(1<<CS10)
-Ich würde die Ausgangspins beim Start nicht manuell hoch setzen. Lassen Sie sie stattdessen beim ersten Rollover des ms-Timers eingestellt werden. Dies verzögert den Start der Signalerzeugung, stellt aber sicher, dass der erste Impuls die richtige Breite hat. Wie geschrieben, wird der erste Impuls kurz sein, wenn Sie den Timer starten und die Stifte hoch setzen. Wenn Sie wirklich möchten, dass die Signalerzeugung so schnell wie möglich nach dem Zurücksetzen beginnt, können Sie die Pins ganz am Anfang des Programms auf High setzen und dann den TCNT initialisieren, um die verlorene Zeit beim ersten Durchgang zu berücksichtigen.
-Sie können die Linien ersetzen ...
IN WR1, PORTD ; Toggle port D
SER WR2
EOR WR1, WR2
OUT PORTD, WR1
...mit dem etwas effizienteren...
SER WR2
OUT PIND, WR1
Aus dem Datenblatt:
14.2.2 Pin umschalten
Das Schreiben einer logischen Eins an PINxn schaltet den Wert von PORTxn um, unabhängig vom Wert von DDRxn.
- Das Löschen der Tx-Register im Haupt-Thread kann zu einer potenziellen Race-Bedingung und verlorenen Ticks führen. Sie kennen den Wert von Tx nicht, wenn Sie ihn bestehen, :Loop
da er theoretisch im Hintergrund vom ISR aktualisiert worden sein könnte, seit Sie ihn das letzte Mal angesehen haben. In der Praxis ist dies mit dem aktuellen Code unmöglich, aber wenn Sie anfangen, Dinge hinzuzufügen, würde dies zu schwer zu findenden Fehlern führen. Stellen Sie sich zum Beispiel vor, dass der Tx zwischen dem Vergleich und dem Löschen von 1.000 auf 1.001 erhöht wird. Sie haben diese Millisekunde jetzt für immer verloren und der nächste Impuls wird 1 ms kurz sein. Im Allgemeinen gilt es aus diesem Grund als schlechte Praxis, einen Wert aus zwei verschiedenen Threads zu aktualisieren.
Eine Möglichkeit, dies zu umgehen, wäre, 1.000 von Tx am Anfang der Schleife zu subtrahieren, anstatt es auf 0 zu löschen. Eine andere Möglichkeit, dies zu umgehen, wäre, die Übereinstimmung mit 1.000 innerhalb der ISR zu überprüfen und stattdessen einfach ein Flag zu setzen, das dann die Hauptschleife ist prüft und löscht (so macht es der Ardunio-Timer-Code).
Aber ich denke, für diesen Code wäre die einfachste und effizienteste Lösung, das Inkrement, den Test, die Aktion und den Rest in die ISR zu verschieben. Während Sie im Allgemeinen so wenig wie möglich in der ISR tun möchten, können wir die ISR durch Verschieben von Dingen immer noch sehr kurz halten. Wie ich sehe, schätzen Sie Carry-Bit-Jonglage, also zähle ich statt von 0 bis 1.000 von -1000 bis 0. Ich denke, dies macht die Grenzüberprüfung innerhalb der ISR etwas effizienter. Ich habe auch die Tx-Register verschoben, um sie zu können LDI
. Hier ist dieser letzte modifizierte Code ...
.def WR1 = R16 ; the working registers
.def WR2 = R17
.def WR3 = R18
.def T1 = r19 ; A 24 bits value used to hold the
.def T2 = r20 ; number of milliseconds
.def T3 = r21 ; In T1 is the LSB, and in T3 the MSB
.cseg
.org $0000
RJMP Start
.org OC1Aaddr ; The address of the Timer1A compare match interrupt
JMP Timer1comp
.org INT_VECTORS_SIZE
;---------------;
; Settings ;
;---------------;
Start:
; Initialize the Stack Pointer
LDI WR1, HIGH(RAMEND)
OUT SPH, WR1
LDI WR1, LOW(RAMEND)
OUT SPH, WR1
LDI T1, BYTE1(-1000) ; Init the ms counter to -1000
LDI T2, BYTE2(-1000) ; we start a a negative number and count up becuase it
LDI T3, BYTE3(-1000) ; is more efficient to test if we got to 0
; Timer1A setup
LDI WR1, (1<<WGM12) ; Timer in CTC mode, TOP = OCR1A
STS TCCR1B, WR1
LDI WR1, HIGH(15999) ; Must wait 16000 clock cycles in order to
LDI WR2, LOW(15999) ; have a frequency of interrupt of 1 kHz.
STS OCR1AH, WR1 ; ISR doesn't interrupt timer, so have
STS OCR1AL, WR2 ; to set OCR1A to 16000 - 1
LDI WR1, 1 << OCIE1A ; Enable Output Compare A match interrupt
STS TIMSK1, WR1
; I/O settings
SER WR1 ; The LED is plugged on any pin of port D
OUT DDRD, WR1
; Enable Timer
LDS WR1, TCCR1B
ORI WR1, (1<<CS10) ; clock prescaler=1
STS TCCR1B, WR1
; Enable interrupts
SEI
;---------------;
; Main Loop ;
;---------------;
Loop:
RJMP Loop
;-----------------;
; Timer1A ISR ;
;-----------------;
Timer1comp:
; Save the current state on the stack
PUSH WR1
IN WR1, SREG
PUSH WR1
LDI WR1,1 ; Increment the ms counter LSB by 1
ADD T1,WR1
LDI WR1,0 ; Increment the rest by zero so only the carry will propigate
ADC T2,WR1
ADC T3,WR1
BRCC Timer1compEnd ; If carry is set, then we rolled the ms counter
;Reset the ms counter back to -1000
LDI T1, BYTE1(-1000) ; Init the ms counter
LDI T2, BYTE2(-1000)
LDI T3, BYTE3(-1000)
SER WR1
OUT PIND, WR1 ;; Toggle all PORTD output pins
Timer1compEnd:
; Reset the state
POP WR1
OUT SREG, WR1
POP WR1
RETI
Dieser Code ist funktional gleichwertig, aber etwa 15 % kleiner und eliminiert einige Race-Conditions, die sich in zukünftigen Versionen zu Fehlern hätten entwickeln können. Lassen Sie mich wissen, wenn ich etwas verpasst habe.
Lassen Sie mich noch einmal wiederholen, dass Ihr ursprünglicher Code sehr schön und viel sauberer war als viele kommerzielle Produktionscodes, die ich gesehen habe. Wenn man bedenkt, dass Sie neu in der Asm-Programmierung sind, denke ich, dass Sie mit ein wenig Erfahrung schnell ein Asm-Meister werden werden!
Der Wert von 0,506 Hz, wenn Sie 0,500 Hz erwartet haben, liegt um einen Faktor von 1,2 % daneben.
Sie sollten die Genauigkeit Ihres 1-ms-Interrupts überprüfen, um festzustellen, ob er ebenfalls um 1,2 % ausgeschaltet ist.
Wenn das ausgeschaltet ist, könnte es sein, dass Ihr Versuch, diesen Interrupt genau 1 ms lang zu machen, um ein oder zwei Zählwerte optimiert werden muss.
Spirin
Spirin
Großer Josch
Spirin
LDI WR[1|2|3], BYTE[1|2|3](10)
), liegt die Frequenz jetzt bei 45,44 Hz, statt bei 50 Hz ...Großer Josch
Spirin
Großer Josch
Spirin
Großer Josch