Initialisierung von Timer 1, dem 16-Bit-Timer auf dem ATmega328:
TCCR1A = 0; // normal operation
TCCR1B = bit(CS10); // no prescaling
OCR1A = 0;
OCR1B = 0;
TIMSK1 |= bit(TOIE1); // Timer/Counter 1, Overflow Interrupt Enable
16-Bit-Überläufe erhöhen einen Überlaufzähler:
ISR(TIMER1_OVF_vect) {
timer1OverflowCount ++;
}
In einer Schleife wird geprüft, ob die beiden 16-Bit-Zähler korrekt hochzählen:
void loop() {
static uint16_t lastOc = 0, lastC = 0;
uint16_t oc, c;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
oc = timer1OverflowCount;
c = TCNT1;
}
if (c < lastC && oc == lastOc) {
print(oc, c, lastOc, lastC);
}
lastOc = oc;
lastC = c;
}
Beispielausgabe an die serielle Konsole:
Bad overflow: oc = 31, c = 49, lastOc = 31, lastC = 65440
Bad overflow: oc = 58, c = 49, lastOc = 58, lastC = 65440
Bad overflow: oc = 66, c = 49, lastOc = 66, lastC = 65440
Bad overflow: oc = 118, c = 49, lastOc = 118, lastC = 65440
Bad overflow: oc = 127, c = 49, lastOc = 127, lastC = 65440
Warum wird der Überlaufzähler manchmal nicht erhöht?
Ich verstehe, dass die Schleife häufig auf den Überlaufzähler zugreift. Während eines Zugriffs werden Interrupts mit abgeschaltet ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
. Der Zugriff ist jedoch schnell, und ich erwarte, dass der Überlauf-Interrupt in die Warteschlange gestellt wird, damit er nie verpasst wird.
Vollständiger Code für den Arduino Pro Mini ATmega328P (5 V, 16 MHz), kompatibel mit der Arduino IDE 1.8.1:
#include <util/atomic.h>
volatile uint16_t timer1OverflowCount = 0;
ISR(TIMER1_OVF_vect) {
timer1OverflowCount ++;
}
void setup() {
TCCR1A = 0; // normal operation
TCCR1B = bit(CS10); // no prescaling
OCR1A = 0;
OCR1B = 0;
TIMSK1 |= bit(TOIE1); // Timer/Counter 1, Overflow Interrupt Enable
Serial.begin(9600);
}
void print(uint16_t oc, uint16_t c, uint16_t lastOc, uint16_t lastC) {
Serial.print("Bad overflow: ");
Serial.print("oc = ");
Serial.print(oc);
Serial.print(", c = ");
Serial.print(c);
Serial.print(", lastOc = ");
Serial.print(lastOc);
Serial.print(", lastC = ");
Serial.println(lastC);
}
void loop() {
static uint16_t lastOc = 0, lastC = 0;
uint16_t oc, c;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
oc = timer1OverflowCount;
c = TCNT1;
}
if (c < lastC && oc == lastOc) {
print(oc, c, lastOc, lastC);
}
lastOc = oc;
lastC = c;
}
Sofern Interrupts vom Lesen eines Werts bis zu seiner Verwendung nicht vollständig deaktiviert sind, sollte man im Allgemeinen die Idee berücksichtigen, dass ein Versuch, einen Wert von etwas zu lesen, das sich ändert, jeden Wert ergeben kann, den das Ding zu jeder Zeit hatte während des Versuchs, ohne sich Gedanken darüber zu machen, wann genau abgetastet wird.
Eine einfache allgemeine Art, einen 32-Bit-Lesevorgang zu handhaben, besteht darin, einen Wert zweimal in n1 und n2 zu lesen (lesen Sie die Bytes in beliebiger Reihenfolge, vorausgesetzt, dass der letzte Lesevorgang für n1 dem ersten Lesevorgang für n2 vorangeht) und nicht. Machen Sie sich keine Sorgen über das Deaktivieren von Interrupts) und sagen Sie dann:
if ((uint8_t)((n1 >> 24) ^ (n2 >> 24)) // MSB has changed
n2 &= 0xFF000000;
else if ((uint8_t)((n1 >> 16) ^ (n2 >> 16))
n2 &= 0xFFFF0000;
else if ((uint8_t)((n1 >> 8) ^ (n2 >> 8))
n2 &= 0xFFFFFF00;
Wenn sich das obere Byte des Timers von xx zu yy ändert, bedeutet dies, dass der Wert des Timers höchstens xxFFFFFF war, wenn das obere Byte xx war, und wenn der Wert yy war, der Wert des Timers mindestens yy000000 war. Daher muss der Wert irgendwann zwischen dem Lesen von xx und dem Lesen von yy yy000000 gewesen sein. Eine ähnliche Logik gilt für die anderen Bytes des Timers.
Beachten Sie, dass die zum Ausführen dieses Codes erforderliche Zeit begrenzt ist, selbst wenn eine starke Unterbrechungsbelastung vorliegt, vorausgesetzt, dass die Belastung weniger als 100 % beträgt. Beachten Sie, dass der Code keinen Versuch unternimmt, z. B. zwischen dem Fall zu unterscheiden, in dem der Timer 0x01FF0000 vor dem Code und 0x02000000 nach [mehr als 65.000 Zyklen in Interrupts verbracht hat] oder 0x01FFFFFF davor und 0x02010000 danach war. Es wird in beiden Fällen 0x02000000 gemeldet, aber das würde einen Wert darstellen, den der Timer irgendwann während der Ausführung dieses Codes gehalten hat.
Das Problem besteht darin, dass Sie das eigentliche Problem mit der ATOMIC-Anweisung nicht beseitigen – der Zähler wird in der Hardware erhöht und es wird Zeiten geben, in denen ein Interrupt ansteht, aber nicht abgeschlossen ist. Es ist schlimmer, weil die Cint-Operationen auf einem 8-Bit-Prozessor viele Zyklen benötigen, aber es würde sogar in engem ASM-Code auftauchen.
Wenn Sie keine anderen Interrupts von langer Dauer haben, müssen Sie nicht einmal Interrupts beenden, Sie können einfach die Zählung korrigieren.
Die allgemeine Vorgehensweise ist wie folgt:
Wenn sich timer1OverflowCount geändert (erhöht) hat, sehen Sie sich das Beispiel von TCNT1 an. Wenn es MSB = 0 hat, verwenden Sie den späteren Timer1OverflowCount. Wenn es MSB=1 hat, verwenden Sie den früheren Timer1OverflowCount.
Es ist nicht erforderlich, Interrupts auszuschalten, es sei denn, es gibt andere Interrupts mit langsamen ISRs, die dazu führen könnten, dass sich die Gesamtzeit für das Obige der Zeit annähert, in der der Hardwarezähler die Hälfte des vollen Zählwerts erreicht. Es ist fast immer unerwünscht, Interrupts auszuschalten, es sei denn, es ist absolut notwendig.
Bearbeiten: In Ihrem Fall haben Sie einen int für den Überlauf, also müssen Sie das Lesen der Variablen innerhalb der ISR atomar ändern und sich mit dem obigen Problem befassen.
timer1OverflowCount
? Ich nehme an, dass es möglich ist, dass timer1OverflowCount
mitten im Lesen hochgezählt wird, also zwischen mehreren LDS
Anweisungen.timer1OverflowCount
ist ein 16-Bit-Wert von: 11111111 00000000
(b) Das erste Byte wird gelesen ( LDS
): 11111111
(c) Der Überlauf-Interrupt wird inkrementiert 11111111 00000000
→ 00000000 00000001
. (Little Endian) (d) Das zweite Byte wird gelesen ( LDS
): 00000001
. Der gelesene Gesamtwert ist 0xb111111111 = 760495542545, statt 0xb100000000 = 760209211392. Das ist falsch. Oder liege ich falsch? Ich sehe nicht, wie Ihr Vorschlag diese Rennbedingung korrigiert.Der ATOMIC_BLOCK wird ohne Unterbrechung ausgeführt, aber die Anweisungen darin werden nicht gleichzeitig ausgeführt.
Von "oc = timer1OverflowCount;" zu "c = TCNT1;" TCNT1 wird inkrementiert und ist nicht dasselbe wie beim Lesen von oc.
Der Fehler zeigt, wenn der Überlauf innerhalb des ATOMIC-Blocks auftritt und timer1OverflowCount nicht aktualisiert werden kann und sogar die Interrupts deaktiviert wurden, zeigt TCNT1 einen späteren Wert als timer1OverflowCount.
Tauschen Sie die beiden Anweisungen aus, platzieren Sie jede in einem eigenen ATOMIC-Block und sehen Sie sich das Ergebnis an. Es ist ein „Falsch-Positiv“. Ich bin davon ausgegangen, dass Sie nur überprüfen möchten, ob Ihr TCNT1-Überlauf-Interrupt gut funktioniert. Er funktioniert, aber der verwendete Code zeigt Fehler an, wo sie nicht sind.
Auf OP-Anfrage füge ich die Erklärung zum Lesen von TCNT1 in seinem eigenen Atomblock hinzu.
Das Lesen des 16-Bit-TCNT1-Registers ist sicher in Bezug auf seine Inkrementierung zwischen zwei 8-Bit-LSB- und MSB-Registerlesungen. Beim Lesen von LSB wird das MSB-Register gepuffert und alle weiteren Lesungen zeigen den gleichen Wert in dem Moment, in dem LSB gelesen wurde.
Aber wenn zwischen den beiden Lesevorgängen ein Interrupt vorhanden ist, der auch TCNT1 liest, wird der Wert des gepufferten MSB mit dem neuen Wert aktualisiert, der unterschiedlich sein kann.
Ich weiß nicht, ob dies hier zutrifft, aber andere 16-Bit-Register teilen möglicherweise denselben Puffer wie TCNT1.
Sie können die Überlaufzählung für eine feste Zeit beobachten, um zu sehen, ob es zu Verzögerungen kommt.
Der Code ist schnell, wird aber auch ohne Unterbrechung zwischen den Schleifen ausgeführt.
In der Schleife haben Sie mehr oder weniger fünf Anweisungen, zwei Übertragungen im Atomblock und drei außerhalb. Kann einen Sprung hinzugefügt werden und Interrupts deaktivieren. Die Wahrscheinlichkeit, dass dieser Überlauf während der Ausführung des atomaren Blocks auftritt und den Fehler auslöst, ist ziemlich hoch.
Nehmen wir also an, der Prozessor gibt 2 us für den ATOMIC-Block und 4 us für den Rest der Schleife aus, dann wieder 2 us im ATOMIC-Block und wieder 4 außerhalb.
Wenn der Überlauf im ATOMIC-Block auftritt (was eine Wahrscheinlichkeit von 1/3 darstellt), während die Interrupts deaktiviert sind und timer1OverflowCount nicht erhöht werden kann, haben Sie eine ziemlich gute Chance, einen falschen Fehler auszulösen.
Die Wahrscheinlichkeit ist viel geringer, weil der Compiler etwas Code für die Schleife hinzufügt und auch innerhalb des ATOMIC-Blocks der Überlauf auftreten muss, bevor TCNT1 LSB gelesen wird.
Dies ist der Arbeitscode, der von OP im Kommentar gepostet wurde:
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { c = TCNT1; }
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { oc = timer1OverflowCount; }
Ich möchte hinzufügen, dass dieser Code mit den Bedingungen aus dem IF-Abschnitt funktioniert
if (c < lastC && oc == lastOc)
Aber wenn Sie überprüfen
if (c >= lastC && oc != lastOc)
Das heißt, wenn Sie die Überlaufzahl ohne einen Überlauf ändern, werden Sie auch falsche Fehler haben.
Wenn es sich in diesem Fall nicht nur um eine Codeprüfung handelt, sollten Sie so etwas wie Spehros Antwort verwenden, indem Sie den TCNT1 erneut lesen (auch in seinem ATOMIC-Block) und den niedrigsten Wert von TCNT1 im Falle einer Änderung der Timer-Zählung verwenden.
Wenn Sie jedoch etwas tun müssen, während die Timer-Zählung aktualisiert wird, überprüfen Sie nur die Timer-Zählung (threadsicher). Es funktioniert gut.
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { c = TCNT1; } ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { oc = timer1OverflowCount; }
Man fragt sich vielleicht: Warum ist es notwendig, c = TCNT1;
in einen eigenen Atomblock zu platzieren? Immerhin geschieht der 16-Bit-Lesezugriff auf TCNT1 mit Hilfe des TEMP-Registers. Siehe Datenblattversion DS40001984A, Seiten 154 und 155. Interessanterweise wird explizit angegeben: „Die folgenden Codebeispiele zeigen, wie auf die 16-Bit-Zeitgeberregister zugegriffen werden kann, vorausgesetzt, dass keine Interrupts das temporäre Register aktualisieren.“ Wird möglicherweise ISR(TIMER1_OVF_vect)
während des Lesens von TCNT1 ausgelöst.Wenn wir uns die „schlechten“ Ausdrucke ansehen, sehen wir lastC = 65440
und c = 49
, was darauf hinweist, dass zwischen jedem Lesen des Timers in der Hauptschleife 96 Zählerticks aufgetreten sind. Ein Überlauf-Interrupt hätte bei 65536 auftreten sollen, was etwa die Hälfte dieser Zeit ist.
Aber Sie lesen auch timer1OverflowCount
ungefähr die Hälfte dieser Zeit, so dass es abhängig vom genauen Zeitpunkt des Interrupts vorkommen kann oder nicht, wenn Sie den Überlaufzähler lesen. Wenn Sie den Timer lesen, ist der Interrupt aufgetreten und hat timer1OverflowCount
sich erhöht, aber Sie verwenden den historischen Überlaufzähler, der möglicherweise vor dem Überlauf des Timers vorhanden war.
Um eine bessere Vorstellung davon zu bekommen, was passiert, könnten Sie direkt nach jedem "schlechten" einen weiteren Ausdruck machen, dann sollten Sie sehen, dass timer1OverflowCount
sich vor dem nächsten Interrupt "auf mysteriöse Weise" erhöht hat!
Um das Problem zu beheben, lesen Sie nicht timer1OverflowCount
bis c
< lastC
. Dann wissen Sie, dass gerade ein Überlauf-Interrupt hätte auftreten müssen (und der nächste ist ~65000 Ticks entfernt), sodass Sie die Überlaufzählung zuverlässig testen können.
Durch Kombinieren von Teilen aus Antworten mit Informationen aus der ATmega328 -Datenblattversion DS40001984A habe ich die folgende Lösung gefunden, die ich für robust halte:
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
c = TCNT1; // TCNT1 increases all the time during the following instructions
bool timerDidOverflow = TIFR1 & 1; // TOV1 Timer/Counter 1, Overflow Flag
byte msb = c >> 15;
if (msb == 0 && timerDidOverflow) {
timer1OverflowCount ++;
TIFR1 &= 1; // Write to TOV1 to clear it and prevent triggering TIMER1_OVF
}
oc = timer1OverflowCount;
}
Die obigen Zeilen ersetzen:
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
oc = timer1OverflowCount;
c = TCNT1;
}
Da in den Kommentaren Bedenken hinsichtlich der Mikrooptimierung geäußert wurden, ist hier der Assemblercode pro Ausgabe von avr-objdump
:
c = TCNT1; // TCNT1 increases all the time during the following instructions
606: c0 91 84 00 lds r28, 0x0084 ; 0x800084 <__stack+0x7ff785>
60a: d0 91 85 00 lds r29, 0x0085 ; 0x800085 <__stack+0x7ff786>
bool timerDidOverflow = TIFR1 & 1; // TOV1 Timer/Counter 1, Overflow Flag
60e: 86 b3 in r24, 0x16 ; 22
610: 81 70 andi r24, 0x01 ; 1
byte msb = c >> 15;
if (msb == 0 && timerDidOverflow) {
612: d7 fd sbrc r29, 7
614: 0e c0 rjmp .+28 ; 0x632 <main+0x144>
616: 88 23 and r24, r24
618: 61 f0 breq .+24 ; 0x632 <main+0x144>
timer1OverflowCount ++;
61a: 80 91 4c 01 lds r24, 0x014C ; 0x80014c <timer1OverflowCount>
61e: 90 91 4d 01 lds r25, 0x014D ; 0x80014d <timer1OverflowCount+0x1>
622: 01 96 adiw r24, 0x01 ; 1
624: 90 93 4d 01 sts 0x014D, r25 ; 0x80014d <timer1OverflowCount+0x1>
628: 80 93 4c 01 sts 0x014C, r24 ; 0x80014c <timer1OverflowCount>
TIFR1 &= 1; // Write to TOV1 to clear it and prevent triggering TIMER1_OVF
62c: 86 b3 in r24, 0x16 ; 22
62e: 81 70 andi r24, 0x01 ; 1
630: 86 bb out 0x16, r24 ; 22
}
oc = timer1OverflowCount;
632: e0 90 4c 01 lds r14, 0x014C ; 0x80014c <timer1OverflowCount>
636: f0 90 4d 01 lds r15, 0x014D ; 0x80014d <timer1OverflowCount+0x1>
Randnotiz: Der 16-Bit-Lesezugriff auf TCNT1
geschieht mit Hilfe des TEMP
Registers. Der Zugriff liefert garantiert einen konsistenten Wert. Siehe Seiten 154 und 155 in der Datenblattversion DS40001984A . Ohne die Gefahr von Unterbrechungszugriffen TCNT1
ist es nicht erforderlich, Lese-/Schreibzugriffe auf TCNT1
in einen atomaren Block zu legen.
Zum Vergleich hier eine Implementierung der Lösung aus der Antwort von @supercat , die beim Lesen überhaupt keine Atomarität erfordert:
uint16_t c0 = TCNT1, oc0 = timer1OverflowCount;
uint16_t c = TCNT1, oc = timer1OverflowCount;
if ((oc0 >> 8) ^ (oc >> 8)) {
oc &= 0xff00;
c = 0;
} else if ((uint8_t)(oc0 ^ oc)) {
c = 0;
} else if ((c0 >> 8) ^ (c >> 8)) {
c &= 0xff00;
}
Terse ist die Lösung von @Bruce Abbot , die ich mit folgender Implementierung erfolgreich getestet habe:
static uint16_t oc = 0;
uint16_t c;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
c = TCNT1;
}
bool timerDidOverflow = c < lastC;
if (timerDidOverflow) {
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
oc = timer1OverflowCount;
}
}
sbrc r29, 7
macht nicht annähernd 32 Schritte. In Bezug auf das Aufteilen des Atomblocks: Zwischen den Blöcken TIMER1_OVF
kann ein Feuer oder eine andere Unterbrechung von unbekannter Dauer auftreten. Zwischen jeweils zwei Schritten hat dies potenziell katastrophale Folgen.oc0 >> 8 ^ oc >> 8
ist dasselbe wie die (oc0 >> 8) ^ (oc >> 8)
folgende C-Operatorpriorität.TIFR1
ähnelt einer flüchtigen C-Variablen. Sein Wert kann sich jederzeit ändern. Der Compiler wäre schlecht beraten, die Position des Zugriffs auf zu ändern TIFR1
.Das ist aber nur meins. Sie überprüfen es gelegentlich nicht in einem festen Intervall.
Es braucht Zeit, um die Schleife zu verarbeiten und Zeichen über UART zu senden! Machen wir eine Vermutung:
Sie verwenden die Baudrate von 9600, Ihr Arduino sendet etwa 57 Zeichen pro Schleife, sodass die Mindestzeit zum Beenden jeder Schleife beträgt:
57 * 8 / 9600 = 47.5 ms ( Rough approximation)
Wie viel Zeit läuft Timer1 über? (Basierend auf Ihrer Einstellung)
65440 / 16000000 ≈ 4 ms
Sie sehen, es kann nicht jeden einzelnen Überlauf auffangen.
Sie sollten Ihren Code ändern und einen anderen Timer verwenden, um ein festes Gating-Intervall zu erstellen, um zu zählen, wie viel timer1OverflowCount
in einer festen Zeit.
Ich habe Ihren Code nicht gründlich analysiert, da ich jetzt erschöpft bin (und auch faul). Dies ist wahrscheinlich eher ein Softwarefehler als ein Hardwaredefekt.
47.5 ms
nicht 6 ms
.
Spehro Pefhany
Mitu Raj
feklee
oc = 31, c = 49, lastOc = 31, lastC = 65440
bedeutet, dassTCNT1
es übergelaufen ist (65440→49), aber der Überlaufzähler ist gleich geblieben (31).Lange Pham
Dorian
alex.forencich
Dorian
feklee
Dorian
Dorian
Dorian