Falsche Frequenz mit Timer1 auf Atmega328p im CTC-Modus

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.

BEARBEITEN 1

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.

BEARBEITEN 2

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.

Antworten (2)

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 NOPnach dem Timer1comp-Vektor brauchst. Dies NOPlandet tatsächlich im nächsten Vektor. Es schadet hier zwar nicht, ist aber zumindest überflüssig.

-Das CS01Bit in der Zeile LDI WR1, (1<<WGM12)|(1<<CS10) ; Timer in CTC mode, TOP = OCR1Aaktiviert 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, :Loopda 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!

Danke für die Klarstellung, ich habe immer gedacht, dass Interrupts nicht nur die Ausführung des Programms stoppen, sondern auch Hardwarefunktionen wie Timer blockieren! Aber das macht doch total Sinn. Ich verstehe jedoch nicht, warum Sie sagen, dass ich die 3 Zeilen eliminieren kann: ok, log<sub>2</sub>(1000) < 16, also könnte ich nur zwei Register verwenden, aber diese sind dazu da stoppen Sie die pseudo-unendliche Verzögerungsschleife ! Warum ändert schließlich die Tatsache, dass OCR1A doppelt gepuffert ist, etwas?
Außerdem habe ich versucht, OCR1A auf 15999 zu ändern: Jetzt beträgt die Blinkfrequenz 565 mHz, während es bei OCR1A = 15976 genau 500 mHz ist ...
Lassen Sie mich mein System hochfahren und einen Blick darauf werfen!
Eine weitere überraschende Tatsache zeigte sich: Wenn ich versuche, nur 10 ms zu verzögern ( LDI WR[1|2|3], BYTE[1|2|3](10)), liegt die Frequenz jetzt bei 45,44 Hz, statt bei 50 Hz ...
Hier tut sich einiges. Geben Sie mir etwas Zeit, um einen 328er auf meiner Werkbank aufzubauen, damit ich ein paar Zielfernrohraufnahmen machen kann.
Endlich habe ich das Problem gefunden: Jetzt beträgt der Fehler etwa 0,1 µs für jede verzögerte Mikrosekunde.
Ok, ich habe alles eingerichtet. Wenn Sie sich nur den Code ansehen, sieht es so aus, als wäre der obige Vergleich jetzt vollständig kaputt. Können Sie die obige Frage mit dem neuesten besten Code aktualisieren, mit dem Sie zurechtkommen, was daran nicht richtig funktioniert, und wir werden uns einarbeiten!
Entschuldigung, ich habe mich falsch ausgedrückt: Ich habe das Problem gelöst, es kam von der Wait-Schleife und nicht von etwas, das mit Timer1 zu tun hat. In dieser Wait-Schleife habe ich bis T > 1000 geloopt, anstatt bis T >= 1000 zu loopen. Wenn ich das ändere, funktioniert alles korrekt, und ich habe im Durchschnitt nur einen Fehler von 0,01 %. Die korrekte Einstellung ist also OCR1A = 15999. Vielen Dank für Ihre Hilfe

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.