Forum: Projekte & Code Warten und Verzögern ohne zu Blockieren (avr/delay.h)


von Johann L. (gjlayde) Benutzerseite


Angehängte Dateien:

Lesenswert?

Hi,

immer wieder sieht man Code, in dem per delay gewartet und Zeit 
vertrödelt wird.

Das funktioniert zwar in einfachen Beispielprogrammen, aber spätestens 
wenn die delay-Routinen in Interrupt-Funktionen landen oder mehrere, 
unabhängige Verzögerungen realisiert werden sollen, stellen sich 
erhebliche Probleme ein und es gibt reihenweise Bauchlandungen.

Fragen dazu und vermurxter Code sind Legion.

Um Verzögerungen zu realisieren verwende ich in praktisch all meinen 
Projekten Countdown-Zähler: Die Anwendung zieht einen Zähler auf, und 
wenn er abgelaufen ist, folgt eine Aktion. Ist der Zähler abgelaufen, 
dann bleibt er bei 0 stehen wie ne Eieruhr.

Damit realisiere ich Dinge wie Taster-Abfrage bzw. -Entperllung, 
Einlesen von DCF-Bits, Entprellen von Drehgebern, Ausgabe von 
Morse-Zeichen, blinkende LEDs, Timeouts für Bildschirmschoner und zig 
andere Funktionalitäten.

Der Beispiel-Code anbei ist zusammengeschrumpft aus einem dieser 
Projekte und mit zusätzlichen Kommentaren angereichert:

countdown.c
   Übernimmt das Runterzählen der Countdown-Werte
countdown.h
   Hier werden Zähler eingetragen und verwaltet
timer2.c
   Initialisiert Timer2 (ATmega88) und implementiert eine ISR,
   die im 10ms-Raster die Zähler bedient.
countdonw-demo.c
   Das Hauptprogramm
Makefile
   Ein Makefile eben

Das Hauptprogramm initialisiert den Controller (einen AVR ATmega88) und 
blinkt mit 2 LEDs:
 *  Eine LED an PortC3/VCC blinkt im Takt von 1 Sekunde.
 *  Eine LED an PortB2/GND blinkt im Takt von 1.1 Sekunden.

Die main-Loop ist natätlich ohne Blockierung.

Das ganze ist was komplexer und nicht so simpel hinzutexten wie mit 
einem blockierenden delay-Ansatz, aber vielleicht kann das Beispiel ja 
ein wenig das Elend der missbrauchten delay-Einsätze, denen man ständig 
begegnet, lindern.

Wenn man den Code erstmal hat, ist er easy zu benutzen und tut seine 
arbeit auch im zeitkritischen Echtzeitumfeld.

Johann

p.s.: hier noch ein Blick in die Hauptschleife:
1
    while (1)
2
    {
3
        // Falls Countdown für LED1 abgelaufen ist, 
4
        // neu aufziehen auf 1 Sekunde (100ms)
5
        // und toggle LED1
6
        if (0 == count.ms10.led1)
7
        {
8
            PIN_LED1 |= (1 << PAD_LED1);
9
            count.ms10.led1 = 100;
10
        }
11
        
12
        // Falls Countdown für LED1 abgelaufen ist, 
13
        // neu aufziehen auf 1.1 Sekunde (110ms)
14
        // und toggle LED2
15
        if (0 == count.ms10.led2)
16
        {
17
            PIN_LED2 |= (1 << PAD_LED2);
18
            count.ms10.led2 = 110;
19
        }
20
    } // Hauptschleife

von Matthias L. (Gast)


Lesenswert?

>        if (0 == count.ms10.led1)
>        if (0 == count.ms10.led2)


Da brauchst du ja für jedes Ereignis einen eigenen Zähler, der 
dekrementiert werden muss....

Beitrag "Re: Programm bzw. Ablaeufe steuern"

von eku (Gast)


Lesenswert?

Jedes vernünftige Betriebssystem hat eine callout-table. Man registriert 
sich mit Wartezeit und wird dann aufgeweckt. Alternativ mit 
Callback-Funktion.

