Forum: Mikrocontroller und Digitale Elektronik Race-Condition "reparieren"?


von TMK (Gast)


Lesenswert?

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

Beitrag #4957275 wurde von einem Moderator gelöscht.
von Tassilo H. (tassilo_h)


Lesenswert?

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_t cnt = TIM5->CNT;
3
uint32_t mic = 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
8
  // 100ms addieren
9
  if (cnt<500000) mic+=100;
10
}
11
__enable_irq();

von Dieter W. (dds5)


Lesenswert?

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.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

Genau das gleiche Problem hatte man schon mit dem AVR, wenn man 
Interruptzählwert mit dem Timerwert verknüpfen will:

Beitrag "AVR Timer mit 32 Bit"

von Tassilo H. (tassilo_h)


Lesenswert?

Tassilo H. schrieb:
> if (cnt<500000) mic+=100;

Mist, muß natürlich
1
mic+=100*1000;
heißen

von Oleh P. (Firma: OpalApps (www.opalapps.com)) (opaliy)


Lesenswert?

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.

von rmu (Gast)


Lesenswert?

Warum nicht gleich die glb_millis variable 64 bittig machen, das sollte 
ausreichen. In der ISR wirds keinen merkbaren Unterschied machen.

von Oleh P. (Firma: OpalApps (www.opalapps.com)) (opaliy)


Lesenswert?

Oh, war dumm von mir so vorzuschlagen - Timer soll Millisekunden zählen 
(und erhöern beim "Overflow"). Habe ich das verpasst, entschuldigung.

Beitrag #4957405 wurde von einem Moderator gelöscht.
von rmu (Gast)


Lesenswert?

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...

von Peter D. (peda)


Lesenswert?

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

von TMK (Gast)


Lesenswert?

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

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.