ich versuche mich gerade daran, eine Art Millis Funktion, wie es auch bei Arduino gibt für meine Projekte umzusetzen.
Leider komme ich irgendwie nicht weiter, bzw. habe ich eine Blockade. Hat jemand für mich eventuell einen Tipp? Bin ich überhaupt auf dem richtigen Weg?
Über den CTC Timer1 wird jede Sekunde die globale Variable millis hochgezählt. Diese greife ich ab und vergleiche Sie mit dem gewünschten Intervall und möchte dann damit die LED blinken lassen.
Warum wird millis nur jede Sekunde um 1 erhöht, und nicht jede Millisekunde?
Der Zugriff auf millis in main() ist nicht atomar. Auf einem AVR sind byteweise Zugriffe atomar, millis besteht aber aus 4 Bytes. Eine mögliche Abhilfe wäre, vor der Zuweisung an currentTime die Interrupts zu sperren, und sie nach der Zuweisung wieder freizugeben.
wenn der Timer auf 1Hz konfiguriert ist und man den Sekundenzähler im 1000er Intervall abfragt, dann wartet man 1000 Sekunden. Konfiguriere den Timer auf 1000Hz, dann macht auch der Variablenname millis wieder Sinn. Nimm die Formel laut Manual für den CTC Mode, stelle nach TOP um und rechne es aus. Berechne es mit allen Prescalern und nimm am Ende einen womit ein Ganzzahl TOP Wert rauskommt.
Die millis Variable muss volatile sein, sonst neigt der Compiler dazu, ihren Wert in CPU Registern zu cachen und dann bekommt main() die Änderungen durch die ISR nicht mit.
Du solltest auch dafür sorgen, dass der Lesezugriff auf die Variable millis nicht durch die ISR gestört/unterbrochen wird, sonst liest main sporadisch Quatsch:
Beispiel:
millis ist gerade 255
main liest das erste Byte als 255
isr erhöht auf 256
main liest das zweite Byte als 1
main liest das dritte Byte als 0
main liest das vierte Byte als 0
Am Ende sieht main den Wert 511, richtig wäre aber 256.
Die millis Variable muss volatile sein, sonst neigt der Compiler dazu,
ihren Wert in CPU Registern zu cachen und dann bekommt main() die
Änderungen durch die ISR nicht mit.
Du solltest auch dafür sorgen, dass der Lesezugriff auf die Variable
millis nicht durch die ISR gestört/unterbrochen wird, sonst liest main
sporadisch Quatsch:
Auf volatile kann und sollte man hierfür verzichten.
Bei jedem Durchlauf der ISR wird "tick" /zweimal/ dekrementiert? Wozu?
Hallo,
wäre immer noch zu viel. Kann man kürzen auf ...
1
ISR(TIMER1_COMPA_vect)
2
{
3
staticinttick{1000};
4
5
tick--;
6
if(tick<1){
7
tick=1000;
8
tickSec=true;
9
}
10
}
11
12
oder
13
14
ISR(TIMER1_COMPA_vect)
15
{
16
staticunsignedinttick{0};
17
18
tick++;
19
if(tick>999){
20
tick=0;
21
tickSec=true;
22
}
23
}
Wäre mir jedoch der falsche Ansatz, wenn man einen ms Zähler haben möchte. Die ISR soll einfach nur eine ms Variable hochzählen. Mehr muss und sollte sie nicht machen. Mittels millis baut man sich dann verschiedene unabhängige Timer. Das hat m.E. nichts in der ISR zu suchen.
Auf volatile kann und sollte man hierfür verzichten.
Kann und sollte man nicht.
Volatile macht Sinn, und findet seinen Zweck wenn es sich um Hardware Register handelt, welche sich Außerhalb des Programms ändern können.
Bei Werten aus einer ISR und ihrer späteren Verarbeitung ist das nicht der Fall.
Interrupts abschalten und Memory Barrier setzen, beides macht das Atomic Block Makro.
Natürlich sowohl in C als auch C++.
Zumindest für AVR.
Da wird nur seitenweise doziert ohne zu zeigen wie es gehen könnte.
Mit avr-gcc gibe es weder _Atomic noch std::atomic. (Und auch keine
libatomic, und wenn würde die keiner verwenden weil extrem
resourcenfressend.)
Womit ist sie Resourcenfressend auffällig?
Und was soll "explizites Abschalten der Nebenläufigkeit" im hiesigen
Kontext anderes sein als das, was avr/atomic.h macht?
Also mal Butter bei die Fische und nach konkrete Codebeispiele.
Man muss ja nicht die std::atomic verwenden. Wie es ohne funktioniert hat Arduino F. schon gezeigt. Wenn man allerdings std::atomic verwendet, nimmt man einen atomic Datentyp und der ATOMIC BLOCK ist auch Geschichte.
ganz ehrlich. Manchmal ist es mit dir wie im Kindergarten.
Du hattest nach einer Toolchain gefragt mit libstdc++.
Ja gibt es. https://github.com/modm-io/avr-libstdcpp
std::atomic ist wie schon geschrieben wurde noch nicht verwendbar.
Es war nur eine Nebenbemerkung von mir, bezüglich Wilhelms volatile Erklärung und deinem Kommentar dazu. Tut nichts zu eigentlichen Sache, außer volatile natürlich. Es ist allen klar, dass sie dem TO nicht hilft.
Sie muss ihm auch nicht helfen, weil der TO sie hier für seinen Code nicht benötigt. Hast du selbst schon klargestellt. ATOMICBLOCK und gut ist.
Um mehr geht es doch hier gar nicht.
So wie ich die dortigen Maintainer verstehe, ist dieses Projekt auf modm-io designed. Mann kann sich damit also nicht einfach eine avr-g++ Toolchain basteln. Siehe auch dortige Bugreports.
Dass der Code nicht zu GCC beigetragen wurde lässt auch tief blicken (Wakely ist ja GCC Maintainer).
Auf volatile kann und sollte man hierfür verzichten.
Kann und sollte man nicht.
Volatile macht Sinn, und findet seinen Zweck wenn es sich um Hardware
Register handelt, welche sich Außerhalb des Programms ändern können.
Bei Werten aus einer ISR und ihrer späteren Verarbeitung ist das nicht
der Fall.
Doch natürlich. Eine Variable, die in einer ISR verändert wird, ist für den Compiler genauso wenig verhersehbar. Weder weiß der Compiler, ob der Interrupt für die ISR überhaupt enabled ist, noch ob die (Hardware!) Bedingung für das Auslösen des Interrupt gegeben ist. Für den Compiler ist das eine Variable, die er nicht in einem Register cachen darf, sondern immer direkt lesen oder schreiben muß.
Interrupts abschalten und Memory Barrier setzen, beides macht das Atomic
Block Makro.
Natürlich sowohl in C als auch C++.
Zumindest für AVR.
Die Dokumentation auf https://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html gibt das nicht her. Sie sagt lediglich aus, daß der Code innerhalb des ATOMIC_BLOCK nicht unterbrochen werden kann. Von Memory Barrier steht da nix und abgesehen davon würde sie auch nicht helfen. Ja mehr noch: das Beispiel da geht genau auf den vorliegenden Fall ein. Und die Variable ist selbstverständlich volatile definiert. Man braucht beides: das volatile, damit der Compiler keine veraltete Kopie der Variable benutzt und den ATOMIC_BLOCK, damit der Compiler Code generiert, der innerhalb des Schreib- oder Lesezugriffs nicht unterbrochen wird.
Interrupts abschalten und Memory Barrier setzen, beides macht
das Atomic Block Makro. Natürlich sowohl in C als auch C++.
Zumindest für AVR.
Die Dokumentation auf
https://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html
gibt das nicht her. Sie sagt lediglich aus, daß der Code innerhalb des
ATOMIC_BLOCK nicht unterbrochen werden kann. Von Memory Barrier steht da
nix und abgesehen davon würde sie auch nicht helfen.
Naja eine Memory Barrier würde den kompletten Speicher invalidieren. Das machen zum Beispiel auch cli() und sei(), zumindest in neueren Versionen der AVR-LibC. Aber gerade bei den AVR Tools sind gerne mal noch antike Versionen im Einsatz...
Bei ATOMIC_BLOCK fehlen teilweise Memory Barriers. Beispiel:
So wie ich die dortigen Maintainer verstehe, ist dieses Projekt auf
modm-io designed. Mann kann sich damit also nicht einfach eine avr-g++
Toolchain basteln. Siehe auch dortige Bugreports.
So wie ich die dortigen Maintainer verstehe, ist dieses Projekt auf
modm-io designed. Mann kann sich damit also nicht einfach eine avr-g++
Toolchain basteln. Siehe auch dortige Bugreports.
Genauso hatte ich das bis vor einer Weile auch verteidigt. Bis mir die Welt hier im Forum es komplett anders erklärt hat, mit Hunderten Antworten und Threads. Ich kann es nicht mehr zählen. Es gab in all den anderen Threads zum Thema keinen Widerspruch. Und jetzt beginnt genau dieses Spiel von vorn. Muss man sich nicht wundern, wenn die Leute pauschal volatile und ATOMIC BLOCK verwenden. Einfach weil sie wissen das es damit funktioniert und damit sicher sind.
So wie ich die dortigen Maintainer verstehe, ist dieses Projekt auf
modm-io designed. Mann kann sich damit also nicht einfach eine avr-g++
Toolchain basteln. Siehe auch dortige Bugreports.
Man kann sie inkludieren und verwenden.
Um das in GCC zu integrieren ist es unbrauchbar.
Du sollst dir damit nicht eine Toolchain kompilieren. Du sollst sie inkludieren mit include <>. Siehe Abschnitt "Using the library with MICROCHIP's ATMEL Studio". Die Beschreibung ist gültig bis avr-gcc 14.x.
Es gab in all den anderen Threads zum Thema keinen Widerspruch.
Und jetzt beginnt genau dieses Spiel von vorn.
Mich nervt es auch. Das ist wie die Diskussion, ob ein Glas halb voll
oder halb leer ist. Es geht den Diskutanten nur um Klugscheißerei.
Ich lehne mich jetzt auch nicht weiter aus dem Fenster. Wie verlinkt von dir ist das der gemeinsame Nenner der immer und jederzeit funktioniert. Unabhängig von irgendwas. Alles andere ist mir jetzt egal. Die Zusammenhänge sind nicht einfach.
Ich habe mir in C++ einen plattformunabhängigen Timer gebaut, welcher (theoretisch) in Mikrosekunden zählen kann und nach ca. 600.000 Jahren (64bit in Mikrosekunden) überläuft.
Ich habe mir in C++ einen plattformunabhängigen Timer gebaut, welcher
(theoretisch) in Mikrosekunden zählen kann und nach ca. 600.000 Jahren
(64bit in Mikrosekunden) überläuft.
Die Frage ist: Was passiert danach?
Hast du das korrekte Überlaufverhalten in Echtzeit überprüft?
Nicht dass man sich da später ein Problem einfängt. 😎
Genauso hatte ich das bis vor einer Weile auch verteidigt. Bis mir die
Welt hier im Forum es komplett anders erklärt hat, mit Hunderten
Antworten und Threads.
Das ist an mir vorbeigegangen. Ist wohl auch besser so.
Und der Text dazu liest sich, als würde das den Compiler dazu zwingen, Variablen-Zugriffe temporär(?) so durchzuführen als wäre die Variable volatile deklariert:
"instructs the compiler to not cache any memory data in registers beyond the barrier. This can sometimes be more effective than ... volatile"
Mit dieser Dokumentation bin ich aus mehreren Gründen nicht glücklich. "beyond the barrier" ... und wo endet diese Barriere? Sollte das nicht eher lauten "to invalidate any register caches established before the barrier"? Und davon mal abgesehen, ich will gar nicht alle Variablen, die in irgendwelchen Registern gecached sind verwerfen, sondern nur die eine, die sich asynchron ändern kann.
Deswegen halte ich immer noch volatile für die bessere Lösung. Zumal das auch außerhalb eines ATOMIC_BLOCK funktioniert. Man stelle sich mal vor, der Nutzer hat Interrupts abgeschaltet und glaubt nun er bräuchte keinen ATOMIC_BLOCK mehr, um auf die Variable zuzugreifen (ist ja auch so). Dummerweise implementiert ATOMIC_BLOCK aber auch das volatile und das fehlt jetzt. Nein, das führt in Teufels Küche. Die Dinge sollten nur das machen, was im Namen drinsteht. Und das Makro _MemoryBarrier() sollte eher _DropCachedVariables() heißen. Sofern es das tut, was ich vermute.
es werden nicht alle Variablen die in irgendwelchen Registern stehen verworfen. Die Funktion dient primär ccp_write_io() und behandelt nur das angegebene Register + Wert. Wurde spätestens mit den neuen AVR Controllern notwendig. Ich habe es jedenfalls vorher nicht benötigt. ccp_write_io() meine ich.
Dummerweise implementiert ATOMIC_BLOCK aber auch das volatile ...
Und der Text dazu liest sich, als würde das den Compiler dazu zwingen,
Variablen-Zugriffe temporär(?) so durchzuführen als wäre die Variable
volatile deklariert:
"instructs the compiler to not cache any memory data in registers beyond
the barrier. This can sometimes be more effective than ... volatile"
Mit dieser Dokumentation bin ich aus mehreren Gründen nicht glücklich.
"beyond the barrier" ... und wo endet diese Barriere?
Ich verstehe die Frage nicht.
Bei
1
intx,y;
2
3
intadd_x(inti)
4
{
5
y=x;
6
...Barrier
7
returnx+i;
8
}
muss /x/ eben 2 mal gelesen werden: Einmal vor der Barriere, und einmal danach. Weil die Barriere ein "Memory Clobber" macht muss der Wert von /x/ neu gelesen werden.
Sollte das nicht eher lauten
"to invalidate any register caches established before the barrier"?
Nein. Registerinhalte überleben ja, etwa der Wert von /i/ unabhängig davon, ob /i/ in einem Register lebt oder im Speicher (Frame).
Und davon mal abgesehen, ich will gar nicht alle Variablen,
Dann brauchst du also echte _Atomic oder Äquivalente, oder anstelle von sei(), cli(), ATOMIC_BLOCK, die komplette Memory Barriers implementieren, eben was eigenes. Zum Beispiel:
Den Begriff "volatile Barrier" hatte ich noch nicht gehört.
Daraufhin danach gesucht...
Nicht gefunden.
Jetzt nach deine Antwort auf meine Frage, muss ich wohl davon ausgehen, dass du dir den gerade aus den Fingern gesaugt hast.
Warum wird millis nur jede Sekunde um 1 erhöht, und nicht jede
Millisekunde?
Haha, ja ich glaube das war das, was ich eine Blockade genannt habe. Wahrscheinlich war das auch mein größtes Problem. Danke für den Hinweis.
Der Zugriff auf millis in main() ist nicht atomar. Auf einem AVR sind
byteweise Zugriffe atomar, millis besteht aber aus 4 Bytes. Eine
mögliche Abhilfe wäre, vor der Zuweisung an currentTime die Interrupts
zu sperren, und sie nach der Zuweisung wieder freizugeben.
Herzlichen Dank, das werde ich gleich mal probieren!
Sicherlich gehen beide Wege.....
Also einen aussuchen und damit glücklich werden.
Auch dir herzlichen Dank, sogar für den angepassten Code!
Ich werde mir mal einen davon aussuchen, auch wenn mir die Frage auf der Zunge brennt, welche optimaler bzw professioneller ist, aber ich glaube mit dem "funktionieren" bin ich für's erste zufrieden.
Den Begriff "volatile Barrier" hatte ich noch nicht gehört.
Ein Compiler muss volatile Aktionen so ausführen wie in der Quelle, darf die Reihenfolge also nicht ändern. Dabei ist es egal, ob es ein "normales" volatile ist oder eines aus Inline Asm. Diesen Aspekt betreffend gehören sie also in die gleiche Klasse.
Da hast du Wahr!
Und ich pures Glück, dass es dennoch funktioniert hat
Interessanterweise, hat es das bei mir auch, als ich es blind übernommen habe. Ist mir dann aber beim Schritt-für-Schritt-Nachkontrolle-zum-Verständnis ins Auge gefallen.
Direkt über deinem Posting findest du ein funktionierendes Beispiel ohne
volatile.
Allerdings mit dem denkbar schlechtesten "Beweis" durch Zeigen eines Codebeispiels, das funktioniert.
Das kann entweder Zufall sein oder eben dem Vorhandensein einer - nennen wir es mal /volatile barrier/ - geschuldet sein. Solange es nicht dokumentiert ist, daß das genauso funktioniert, verwende ich lieber ein explizites /volatile/. Auf die paar Zyklen kommt es mir dann auch nicht an. Lieber habe ich nachvollziehbar korrekten Code.
In der Praxis Dein einziges. Der Rest des Codes ist sehr gut:
Auslesen von millis einmal (statt beim Vergleich und beim "restart")
zuerst die Subtraktion, dann der Vergleich (der umgekehrte Weg: currentTime >= previousTime + 1000 funktioniert NICHT beim Überlauf)
Die fehlende Interrupt-Sperre wird Dir in der Praxis bei diesem Code nur selten Probleme verursachen. Nur dann, wenn der 4-Byte-Zugriff nicht atomar ist und das high-byte vor dem low-byte gelesen wird und dann vermutlich "gutmütig".
(0muss natürlich trotzdem behoben werden! Dein Code erweitert sich und es ist letztendlich unvorhersehbar)
Das war nur eine freundliche Floskel. Werners Ansatz solltest Du NICHT in Erwägung ziehen. Es ist ein Hack für Programmier-Anfänger die mit Deinem Ansatz noch überfordert sind. Es skaliert nicht und ist deshalb auch nicht weiter hier rezipiert worden.
Dein Ansatz erlaubt hunderte oder tausende Timer, lokal und unabhängig. Einzige Bedingung: garantierte Auswertung zwischen Ablauf und Maximalzeit. Beispiel: Bei 16-Bit und 1ms Takt ist die Maximalzeit 65s. Bei einer Timer-Zeit von 1 Minute musst Du nur sicherstellen, dass Du nach spätestens 5s auswertest.
Mirko’s approach to implementing a Millis function on AVR is practical and scalable. Using atomic macros or temporarily disabling interrupts ensures safe access to the multi-byte millis variable. Avoiding hacks like Werner’s tick method is recommended. For fun, even when coding, I enjoy designing custom printed popcorn bags while testing microcontrollers.
https://www.packagingunit.com/custom-popcorn-bags/
Schade, das c11 atomics für AVR noch nicht implementiert sind. (Folgendes ist vermutlich keine gute Idee, war nur mal zum ausprobieren, was passiert: https://godbolt.org/z/hh8jbfscj)
mögliche Abhilfe wäre, vor der Zuweisung an currentTime die Interrupts
zu sperren, und sie nach der Zuweisung wieder freizugeben.
Und von Arduino F., die Atomic Makros zu nutzen.
Ach so...
Das Atomic Makro, so wie ich es da verwende, macht genau das:
Interrupt für den Vorgang sperren und wieder lösen.
Zusätzlich setzt es die Memory Barriers.
Aber auch sei() und cli() alleine verwendet, setzen Memory Barriers.
Es ist also technisch egal!
Mir gefallen die Atomic Makros besser,
Weil man damit schön Anweisungsblöcke bauen kann, das macht es übersichtlicher
Man kann das sei() und/oder cli() nicht vergessen/vertauschen, oder sonstigen Mist damit versehentlich anstellen.
Dass der Code macht, was du erwartest, ist
notwendig dafür, dass der Code korrekt ist, aber nicht hinreichend.
Das Verwechseln von "notwendig" und "hinreichend" erlebt man sehr oft bei Leuten, die mit "im Umkehrschluss ist also ..." argumentieren.
Allerdings stimmt oft mit der Erwartung etwas nicht, einfach deshalb, weil man oft aus Blindheit nicht sämtliche möglichen Bedingungen/Szenarien in die Erwartung einbezieht.
Im Extremfall lässt sich die Erwartung grundsätzlich nicht mit dem Ergebnis vergleichen, z. B. wenn ein Code den Wert von Wurzel(2) ziffernweise ausgeben soll, ohne jemals anzuhalten. ;-)
Schade, das c11 atomics für AVR noch nicht implementiert sind.
(Folgendes ist vermutlich keine gute Idee, war nur mal zum ausprobieren,
was passiert: https://godbolt.org/z/hh8jbfscj)
Die 64-Bit und 32-Bit Versionen sind falsch. Man kann einfach:
Korrekt ist das hier:
Beitrag "Re: rollover save timer in c"
( Aufpassen mit kleiner und kleiner-gleich )
Ja, das ist identisch.
Erweitert um casts für Datentypen kleiner int (sonst geht das wegen "integral promotion" schief, hat für den TO aber keine Relevanz)
Und mit "auf der Stelle warten", was nie, bzw. nur für wenige µs genutzt werden sollte.
Auch 16 Bit Variablen müssen Atomar gelesen werden.
Ich denke, hier geht es um einen AVR !? Beim AVR ist das Lesen von 16 Bit Variablen stets atomar! Deshalb der vereinfachte Vorschlag von 16 Bit Variablen wenn es nur um relativ kurze Intervalle ghet wie hier beim Blinken der LED (1- Minute-Intervall sollte ausreichend sein)
Das Lesen von bestimmten 16 Bit Registern ist atomar. Bzw. wird ein Registerpaar beim lesen des 1. Byte gelockt damit sicher das zugehörige 2. Byte gelesen werden kann.
Für normale 16 Bit Variablen gilt das natürlich nicht.
Beim AVR ist das Lesen von 16
Bit Variablen stets atomar!
Kannst du dafür einen glaubhaften Beleg bringen?
Bisher dachte ich immer, dass die kleinen AVR alle 8 Bit Prozessoren
sind....
Bei der alten AVR Architektur hast du recht, da müssen 16Bit auch atomar gelesen werden, mit der neuen Architektur (ab 2020?) meines Wissens nicht mehr... ist mein letzter Stand
Deshalb der vereinfachte Vorschlag von 16
Bit Variablen wenn es nur um relativ kurze Intervalle ghet wie hier beim
Blinken der LED (1- Minute-Intervall sollte ausreichend sein)
wichtig ist nur, dass int auch 16 bit ist. Sonst fehlt da ein Schritt. Entweder ein Cast oder die Differenz braucht ein eigenes Statement.
Das Lesen von bestimmten 16 Bit Registern ist atomar. Bzw. wird ein
Registerpaar beim lesen des 1. Byte gelockt damit sicher das zugehörige
2. Byte gelesen werden kann.
Ich glaube, das gilt allerdings nur für 16 Bit Register, nicht 16 Bit Variablen (im RAM).
Das Lesen von bestimmten 16 Bit Registern ist atomar. Bzw. wird ein
Registerpaar beim lesen des 1. Byte gelockt damit sicher das zugehörige
2. Byte gelesen werden kann.
Bei Xmega werden beim Lesen / Schreiben des 1. Bytes eines 16-Bits SFRs wie SP die IRQs für einige Takte deaktiviert, so dass der Zugriff atomar ist. Dazu muss eine bestimmte Reihenfolge eingehalten werden. Aus den avr-gcc Quellen:
1
| Xmega | non-Xmega
2
------+-----------------+----------------
3
load | read LSB first | read LSB first
4
store | write LSB first | write MSB first
Diese Reihenfolge muss nur bei volatile Zugriffen eingehalten werden und ist der Grund, warum SP-Zugriffe im folgenden Code keine IRQ-Sperren brauchen:
1
voidfun(int*);
2
3
voidbar(void)
4
{
5
intx[20];
6
fun(x);
7
}
1
bar:
2
pushr28
3
pushr29
4
inr28,__SP_L__
5
inr29,__SP_H__
6
sbiwr28,40
7
out__SP_L__,r28
8
out__SP_H__,r29
9
/* prologue: function */
10
movwr24,r28
11
adiwr24,1
12
callfun
13
/* epilogue start */
14
adiwr28,40
15
out__SP_L__,r28
16
out__SP_H__,r29
17
popr29
18
popr28
19
ret
Für normale 16 Bit Variablen gilt das natürlich nicht.
Die Statistik gibt dir und mir recht, zumindest in meiner kleinen Arduino AVR Welt habe ich eine Trefferrate von 100%.
Also ein voller Erfolg beim volatile Verzicht.
Wie oben schon angemeckert wurde, ist das alles nur Zufall.
Diese Behauptung ist ohne Nachweis geblieben. (heiße Luft)
Natürlich kann ein Zufallsgenerator 10 oder 100 mal den gleichen Wert nacheinander liefern.
Aber sonderlich wahrscheinlich ist das nicht.
Dann wurde an anderer Stelle nach einer Doku gefragt, die die in Frage stehende Funktionalität belegt. Eine solche habe ich verlinkt.
Diese wurde sofort pauschal als ungenügend/falsch abgeurteilt.
Wieder ohne Beleg. (auch wieder heiße Luft)
solche Kurzantworten "ist falsch", "funktioniert so nicht" o,ä. stören mich auch. Ich hätte dann schon eine nähere Erklärung die zum Nachdenken anregt bzw. worüber man sich unterhalten kann.
Allgemein bin ich immer noch verdutzt darüber, dass dieser Thread der Erste seiner Art ist, der alles bisher im Forum gesagte in Frage stellt. Mich wundert das. Man hätte schon früher gegen Wilhelm u.a. argumentieren können. Hat man nie getan. Irgendwas stimmt nicht. Ich komme mir vor wie im falschen Film. Ich weiß nur nicht welche Filme falsch waren/sind.
Ich werde den Test noch ausbauen, ggf. über Stunden, Tage, Wochen.
Man hört sich. Ich halte mich erstmal zurück.
solche Kurzantworten "ist falsch", "funktioniert so nicht" o,ä. stören
mich auch. Ich hätte dann schon eine nähere Erklärung die zum Nachdenken
anregt bzw. worüber man sich unterhalten kann.
Das ist doch ganz simpel: Das mögliche Problem bei der Abwesenheit von volatile besteht darin, dass der Compiler Maschinencode schreibt, der die RAM-Inhalte in (MCU-)Registern cached und mit diesen Kopien arbeitet, obwohl sich die Inhalte im RAM mittlerweile geändert haben.
Das Problem ist: der Compiler cached nur unter bestimmten Bedingungen. Also muss die erste Maßnahme sein, ihn dazu zu bringen, etwas falsch zu machen, weil er es nicht besser weiß.
Also auf jeden Fall erst mal die Optimierung auf maximale Geschwindigkeit trimmen, also mit O2 oder O3 übersetzen.
Außerdem einen Code verwenden, der den Compiler geradezu dazu einlädt, seine Optimierungen anzuwenden. Also z.B. sowas in der mainloop aufrufen:
Wenn er daraus in Asm eine Endlosschleife baut, dann hat das Problem zugeschlagen. Wenn nicht, wird irgendwo im Code von millies() dafür gesorgt, dass es eben nicht zuschlägt.
Das mögliche Problem bei der Abwesenheit von
volatile besteht darin, dass der Compiler Maschinencode schreibt, der
die RAM-Inhalte in (MCU-)Registern cached und mit diesen Kopien
arbeitet, obwohl sich die Inhalte im RAM mittlerweile geändert haben.
Dagegen gibts die Memory Barriers.
Die erledigen das, auch wenn das hier nicht jedem schmeckt.
Dagegen gibts die Memory Barriers.
Die erledigen das, auch wenn das hier nicht jedem schmeckt.
Das stellt ja auch niemand in Frage, wenns um aktuelles C++ geht.
Das wurde ja alles auch hier lang und breit vor längerer Zeit diskutiert, als der C++ Standard volatile ganz rauswerfen wollte. Auf modernen CPU-Architekturen mit Multi core, Pipelining, Cachlines etc, ist volatile da völlig fehl am Platz.
Was in Frage gestellt wird, ist, ob diese memory barriers in C auf einem AVR mit einer avrlibc vollumfänglich implementiert sind, und dazu auch noch, warum die dort nachweislich funktionierende klassische Lösung mit volatile nicht weiterhin funktioniert, wenn man den automic-Zugriff beachtet oder wie bei einem uint8_t damit kein Problem hat.
Was in Frage gestellt wird, ist, ob diese memory barriers in C auf einem
AVR mit einer avrlibc vollumfänglich implementiert sind,
Ist abhängig von der Version der LibC. Für den konkreten Code siehe https://avrdudes.github.io/avr-libc/ Für älteren Code musst nach Savannah gehen oder entspreachende GIT Tags direkt ansteuern.
warum die dort nachweislich funktionierende klassische Lösung
mit volatile nicht weiterhin funktioniert, wenn man den
automic-Zugriff beachtet
Wo wurde denn gesagt dass das nicht (mehr) funktionieren soll?
Es wurde nur ein riesen Bohei darum gemacht, dass wenn man volatile Variablen ungeschickt anwendet, es etwas Overhead gibt. Und dieses absolute Drama lässt sich mit Memory Barriers umgehen.
Dazu muss am Ende und am Anfang von ATOMIC_BLOCK eine Memory Barrier gesetzt werden. Und eine volatile Barrier, weil man nicht will, dass ein volatile Asm in den Block oder aus dem Block verschoben werden darf.
a) volatile sollte nie komplett abgeschafft werden. Da wurde etwas falsch verstanden.
b) Die avrLibC wird für C und C++ verwendet. Die avrLibCpp hat nicht jeder und steht nicht Standardmäßig zur Verfügung. Also reden wir nur von avrLibC.
c) Es geht darum, eine beliebige globale Variable muss nicht volatile deklariert sein, nur weil sie bspw. in einer ISR verändert wird. Der Mythos das sie ohne volatile Deklaration wegoptimiert wird ist falsch.
c) Es geht darum, eine beliebige globale Variable muss nicht volatile
deklariert sein, nur weil sie bspw. in einer ISR verändert wird. Der
Mythos das sie ohne volatile Deklaration wegoptimiert wird ist falsch.
Nein, dass kann leicht passieren. Das Problem ist, dass Compiler sehr lokal optimieren.
Wenn in der Hauptschleife überall nur lesend auf die Variable zugegriffen wird, kommt der Compiler hier zu dem Schluß, dass jeglicher Zugriff darauf nutzlos ist. Statt dessen wird der initiale Inhalt der Variablen als Konstante benutzt.
Wenn in der ISR nur schreibend auf die Variable zugegriffen wird, dann läuft das Spiel umgekehrt: der Compiler stellt fest, dass da völlig sinnlos am Inhalt der Variablen rumgemurkelt wird, da sie nie gelesen wird und somit ihr Inhalt völlig unwichtig sein muss. Also werden konsequenterweise hier diese Schreibzugriffe wegoptimiert.
Noch existiert die Variable aber in irgendeinem Objectfile. Den Todesstoß bekommt sie dann bei der LTO (link time optimization). Da stößt der Linker auf den Sachverhalt, dass da eine Variable existiert, auf die von nirgendwo aus zugegriffen wird. Was macht der jetzt wohl mit dieser Variablen, drei Mal darfst du raten...
Im konkreten Fall, einer Implementierung von Millis, wird aber in der ISR nicht nur geschrieben, sondern auch gelesen. Increment für mehr als 8Bit geht nicht anders beim AVR. Sprich: Die ISR-Zugriffe bleiben erhalten. Dafür genügt die Tatsache, dass die Variable statisch ist, also zwischen Aufrufen der ISR erhalten bleiben muss. Das wiederum bewirkt, dass auch die LTO nicht greift. Dieser Seite der Sache funktioniert also auch ohne volatile.
Nützt nur nix, weil in main an keiner Stelle von dem hochlaufenden Zähler Notiz genommen wird...
Der Kernpunkt ist also nur, dass dem Compiler irgendwie verklickert werden muss, dass die Variable außerhalb seines jeweiligen Scopes eine Bedeutung besitzt.
Das Mittel dazu ist entweder, sie als volatile zu deklarieren oder halt mit memory barriers.
Theoretisch ist es möglich, dass im Vergleich die memory barriers eine höhere Performance des generierten Codes ermöglichen, weil an der einen oder anderen Stelle vielleicht doch ein Caching möglich bleibt, die Variable also nicht bei wirklich jedem Zugriff aus dem Speicher in die Arbeitsregister gelesen werden muss, sondern zumindest gelegentlich eben auch die Kopie ausreicht, die bereits in den Arbeitregistern ist. Das ist eigentlich alles.
Ich wage zu behaupten, dass der Vorteil bei der einfachen Architektur der AVRs minimal bis überhaupt nicht vorhanden ist.
Nein, dass kann leicht passieren. Das Problem ist, dass Compiler sehr
lokal optimieren.
Da schießt deine Vorstellund davon, was ein Compiler optimiert, weit über das hinaus, was ein Compiler wirklich macht bzw. überhaupt machen darf.
Wenn in der Hauptschleife überall nur lesend auf die Variable
zugegriffen wird, kommt der Compiler hier zu dem Schluß, dass jeglicher
Zugriff darauf nutzlos ist. Statt dessen wird der initiale Inhalt der
Variablen als Konstante benutzt.
Das würde LTO voraussetzen und dass die Variable /nie/ geschrieben wird. Ist aber nicht der Fall, weil sie (zumindest) in einer ISR geschrieben wird.
Das Lesen der Variablen in der Haupschleife kann nur dann gegen andere Lesezugriffe auf diese Variable optimiert werden, wenn der Compiler den Wert kennt (bzw. dass er sich seit einem anderen Zugriff nicht geändert hat). Diese Kenntnis wird dem Compiler aber durch die Memory Barriers (eingeführt per sei(), cli() oder ATOMIC_BLOCK) genommen.
Daher muss der Compiler jedes Mal lesen — zumindest wenn der gelesene Wert verwendet wird. Wenn der gelesene Wert /nicht/ verwendet wird, kann der Compiler den Lesezugriff tatsächlich weglasssen (falls nicht volatile). Ist dann aber kein Problem, ausser dass man einen nutzlosen ATOMIC_BLOCK hat — was aber kein wirklichen Problem darstellt.
Wenn in der ISR nur schreibend auf die Variable zugegriffen wird, dann
läuft das Spiel umgekehrt: der Compiler stellt fest, dass da völlig
sinnlos am Inhalt der Variablen rumgemurkelt wird, da sie nie gelesen
wird und somit ihr Inhalt völlig unwichtig sein muss. Also werden
konsequenterweise hier diese Schreibzugriffe wegoptimiert.
Wenn die Variable nirgends gelesen wird, dann wäre das kein Problem. Die Variable /wird/ aber gelesen — zumindest wenn main Lesezugriffe hat, die nicht tot oder ungenutzt sind.
Noch existiert die Variable aber in irgendeinem Objectfile. Den
Todesstoß bekommt sie dann bei der LTO (link time optimization). Da
stößt der Linker auf den Sachverhalt, dass da eine Variable existiert,
auf die von nirgendwo aus zugegriffen wird. Was macht der jetzt wohl mit
dieser Variablen, drei Mal darfst du raten...
Komplett falsch abgebogen.
LTO wird nicht vom Linker durchgeführt, sondern vom Compiler. Und zwar zur Linkzeit. Daher der Name. Der Linker macht nix anderes als den LTO-Compiler auszurufen. Bei GCC also lto1.
Zu dem Szenario kommt es wie oben beschrieben erst garnicht. Es sei denn main liest die Variable (effektiv) nicht.
Nützt nur nix, weil in main an keiner Stelle von dem hochlaufenden
Zähler Notiz genommen wird...
Wie gesagt, irgendwo dreht dein Brain v0.9b Compiler durch. Nicht das, was ein realer C/C++ Compiler macht :-)
Und ich habe schon gedacht, ich muß meinen ganzen alten Code wegschmeißen.
Da ich in der ISR fast immer nur ein Flag (Typ uint8_t) setze, bin ich ja mit volatile bestens bedient...
Die in diesem Thread diskutierten Alternativen sind:
A) volatile + atomar
B) Memory-Barrier + atomar
C) Einfach nur _Atomic.
Ist das bei avr-gcc implementiert? Um den geht es hier nämlich. Zum Beispiel wird die Signatur von __atomic_load_4 etc erkannt liefert aber undefined reference vom Linker. Eben weil avr-gcc das nicht implementiert.
Volatile ist halt das einzig verfügbare C-Sprachmittel. Ohne Plattform-/Compilerspezifische Methoden kann man das Auslesen mit volatile und ein wenig Aufwand oft C-konform gestalten.
Z.B. beim tick, der in der ISR erhöht wird: 3 Mal nacheinander auslesen und 2 benachbarte gleiche nehmen oder welche die genügend eng zusammen sind.
(Wobei es wenige Plattform/Compiler-Kombinationen geben dürfte, wo eine memory barrier notwendig und nicht verfügbar ist)
Wow!
Sowas habe ich ja noch nie gesehen/gehört.
Das soll dein Ersatz sein?
Für was? Für eine Interrupt Sperre?
Da gab's eine Menge Systeme die zB. bei
1
SEI
2
LDA <zp>
3
LDY <zp>
4
CLI
für 10µs Interrupts still legten. Beim Zugriff auf die anderen 255 Pages sogar für 12µs. Das konnte/kann in vielen Fällen eine verdammt lange Zeit sein und man konnte dies einfach durch diese Methode verhindern.
Beim AVR kann man natürlich die IRQs abschalten ohne dass
sie abgeschaltet sind.
Jetzt wirds aber besonders logisch!
Oder?
Soweit mir bekannt, ist ein Interrupt eine Unterbrechung des Hauptprogramms, um zwischendurch was anderes zu erledigen.
Diese Unterbrechung wird von cli() wirksam unterbunden.
Da kann die Hardware zucken wie sie will.
Man könnte temporär die IRQs deaktivieren, die auf der / den fraglichen
Variablen rumnudeln, anstatt per CLI Interrupts global zu deaktivieren.
Was allerdings dazu führen würde, dass man u.U. Interrupts verliert.
Das Problem hat man mit globaler Interruptsperre aber auch, nämlich wenn während der Interuptspette die IRQ mindestens 2× triggert. Ein einzelner Trigger ist kein Problem, weil das entsprechende IRQ-Flag gesetzt wird, das den Interrupt nach Beendung der Sperre auslöst — es sei denn es ist eine Level-getriggerte IRQ. Aber dann hast du das gleiche Problem mit globale IRQ-Sperre.
Mir ist klar, dass man das tun kann, aber ob das Sinn macht?
Ich hab's noch nie gebraucht; IRQ-Sperren sind ja nur kurz. Und wenn sie lange sind und man zB ein printf atomar macht, dann ist das das Problem.
Vergleichbar mit dem Arduino Serial.print() in einer ISR. Das geht solange gut bis die FIFO voll ist, danach blockiert es.
Ist also auch keine kluge Idee.
An dummen Ideen gibt es keinen Mangel. Ist für jeden was dabei.
Es gibt ja auch noch eine dritte Möglichkeit, die am Rande dessen wohnt.
Wiedereintrittsfähige ISR.
Also dürfen ISR keinen globalen Status haben, konnen also keine globalen Variablen verwenden sondern müssen auf Features wie TLS (Thread Local Storage) zurückgreifen. Steht auf AVR nicht zur Verfügung, und würde zudem ein dazu passendes Betriebssystem erfordern.
Es gibt ja auch noch eine dritte Möglichkeit, die am Rande dessen wohnt.
Wiedereintrittsfähige ISR.
Also dürfen ISR keinen globalen Status haben, konnen also keine globalen
Variablen verwenden sondern müssen auf Features wie TLS (Thread Local
Storage) zurückgreifen. Steht auf AVR nicht zur Verfügung, und würde
zudem ein dazu passendes Betriebssystem erfordern.
Ähem, ihr redet offensichtlich aneinander vorbei. Das, was Arduino F. meinte, hat zunächst rein garnix mit irgendwelchen Variablen zu tun. Es ist vielmehr nur die Fähigkeit einer ISR, durch eine weitere Instanz ihrer selbst unterbrochen zu werden (und das dann sinnvoll zu handeln).
Das ist einerseits durchaus möglich, aber andererseits auch die hohe Schule der Programmierung. Es läuft i.d.R. darauf hinaus, die ISR in zwei Teile zu zerlegen. Einen kurzen Teil zu Anfang, der wirklich exklusiv läuft. Und einen zweiten Teil, der unterbrechbar ist, also den Rest seiner Laufzeit als eine Art hochpriorisierter Task eines kooperativen MT-OS läuft. Das geht, OHNE wirklich ein MT-OS zu haben. Das ist der eigentliche Witz an der Sache.
Und es ist natürlich ein zweischneidiges Schwert. Einerseits zeigt es auf, das kooperative RTOS eigentlich völlig überflüssig sind. Man kann alles, was die leisten können, mit diesem simplen Schema ebenfalls erreichen. Und zwar sehr viel effizienter. Es zeigt andererseits aber auch auf, dass es sehr guter Programmierer bedarf, sowas hinreichend fehlerfrei umzusetzen. Insbesondere in Bezug auf das Problem der endlichen Resourcen bzgl. Speicher und Rechenzeit.
Ich denke schon, dass beide da auf gleichem Kurs sind.
Das, was Arduino F. meinte, hat zunächst rein garnix mit
irgendwelchen Variablen zu tun.
Es ging hier explizit darum, Interruptsperren zu vermeiden. Da es andererseits um das Incrementieren von millis geht, beißt sich die Katze in den Schwanz. Millis ist die von Johann erwähnte globale Variable.
Einzelne Aufgaben eines RTOS mittels (priorisierter) Interrupts zu realisieren, ist eher ein Hack (für zu kleine Plattformen) als eine Alternative.
sondern müssen auf Features wie TLS (Thread Local
Storage) zurückgreifen. Steht auf AVR nicht zur Verfügung, und würde
zudem ein dazu passendes Betriebssystem erfordern.
Zu wiedereinrittsfähigen Interrupts auf dem AVR finde ich keine expliziten Aussagen in diesem Zusammenhang. Kann ich auch nix zu sagen. Also nicht AVR, sonst ist mir das schon klar.
Zu wiedereinrittsfähigen Interrupts auf dem AVR finde ich keine
expliziten Aussagen in diesem Zusammenhang.
Die braucht man auch nicht.
Die Bedingungen sind recht klar.
z.B. Mit sti() und cli() kann man das "globale Interrupt enable Flag" manipulieren.
Irgendwo muss ein Merker gehalten werden, damit in der ISR festgestellt werden kann ob der Interrupt Request als Wiedereintritt erfolgt und natürlich muss entsprechend gehandelt werden.
Die Probleme sind schon genannt worden!
Erhöhte Sorgfaltspflicht.
Kein OS welches das unterstützt.
Wenig Speicher, so dass mehrere solcher Reentranten Interrupts schwierig werden, u.A. wegen dem Stackbedarf.
Es ist meines Erachtens nach ein Mittel welches nur im Notfall eingesetzt werden sollte und keineswegs zum "heiligen Gral" erhoben werden darf.
Zumindest nicht auf den AVR Zwergen.
Meist besser:
Weit oben wurde die Arduino übliche Methode mit der millis() Verwendung als kompliziert bezeichnet. Und dennoch liefert sie eine recht einfache Möglichkeit quasi Nebenläufigkeiten zu bauen, ohne kaskadierende Stackframes. Das Prinzip weiter ausgeführt, landet man bei den ProtoThreads.