Dein Ansatz geht in diese Richtung.

Deine Main-Schleife sollte den AVR bis zum nächsten Interrupt in den 
Sleep-Modus schicken, da bis zum nächsten Timer die Counter unverändert 
sind. Spart Energie.

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Matthias Lipinsky schrieb:
>>        if (0 == count.ms10.led1)
>>        if (0 == count.ms10.led2)
> Da brauchst du ja für jedes Ereignis einen eigenen Zähler, der
> dekrementiert werden muss...

Jepp. Ein Byte wird man noch übrig haben, und dekrementiert wird er nur 
alle 10ms.

Binnen 10ms sind schon Weltreiche aufgestiegen und wie der zerfallen... 
Un die 1-Sekunden Zähler werden gar nur 1x pro Sekunde angefasst.

Zum Entprellen von Tastern braucht man übrigens keine eigenen Zähler: 
die Taster-Routine klinkt man einfach in die 10ms-ISR ein.

Mit einem Callback-Mechanismus oder wie auch immer musst du auch 
irgenwdo und irgendwie die Zeit verwalten oder zählen.

Eine der obigen Abfragen kostet inclusive Sprung 6 Ticks, ich verstehe 
das Problem daher nicht wirklich.

Johann

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

eku schrieb:
> Jedes vernünftige Betriebssystem hat eine callout-table. Man registriert
> sich mit Wartezeit und wird dann aufgeweckt. Alternativ mit
> Callback-Funktion.

Das ist der Fall, wenn man ein Betriebssystem einsetzen kann und will.

Ich muss ehrlich gestehen, daß es mir im Hobby-Bereich zu weit geht ein 
OS einzusetzen und damit ein Großteil der Resources eines µC 
aufzufressen. Das wäre Overkill :-)

Ausserdem steht der obige Code für sich, es verwendet keine Externa 
wie Lib-Funktionen oder ein OS, braucht keine Task-Umschaltung etc.

> Deine Main-Schleife sollte den AVR bis zum nächsten Interrupt in den
> Sleep-Modus schicken, da bis zum nächsten Timer die Counter unverändert
> sind. Spart Energie.

Ja, das ist sinnvoll.

Der Code vermeidet aber bewusst alle unnötigen Erweiterungen, er dient 
dazu, eine simple und überschaubare Alternative zu blockierenden 
Routinen aufzuzeigen, ohne die Komplexität eines OS zu enthalten oder 
Dinge zu implementieren, die nichts mit der angesprochenen Aufgabe zu 
tun haben.

Johann

von Matthias L. (Gast)


Lesenswert?

>ich verstehe das Problem daher nicht wirklich.

Was ich nur meine ist folgendes:

Nimm einfach eine Varaible, die zyklisch hochgezählt wird.
Und jedes zeitliche Ereignis benötigst du dann eine Speichervariable als 
aktuellen Zeitstempel.

Also quasi Kosntante und Zählwert vertauscht..

Ist kein Problem, nur ein Vorschlag

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Matthias Lipinsky schrieb:

> Was ich nur meine ist folgendes:
>
> Nimm einfach eine Varaible, die zyklisch hochgezählt wird.
> Und jedes zeitliche Ereignis benötigst du dann eine Speichervariable als
> aktuellen Zeitstempel.
>
> Also quasi Kosntante und Zählwert vertauscht..
>
> Ist kein Problem, nur ein Vorschlag

äh... sorry, ich steh am Schlauch

dann braucht man doch auch ne Variable. Dann muss man eben Vergleichen, 
nicht Vermindern.

Im Beispielcode sind nur zyklische Ereignisse drin; bei abgelaufenem 
Zähler wird direkt wieder aufgezogen.

Ebenso einfach könnte man aber auch One-Shot-Zähler umsetzen, also zB 
sowas:

Auf Tastendruck hin wird immer wieder ein Zähler aufgezogen. Ist er 
abgelaufen, etwa nach 2 Minuten oder so, wird ein Display ausgeschaltet 
oder ein Bildschirmschoschoner aktiviert.

