Forum: Compiler & IDEs Timer geschickter programmieren


von Stefan F. (Gast)


Lesenswert?

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?

von Timmo H. (masterfx)


Lesenswert?

Bevor du das Rad neu erfindest... hast du dir mal anderen scheduler 
angesehen, z.B. hier: 
https://www.mikrocontroller.net/articles/AVR_Softwarepool#Betriebssysteme_und_Scheduler

von Peter II (Gast)


Lesenswert?

ich würde die "Wartezeit" einfach in jedem Tick herunterzählen.

von Stefan F. (Gast)


Lesenswert?

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?

von Peter D. (peda)


Lesenswert?

Überlauf fest:
1
  uint16_t warte = now() + 600;
2
3
  while( (int16_t)(now() - warte) < 0 );

Für Delays bis 32767.

: Bearbeitet durch User
von BirgerT (Gast)


Lesenswert?

Entspricht jetzt vielleicht nicht genau Deiner Fragestellung, eher als 
Vorschlag zur Lösung der gestellten Aufgabe sehen..
1
// tick wird in der Timer ISR alle 10ms inkrementiert
2
extern volatile uint8_t tick; 
3
4
// Feste Umlaufzeit der mainloop * 10ms 
5
// (1, 2, 5, 10, 20, 50 oder 100)
6
#define ZYKLUS 10
7
8
void task1(uint16_t intervall) 
9
{
10
    static uint16_t warte = 0;
11
12
    if(warte == 0) {
13
       warte = intervall;
14
       return;
15
    }
16
    if (--warte > 0) {
17
       return;
18
    }
19
20
    // mach deinen Job
21
22
}
23
24
void main()
25
{
26
    while (1) 
27
    {
28
        do {
29
            ; // hier könnte auch etwas erledigt werden
30
        } while(tick <= ZYKLUS) {
31
        tick -= ZYKLUS;
32
33
        // Alle ZYKLUS * 10ms
34
35
        task1(60);  // alle 6 Sekunden (60 * 10 * 10ms)
36
        task2(3);   // alle 0,3 Sekunden (3 * 10 * 10ms)
37
        task3(1337);
38
        task4(4711);
39
    }
40
}

Aber 10ms Timerinterrupt ist schon gemächlich..

von Stefan F. (Gast)


Lesenswert?

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.

von Stefan F. (Gast)


Lesenswert?

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.

von BirgerT (Gast)


Lesenswert?

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()".

von Eric B. (beric)


Lesenswert?

Peter D. schrieb:
> Überlauf fest:
>
1
>   uint16_t warte = now() + 600;
2
> 
3
>   while( (int16_t)(now() - warte) < 0 );
4
>
>
> 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!

: Bearbeitet durch User
von Clemens L. (c_l)


Lesenswert?

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.

von W.S. (Gast)


Lesenswert?

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.

von Stefan F. (Gast)


Lesenswert?

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

von Stefan F. (Gast)


Lesenswert?

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

von Eric B. (beric)


Lesenswert?

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.

: Bearbeitet durch User
von Clemens L. (c_l)


Lesenswert?

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.

von Eric B. (beric)


Lesenswert?

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
1
uint16_t stamp = now();
2
 
3
while((now() - stamp) < DELAY )
4
{
5
  /* nutt'n - just wait */
6
}

: Bearbeitet durch User
von W.S. (Gast)


Lesenswert?

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.

von BirgerT (Gast)


Lesenswert?

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?

von Stefan F. (Gast)


Lesenswert?

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.

von W.S. (Gast)


Lesenswert?

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.

von Wolfgang (Gast)


Lesenswert?

Stefan U. schrieb:
> Blöderweise muss ich dafür sorgen, dass der Timer niemals überläuft.

Warum? Du rechnest doch mit unsigned.

von Stefan F. (Gast)


Lesenswert?

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

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.