Hallo zusammen,
ich bin gerade dabei mein System neu aufzusetzen und mache mir Gedanken
über grundsätzliche Dinge die mir in der Vergangenheit aufgefallen sind.
µC: Cortex-M4
Wie es bis jetzt war:
Im SysTick Handler (1ms) wurde eine uint32_t Variable inkrementiert.
Die aufgerufenen Module aus meinem Round-Robin Scheduler nutzten den
System-Tick um Timeouts zu berechnen oder als Zeitstempel für erzeugte
Datenströme.
Das mit den Timeouts habe ich dann irgendwann geändert, so dass die
Timeout Variablen vom gewünschten Wert aus dekrementiert und auf 0
geprüft werden.
Es blieb jedoch noch das Problem des Zeitstempels für erzeugte Daten.
Würde die Hardware länger als 49,71... Tage laufen, käme der Überlauf.
Nun hatte ich die Idee eine Uptime zu nutzen:
1
structuptime
2
{
3
uint8_tday;
4
uint8_thour;
5
uint8_tmin;
6
uint8_tsec;
7
uint16_tms;
8
};
1)
Wenn ich diese Uptime im SysTick aktualisiere und sie global verfügbar
mache, hätte ich doch das Problem, dass mir der SysTick dazwischen
funken könnte, z.B. bei:
2)
Ich verwende eine get_uptime Funktion in der ich die Interrupts sperre
und eine Kopie der Uptime zurückgebe.
Was jedoch dazu führen würde, dass ich jedes mal eine struct uptime
Variable anlegen müsste - überall dort wo ich es verwenden möchte.
Weiterhin ist die Handhabung von einzelnen Variablen in der struct für
einen Zeitstempel eher unpraktisch.
...das alles stellt mich noch nicht wirklich zufrieden.
3)
ich mache es ganz anders?
Mir fehlt irgendwie der richtige Ansatz.
Habt ihr Ideen / Vorschläge?
So ist das halt, ich denke dass dein Lösungsansatz schon OK ist.
Im Vergleich zu deinem sprintf ist der Zeitaufwand für das Kopieren der
Struktur verschwindend gering.
Beim Kopieren der Struktur kannst du das Sperren der Interrupts
einsparen, indem du die ms zweimal liest und miteinander vergleichst.
Wenn sie sich geändert haben, funkte der Systick Interrupt dazwischen.
Nur dann liest du noch ein drittes mal. Die anderen Felder (sec, min,
hour, day) können sich unmittelbar danach nicht mehr ändern, sind also
ohne Extra-Aufwand zu kopieren.
Adam P. schrieb:> Das mit den Timeouts habe ich dann irgendwann geändert, so dass die> Timeout Variablen vom gewünschten Wert aus dekrementiert und auf 0> geprüft werden.
Das ist eigentlich ein Rückschritt.
Adam P. schrieb:> Würde die Hardware länger als 49,71... Tage laufen, käme der Überlauf.
Dann mache einen Reset (ist das sicherste) oder inkrementiere eine
weitere Variable.
Dann hast Du das Problem prinzipiell zwar noch immer, aber nur sehr
selten :-)
Generell kostet konsistentes lesen: entweder Interrupt so lange sperren,
oder zweimal lesen und vergleichen
Alternative, wenn Du sicherstellen kannst, dass Du einmal pro 40 Tage
fragst: die inkremente aufaddieren. Dann brauchst Du keine Interrupts
sperren, solange systicker konsistent gelesen wird in der Loop.
1
t=Systicker;
2
diff=t-told;
3
told=t;
4
5
Systemzeitumdiffupdaten
Noch größerer Vorteil: du rechnest die Stunden etc nur, wenn du es
brauchst, nicht jeden Tick.
Adam P. schrieb:> Nun hatte ich die Idee eine Uptime zu nutzen:
Ob 49d oder 256d, ist doch kein großer Unterschied.
Was sind das für komische Datenströme, die nach 49d immer noch nicht
verarbeitet wurden?
Für Zeitabstände <49d nimmt man einfach die Differenz zum Tick.
Differenzen stimmen immer, auch bei einem Überlauf des Ticks.
A. S. schrieb:> Das ist eigentlich ein Rückschritt.
Warum das?
Wenn ich ein Timeout benötige, dann setz ich den Wert und dieser wird
durch den SysTick oder durch einen Timer auf 0 dekrementiert.
Besser als sowas:
1
timeout=sys_tick+MY_TIMEOUT;
2
3
if(timeout<=sys_tick)
4
foo();
A. S. schrieb:> Dann mache einen Reset (ist das sicherste)
Das mache ich ja auch. Habe mich nur gefragt, ob es auch eine elegantere
Lösung gibt.
Peter D. schrieb:> Was sind das für komische Datenströme, die nach 49d immer noch nicht> verarbeitet wurden?
Ja es betrifft nicht direkt die Datenströme, viel mehr geht es um den
Überlauf.
Ich versuch es irgendwie "richtig" zu machen, aber ich glaub, es gibt
gar kein "richtig"-richtig :-)
Adam P. schrieb:> timeout = sys_tick + MY_TIMEOUT;
Weil Systick irgendwann man kurz vor dem Überlauf steht, und die
Addition dann ein falsches Ergebnis liefert. Bei der Subtraktion hast du
das Problem nicht:
1
uint32_tstart=sys_tick;
2
3
if(sys_tick-start>MY_TIMEOUT){...}
sys_tick-start liefert immer den richtigen Wert, auch wenn
zwischenzeitlich ein Überlauf statt fand.
Stefan ⛄ F. schrieb:> sys_tick-start liefert immer den richtigen Wert, auch wenn> zwischenzeitlich ein Überlauf statt fand.
Ja ok, das ergibt Sinn.
Aber ist das besser/schöner, als wenn ich in Modul X eine timeout
Variable anlege, diese am "Timer" anmelde und dann setze und auf 0
prüfe?
Oder sogar eine Funktion am Timer anmelde, die mir dann aufgerufen wird.
Was ich halt in den letzten Jahren immer festgestellt habe:
Im ersten Moment scheint eine Lösung perfekt, bis mehr und mehr dazu
kommt und man dann merkt, ohhh neee, hätte ich das Grundsystem mal
anders aufgebaut.
Deshalb meine jetzigen Gedanken zum grundlegenden Aufbau.
Adam P. schrieb:> Aber ist das besser/schöner, als wenn ich in Modul X eine timeout> Variable anlege, diese am "Timer" anmelde und dann setze und auf 0> prüfe?
Hängt von deinem Design ab. Eventgetrieben oder Mainloop (SPSloop).
Die Timer mit Systicker sind brotlos (fressen kein Brot = werden nur zur
Auswertung geprüft), beliebig viele, lokal ohne anmelden, synchron zur
Loop, ...
Von daher kann hier keiner einen Ansatz empfehlen ohne Deine Struktur zu
kennen.
Trotzdem: Timer auf Basis Systicker solltest Du kennen. Sie sind so
effizient, dass sie sich selbst dann lohnen, wenn Du Deine Timer
vielfach verwendst.
Es gibt nicht die eine ideale Lösung. Ohne dein gesamtes Projekt zu
sichten kann ich dir nicht sagen, welche für dich die beste Lösung ist.
Ich kann nur Alternativen aufzeigen, mit denen ich gut klar komme.
A. S. schrieb:> Von daher kann hier keiner einen Ansatz empfehlen ohne Deine Struktur zu> kennen.Stefan ⛄ F. schrieb:> Es gibt nicht die eine ideale Lösung. Ohne dein gesamtes Projekt zu> sichten kann ich dir nicht sagen, welche für dich die beste Lösung ist.
Bis jetzt gibt es keinen Ansatz.
In der Vergangenheit sah das System so aus:
- Das Sampling wurde vom Timer jede 1ms aufgerufen, das hat die ADC
getriggert und Daten in ein FIFO geschoben.
- Alles andere lief in der "Main" und hat entweder auf den SysTick oder
auf verfügbare Daten reagiert.
Zusätzlich hatten die einzelnen Module noch eigene Timeouts, bzgl.
Peripherie (USB/UART/I²C/SPI).
Nun wollt ich das alles besser strukturieren und es evtl. effizienter
gestalten.
Adam P. schrieb:> Nun wollt ich das alles besser strukturieren und es evtl. effizienter> gestalten.
Wie gesagt kenne ich dein Projekt nicht. Mein Bauch grummelt, dass du
dich eventuell zu sehr auf unwichtige Details konzentrierst.
Die Struktur sollte für alle am Projekt beteiligten offensichtlich sein,
also leicht nachvollziehbar. Dann ist sie gut. Primitive Strukturen sind
manchmal sogar besser als ausgefuchste akademische Bauwerke.
Zur Effizienz: Solange dein Code sprintf() enthält, erscheint mir das
Feilschen um einzelne Bytes und Taktzyklen geradezu lächerlich. Zudem
ist dein Mikrocontroller bereits erheblich schneller als mein erster PC,
auf dem ich ernsthafte Datenbank-Anwendungen nutzen musste.
Optimiere Effizienz nur da, wo es nötig ist. Solange dein Gerät
funktioniert, ist es nicht nötig. Verschwende diese Zeit deines Lebens
lieber damit, einen Sexual-Partner zu finden oder die um die Bedürfnisse
Familie zu kümmern.
für allgemeine Strukturierung wurden Betriebssysteme entwickelt, die
enthalten die passende Elemente dafür.
Ein Systick ist keine für alles passende Lösung, bei Batteriebetrieb ist
es z.B. nicht unbedingt gut das der Prozessor ständig geweckt wird.
Etwas Tickless zu bauen ist aber auch nicht trivial, ich hätte keine
Lust das Rad immer wieder neu zu erfinden.
Stefan ⛄ F. schrieb:> Optimiere Effizienz nur da, wo es nötig ist. Solange dein Gerät> funktioniert, ist es nicht nötig. Verschwende diese Zeit deines Lebens> lieber damit, einen Sexual-Partner zu finden oder die um die Bedürfnisse> Familie zu kümmern.
Ja ich glaub das ist diese immer wiederkehrende "Perfektion" die dann
doch voll unnötig war.
Ich fang jetzt einfach mal mit KISS an und schau was das System mit der
Zeit an zusätlichen Funktionen benötigt...
Mach mir wohl wieder zuviele Gedanken um Dinge die vllt. nie gebraucht
werden.
Adam P. schrieb:> Ja ich glaub das ist diese immer wiederkehrende "Perfektion" die dann> doch voll unnötig war.
Es ist schon sinnvoll, für immer wiederkehrende Aufgaben sich ein
passendes Werkzeug einzurichten.
Mich hat das genervt, für jede Zeitverzögerung erst umständlich eine
Variable anlegen zu müssen und im Timertick extra zu behandeln.
Die Idee mit der sortierten Liste und dem Callback habe ich in einem
älteren Steuerprogramm gefunden und ich fand sie so bequem, daß ich sie
übernommen habe.
Adam P. schrieb:> Im SysTick Handler (1ms) wurde eine uint32_t Variable inkrementiert.> ...> Würde die Hardware länger als 49,71... Tage laufen, käme der Überlauf.
Auf einem M4 habe ich keine Bedenken, einfach uint64_t zu verwenden.
m.n. schrieb:> Auf einem M4 habe ich keine Bedenken, einfach uint64_t zu verwenden.
Da die Zugriffe auf uint64 nicht atomar sind, muss man dann aber wieder
Interrupts sperren. Ist nur schlimm, wenn man es vergisst.
m.n. schrieb:> Auf einem M4 habe ich keine Bedenken, einfach uint64_t zu verwenden.
Ja das habe ich mir auch schon überlegt.
Peter D. schrieb:> Mich hat das genervt, für jede Zeitverzögerung erst umständlich eine> Variable anlegen zu müssen und im Timertick extra zu behandeln.> Die Idee mit der sortierten Liste und dem Callback habe ich in einem> älteren Steuerprogramm gefunden und ich fand sie so bequem, daß ich sie> übernommen habe.
Ja schon, ich muss mir nochmal überlegen wie ich meine "Software-Tasks"
also die Module die vom Scheduler in der main aufgerufen werden,
behandel - sowie die Timeouts.
Habe da zwar schon die Funktionalität, dass jedes Modul seine
Zeitscheibe zum Aufruf selbst einstellen kann, jedoch wäre ein Aufruf
der Eventbezogen geschieht, noch eleganter.
Aber das kommt auch wieder auf den Einsatzzweck drauf an.
Stefan ⛄ F. schrieb:> Ist nur schlimm, wenn man es vergisst.
Der Overflow, also der Update der oberen 32 Bit findet ja nur
alle 49,71... Tage statt. Also halb so wild ;-)
Stefan ⛄ F. schrieb:> Da die Zugriffe auf uint64 nicht atomar sind, muss man dann aber wieder> Interrupts sperren.
Ach Gott! Interrupts, die nur alle ms bedient werden müssen, kann man
locker mal um < 50 ns verzögern.
Adam P. schrieb:> Nun wollt ich das alles besser strukturieren und es evtl. effizienter> gestalten.
Wenn Mainloop, dann brotlose Timer.
Und einmal mit den Eigenschaften der Überlaufrechnung vertraut machen.
Wenn Du ein If oder ein % dabei verwendest, ist was falsch. Wenn ein
Tick verloren gehen kann, ist was falsch. Wenn Du Interrupts sperren
musst, ist was falsch.
Das Prinzip ist nicht nur bei Timern wichtig, sondern bei allen
asynchronen Zählern..
Als Übung mach den Systicker z.b. nur 16 Bit groß und alle Zeiten unter
30s. Wenn dann was falsch ist, fällt es eher auf
m.n. schrieb:> Ach Gott! Interrupts, die nur alle ms bedient werden müssen, kann man> locker mal um < 50 ns verzögern.
Eben, deswegen schrieb ich im selben Absatz
> Ist nur schlimm, wenn man es vergisst.
Was wolltest du wirklich sagen?
Stefan ⛄ F. schrieb:> Was meinst du mit "brotlos"? Ich kenne den Begriff nicht
Fressen kein Brot, kosten nur im Augenblick der Abfrage Rechenzeit. Und
brauchen keinen feste reservierten Speicher wie ein Array von TIM_MAX.
Davon kannst Du also einen verwenden oder 5000, ohne dass dein
Tickerinterrupt etwas davon weiß. Du kannst sie im Stack anlegen,
brauchst sie nirgends anmelden, etc
Und bei einem 4-Byte Systicker braucht es nur 4-10 Byte RAM, je nach
Funktionalität.
A. S. schrieb:> Fressen kein Brot, kosten nur im Augenblick der Abfrage Rechenzeit. Und> brauchen keinen feste reservierten Speicher wie ein Array von TIM_MAX.>> Davon kannst Du also einen verwenden oder 5000, ohne dass dein> Tickerinterrupt etwas davon weiß. Du kannst sie im Stack anlegen,> brauchst sie nirgends anmelden, etc
Jetz bin ich ein wenig verwirrt.
Könntest du mir ein kleines Bsp. zeigen?
Das der Interrupt davon nichts wissen muss, kann ich ja noch
nachvollziehen.
Aber wenn ich mir eine Variable anlege, dann braucht die doch auch
Speicherplatz.
Adam P. schrieb:> Aber wenn ich mir eine Variable anlege, dann braucht die doch auch> Speicherplatz.
Ja, aber nur
A. S. schrieb:> 4-10 Byte RAM, je nach Funktionalität.
Peter z.b. braucht für seine eine feste Liste, Du musst also wissen, ob
Du 3 oder 1000 brauchst.
Die einfachste Version besteht aus timStart/Stop/Running/Expired braucht
2/4byte RAM und ein Dutzend Zeilen Code.
Später am PC gerne ein Beispiel.
Adam P. schrieb:> Mir fehlt irgendwie der richtige Ansatz.
Zu allererst mache dir klar, daß du immer mit Überläufen leben mußt - es
sei denn, du zählst deine Zeitstücke in einer Variablen unendlicher
Bitbreite - was praktisch nicht zu realisieren ist.
Also ist ein wenig Bescheidenheit angesagt. Die allumfassende
Generallösung wirst du nicht erreichen. Wähle am sinnvollsten die
Bitbreite der Variablen für das Zählen deiner Millisekunden so, daß
deine HW damit am besten klarkommt, zähle damit nur die Zeit innerhalb
einee Tages - und benutze für das Zählen der Tage ab einem gewählten
Stichdatum eine andere Variable. Wenn du für beide Variablen long
nimmst, reicht das für mehr als die nächsten 5 Mio Jahre. Das ist
vemutlich die beste Lösung deines Problems.
W.S.
A. S. schrieb:> Peter z.b. braucht für seine eine feste Liste, Du musst also wissen, ob> Du 3 oder 1000 brauchst.
Einspruch. Du mußt nur wissen, ob Du 3 oder 1000 gleichzeitig
brauchst.
Wenn Du 1000 insgesamt, aber nur 100 gleichzeitg brauchst, spart die
Liste ne Menge Speicherplatz und CPU-Zeit.
/* Gestartet und Zeit ist noch nicht abgelaufen */
24
intTimRunning(TimType&t)
25
{
26
TickTypeticks=t->ticks-SysTicker;
27
28
if(!t->ticks)return0;/* 0 wenn nicht gestartet */
29
if(ticks<=MAX_TICKS+1)return1;/* 1 wenn noch läuft */
30
t-ticks=0;/* stoppen */
31
return0;/* 0, läuft nicht mehr */
32
}
33
34
/* Elapsed: Gestartet UND Zeit ist gerade abgelaufen */
35
intTimElapsed(TimType&t)
36
{
37
if(!t->ticks)return0;/* 0 wenn nicht gestartet */
38
return!TimRunning(t);/* das Gegenteil von Running */
39
}
40
41
42
/* Verwendung im Modul Lampe, mit Elapsed */
43
44
staticTimTypeTimAn;
45
46
voidLampeAn(void)
47
{
48
TimStart(&TimAn,1000);
49
LampeEinschalten();
50
}
51
52
/* zyklische Funktion */
53
voidLampeMain(void)
54
{
55
...
56
if(TimElapsed(&TimAn))LampeAusschalten();
57
...
58
}
59
60
/* Verwendung im Modul ADC, BurstMessung für 3 Sekunden */
61
62
staticTimTypeTimBurst;
63
64
voidADCBurstStart(void)
65
{
66
TimStart(TimBurst,3000);
67
}
68
69
voidADCMain(void)
70
{
71
...
72
if(TimRunning(&TimBurst))ADCBurst(...);
73
...
74
}
Achtung: Die Timer müssen zyklisch ausgewertet werden. Genauer:
innerhalb des "Deutlich" aus Zeile 3, hier also innerhalb 10s.
Bei einem Start und a oder b, da beide den Timer ggf. stoppen.
Man kann das dann sukzessive erweitern: Mit eigenem Bit für Start (dann
fällt der offset von 1ms bei 0 weg), mit Speichern des Reload-Wertes,
mit zyklischem Timer, der sich immer wieder neue aufzieht (Tick-Genau
oder ab jetzt), selbstständig Funktionen startet, ...
Adam P. schrieb:> Im SysTick Handler (1ms) wurde eine uint32_t Variable inkrementiert.> Die aufgerufenen Module aus meinem Round-Robin Scheduler nutzten den> System-Tick um Timeouts zu berechnen oder als Zeitstempel für erzeugte> Datenströme.>> Das mit den Timeouts habe ich dann irgendwann geändert, so dass die> Timeout Variablen vom gewünschten Wert aus dekrementiert und auf 0> geprüft werden.
Das beides hat erstmal nicht direkt miteinander zu tun. Die Timeouts
kann man also schlicht aus den Betrachtungen eleminieren.
> Es blieb jedoch noch das Problem des Zeitstempels für erzeugte Daten.> Würde die Hardware länger als 49,71... Tage laufen, käme der Überlauf.
Na und? Dann muss man halt einfach "breiter" zählen. Schon mit einem
weiteren 32Bit-Wort als Zählererweiterung wirst du ganz sicher keinen
Überlauf mehr erleben...
Und der Performanceverlust ist minimal, da nur alle ca. 4 Milliarden
Increments mal die Zählererweiterung auf den Stand der Dinge gebracht
werden muss.
Ich begreife dein Problem nicht.
Eine dritte Möglichkeit auf Cortex-M wäre natürlich LDRD. Das sind zwar
auf dem Bus zwei Speicherzugriffe, aber wenn da ein Interrupt
reingrätscht, wird danach LDRD komplett von vorne gestartet - was hier
genau das Verhalten wäre, was man braucht.
Mit etwas Glück macht der Compiler sowieso ein LDRD aus dem Laden eines
uint64_t, aber das sollte man im Disassembly nachprüfen. Für LDM gilt
das übrigens gerade nicht, sondern das würde stattdessen an der Stelle
fortgesetzt, wo der Interrupt passiert ist, so daß eine Inkonsistenz
möglich wäre.
Nop schrieb:> Mit etwas Glück macht der Compiler sowieso ein LDRD aus dem Laden eines> uint64_t, aber das sollte man im Disassembly nachprüfen.
In einem Programm, wo viel mit uint64_t gearbeitet wird, wimmelt es nur
so von LDRD und STRD. Das werden wohl alle Compiler verwenden.
Danke für den Hinweis!
Wenn man viele Timer benoetigt, zB bei einem Multitasking System,
verwendet man Timer Queues, bei denen die Zeiten einsortiert werden. Der
einsortierte Wert ist dann nur noch die Differenz zum Vorhergehenden.
Eine Queue muss nicht als dynamische Struktur vorliegen, sondern kann
auch als festes Array mit Pointern implementiert werden.
Til S. schrieb:> Man könnte alternativ auch ein RTOS verwenden das dieses Problem> bereits gelöst hat:
Man könnte auch einfach mal nicht spammen. Hier offtopic kommerzielle
eigene Produkte reinzuspammen ist nämlich auch mit Smiley immer noch
Sch**ße.
Nop schrieb:> Til S. schrieb:>> Man könnte alternativ auch ein RTOS verwenden das dieses Problem>> bereits gelöst hat:>> Man könnte auch einfach mal nicht spammen.
Hey Til,
danke für deine Info, aber ein RTOS würde in diesem Fall nicht in Frage
kommen.