Mit nur einer Zählvariable müsste man immer schauen, wo diese gerade 
steht um die richtige Differenzzeit bis zum endgültigen Eintreffen des 
Events zu bestimmen. Bei zyklischen Variablen etwas unangenehm, der Code 
müsste zudem atomar sein.

Es können sich dann auch Glitches ergeben wie bei 
nicht-phasen/-frequenzkorrekter PWM wenn man die Wartezeit 
erhöht/erniedrigt.

Die Vergleiche auf die 8-Bit Countdown-Zähler sind hingegen atomar ohne 
spezielle Vorhehrungen -- die braucht man oben nur bei den 16-Bit 
Zählern, falls man ne hohe 10ms-Auflösung haben muss, was nur selten der 
Fall ist.

Johann

von Default U. (shyguy)


Lesenswert?

Also mir gefällt die Demo sehr. Der gewählte Ansatz ist ja nicht 
unbekannt und wird von so manchem anderen Compiler nativ unterstützt; 
aber ich finde es wunderbar, dass man anderen, die immer stur mir DELAY 
coden auch 'mal einen anderen Ansatz aufzeigt - So lange man auf den 
verwendeten Timer global verzichten kann :)

von Peter D. (peda)


Lesenswert?


von Johann L. (gjlayde) Benutzerseite


Angehängte Dateien:

Lesenswert?

Default User schrieb:
> Also mir gefällt die Demo sehr. Der gewählte Ansatz ist ja nicht
> unbekannt und wird von so manchem anderen Compiler nativ unterstützt;
> aber ich finde es wunderbar, dass man anderen, die immer stur mir DELAY
> coden auch 'mal einen anderen Ansatz aufzeigt - So lange man auf den
> verwendeten Timer global verzichten kann :)

Jo, wurde bestimmt schon 1000x in unterschiedlichsten 
Geschmacksrichtungen implementiert; hier ist die 1001...

Natürlich braucht man dafür einen Timer. Für nicht verwendete Timer gilt 
das gleiche wie für nicht belegten Speicher: Es gibt dafür keine Knete 
vom Hersteller zurück ;-)

Falls schon alle Timer belegt sind dann ist guter Rat teuer. Oft ist ein 
Takt von 10ms nicht essenziell, zum Entprellen etc. gehen auch 8ms oder 
15ms. Das öffnet die Möflichkeit, sich in einen schon belegten Timer 
einzuklinken und in einer seiner ISRs die Jobs zu erledigen und damit 
den Timer mehrfach zu nutzen.

Beispiel wäre ein Timer im PWM-Mode, der beim Überlauf die Jobs bedient.

Was mit nicht so gut gefällt ist die countdown.c. Für Anfänger dürfte 
die zu kompliziert sein und schwer nachvollziehbar. Einfacher und klarer 
wird es, wenn man auf allen SchnickSchnack verzichtet wie im Anhang 
gezeigt (ohne 100µs-Zähler, ohne 16-Bit-Zähler, ohne asm-Variante)

Johann

von Gast (Gast)


Lesenswert?

und in Geschmacksrichtung Jörg Wunsch auch noch mal:

http://www.sax.de/~joerg/avr-timer/


ich würde mir wünschen, dass Beiträge des Zuschnitts "Rad erfunden die 
257ste" nicht automatisch in "Codesammllung" landen, sondern das vorab 
mal diskutiert.

Oder vielleicht mal prüfen, ob es das schon gibt - zB tauchen hier X 
Sonnenstands-Algorithmen auf, dabei gibts die in C per Festmeter und 
funktioniered - liest da noch jemand, vor codiert wird?

-mah

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Gast schrieb:

> ich würde mir wünschen, dass Beiträge des Zuschnitts "Rad erfunden die
> 257ste" nicht automatisch in "Codesammllung" landen, sondern das vorab
> mal diskutiert.

