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