Ich habe eine uint16_t Variable, die von einem Timer-IRQ alle 10ms
hochgezählt wird. Und dazu passend die Funktion now(), welche den Wrt
atimar ausliest und zurück liefert.
Meine Anwendung hat mehrere Zustandsautomaten, die in der Hauptschleife
nacheinander ausgeführt werden, also mehrere Threads. Jeder dieser
Threads wartet für eine gewisse Zeit.
Ich habe mal versucht, den Code auf ein Minimum zu reduzieren, um die
Aufgabe zu verdeutlichen:
1
void task1()
2
{
3
static uint16_t warteBis;
4
static uint8_t state=0;
5
switch (state)
6
{
7
8
case 0: warteBis = now() + 600;
9
state=1;
10
break;
11
12
case 1: if (now() >= warteBis)
13
{
14
state=2;
15
}
16
break;
17
18
case 2: // mach irgendwas
19
state=0;
20
break;
21
}
22
}
23
24
void main()
25
{
26
while (1)
27
{
28
task1();
29
task2();
30
task3();
31
task4();
32
}
33
}
Blöderweise muss ich dafür sorgen, dass der Timer niemals überläuft.
Genauer gesagt muss ich sogar genug Reserve vorsehen, dass meine
Additionen niemals zum Überlauf führen.
Bei Geräten, die "Ewig" durchlaufen sollen, ist das aber gar nicht so
einfach realisierbar. Natürlich könnte ich auf bis zu 64bit wechseln,
aber das wäre wiederum für die Performance schlecht.
Kennt jemand eine bessere Methode, die zumindest einen Timerüberlauf im
jeweiligen Intervall toleriert?
Ich glaube diese Scheduler sind was anderes, passt nicht zu meinem Fall.
> ich würde die "Wartezeit" einfach in jedem Tick herunterzählen.
Ja, das kann ich notfalls machen.
Aber ich wollte gerne mal die andere Methode anwenden, wo man einen
einzigen Timer-Counter für alle Threads gemeinsam verwendet.
Hauptsächlich als lern-Übung.
Nur stelle ich mich gerade vermutlich sau blöd an. Ich komme nicht
drauf, wie man elegant mit dem Überlauf umgehen kann. Mein Bauchgefühl
sagt mir, dass es ganz simpel gehen muss - aber wie?
Danke Peter, das war die Lösung, nach der ich suchte.
Auch Besten Dank an Birger, an deinen völlig anderen Lösungsansatz hatte
ich auch noch nicht gedacht. Die Regelmäßigen Ausführungsintervalle
reduzieren die Rechnerei deutlich. Ich muss mal schauen, ob das zu
meiner Anwendung passt. Vorraussetzung wäre dann ja, dass die Tasks
niemals länger dauern, als ein Intervall.
Aufgrunf Peters Vorschlag habe ich im Internet noch einen leicht anderen
gefunden, und diese beiden miteinander verglichen:
1
// Anderer Vorschlag
2
void test1()
3
{
4
uint16_t start=jetzt();
5
while (! ((jetzt()-start)>=33)) {};
6
}
7
8
// Peters Vorschlag
9
void test2()
10
{
11
uint16_t ende=jetzt()+33;
12
while ( (int16_t)(jetzt()-ende)<0) {};
13
}
Interessant ist dabei, dass test1 minimal kürzeren Code erzegt. Ich
hatte umgekehrt erwartet, dass test1 mehr Code erzeugt, weil in der
while Schleife nicht nicht mit 0 sondern mit 33 verglichen wird.
Da zeigt sich mal wieder, dass ein Blick ins Assembler Listing lohnt.
1
000002e6 <test1>:
2
2e6: cf 93 push r28
3
2e8: df 93 push r29
4
2ea: e4 de rcall .-568 ; 0xb4 <jetzt>
5
2ec: ec 01 movw r28, r24
6
2ee: e2 de rcall .-572 ; 0xb4 <jetzt>
7
2f0: 8c 1b sub r24, r28
8
2f2: 9d 0b sbc r25, r29
9
2f4: 81 97 sbiw r24, 0x21 ; 33
10
2f6: d8 f3 brcs .-10 ; 0x2ee <test1+0x8>
11
2f8: df 91 pop r29
12
2fa: cf 91 pop r28
13
2fc: 08 95 ret
14
15
000002fe <test2>:
16
2fe: cf 93 push r28
17
300: df 93 push r29
18
302: d8 de rcall .-592 ; 0xb4 <jetzt>
19
304: ec 01 movw r28, r24
20
306: a1 96 adiw r28, 0x21 ; 33
21
308: d5 de rcall .-598 ; 0xb4 <jetzt>
22
30a: 8c 1b sub r24, r28
23
30c: 9d 0b sbc r25, r29
24
30e: 97 fd sbrc r25, 7
25
310: fb cf rjmp .-10 ; 0x308 <test2+0xa>
26
312: df 91 pop r29
27
314: cf 91 pop r28
28
316: 08 95 ret
Wie dem auch sei, beide Varianten passen perfekt in meine Anwendung.
Vielen Dank nochmal.
Stefan U. schrieb:> Vorraussetzung wäre dann ja, dass die Tasks> niemals länger dauern, als ein Intervall.
nicht ganz, im Extremfall würden in einem mainloop alle "Tasks"
ausgeführt, das kann dazu führen dass ticks > ZYKLUS ist (vielleicht
12). Darum wird ZYKLUS von den ticks abgezogen, und die nächste
Verrzögerung der main würde dann entsprechend kürzer ausfallen.
Ich hatte mich auf 100ms Zykluszeit eingeschossen, weil Ausgaben an ein
GLCD manchmal 60..80ms dauerten (blockiernde Funktion).
Stefan U. schrieb:> Wie dem auch sei, beide Varianten passen perfekt in meine Anwendung.> Vielen Dank nochmal.
Die beiden Varianten blockieren aber die Ausführung
(while(!Bedingung){tue nix;})..solange für den 1. Task die Zeit nicht
gekommen ist, würde ein nachfolgender Task nicht ausgeführt werden, auch
wenn dessen Zeit schon längst um ist..
Und wenn Du auch in anderen Quellen guckst - schon die Libs vom RP6V2
auf der Arrexx Page gefunden; dort gibt es das System mit den
"stopwatches()".
>> Für Delays bis 32767.
Warum den cast auf (signed) int16_t? Ohne den geht's genau so gut und
dann hat man sogar delays bis 65535! Überlauf ist da Gut[TM] :-)
Nur aufpassen: das now() soll dann wirklich von 0 bis 65535 zählen,
sonst funktioniert es mit dem Überlauf nicht!
Eric B. schrieb:> Warum den cast auf (signed) int16_t? Ohne den geht's genau so gut und> dann hat man sogar delays bis 65535!
Und wenn es durch Verzögerungen möglich ist, dass der passende Tick-Wert
nicht geprüft wird, dann hat man das Ereignis verpasst.
Es ist natürlich möglich, dass alle Tasks zusammen im schlimmsten Fall
nicht mehr Zeit benötigen als ein Tick. Aber ich würde mich nicht auf
diese Annahme verlassen wollen.
Stefan U. schrieb:> void main()> {> while (1)> {> task1();> task2();> task3();> task4();> }> }
Ja, grandios. Warum bloß willst du immerzu nur geradeaus mit dem Kopf
durch die Wand? Da holst du dir bloß ne Beule.
Hier mal ne aus dem Stegreif formulierte Alternative.
void MacheIrgendwas(void)
{ AddDelayedEvent(mache_irgendwas,600);
hier tut er 'irgendwas'...
}
void DispatchEvent(EVENT aEvent)
{ if (aEvent==mache_irgendwas) MacheIrgendwas();
...
}
..main(..)
{ InitSysTeck();
AddDelayedEvent(mache_irgendwas,600);
immerzu:
if (EventAvail()) DispatchEvent(GetEvent());
KümmereDichUmSonstwas();
if (GetNumOfDelayedEvents()==0)
AddDelayedEvent(mache_irgendwas,600); //Zwangs-Restart
goto immerzu;
}
So. Das Verwalten der Uhrzeit und der Events solltest du einem einzigen
Modul überlassen, der bei 1 ms großen Zeitscheiben die Uhrzeit per long
führen sollte - und der um Mitternacht (oder eben nach 24 Stunden)
sowohl die Uhrzeit, als auch alle noch anstehenden delayed Events
korrigiert. Schließlich ist es ja nicht dein Anliegen, in jedem zyklisch
aufgerufenen (und wieder mal BLOCKIEREND geschriebenen) Unterprogramm
mit der Uhrzeit herumzurechnen.
W.S.
> Die beiden Varianten blockieren aber die Ausführung
Das ist ein Missverständnis. Diese blockierenden Warteschleifen sind
unrealistisch. Ich hatte sie lediglich verwendet, um die Ausdrücke in
den Klammern zu testen, um zu sehen, wie sich deren Assembler-Code
unterscheidet.
In den Tasks darf ich das so nicht machen, ist klar.
@W.S.
Dein Vorschlag mit den Events ist völlig Ok.
Er passt allerdings nicht gut in meine Anwendung. Meine Tasks haben
nicht einfach nur leere Warteschleifen. Während sie Warten, tun sie noch
viel mehr. Ich darf nur nicht den ganzen Quelltext veröffentlichen.
Trotzdem Danke für deine Mühe. Immerhin ist das ein weiterer ganz
anderer Lösungsansatz der sicher auch passende Anwendungen hat.
Clemens L. schrieb:> Eric B. schrieb:>> Warum den cast auf (signed) int16_t? Ohne den geht's genau so gut und>> dann hat man sogar delays bis 65535!>> Und wenn es durch Verzögerungen möglich ist, dass der passende Tick-Wert> nicht geprüft wird, dann hat man das Ereignis verpasst.
Eh? Das trifft dann aber genau so zu auf der Lösung mit cast.
Es wird auch nicht auf der genau passende Tick-Wert gewartet, sondern
bis der Tick "vorbei" ist.
Eric B. schrieb:> Clemens L. schrieb:>> Eric B. schrieb:>>> Warum den cast auf (signed) int16_t? Ohne den geht's genau so gut und>>> dann hat man sogar delays bis 65535!>>>> Und wenn es durch Verzögerungen möglich ist, dass der passende Tick-Wert>> nicht geprüft wird, dann hat man das Ereignis verpasst.>> Eh? Das trifft dann aber genau so zu auf der Lösung mit cast.
Nein. Ein int16_t hat 65536 Werte. Wenn 65535 davon als "in der Zukunft"
interpretiert werden, gibt es genau einen Wert für "jetzt", und keinen
für "in der Vergangenheit".
> Es wird auch nicht auf der genau passende Tick-Wert gewartet, sondern> bis der Tick "vorbei" ist.
Stimmt, "<= 0" wäre richtiger.
Clemens L. schrieb:> Nein. Ein int16_t hat 65536 Werte. Wenn 65535 davon als "in der Zukunft"> interpretiert werden, gibt es genau einen Wert für "jetzt", und keinen> für "in der Vergangenheit".
Es geht dann auch besser wenn man alles als "in der Vergangenheit"
betrachtet
Stefan U. schrieb:> Meine Tasks haben> nicht einfach nur leere Warteschleifen. Während sie Warten, tun sie noch> viel mehr.
Dann hast du deine Tasks falsch konstruiert.
Bedenke mal folgendes:
1. Du brauchst eine Uhr bzw. Zeit-Instanz, die nach Ablauf von
vorgebbaren Zeitspannen oder zyklisch zu bestimmten Absolutzeiten
Ereignisse generiert, die dann an anderer Stelle als Anlaß genommen
werden, Aktionen (eben Tasks) zu starten.
2. So eine Aktion (Task) wird entweder als Unterprogramm gestartet und
rasselt durch bis zum Ende - oder es ist ein Prozeß in einem RT-OS, der
auf sein Aufwecken durch ein bestimmtes Ereignis auf Eis liegt und keine
Rechenzeit derweil verbraucht. Da wird NICHTS zwischendurch gemacht,
weil sowas konzeptionswidrig ist.
3. Der Event-Dispatcher, den ich mal fix skizziert habe, ist bei
richtigen Systemen etwas komplexer. Er unterscheidet dabei die
Ereignisse danach, ob es welche sind, die zu allererst an ein
fokussiertes Objekt gehen oder andere, die als "broadcast" an alle
eingetragenen Interessenten gehen. Sowas ist sowohl für
Hardware-Aktivitäten als auch für Menü-Aktivitäten gleichermaßen
geeignet. Es ist ganz grob auch dem Funktionsprinzip von Windows ähnlich
- dort hat jedes grafische Element auf dem Display seine
"Windows-Funktion" und die wird vom Scheduler so ziemlich
gleichbehandelt wie andere Tasks. Wie sowas im Kleinen und im Detail
gemacht werden kann, kannst du in der Lernbetty (hier im Forum)
nachlesen.
W.S.
W.S. schrieb:> Wie sowas im Kleinen und im Detail> gemacht werden kann, kannst du in der Lernbetty (hier im Forum)> nachlesen.
Und in welchem der bis jetzt 79 Threads findet man das Aktuelle?
Gibt's das nur für ARM oder auch für kleine ATmegas?
Den Vorschlag von W.S., auf Zeit-Ereignisse zu reagieren, anstatt auf
eine Zeit zu warten, finde ich gar nicht so schlecht. Richtig umgesetzt
kann man damit bestimmt ein Programm durchaus gut lesbar gestalten.
PC's programmiert man in der Regel ja auch ereignisorientert.
Wenn ich ein paar hundert Bytes mehr Spreicher frei hätte, würde ich das
auch gerne mal ausprobieren. Momentan bin ich jedoch froh, mit den
vorgegebenen 1kB so gerade eben auszukommen.
Beim nächsten Projekt werde ich nochmal an W.S. Vorschlag denken und es
ausprobieren.
BirgerT schrieb:> Und in welchem der bis jetzt 79 Threads findet man das Aktuelle?> Gibt's das nur für ARM oder auch für kleine ATmegas?
Erstens gibt es nur 2 (in Worten ZWEI) Threads für die Lernbetty. Der
eine ist bei Projekten+Code und dort findet man die Quellen. Der andere
ist in µC+Elektronik und der war für das Diskutieren vorgesehen.
Siehe "Beitrag "Die Lernbetty: Die SwissBetty von Pollin als ARM-Evalboard";
Zweitens ist (war) die Lernbetty ein ARM7TDMI und die unterste Ebene der
hardwarebezogenen Teile ist natürlch auf die betreffende Hardware
zugeschnitten. Erwarte also nicht, daß ein UART-Treiber der Lernbetty
auf einen AVR paßt.
Drittens sind die nicht hardwarebezogenen Teile durchaus auch auf
anderen Systemen verwendbar.
Viertens soll die ganze Lernbetty zum Lernen und Verstehen von
Funktionsprinzipien da sein und und nicht zum blinden copy&paste -
obwohl das bei einigen Teilen durchaus geht.
W.S.
>> Blöderweise muss ich dafür sorgen, dass der Timer niemals überläuft.>Warum?
Siehe ganz oben, der erste Beitrag.
Langer Rede kurzer Sinn: Weil ich ungeschickt gerechnet habe. Was ja
auch das Thema dieses Threads ist. Für die Lösung(en) bin ich den
Helfern hier dankbar. Es klappt nun einwandfrei - auch mit
Timer-Überlauf.