Hallo Forenuser,
mir ist heute beim Programmieren eines STM32F411 Controllers durch ein
zunaechst nicht funktionierendes Programm eine Sache aufgefallen, die
ich gerne mit Euch diskutieren würde:
Ich will eine Funktion programmieren, die ganz so wie im Arduino-Code
einen Wert für die abgelaufenen Mikrosekunden nach dem Programmstart
(bzw. Timer-Start) liefert.
Also eben: uint32_t micros(void) sollte eigentlich problemlos sein, aber
- wir werden sehen.
Ich habe mir einen Timer gesetzt, der alle 100ms einen
Overflow-Interrupt erzeugt. Dann erhoehe ich einen globalen Zaehler
glb_Millis um 100, und kehre sofort zurueck. Diese Variable ist per
"volatile" geschützt. glb_Millis += 100; ist zwar nicht atomar, aber ich
schreibe ja nur in der Interrupt-Routine in die Variable.
void TIM5_IRQHandler(void)
{
glb_Millis += 100;
// Interrupt-Flag löschen usw. habe ich hier weggelassen.
}
Die Funktion muss dann nur noch den Wert der Mikrosekunden als micros =
Millisekunden * 1000 + Offset berechnen. Millisekunden * 1000, weil ja
eine Millisekunde 1000 Mikrosekunden hat. Den Offset muss man aus dem
aktuellen Zaehlerstand TIM5->CNT des Timers bestimmen, der bei mir von 0
bis 999.999 hoch zaehlt. Der Timer 5 ist ein 32-Bit Timer, das sollte
also funktionieren. Die Taktfrequenz ist 100 Mhz.
Offset = TIM5->CNT / 10; und zusammen:
uint32_t micros(void)
{
__disable_irq();
uint32_t mic = glb_Millis * 1000 + TIM5->CNT / 10;
__enable_irq();
return (mic);
Die Funktion funktioniert im Prinzip - aber irgendwann mal alle heiligen
Zeiten nicht. Natürlich gerade dann, wenn der Timer zwischendurch
übergelaufen ist. Logischerweise sollte eine Zeitmessung immer
aufsteigende Wert liefern, das tut sie aber nur in 99.999% aller Fälle.
Und nur, wenn man sie zweimal in ganz ganz kurzen Abständen aufruft.
Beispiel:
1. Aufruf:
glb_Millis = 5000; TIM5->CNT = 999990;
Der Timer ist also kurz vor dem Überlauf.
Funktionsergebnis: mic = 5000 * 1000 + 999990 / 10 = 5099999 us.
2. Aufruf (unmittelbar nachher)
glb_Millis = 5000; (!!!!) TIM5->CNT = 3;
Der Timer ist übergelaufen, und wurde wieder von 0 auf aufwärts zählend
gestartet. Aber glb_Millis ist noch immer 5000, anstatt 5100, d.h. die
Interrupt-Routine ist noch nicht aufgerufen worden!
Nun geht die Berechnung natürlich schief:
Funktionsergebnis: mic = 5000 * 1000 + 3 / 10 = 5000000 us.
Richtig wäre natürlich 5100 * 1000 + 3 / 10 = 5100000 us.
Natürlich kommt das sehr selten vor, aber so alle 1-2 min ist es mal
soweit. Mein Fix bisher: Alten Funktionswert mic abspeichern, in
prev_mic. Und sollte mal mic < prev_mic sein, dann mic += 100000; um die
Overflow-Bedingung zu erkennen. Das läuft, aber es kann mich nicht ganz
zufrieden stellen! Der Post-Race Fix? Es sollte doch gar nicht erst dazu
kommen!
Es kann nie schaden nach zu sehen, wie es denn andere machen. So lag es
nahe, im Original-Arduino Code zu schauen. Dort steht (überflüssiges
entfernt):
#ifdef TIFR0
if ((TIFR0 & _BV(TOV0)) && (t < 255))
m++;
#else
if ((TIFR & _BV(TOV0)) && (t < 255))
m++;
#endif
Oh! Die #ifdef behandeln nur zwei unterschiedliche Namen des Timers,
aber der Fix ist derselbe: Wenn _BV(TOV0), also Overflow stattgefunden
hat, und der Zähler < 255 ist (Overflow also nicht lange her sein
kann...) dann erhoehe m einfach um 1. Später kommt dann:
return ((m << 8) + t) * (64 / clockCyclesPerMicrosecond());
Also im Prinzip derselbe Fix, nur mit anderen Werten.
Tja, und nun natürlich die entscheidende Frage:
Ist es wirklich unmöglich, jederzeit korrekte Werte von glb_Millis und
TIM5->CNT zu lesen (das ist ja das Problem, die Werte passen im
Fehlerfalle nicht zusammen) oder waren die Arduino Programmierer ebenso
blind oder unwissend wie ich?
Viele Grüße
Thomas
Wenn du Timer und glb_Millis ausliest, ohne die Interrupts zu sperren,
kann es sein, daß der Timerinterrupt gerade zwischendrin auftritt und
Timer-Count und glb_Millis nicht zusammengehören.
Wenn du die Interrupts sperrst, kann halt der Timer überlaufen, aber
glb_Millis wird nicht erhöht (da die Interrupts ja gesperrt sind).
In beiden Fällen kannst Du nicht garantieren, daß glb_Millis und
TIM5->CNT zusammengehören, wenn du sie liest.
Mach es so ähnlich wie die Arduino-Leute:
1
__disable_irq();
2
uint32_tcnt=TIM5->CNT;
3
uint32_tmic=glb_Millis*1000+cnt/10;
4
// anpassen je nachdem welchen Interrupt du verwendest
5
if(TIM5->SR&(TIM_SR_CC1IF)){
6
// overflow aufgetreten, wenn cnt kleiner als halber Bereich, dann
7
// wurde cnt gelesen als timer schon übergelaufen war, dann entsprechend
TMK schrieb:> Der Timer ist übergelaufen, und wurde wieder von 0 auf aufwärts zählend> gestartet. Aber glb_Millis ist noch immer 5000, anstatt 5100, d.h. die> Interrupt-Routine ist noch nicht aufgerufen worden!
Ja klar, vom Überlauf des Timers bis zur Ausführung von glb_Millis +=
100 im IntHandler läuft erstens die IntLatenz und zweitens führt der µC
ja einiges an Code aus (u.a. Register retten) und das braucht eben seine
Zeit.
Hallo,
Wäre es möglich den Timer auszuschalten/deaktivieren, der Zeit berechnen
und den Timer wieder aktivieren? Natürlich verlieren wir der
Genauichkeit, aber zwischen zwei Anrufe die Zeit soll genau sein. Es
passt offensichtlich nicht für jede Aufgabe.
WaMin schrieb im Beitrag #4957405:
> rmu schrieb:>> Warum nicht gleich die glb_millis variable 64 bittig machen, das sollte>> ausreichen.>> Weil das nichts bringt.
hab offenbar das problem falsch gelesen.
man könnte den overflow auf 200ms festlegen, einen channel im output
capture mode alle 100ms den interrupt generieren lassen, dann würde der
zähler nicht bei 0 weitermachen wenn man zufällig bei zählerstand 99 den
IRQ sperrt...
Oleh P. schrieb:> Wäre es möglich den Timer auszuschalten/deaktivieren, der Zeit berechnen> und den Timer wieder aktivieren?
Außer, daß es umständlicher ist und die Genauigkeit geringer ist, hätte
es keinerlei Vorteile.
Und wenn beim Timerstop noch Interrupts höherer Priorität auftreten,
vergrößert sich der Fehler noch weiter.
Die richtige Lösung ist doch popeleinfach und kostet kaum Code:
- Test Überlaufbit
- Test Timerwert
- Korrekturaddition
Vielen Dank für die zahlreichen Antworten! Man muss den Fehler offenbar
wirklich "nachher" korrigieren. Ob man es durch Test des Overflow-Bits
oder den vorherigen Zählerstand macht, ist egal.
Während ich über das Problem nachgedacht habe, ist mir noch eine andere
Möglichkeit eingefallen, die ich aber noch nicht zuende gedacht habe.
(Möglicherweise führt sie am Ende auf dasselbe Problem). Im STM32 kann
man ja Timer "verknüpfen". Vielleicht kann man einen anderen Timer die
Overflows des Timer 5 zählen lassen? Die Abschnitte im Datenblatt sind
sehr lang, also wird es kaum lohnend sein, wenn man schon eine
funktionierende Lösung hat, aber es klingt interessant.
Vielen Dank und viele Grüße
Thomas