Nirgendwo wird behauptet, das Rad oder irgendein Rad neu erfunden zu 
haben. Trotzdem begegnet man alle Nase lang immer wieder problematischem 
Code durch blockierende Verzögerungsschleifen. Da wirst du mir 
zustimmen.
Zwar scheinen auch andere Codevorlagen das nicht aus der Welt schaffen 
zu können, aber man darf ja noch daran glauben und drauf hoffen :-)

Und ja, ich habe die Suche bemüht und nix ansprechendes in die Richtung 
gefunden.

Über unterschiedliche Ansätze kann man natürlich diskutieren und ihre 
Vor- und Nachteile darstellen.

-1-
Solche Countdown-Zähler sind in fast allen meinen Projekten enthalten. 
Das bezieht sich auf kleine, private Bastelprojekte. Der Code ist also 
genügend in der Praxis erprobt und easy auf andere Projekte anpassbar.

-2-
Die Technik ist bewusst simpel gehalten: Es ist nicht möglich, zur 
Laufzeit neue Zähler hinzuzufügen oder zu entfernen -- ich hab das 
schlicht und einfach noch nie genbraucht in dem Umfeld.
Wenn eine Komponente/Funktionalität wie ein Bildschirmschoner vorhanden 
ist, dann ist sie eben vorhanden und bekommt ihren Zähler statisch 
zugewiesen. Das ist erstens codemässig leichter nachzuvollziehen und 
zweitens schlicht und einfach schlanker. Nirgendwo brauch ich mehr als 
10 Zähler, die sich grob fifty-fifty auf 10 Millisekunden bzw- 1 
Sekunden-Raster aufteilen. Der Overhead durch das Runterzählen ist 
praktisch vernchlässigbar. Es braucht keine Tastwechsel o.ä., keine 
dynamische Verwaltung, keine Semaphore, etc.

-3-
Viele existierende Beispiele mag ich persönlich nicht. Ich habe keine 
Lust, bei Verfahren, der in gewissem Grade exemplarisch sein sollen (das 
erwarte ich von Code in einer Codesammlung) mir erst mal wegen kaum 
vorhandener oder trivialer Kommantare aus nem Spaghetti-Code zu 
extrahieren, was jede beteiligte Funktion macht oder wozu die 
beteiligten Dateien da sind. Ich will den Code nicht nur einsetzen, 
sondern auch verstehen, ohne daß meine grauen Zellen Rauchschwaden 
produzieren und ich ewig Zeit drauf verwende, das nachzukommentieren.
Aus dem Grunde habe ich beim vorliegenden Code mit Kommentaren nicht 
gegeizt, auch wenn das ein paar mehr Sätze gibt als notwendig wären, um 
die Sache anhand der Kommentare zu verstehen.

-4-
Dito für Beispiele, wo ich erst mal mit dem Tranchierbesteck an die 
beteiligten Dateien ansetzten müsste, um unabhängige, wiederverwendbare 
Module zu erhalten.

-5-
Es ist ein Vorschlag, eine Anregung, eine Alternative. Mehr nicht. Wer 
es nicht mag, wird es nicht verwenden.
Vielleicht gibt es aber auch jemand, der damit zurande kommt und dem es 
ein lästiges Thema vom Hals schafft.

> Oder vielleicht mal prüfen, ob es das schon gibt

Da brauche ich nix zu prüfen. Ich bin nicht so vermessen zu glauben, 
noch nie sei jemand auf die Idee gekommen, ähnliche Aufgaben zu lösen.
Aber auch hier siehe die Punkte oben.

Johann

von R. M. (rmax)


Lesenswert?

Johann L. schrieb:

> Der Code vermeidet aber bewusst alle unnötigen Erweiterungen,

Einen schlichten sleep_mode()-Aufruf am Anfang der Hauptschleife, um die 
CPU bis zum nächsten Interrupt schlafen zu legen, würde ich nicht als 
unnötige Erweiterung betrachten, sondern als konsequentes Umsetzen 
Deines Anspruchs, mit dem Code zur Vermeidung von Busy-Loops 
beizutragen.

von Peter D. (peda)


Lesenswert?

Reinhard Max schrieb:
> Einen schlichten sleep_mode()-Aufruf am Anfang der Hauptschleife, um die
> CPU bis zum nächsten Interrupt schlafen zu legen

Naja, die sleep-Macros des AVR-GCC sind kreuzgefährlich, da nicht 
interruptfest!!!

Besser ist es daher, wenn man die entsprechenden Modebits selber setzt, 
dann weiß man was man tut und wo man atomic verwenden muß.

Bei Netzbetrieb benutze ich generell kein Sleep.
Der Programmieraufwand und das Einbauen zusätzlicher Fehlerquellen ist 
den geringen Effekt nicht wert.
Die Stromabsenkung bei Idle ist zwar meßbar, haut einen aber nicht vom 
Hocker. Und in der Regel ist die CPU ja nicht der Hauptverbraucher.
Der Sleep-Mode für die ADC-Wandlung hatte bei mir keinen merkbaren 
Effekt, die Lesungen waren gleicht gut, wie ohne Sleep. Dafür geht dann 
die RTC nach, die man mit nem Timer macht.


Sleep zu verwenden, nur weil noch etwas Flash frei ist, halte ich daher 
nicht für sinnvoll.


Und bei Verwendung von Power-Down im Batteriebetrieb, immmer höllisch 
aufpassen, daß man sich nicht vom Aufwachen aussperrt!
Kann man nicht oft genug betonen.


Peter

von R. M. (rmax)


Lesenswert?

Peter Dannegger schrieb:
> Naja, die sleep-Macros des AVR-GCC sind kreuzgefährlich, da nicht
> interruptfest!!!
>
> Besser ist es daher, wenn man die entsprechenden Modebits selber setzt,
> dann weiß man was man tut und wo man atomic verwenden muß.

Ich habe mir eben mal die Definition von sleep_mode() angeschaut:
1
#define sleep_mode()                           \
2
do {                                           \
3
    _SLEEP_CONTROL_REG |= _BV(SE);             \
4
    __asm__ __volatile__ ("sleep" "\n\t" :: ); \
5
    _SLEEP_CONTROL_REG &= ~_BV(SE);            \
6
} while (0)

Kann man daran in Sachen Interruptfestigkeit etwas verbessern, wenn man 
es händisch macht, anstatt das Makro zu verwenden?

> Der Programmieraufwand und das Einbauen zusätzlicher Fehlerquellen ist
> den geringen Effekt nicht wert.

Mal abgesehen vom Stromspareffekt kann es sogar Fehlerquellen 
ausschließen: Wenn ich eine Hauptschleife habe, die wie im obigen 
Beispiel immer erst nach dem nächsten Timer-Interrupt wieder etwas zu 
tun hat, bekomme ich durchs Schlafenlegen ein definiertes Verhalten, 
weil die Schleife nach dem Interrupt immer an der gleichen Stelle 
losläuft. Klar kann man das auch durch eine innere Schleife lösen, die 
auf ein Flag wartet, das von der ISR gesetzt wird, das ist dann aber 
mehr Aufwand als ein einfaches sleep_mode().

von Peter D. (peda)


Lesenswert?

Reinhard Max schrieb:

> Kann man daran in Sachen Interruptfestigkeit etwas verbessern, wenn man
> es händisch macht, anstatt das Makro zu verwenden?

Ich setzte das SE-Bit zusammen mit den Modebits als Zuweisung (=), dann 
ist es atomar.
Ich finde es reichlich sinnlos, das SE-Bit alleine zu setzen.

Die Mainloop macht dann nur den Assemblerbefehl "SLEEP".


Peter

von R. M. (rmax)


Lesenswert?

Peter Dannegger schrieb:
> Ich setzte das SE-Bit zusammen mit den Modebits als Zuweisung (=), dann
> ist es atomar.

... vorausgesetzt Du hast keinen Tiny, bei dem in dem Register auch noch 
andere Bits liegen.

Warum muß das denn überhaupt atomar sein? Solange keine ISR die 
Sleep-Bits anfaßt ist es doch egal, wenn dazwischen Interrupts 
passieren.

> Ich finde es reichlich sinnlos, das SE-Bit alleine zu setzen.

Dann mach mal einen Bugreport an Atmel, die Manuals emfehlen nämlich 
genau diese Vorgehensweise: SE an, sleep, SE aus, um zu vermeiden, daß 
man die CPU versehentlich schlafen legt.

> Die Mainloop macht dann nur den Assemblerbefehl "SLEEP".

Auch dafür hat die avr-libc ein Makro. ;)

von Peter D. (peda)


Lesenswert?

Reinhard Max schrieb:
> ... vorausgesetzt Du hast keinen Tiny, bei dem in dem Register auch noch
> andere Bits liegen.

Dann muß man es atomar klammern.


> Warum muß das denn überhaupt atomar sein? Solange keine ISR die
> Sleep-Bits anfaßt ist es doch egal, wenn dazwischen Interrupts
> passieren.

Genau das ist aber sehr gebräuchlich:
Der Pin-Change-Interrupt weckt die CPU auf und damit sie nicht weiterhin
mit Interrupts geflutet wird, disabled er sich selber und schaltet 
Power-Down aus.
Aber das nicht atomare sleep_mode() schaltet Power-Down wieder ein und 
schon haben wir den Salat.

Bei den ATTiny könnte auch ein externer Interrupt die Flanke umschalten 
und der zufällig unterbrochene sleep_mode() macht dies dann zunichte.
Zwar friert die CPU nicht ein, aber ist ja auch unschön, wenn dann die 
Impulsflanken totalen Mumpitz messen.


> Dann mach mal einen Bugreport an Atmel, die Manuals emfehlen nämlich
> genau diese Vorgehensweise: SE an, sleep, SE aus, um zu vermeiden, daß
> man die CPU versehentlich schlafen legt.

Es steht drin, aber ohne genaue Erklärung.
Die einzig mögliche Erklärung wäre, er könnte schon bei gesetztem SE-Bit 
zufällig in Sleep gehen, ohne das der Sleep-Befehl ausgeführt wurde :-(
Damit würden Sie aber dem AVR das Armutszeugnis ausstellen, er wäre 
unzuverlässig.
Entweder eine CPU macht etwas immer auf einen Befehl hin oder macht es 
nie. Ein "vielleicht" oder "manchmal" darf es nicht geben.



>> Die Mainloop macht dann nur den Assemblerbefehl "SLEEP".
>
> Auch dafür hat die avr-libc ein Makro. ;)

O.k., dieses Macro wäre brauchbar.


Peter

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Reinhard Max schrieb:
> Johann L. schrieb:
>
>> Der Code vermeidet aber bewusst alle unnötigen Erweiterungen,
>
> Einen schlichten sleep_mode()-Aufruf am Anfang der Hauptschleife, um die
> CPU bis zum nächsten Interrupt schlafen zu legen, würde ich nicht als
> unnötige Erweiterung betrachten, sondern als konsequentes Umsetzen
> Deines Anspruchs, mit dem Code zur Vermeidung von Busy-Loops
> beizutragen.

Das hat der Code garnicht als Anspruch. Daß die Vermeidung von 
Unbusy-Loops einfach umzusetzen ist, ist allerdings ein angenehmer 
Nebeneffekt (den man mit jedem anderen nichtblockierenden Ansatz auch 
erreicht).

Ein Sleep stünde genau da, wo es auch mit Verwendung von Delay als 
"Design-Pattern" stehen würde: am Anfang der Main-Loop zum Beispiel.

Der Effekt ist nur, daß der Code mit Delay viel Zeit vertrödelt und 
daher weniger schläft -- daß weniger Zeit über bleibt trifft aber auf 
alle anderen Teilnehmer an der Main-Loop ebenso zu, nicht nur auf Sleep.

Ohne Blockierung stehen sich die Teilnehmer nicht gegenseitig auf den 
Füßen rum. Das ist alles. Sie arbeiten effektiver -- damit natürlich 
auch ein denkbares Sleep -- oder die Zusammenarbeit/Unabgängigkeit wird 
dadurch erst ermöglicht.

Johann

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.