Bei meiner ISR-BLDC-Steuerung kämpfte ich bisher bei Chip-Temperaturen >60°C mit seltsamen Aussetzern.
Die Anwendung griff währenddessen hochfrequent auf volatile 8bit UART-Statusflags zu. Nachdem ich die Zugriffsraten durch ein 10ms-Delay ausbremse, treten die Aussetzer nicht mehr so oft auf.
Das verstehe ich nicht. Zugriffe auf 8bit-Variablen sind doch atomarer Natur.
Wieso, kann dadurch die ISR-Welt durcheinander kommen? Welche Dreck-Effekte können mit der Temperatur zusammenhängen?
seltsamen Aussetzern.
Die Anwendung griff währenddessen hochfrequent auf volatile 8bit
UART-Statusflags zu. Nachdem ich die Zugriffsraten durch ein 10ms-Delay
ausbremse, treten die Aussetzer nicht mehr so oft auf.
Das verstehe ich nicht.
Eigentlich ganz einfach: wenn du etwas weniger oft machst, passiert das auch weniger oft. Allein das Verhältnis von "Zahl der fehlerverursachenden Zugriffen zu der Gesamtanzahl aller Zugriffe" ist relevant.
Danke an alle!
Ich vergaß zu erwähnen, dass es sich um einen ATMega644 handelt. Ich verstehe "volatile" als einen MB-Mechanismus zwischen ISR und Singletask-App.
Die Platine arbeitet unter harschen Bedingungen durch die direkte Verschraubung mit dem Verbrennungsmotor. (Vibrationen, Temperatur, hohe Ströme). Möglicherweise hat der MC auch einen Temperaturschaden vom Heissluftlöten. Beim Löten ohne Hotplate ist mir letztens ein ACS712 hops gegangen.
Danke an alle!
Ich vergaß zu erwähnen, dass es sich um einen ATMega644 handelt. Ich
verstehe "volatile" als einen MB-Mechanismus zwischen ISR und
Singletask-App.
Das ist eine ganz normale Anwendung von volatile und memory barrier.
Aber hört bitte auf, für jeden Scheiß euer privaten Abkürzungen zu erfinden! Das nervt und ist Schwachsinn!
kmh ICE und Eschede, PVC FCKW is nich OK!
Die Platine arbeitet unter harschen Bedingungen durch die direkte
Verschraubung mit dem Verbrennungsmotor. (Vibrationen, Temperatur, hohe
Ströme). Möglicherweise hat der MC auch einen Temperaturschaden vom
Heissluftlöten.
Unwahrscheinlich. Dann eher Vibrationen oder kalte Lötstellen. Der heißeste Kandidat ist aber ein Softwarefehler.
Ich denke der Code ist unspektakulär:
Auch dort können mehr als genug Fehler drinstecken. Betreibe eine systematische Fehlersuche. Versuche den Fehler außerhalb des Motors ohne Hitze und Vibrationen zu reproduzieren. Stresse deine Schaltung LOGISCH so stark wie irgend möglich. Damit kann man Hitze und Vibrationen als Fehler ausschließen.
Dein Code ist schlecht formatiert und fragwürdig. Wenn man schon if-else ohne geschweifte Klammern verwendet, muss trotzdem die Einrückung der restlichen Klammern stimmen! Schließende Klammern müssen die gleiche Ebene wie öffnende haben! Ich empfehle stark, IMMER Klammern für if-else zu verwenden, auch wenn da nur eine Zeile drin steht!
1
ISR(USART0_RX_vect){// Empfang bis NL-Terminator auf Funkebene
2
chardata;
3
4
data=UDR0;// Daten auslesen, Interrupt flag gelöscht
5
urti0=0;// Tout reset
6
if(!uart0_rx_flag){// Ist Puffer frei für neue Daten?
7
if(data==URXTERM){// ja, ist Ende des Strings (RETURN) erreicht?
8
uart0_rx_buffer[uart0_rx_cnt]=0;// ja, dann String terminieren
9
uart0_rx_flag=1;// Flag für 'Empfangspuffer voll' setzen
Das richtige Instrument im Zusammenhang mit Nebenläufigkeit durch ISRs
ist eine memory-barrier.
Gleich wirst du uns noch die dazu nötigen C befehle für AVR zeigen, und wie der Compiler das in Assembler umsetzt. In der Zwischenzeit hole ich mir mal Chips.
Dein Code ist schlecht formatiert und fragwürdig.
a) uart0_rx_flag noch true ist?
b) der Puffer voll ist?
Bei mir im AVR-Studio sehen die Klammern aus wie sie sein sollen....
zu a) altes Kommando noch nicht ausgewertet -> Nix neues wird gelesen
zu b) Puffer voll -> Puffer löschen, weil nur Müll drin steht (letzes else)
Die gibt es sei langem mit ATOMIC_BLOCK(), da ist das garantiert.
Warum sollte man das bei 8bit-Variablen tun? Bei long ist es ja klar.
Die gibt es sei langem mit ATOMIC_BLOCK(), da ist das garantiert.
Warum sollte man das bei 8bit-Variablen tun? Bei long ist es ja klar.
Weil in dem Macro die Memory barrier drin steckt, ebenso wie bei cli() und sei(). Volatile allein GARANTIERT das NICHT, auch wenn es meistens funktioniert. Vor VIELEN Jahren war das selbst mit sei() und cli() NICHT garantiert, wurde dann aber im avr gcc repariert.
Die gibt es sei langem mit ATOMIC_BLOCK(), da ist das garantiert.
OK, ich war voreilig frech.
Das Schlüsselword "volatile" braucht man laut Doku aber trotzdem noch. Sehe ich richtig, dass dieses Makro letztendlich nichts anderes macht als das?:
1
uint8_tsreg_save=SREG;
2
cli();
3
__asm__volatile("":::"memory");
4
...waszutunist
5
SREG=sreg_save;
Wenn aber cli() und sei() schon die Memory Barrier enthalten, wo ist dann der Vorteil dieses Makros? Lesbarkeit könnte ein Argument sein, allerdings finde ich es wesentlich geradliniger, direkt cli() und sei() hin zu schreiben, als in so einem Makro zu verstecken.
Die gibt es sei langem mit ATOMIC_BLOCK(), da ist das garantiert.
…
Wenn aber cli() und sei() schon die Memory Barrier enthalten, wo ist
dann der Vorteil dieses Makros? Lesbarkeit könnte ein Argument sein,
allerdings finde ich es wesentlich geradliniger, direkt cli() und sei()
hin zu schreiben, als in so einem Makro zu verstecken.
Denk mal kurz nach, besonders über potentielle Änderungen.
Wenn sich mal etwas ändert, z.B. wegen Compilerupdates, werden die Leute, die das Makro warten, es dann anpassen - Du musst nix tun.
Wenn man alles selbst von Hand macht - muss man dann auch selbst alles von Hand anpassen.
Ja, ist im vorliegenden Fall nicht so wahrscheinlich, aber…
Ist es richtig das global variable mit volatile qualifiziert wird.
Ist dieses Beispiel korrekt?
So ist das jedenfalls in der avr libc dokumentiert. Hier geht es um zweierlei Dinge:
a) Volatile sagt dem Compiler, dass jeder Schreib- und Lesezugriff direkt auf die Speicherzelle im RAM stattfinden muss. Bei wiederholtem Zugriff wird immer wieder auf das RAM zugegriffen. Der Compiler darf den Wert nicht in einem CPU Register cachen.
Wenn im Hauptprogramm z.B. eine Schleife wäre:
1
int8_tglobal_variable=100;
2
3
ISR(TIMER0_OVF_vect){
4
global_variable++;
5
}
6
7
8
for(inti=0;i<10;i++){
9
...
10
global_variable--;
11
}
Dann könnte der Compiler ohne Volatile die Variable aus dem RAM in Register R2 kopieren, dann dieses Register wiederholt decrementieren und erst zum Schluss den Wert von R2 zurück ins RAM schreiben. Ohne Unterbrechungen hast du am Ende den korrekten Wert 90 in der Variable.
Jetzt stelle dir vor, während die for Schleife läuft kommen drei gewollte Interrupts. Dann bekommst du am Ende nicht die erwarteten 93 sondern 90. Die Änderungen durch den Interrupt wurden während der for Schleife völlig ignoriert.
Volatile verbietet solche Abkürzungen, so dass Unterbrechungen der For Schleife korrekte Ergebnisse erzeugen. (Solche Fehler passieren nicht nur bei for Schleifen.)
b) Die Memory Barrier (also das Sperren von Interrupts) verhindert, dass der Interrupt dazwischen "funkt".
Das ist auch bei Variablen wichtig, die mehr als ein Byte groß sind. Auf diese muss die CPU in mehreren Schritten zugreifen. Wenn sie dabei durch eine ISR unterbrochen wird, und diese ISR ausgerechnet auf die selbe Variable Zugreift, kann Kudeelmuddel entstehen.
Beispiel:
1
inti=50;
2
3
ISR(TIMER0_OVF_vect){
4
i=256;
5
}
6
7
while(1){
8
if(i==0)tueetwas;
9
}
Die CPU holt zuerst die oberen 8 Bit und sieht 0. Dann holt sie die unteren 8 Bit und sieht 50. Die Bedingung ist nicht erfüllt. So weit alles gut. Wenn aber ein Interrupt dazwischen kommt, passiert folgendes:
Die CPU holt zuerst die oberen 8 Bit und sieht 0. Nun macht die ISR i=256. Dann holt die CPU die unteren 8 Bit und sieht 0. Die if-Bedingung ist erfüllt, obwohl i überhaupt nicht 0 ist.
Ich habe mir folgendes eingeprägt:
Variablen die in einer ISR geändert werden und außerhalb der ISR gelesen werden (oder umgekehrt), müssen volatile sein. Zugriffe auf Variablen, die größer als 8 Bit sind, müssen außerdem davor beschützt werden, von dem Interrupt unterbrochen zu werden.
Das kann man mit cli() und sei() machen, je nach µC Modell gibt es aber eventuell elegantere Methoden wo man nur die kritischen Interrupts sperrt anstatt alle.
deine Annahme ist richtig. Zugriff und Schreiben einer 8-bit-Variable sind auf dem Atmega jeweils atomar. Wenn solche Variablen als volatile gekennzeichnet sind, dann kann man sie problemlos zur Kommunikation zwischen ISR-Kontext und main-Kontext verwenden.
Dein Problem liegt nicht an dem Code den du bisher gezeigt hast.
Das löst jetzt Dein Problem wahrscheinlich nicht, ist aber trotzdem ein
sehr beliebter Fehler,
Durch gebetsmühlenartiger Wiederholung wird es auch kein Fehler. Es ist keiner. Erzähl nicht ständig solch einen Unfug.
Es nervt und hat in diesem Fall noch nicht einmal etwas mit dem Problem zu tun.
Also halte bitte die Finger still. Wir wissen schon dass du das gerne mit memory barrier löst. Danke.
Ist es richtig das global variable mit volatile qualifiziert wird.
Ja.
Ist dieses Beispiel korrekt?
Nicht ganz. Die lokale Variable im Atomic Block ist nur dort gültig. Kann man THEORETISCH machen, ist aber praktisch Unsinn. Denn man will ja möglichst schnell die lokalen Kopien erstellen und dann die Interrupts wieder freigeben. Also muss die lokale Variable wenigstens in main definiert werden. Über ATOMIC_FORCEON kann man streiten, meiste wird man eher ATOMIC_RESTORESTATE nutzen.
Du musst das Rad nicht neu erfinden. Die memory barriers stecken in ATOMIC_BLOCK! Und in der ISR ist ATOMIC totaler Unsinn. Zumindest beim AVR, der von Haus aus KEINE verschachtelten Interrupts hat.
Die gibt es sei langem mit ATOMIC_BLOCK(), da ist das garantiert.
Warum sollte man das bei 8bit-Variablen tun? Bei long ist es ja klar.
Weil in dem Macro die Memory barrier drin steckt, ebenso wie bei cli()
und sei(). Volatile allein GARANTIERT das NICHT, auch wenn es meistens
funktioniert. Vor VIELEN Jahren war das selbst mit sei() und cli() NICHT
garantiert, wurde dann aber im avr gcc repariert.
Nirgends ein Hinweis, das Atomic-Makros auch bei 8bit-Variablen nötig sind. Wenn ja, wäre der einfache Schreib- oder Lesezugriff auf die Ports - oder was alles unter "Memory-Mapped" zählt - nicht so einfach.
@ Gerald K: Mit Aussetzern meine ich ein gestörtes Timing der sensorlosen Kommutierung bei erhöhter Temperatur. Leider kommen da vielfältige Ursachen infrage. Einen Beweis, dass die ISR wirklich Aussetzer hat, würde die Zeitmessung mit einem unabhängigen Timer liefern.
b) Die Memory Barrier (also das Sperren von Interrupts) verhindert, dass
der Interrupt dazwischen "funkt".
FALSCH! Eine Memory barrier ist was GANZ ANDERES! Es ist eine Grenze im Programmfluß, an der alle durch Optimierung gepufferten Variablen in den Speicher geschrieben werden müssen.
Dein Problem liegt nicht an dem Code den du bisher gezeigt hast.
Doch, vermutlich. Wenn das der ganze Code ist, dann bekommt er zeitweise vermutlich nur korrupte Telegramme. Weil er weder Startzeichen noch irgendeine Plausibilität prüft.
Dein Problem liegt nicht an dem Code den du bisher gezeigt hast.
Doch, vermutlich. Wenn das der ganze Code ist, dann bekommt er zeitweise
vermutlich nur korrupte Telegramme. Weil er weder Startzeichen noch
irgendeine Plausibilität prüft.
Während des kritschen Motorstarts kommen überhaupt keine Telegramme.
Der Motor muss gerade noch mein Haus heizen, weswegen ich zur Zeit mit dem Fehler leben muss. Ich hab noch ein Austausch-BHKW mit verbesserter Hardware und einer etwas anderen Kommutierungs-ISR. Womöglich löst sich so das Problem in Luft auf. Dank des Ausbremsens der getcmd()-Funktion mit 10ms delay - warum auch immer - kann ich damit gut leben. Perfekt ist aber etwas anderes.
Wenn ich etwas rausgefunden habe, schreibe ich es rein.
Danke nochmal, dass ihr alle auch die Zeit genommen habt!
FALSCH! Eine Memory barrier ist was GANZ ANDERES! Es ist eine Grenze im
Programmfluß, an der alle durch Optimierung gepufferten Variablen in den
Speicher geschrieben werden müssen.
Warum muss die Variable dann trotzdem volatile sein?
FALSCH! Eine Memory barrier ist was GANZ ANDERES! Es ist eine Grenze im
Programmfluß, an der alle durch Optimierung gepufferten Variablen in den
Speicher geschrieben werden müssen.
Warum muss die Variable dann trotzdem volatile sein?
Um Zugriffsoptimierungen und Verschiebungen über die memory barrier zu verhindern. RTFM!
Ich denke der Code ist unspektakulär:
App:char* getcmd(void){
int rval=0;
if (uart0_rx_flag==1)
{uart=UART0;strcpy(cmd,uart0_rx_buffer);uart0_rx_flag=0;rval=1;}
if (rval==1) return cmd; else return NULL;
}
Stell dir vor in "uart0_rx_buffer" steht keine abschliessende 0x00,
was macht dein "strcpy" dann?
eben, scope-Begrenzung. Ist es nicht sinvoll alle globalen Variablen die
in anderen c Dateien nicht benutzt werden mit static zu markieren?
Wozu? Sie sind doch global. Bei mir liegen alle globalen Variablen in einem größeren Projekt in einer eigenen Datei. Einzelne Quelldateien haben keine globalen Variablen.
eben, scope-Begrenzung. Ist es nicht sinvoll alle globalen Variablen die
in anderen c Dateien nicht benutzt werden mit static zu markieren?
Das ist absolut sinnvoll, weil man auf Anhieb sieht, daß die Variable nur in dieser Datei (bzw. in dieser Funktion) verändert wird. Aus demselben Grund macht man ja auch Funktionen static, sofern möglich, und übergibt Pointer-Parameter mit const, sofern möglich.
Wozu? Sie sind doch global. Bei mir liegen alle globalen Variablen in
einem größeren Projekt in einer eigenen Datei.
OMG. Naja, wenn's nicht wesentlich über LED-Blinkies für Hobbyprojekte hinausgeht, wo eh kein anderer Entwickler jemals das Mißvergnügen haben wird, sich in so einen Misthaufen einarbeiten zu müssen, ist es auch egal.
Wozu? Sie sind doch global. Bei mir liegen alle globalen Variablen in
einem größeren Projekt in einer eigenen Datei. Einzelne Quelldateien
haben keine globalen Variablen.
Bei mir gibt es Module, die eigene globale Variablen haben. Auf diese Variablen kann man von außen nicht zugreifen. Auf manche 'Modul Variablen' wo ein Zugriff von außen notwendig ist, stellt das Modul eine lese, bzw. Eine schreibfunktion (ähnliches wie z.B. Get, set bei c#). Auf diese Weise kann man Daten kapseln.
OMG. Naja, wenn's nicht wesentlich über LED-Blinkies für Hobbyprojekte
hinausgeht, wo eh kein anderer Entwickler jemals das Mißvergnügen haben
wird, sich in so einen Misthaufen einarbeiten zu müssen, ist es auch
egal.
Erzähl kein Blech. Wenn gleich ich sicher kein Softwerker bin und auch keine wirklich großen Projekte bearbeite, wage ich zu behaupten, daß diese Methode legitim ist und auch gut funktioniert. Vielleicht nicht bei ganz großen Projekten.
Static zur Scopebegrenzung ist immer sinnvoll. Das weiß der echte Falk
auch
Hab ich noch nie benutzt, bin aber auch kein Softwerker. Ich programmier auch als Profi bestenfalls 10% meiner Zeit, eher weniger. Und dort auch nur kleine Projekte, die praktisch Ein Mann Projekte sind.
Erzähl kein Blech. Wenn gleich ich sicher kein Softwerker bin
Merkt man.
keine wirklich großen Projekte bearbeite, wage ich zu behaupten, daß
diese Methode legitim ist und auch gut funktioniert.
"Funktionieren" ist nicht das Thema. Die mangelhafte Wartbarkeit schon - und das ist bei professioneller Entwicklung immer ein Thema, weil unwartbarer Müll letztlich eine Menge Geld kostet.
Für kleine Einmann-Wegwerfprojekte ist das natürlich egal, wenn Du solche Grütze zusammenpfuscht. Du könntest aber auch lernen, Dich zu verbessern, statt den Mist damit zu rechtfertigen, daß Du es schon immer falsch gemacht hast.
keine wirklich großen Projekte bearbeite, wage ich zu behaupten, daß
diese Methode legitim ist und auch gut funktioniert.
"Funktionieren" ist nicht das Thema. Die mangelhafte Wartbarkeit schon -
und das ist bei professioneller Entwicklung immer ein Thema, weil
unwartbarer Müll letztlich eine Menge Geld kostet.
Was zum Geier ist daran unwartbar? Es ist weder Spaghetticode noch sonstiges Chaos!
Was zum Geier ist daran unwartbar? Es ist weder Spaghetticode noch
sonstiges Chaos!
Manche Leute haben recht strenge Vorstellungen davon, welchen Programmierstil sie als brauchbar akzeptieren. Man könnte es auch "mangelnde Flexibilität" nennen.
Ich habe das Programmieren weitgehend alleine gelernt und kam erst Jahre Später in den Genuss von Teamarbeit. Da wir alle so drauf waren, hatten wir viel über Programmierstil diskutiert und Regelwerke verfasst. Wir fanden das damals ungeheuer wichtig.
Heute beklage ich mich nur noch sehr selten über den Programmierstil anderer. Also nur, wenn es wirklich ganz schlimm und wirklich massiv Zeit gekostet hat. Hätte-Hätte-Fahrradkette Diskussionen bringen hingegen nichts, man schließt sich damit bloß selbst vom Team aus.
Static zur Scopebegrenzung ist immer sinnvoll. Das weiß der echte Falk
auch
Hab ich noch nie benutzt,
Wenn du eine Bibliothek entwickelst und zur Verfügung stellst, dann möchtest du nicht, dass deren dateilokalen privaten globalen Variablen extern sichtbar werden, weil es sonst evtl. Namenskonflikte gibt.
Static zur Scopebegrenzung ist immer sinnvoll. Das weiß der echte Falk
auch
Hab ich noch nie benutzt,
Wenn du eine Bibliothek entwickelst und zur Verfügung stellst, dann
möchtest du nicht, dass deren dateilokalen privaten globalen Variablen
extern sichtbar werden, weil es sonst evtl. Namenskonflikte gibt.
Daß jeder von überall her auf jede globale Variable zugreifen kann, auch wenn das nicht nötig wäre. Das ist kompletter Murks.
Man merkt, daß Du bislang nur Mini-Wegwerfprojekte geschrieben hast - als Softwerker muß man auch bestehende Codebasen anderer übernehmen, und nach dem ersten Mißvergnügen mit so einer Schrott-Codebasis will man sowas nicht nochmal durchmachen müssen.
Es geht nicht um Dich und auch nicht darum, ob der Compiler das frißt. Es geht um die Entwickler nach Dir. Daran zu denken ist eine der elementaren Überlegungen in professioneller Software-Entwicklung.
Gibt es da eigentlich ein empfehlenswertes Buch, welches das Thema "Gute Programmierpraxis" jenseits der Basics sowie die hier diskutierte Themen abhandelt?
Ich hab den Eindruck, dass viele das Rad neu erfinden. Ich mache z.B. extensiven Gebrauch von kleinen gekapselten State-Machines. Liebgewonnen habe ich die durch LabView's "Globale Funktionale Variablen". (Beispiel im Anhang)
Dadurch braucht man kaum noch globale Variablen.
Daß jeder von überall her auf jede globale Variable zugreifen kann, auch
wenn das nicht nötig wäre. Das ist kompletter Murks.
Jaja.
Man merkt, daß Du bislang nur Mini-Wegwerfprojekte geschrieben hast -
Sagt der Names- und gesichtslose NOP.
als Softwerker muß man auch bestehende Codebasen anderer übernehmen, und
nach dem ersten Mißvergnügen mit so einer Schrott-Codebasis will man
sowas nicht nochmal durchmachen müssen.
Es geht nicht um Dich und auch nicht darum, ob der Compiler das frißt.
Es geht um die Entwickler nach Dir. Daran zu denken ist eine der
elementaren Überlegungen in professioneller Software-Entwicklung.
Stimmt. Aber keine Bange, das tu ich, auch wenn da nach mir nicht soo wirklich viel dran rumgeschraubt werden wird. Sind zu 95% nur kleine Ein Mann Projekte, sagte ich schon. Da muss man nicht den ganzen Zirkus der Softwarteentwicklung durchexerzieren. Klein aber fein.
Wer etwas sagt, spielt keine Rolle dafür, ob es stimmt - und daß Du Dich statt des Inhalts jetzt auf sowas konzentrierst, disqualifiziert Dich bloß noch mehr.
Klein aber
... Murks. Und da Du offensichtlich auch nicht lernwillig bist, sondern weiterhin Murks fabrizieren möchtest, ist eine weitere Diskussion bei soviel geballter Ignoranz Deinerseits wohl auch nicht sinnvoll.
Wer etwas sagt, spielt keine Rolle dafür, ob es stimmt -
Stimmt. Aber ich frag ja auch nich den Bäcker, wenn der Wasserhahn tropft.
und daß Du Dich
statt des Inhalts jetzt auf sowas konzentrierst, disqualifiziert Dich
bloß noch mehr.
Uhhhh, ich bin tief getroffen!
Klein aber
... Murks. Und da Du offensichtlich auch nicht lernwillig bist,
Wer sagt, daß du definierst was richtig und falsch ist? Und vor allen in allen Lebenslagen und Anwendungsbreiten? Mach deinem Namen alle Ehe und lass es gut sein, du Oberlehrer.
sondern
weiterhin Murks fabrizieren möchtest, ist eine weitere Diskussion bei
soviel geballter Ignoranz Deinerseits wohl auch nicht sinnvoll.
Und schon wieder erzittere ich in geballter Erfurcht vor dem Inhaber des Steins der Weisen. Schönen Abend noch!
Wenn es nur innerhalb der ISR verwendet wird, dann nicht.
Nein, niemals.
Die volatile-Qualifizierung wird nur für sog. besondere
Speicherzellen benötigt.
Sie wird nie benötigt und ist auch falsch für normale Speicherzellen
im Sinne
des C/C++-Speichermodells: also auch nicht für Variablen mit
nebenläufigem Zugriff.
Alle Howtos und Tutorials, die derartiges behaupten, sind schlicht
falsch.
In C/C++ ist es grundsätzlich UB, wenn nebenläufig (mit zwei oder mehr
Aktivitätsträger oder
auch main() und ISR) auf dieselben Speicherzellen zugegriffen
wird, sofern nicht
atomare Operationen, oder
eine strenge happens-before Beziehung
garantiert wird.
volatile bedeutet nicht atomar!
'volatile' etabliert auch keine strenge happens-before Relation
zwischen konkurrierenden
Zugriffen auf dieselbe Spiecherzelle. Dies kann man nur mit geeigneten
Synchronisationsprimitiven
wie mutex erreichen (zwei oder mehr Aktivitätsträger), oder aber mit
mit anderen ausreichenden
Mitteln wie Interrupt-Sperre zusammen mit einer Memory-Barrier (sowohl
Compiler- als auch
CPU-Memory-Barrier). Das letztere ist dann eine implementation-defined
Variante.
Daher: volatile ist nicht-geeignet und - allein eingesetzt - falsch
für nebenfäufigen Zugriff auf
dieselben Objekte. In C/C++ heisst das dann ein conflict, der wie oben
gelöst werden muss, um nicht
in UB zu enden.
volatile hat die folgenden Eigenschaften:
kein atomarer Zugriff,
Verschiebung einer Lese-Operation einer normalen Speicherzelle bzgl.
volatile ist möglich,
Verschiebung von Operationen auf volatile untereinander ist nicht
möglich.
Damit eignet sich volatile nur für Operationen auf memory-mapped
HW-Registern (und ist auch nur
genau dafür erfunden worden). Denn diese besonderen Speicherzellen
haben folgende Eigenschaften:
sie haben einen Seiteneffekt, und
ihre Werte erscheinen nicht stabil: ein Lesen nach einem Schreiben
muss nicht denselben Wert ergeben
wie auch zwei aufeinander folgende Lese-Operationen nicht denselben Wert
ergeben müssen, und
sie haben eine semantische Abhängigkeit: das Lesen/Schreiben eines
HW-Registers beeinflusst das
Lesen/Schreiben eines anderen HW-Registers.
(Achtung: in anderen Sprachen als C/C++ wie etwa Java hat volatile
eine andere Bedeutung.)
Für den nebenläufigen Zugriff auf dieselben Variablen / Datenstrukturen
bleiben also nur
atomare Datentypen (_Atomic bzw. std::atomic<>), oder
explizite Synchronisation durch:
pthread_mutex_lock()/ pthread_mutex_unlock() oder std::mutex
oder ähnliche, oder
explizites Abschalten der Nebenläufigkeit zusammen mit einer
Memory-Barrier
Für die Kommunikation zwischen ISR und main() bzw. weiteren ISR
benutzt man daher
eine geeignete Interrupt-Sperre (Abschalten der Nebenläufigkeit) und
eine Memory-Barrier (immer
eine Compiler-Barrier und falls nötig eine CPU-Barrier). volatile ist
aus den o.g. Gründen hier
falsch.
Diese ganzen Betrachtungen gelten generell und haben erstmal gar nichts
mit heftigen Optimierungen
eines Compilers zu tun. Aber natürlich werden bestimmte Effekte bei der
Optimierung und damit
der Ausnutzung der Regeln für normale Speicherzellen besonders
sichtbar.
Im übrigen bedeutet _Atomic oder std::atomic<> nicht, dass Operation
nicht optimiert werden:
Auch manche Operationen bzgl. atomarer Datentypen können zusammengefasst
werden wie etwa
aufeinanderfolgende Schreiboperationen. Nur tun das die meisten Compiler
(derzeit) noch nicht.
Das richtige Instrument im Zusammenhang mit Nebenläufigkeit durch ISRs
ist eine memory-barrier.
Bitte korrigiere mich, wenn ich falsch liege....
Aber:
Meines bisherigen Wissens nach, wirkt sich eine MB auf ALLE Variablen
aus, welche gerade in Registern lagern.
Nein. So dumm ist der Compiler nicht.
Eine solche globale MB wie durch das asm-Statement in sei()/cli() bzw. _MemoryBarrier() wirkt sich auf alle Objekte aus, deren Adresse den aktuellen Scope verlassen haben kann. Nennt sich im Jargon der Compiler-Bauer "escape analysis"
Volatile nur auf die "eine".
Sicher. Aber eben dann auf jeden Zugriff und verhindert alle(!) Optimierungen bzgl. dieser Variable, auch die loads und kommt dem Aufruf einer non-inline-Funktion gleich.
Jetzt bin ich verwirrt, der eine sagt : volatile ist völlig falsch an dieser Stelle, der andere sagt : volatile + ATOMIC_BLOCK(?)
Ich finde die Diskussion sehr interessant, auch die andren Threads zu diesem Thema, lass uns bitte dabei sachlich bleiben.
Könntet ihr ein einfaches AVR-GCC Beispiel liefern für :
Schreiben + lesen einer Variable in einer interrupt routine und in main.
-Spielt es eine Rolle ob es ein 8-Bit oder >8-Bit Variable ist?
-Müssen wir an dieser Stelle zwischen den AVR-GCC Versionen unterscheiden?
Es wäre schön wenn wir dort Einigkeit erreichen würden.
Dieses oder ähnliches Konstrukt benutzt nämlichffast jeder von uns.
Ich kann mich einmal errinern (es war gefühlt vor ca. 10 Jahren ) volatile vergessen zu haben. Es hat dan nicht funktioniert. Main hat nicht mitbekommen das ein Interrupt eine Variable verändert hat.
Ansonsten hatte ich nie Probleme mit volatile beobachtet in meinem Projekten (soll nicht heißen das meine Lösung richtig ist und immer funktioniert) . Ich habe bis jetzt immer eine 8-Bit Variable verwendet.
Jetzt bin ich verwirrt, der eine sagt : volatile ist völlig falsch an
dieser Stelle, der andere sagt : volatile + ATOMIC_BLOCK(?)
"Volatile" wird halt den Anfängern gerne hingeworfen, weil es erstmal das Problem fixed. Zwar auf eine unschöne Holzhammer-Methode, aber es tut.
Und ist immer noch besser als die Vorschlaghammer-Methode mit "Stell den Compiler auf -O0".
Ist also eine Bequemlichkeits-Sache auf Seiten der Tipp-Geber und Tutorial-Verfasser.
"Schreib volatile davor" sind drei Worte, eine Diskussion über Sprach-Freiheiten des C-Standards, Memory-Barriers, Compiler-Barriers, Atomic-Blocks usw. füllt viele viele Seiten, wie man hier sieht.
Also, wenn du bei "volatile" bleiben willst, tu das, aber behalte im Hinterkopf dass es eben nicht die "pure, korrekte, schöne" Lösung ist, sondern eben ein quick&dirty Workaround.
Wilhelm, ich bewundere den Evangelismus, mit dem du hier die große Welt der Möglichkeiten zur Kommunikation zwischen nebenläufigen Aktoren predigst, und finde vieles davon auch sehr interessant.
Aber auf einem Atmega ist ein volatile uint8_t eine garantiert sichere Methode, um zwischen ISR und main zu kommunizieren. Da beißt die Maus keinen Faden ab.
Im obigen Beispiel benötigen wir das Abschalten der Nebenläufigkeit in main() wegen des nicht-atomaren Zugriffs auf größere DT als Byte-Typen bei AVR.
Zudem besteht hier ggf. ein semantisches Problem: stellt das gesamte if-statement einen "kritschen Abschnitt" dar? (Typischerweise würde man das natürlich in einer Funktion kapseln). In diesem Beispiel soll das so sein. Daher kann der Optimizer das "v += 1" durch ein "v = 43" ersetzen. Dies ist so absolut korrekt. Fügen wir fälschicherweise ein "volatile" bei der Definition der Variablen "v" ein, so erfolgen sowohl im Bedingungsteil des if() ein Lesen von v und auch danach noch ein RMW Zyklus.
Die sei() und cli() Macros bei AVR beinhalten auch eine Memory-Barrier. Dies ist auch nötig, damit Anweisungen nicht aus dem Inneren des KA heraus gezogen werden können.
Ein Memory-Barrier hat denselbe Effekt wie der Aufruf einer non-inline-Funktion, denn auch dann kann der Compiler nicht mehr davon ausgehen, dass "v" stabil bleibt.
Bei den alten AVR ist in der ISR auch keine weitere Maßnahme mehr nötig. Bei den neueren gibt es nested-interrupts, und ggf. muss man dann auch in den ISRs wieder kritische Abschnitte einfügen.
Schreiben + lesen einer Variable in einer interrupt routine und in main.
s.o.
-Spielt es eine Rolle ob es ein 8-Bit oder >8-Bit Variable ist?
Ja, der Zugriff muss atomar erfolgen. Dies ist bei AVR8 nur für 8-Bit primitiven DT gegeben.
-Müssen wir an dieser Stelle zwischen den AVR-GCC Versionen
unterscheiden?
Nein, das hat weder was mit AVR noch mit gcc zu tun. Es ist ganz normal nebenläufiges Programmieren.
Es wäre schön wenn wir dort Einigkeit erreichen würden.
Dieses oder ähnliches Konstrukt benutzt nämlichffast jeder von uns.
Wie gesagt: die meisten HowTos sind in diesem Punkt einfach schlicht falsch.
Wilhelm, ich bewundere den Evangelismus, mit dem du hier die große Welt
der Möglichkeiten zur Kommunikation zwischen nebenläufigen Aktoren
predigst,
Es ist keine Predigt und ich bin kein Evangelist: es sind schlicht Grundlagen.
Aber auf einem Atmega ist ein volatile uint8_t eine garantiert sichere
Methode, um zwischen ISR und main zu kommunizieren.
Es ist eine Methode, um
garantiert bei einer Modifikation des Codes auf die Nase zu fallen, weil es ein Spezialfall ist.
garantiert den Optimizer deaktiviert, an wir uns doch so schön gewöhnt haben.
Und insofern ist "niemals" schlicht falsch.
Es ist schlicht immer vollkommen unnötigt und niemals dafür vorgesehen gewesen. Daher ist es einfach falsch, auch wenn es unter speziellen Annahmen zufälligerweise funktioniert.
Oben habe ich vergessen zu sagen, dass das Convenience-Macro ATOMC-BLOCK für das C Beispiel dasselbe ist wie die RAII-Style Verriegelung (inkl. MB) im C++ Beispiel.
Meines bisherigen Wissens nach, wirkt sich eine MB auf ALLE Variablen
aus, welche gerade in Registern lagern.
Volatile nur auf die "eine".
Du kannst ja mal einen Vergleich konstruieren.
1
>__asm____volatile__("":::"memory");
2
>
macht die Optimization Barrier auf alles im RAM,
Nein.
Lokale Variablen liegen auch im RAM.
Eine globale MB wirkt sich nur auf die Objekte aus, deren Adresse aus dem lokalen Block, in dem die MB steht, "entweichen" kann. Dies ist natürlich bei globalen Objekten per-definitionem so.
The "memory" clobber tells the compiler that the assembly code performs memory reads or writes to items other than those listed in the input and output operands (for example, accessing the memory pointed to by one of the input parameters). To ensure memory contains correct values, GCC may need to flush specific register values to memory before executing the asm. Further, the compiler does not assume that any values read from memory before an asm remain unchanged after that asm; it reloads them as needed. Using the "memory" clobber effectively forms a read/write memory barrier for the compiler.
auch wenn es unter speziellen
Annahmen zufälligerweise funktioniert
Unter diesen speziellen Annahmen (Atmega, volatile, uint8_t) funktioniert es zufälligerweise immer. Insofern danke für deine Bestätigung dass "Nein, niemals." überzogen war.
Ich gebe die recht, dass "volatile" Optimierung verhindert und zu unnötigen RMWs führt. Wenn man "volatile" so benutzt solle man daher innerhalb und außerhalb von ISRs, falls nötig, möglichst mit Kopien solcher Variablen arbeiten. Das ist natürlich etwas unschön, weil der unerfahrene Leser den Grund dafür eventuell nicht sofort erkennt.
Andererseits hat dieses asmvolatile ("" : "=m"(flag) ) Konstrukt einfach zu viele Unterstriche um schön zu sein ... :)
Eine Frage noch., verstehe ich es richtig das volatile und uint8_t auf
einem Atmega eine legitime Lösung ist?
Abgesehen davon das es eine Holzhamer Methode ist, die nicht optimal
ist.
(mit legitim meine ich das der Datenaustausch zwischen Main und
Interrupt routine immer funktioniert)
Jein.
Du hast nach wie vor RMW-Zyklen, denn ein
1
>volatileuint8_tv;
2
>
3
>voidf(){
4
>v+=1;
5
>}
6
>
ist nicht-atomar, so dass Du ggf. ein lost-update hast.
Ich habe das missverständlich geschrieben!
Gemeint war, das es in der Praxis meistens so funktioniert (mir ist kein Gegenbeispiel bekannt), wobei das lost-update-Problem natürlich immanent ist.
Im Sinne eines language-lawyer muss man natürlich sagen, das auch auf dem simplen AVR ohne eine ISR-Sperre keine happens-before Relation zwischen f() und ISR() gerantiert werden kann. Daher muss man die gemeinsame Variable v als _Atomic (in C), std::atomic (in C++) deklarieren, andernfalls ist es formal undefined-behaviour. Leider sind die internen Hilfsfunktionen für _Atomic im avr-gcc nicht implementiert. In C++ kann man sich natürlich auf Basis der zuvor von mir genannten Randbedingungen leicht eine Klasse/Template std::atomic schreiben.
Oder man setzt ISRs und Signal-Handler gleich (das steht so im C Standard natürlich nicht drin) und stellt fest, dass main() und die ISR() durch denselben(!) Aktivitätsträger ausgeführt werden, dann müsste der Typ sig_atomic_t (ebenfalls nicht für avr realisiert, aber SIG_ATOMIC_TYPE) verwendet werden. Ansonsten ist das Ergebnis unspecified-behaviour.
Gemeint war, das es in der Praxis meistens so funktioniert (mir ist
kein Gegenbeispiel bekannt), wobei das lost-update-Problem natürlich
immanent ist.
Was aber gerade gefährlich ist, denn solche Fehler sind EXTREM schlecht reproduzierbar, treten extrem selten auf und können doch im Extremfall viel Schaden machen! Der Software- und Konzeptfehler des Therac 25 sollte ALLEN Soft- und Hardwerkern ein mahnendes Beispiel sein!
Gemeint war, das es in der Praxis meistens so funktioniert (mir ist
kein Gegenbeispiel bekannt), wobei das lost-update-Problem natürlich
immanent ist.
Was aber gerade gefährlich ist, denn solche Fehler sind EXTREM schlecht
reproduzierbar, treten extrem selten auf und können doch im Extremfall
viel Schaden machen!
Das brauchst Du mir nicht zu sagen!
Er hat gefragt, ob es als Holzhammer funktioniert bei AVR8. Das es nicht korrekt ist, habe ich ja schon zu Hauf geschrieben.
Was aber gerade gefährlich ist, denn solche Fehler sind EXTREM schlecht
reproduzierbar, treten extrem selten auf und können doch im Extremfall
viel Schaden machen!
Das brauchst Du mir nicht zu sagen!
Das war nicht explizit an dich gerichtet, mehr so als allgemeine Feststellung.
Könntet ihr ein einfaches AVR-GCC Beispiel liefern für :
Wie siehst ist das Thema nicht einfach zu erkläüren. Erklärungsversuche gab es genug.
Spielt es eine Rolle ob es ein 8-Bit oder >8-Bit Variable ist?
Ja, auch das wurde bereits erklärt.
Müssen wir an dieser Stelle zwischen den AVR-GCC Versionen unterscheiden?
Nein, wenn wir mal Versionen auslassen, die älter als 23 Jahre sind.
Ich kann mich einmal errinern (es war gefühlt vor ca. 10 Jahren )
volatile vergessen zu haben. Es hat dan nicht funktioniert. Main hat
nicht mitbekommen das ein Interrupt eine Variable verändert hat.
Eben dafür deklariert man sie als volatile. Die Variable ist "unbeständig", der Compiler muss davon ausgehen, dass sie jederzeit verändert wird. Volatile verbietet dem Compiler gewisse Optimierungen, die davon ausgehen, dass sonst niemand anderes auf die Variable Zugreift.
Wenn diese Variable so groß ist, dass die CPU sie nicht in einem Rutsch (atomar) lesen oder schreiben kann, dann musst du Interrupts sperren, damit sie den Zugriff nicht unterbrechen können und dabei den Inhalt verändern.
Stelle dir einen 16 Bit Zähler vor, der gerade den Wert 255 (0x00ff). Die CPU liest zuerst das höherwertige Byte als 0x00. Dann stört ein Interrupt und inkremetiert die Variable auf 256 (0x0100). Nun liest die CPU das niederwertige Byte aus, also 0x00.
Die CPU hat nun den effektiv Wert 0 (0x0000) gelesen, was völlig falsch ist. Richtig wäre entweder 255 (0x00ff) oder 256 (0x0100).
Eben dafür deklariert man sie als volatile. Die Variable ist
"unbeständig", der Compiler muss davon ausgehen, dass sie jederzeit
verändert wird. Volatile verbietet dem Compiler gewisse Optimierungen,
die davon ausgehen, dass sonst niemand anderes auf die Variable
Zugreift.
Mit volatile markiert man 'Daten' als flüchtig, und hindert den compilier an dieser Stelle zu optimieren. Mann sieht z.B. volatile immer in header Dateien wo hardware (ports, etc) an Adressen 'gebunden' wird.
Oder bei c#, dort wird es in Zusammenhang mit multithreading verwendet., das ist an dieser Stelle aber anders als bei C.
Bei C, hat es aber in Zusammenhang mit interrupts nichts zu suchen, zu mindestens so verstehe ich Wilhelm. In seinen Beispielen ist kein volatile zu sehen, dafür benutzt er cli() und sei() welche die MB beinhalten. Verstehe ich es richtig das durch die Verwendung von cli() der compiler gezwungen wird die Variable aus dem sram zu lesen? Es verbietet ihm ein temporere kopie im register zu verwenden.
Anstatt von cli(), sei() kann das Makro ATOMIC_BLOCK() verwendet werden.
Ich glaube und hoffe es verstanden zu haben :-)
Danke allen.
Gruss,
Jan
Bei C, hat es aber in Zusammenhang mit interrupts nichts zu suchen
Falsch. Nochmal wiederhole ich die Erklärung aber nicht. Du kannst diesen Threads ja im laufen der nächsten Monate 20x durchlesen, dann macht es vielleicht "klick".
Bei C, hat es aber in Zusammenhang mit interrupts nichts zu suchen
Falsch. Nochmal wiederhole ich die Erklärung aber nicht. Du kannst
diesen Threads ja im laufen der nächsten Monate 20x durchlesen, dann
macht es vielleicht "klick".
Auch 30 mal durchlesen ändert nichts daran, daß Wilhelm recht damit hat, daß volatile dafür offiziell das falsche Werkzeug ist, und du nur in dem Sinne, daß es im einfachen Fall eines AVRs in der Praxis trotzdem funktioniert.
Auch 30 mal durchlesen ändert nichts daran, daß Wilhelm recht damit hat,
daß volatile dafür offiziell das falsche Werkzeug ist, und du nur in dem
Sinne, daß es im einfachen Fall eines AVRs in der Praxis trotzdem
funktioniert.
Falls es dir nicht aufgefallen ist: Es geht hier um AVR.
Das steht kein "volatile" im Beispiel-Code. Das sieht nur so aus.
Ist trotzdem Murks.
Hinweis für Jan: Die Variable kann hier ohne Interrupt-Sperre gelesen
werden, weil das eine 32 Bit CPU ist.
Mag sein, dass das für den Keil-Compiler gilt: laut C-Standard ist es undefined-behaviour oder maximal unspecified-behaviour (s.a. mein Beitrag von oben).
Diese Lücke des unspecified-behaviour kann der Keil-Compiler natürlich füllen.
Bei C, hat es aber in Zusammenhang mit interrupts nichts zu suchen, zu
mindestens so verstehe ich Wilhelm.
Genau!
In seinen Beispielen ist kein
volatile zu sehen, dafür benutzt er cli() und sei() welche die MB
beinhalten. Verstehe ich es richtig das durch die Verwendung von cli()
der compiler gezwungen wird die Variable aus dem sram zu lesen? Es
verbietet ihm ein temporere kopie im register zu verwenden.
Zumindest nicht über die mit cli()&sei() gezogenen Grenzen hinweg.
Anstatt von cli(), sei() kann das Makro ATOMIC_BLOCK() verwendet werden.
Ja, ist dann wie C++ RAII-Style.
Makroskopisch ist das also richtig ;-) Glückwunsch, Du hast es verstanden.
Daher muss man die
gemeinsame Variable v als _Atomic (in C), std::atomic (in C++)
deklarieren, andernfalls ist es formal undefined-behaviour. Leider sind
die internen Hilfsfunktionen für _Atomic im avr-gcc nicht implementiert.
Was heißt das? Was ist nicht implementiert? Ergeben sich daraus Einschränkungen?
Das steht kein "volatile" im Beispiel-Code. Das sieht nur so aus.
Ich habe mit im Zusammenhang mit der Ankündigung der fast vollständigen Abkündigung ;) von volatile in C++ einige Beispiele angesehen, und dabei mehr als nur gestaunt. Zunächst hatte ich das C++-Standardkomitee ja für völlig durchgeknallt gehalten, aber was da an Code unterwegs ist, in dem volatile zur „Beherrschung“ von Nebenläufigkeit (Multithreading/Multitasking) auch und gerade im PC-Umfeld missbraucht wird, war selbst für mich als Dilettanten erschreckend.
Der Ansatz: „es ist zwar irgendwie nicht richtig, scheint aber heute und jetzt zu funktionieren“ ist niemals gut und schon gar nicht professionell.
Welche
Dreck-Effekte können mit der Temperatur zusammenhängen?
Wenn Du keinen Quarz am µC dran hast (und den auch aktiv als Taktquelle nutzt) kann Dir bei 60°C der interne Takt soweit daneben liegen das UART nicht mehr fehlerfrei funktioniert.
Da kannste dann an der Software schrauben bis Du schwarz wirst.
Oszi müsste das veränderte Timing aber sichtbar machen.
Wie schon gesagt: "volatile" erfüllt nicht die Garantien, die wir für
nebenläufigen Zugriff benötigen.
Es gibt Fälle, wo volatile alleine nicht ausreicht (bspw. bei Objekten,
die keinen lock-freien atomaren Zugriff erlauben), das heißt aber noch
lange nicht, dass volatile grundsätzlich falsch ist.
Ein häufig vorkommendes Muster für das Interrupthandling auf einem
Mikrocontroller sieht folgendermaßen aus:
Die ISR setzt beim Erreichen eines bestimmten Zustands ein Flag. Das
Vordergrundprogramm fragt zwischen anderen Aktivitäten dieses Flag ab
und reagiert entsprechend darauf. Das Flag ist eine 1-Byte-Variable, so
dass darauf ohne weitere Maßnahmen atomar zugegriffen werden kann.
Auch das Programm des TE fällt in dieses Muster, das Flag heißt dort
/uart0_rx_flag/.
Hier ist ein anderes Beispiel, an dem sehr schön die Unterschiede
zwischen volatile und einer Memory Barrier gezeigt werden können:
irqtest.c
1
#include<stdint.h>
2
#include<avr/interrupt.h>
3
4
#if VARIANT == 3
5
volatileuint8_tflag;
6
#else
7
uint8_tflag;
8
#endif
9
10
ISR(USART_RXC_vect){
11
// Setze unter bestimmtmen Bedingungen ein Flag
12
if(UDR=='S')
13
flag=1;
14
}
15
16
uint8_tg0,g1,count;
17
18
voidfunc(void){
19
for(uint8_ti=0;i<20;i++){
20
// Mach etwas Lustiges mit globalen Variablen
21
g1+=g0++;
22
23
// Allgemeine Memory Barrier
24
# if VARIANT == 1
25
__asm____volatile__("":::"memory");
26
# endif
27
28
// Memory Barrier spezifisch für Variable "flag"
29
# if VARIANT == 2
30
__asm____volatile__("":"=m"(flag));
31
# endif
32
33
// Führe jedesmal, wenn das Flag gesetzt wurde, eine Aktion aus
34
if(flag){
35
count++;
36
flag=0;
37
}
38
}
39
}
Es kann durch Setzen des Makros VARIANT auf 1, 2 oder 3 in drei
Varianten übersetzt werden:
Variante 1: Allgemeine Memory Barrier
Variante 2: Memory Barrier spezifisch für das Flag
Variante 3: Flag als volatile deklariert
Alle drei Varianten sorgen dafür, dass das von der ISR geschriebene Flag
in Vordergrundprogramm korrekt gelesen und zurückgesetzt wird.
In der Variante 1 wird /func/ wie folgt übersetzt:
1
func:
2
ldi r24,lo8(20)
3
4
.L5:
5
lds r18,g0 <--
6
ldi r25,lo8(1)
7
add r25,r18
8
sts g0,r25 <--
9
lds r25,g1 <--
10
add r25,r18
11
sts g1,r25 <--
12
lds r25,flag <-
13
tst r25
14
breq .L4
15
lds r25,count <--
16
subi r25,lo8(-(1))
17
sts count,r25 <--
18
sts flag,__zero_reg__ <--
19
.L4:
20
subi r24,lo8(-(-1))
21
cpse r24,__zero_reg__
22
rjmp .L5
23
24
ret
Die Memory Barrier sorgt richtigerweise dafür, dass in jedem Durchlauf
der For-Schleife das Flag erneut gelesen wird. Allerdings werden auch
die Variablen /g0/, /g1/ und /count/ in jedem Durchlauf jeweils einmal
gelesen und beschrieben, obwohl sie überhaupt nichts mit dem Interrupt
zu tun haben.
In Variante 2 wird die Barrier auf die Variable /flag/ beschränkt,
weswegen ich erwartet hätte, dass die Anzahl der Speicherzugriffe für
die anderen Variablen deutlich reduziert wird. Tatsächlich ist das
Kompilat aber exakt das gleiche wie in Variante 1. Speziell innerhalb
von Schleifen scheinen solche eingeschränkten Barriers denselben
(subopotimalen) Effekt wie allgemeine Barriers zu haben.
In Variante 3 werden keine Memory Barriers verwendet, sondern /flag/ als
volatile deklariert. Der Code wird jetzt sehr stark optimiert:
1
func:
2
lds r25,g0
3
lds r24,g1
4
lds r18,count
5
ldi r19,lo8(20)
6
7
.L5:
8
lds r20,flag <--
9
tst r20
10
breq .L4
11
subi r18,lo8(-(1))
12
sts flag,__zero_reg__ <--
13
.L4:
14
subi r19,lo8(-(-1))
15
cpse r19,__zero_reg__
16
rjmp .L5
17
18
sts count,r18
19
ldi r18,lo8(20)
20
mul r25,r18
21
add r24,r0
22
clr __zero_reg__
23
subi r24,lo8(-(-66))
24
sts g1,r24
25
subi r25,lo8(-(20))
26
sts g0,r25
27
ret
Ein großer Teil des Schleifenrumpfs wird jetzt als Invariante nach außen
verlagert, so dass innerhalb der Schleife nur noch zwei Speicherzugriffe
übrig bleiben, nämlich das (zwingend erforderliche) Lesen und Schreiben
von /flag/.
In dem Beispiel behindert die Memory Barrier die Optimierung massivst,
weil sich ihre Wirkung nicht auf die gewünschte Variable beschränkt,
sondern sich auf alle globalen Variablen erstreckt. Natürlich kann man
sie als volatile-Ersatz verwenden, aber ich würde das eher als Hack
sehen, weil sie eigentlich für ganz andere Dinge gedacht ist.
Auch volatile kann die Optimierung behindern, nämlich dann, wenn auf die
entsprechende Variable mehrfach lesend oder schreibend zugegriffen wird
(auch wenn das beim Interrupt-Handling eher selten vorkommt). Wenn man
nicht möchte, dass dabei jedesmal ein Speicherzugriff erfolgt, arbeitet
man einfach mit einer lokalen Kopie der Variable.
Eine andere Möglichkeit besteht darin, die Variable nicht als volatile
zu deklarieren und sie stattdessen nur an den Stellen, wo man den
Speicherzugriff erzwingen möchte, in volatile umzucasten:
1
*(volatileuint8_t*)&flag
Mit diesen beiden Vorgehensweisen kann man sehr feingranular festlegen,
wann Speicherzugriffe erzwungen werden sollen und wann man lieber den
Compiler optimieren lassen möchte.
Bei der Entscheidung, welchen Weg man beim Interrupt-Handling gehen
möchte, ist es das Beste, sich nicht so sehr von Aussagen, die die
Wörter "immer" oder "niemals" enthalten, beeinflussen zu lassen, sondern
sich selber vor Augen zu führen, was die entsprechenden Hilfsmittel wie
volatile, Memory Barriers und die Dinge in stdatomic.h (sofern
verfügbar) genau tun, um darauf basierend eine dem jeweiligen Kontext
bestmöglich angepasste Lösung zu finden.
Das löst jetzt Dein Problem wahrscheinlich nicht, ist aber trotzdem ein
sehr beliebter Fehler,
Durch gebetsmühlenartiger Wiederholung wird es auch kein Fehler. Es ist
keiner.
Volatile funktioniert doch in allen Fällen, wo eine Variable, egal wie breit sie ist, mit einer(!) nicht unterbrechbaren Maschineninstruktion gelesen oder geschrieben werden kann (atomar gelesen/geschrieben werden kann).
Beispiele: Beim AVR 8 Bit, beim ARM Cortex-M3 32 Bit, beim SPARC V8 32 Bit. Auch der Z80 mit 8 Bit :)
Zweite Bedingung, die CPU/Compiler macht kein reordering an den relevanten Stellen, was ich bei diesen "einfachen" Maschinen bisher nicht gesehen habe. Und bei den meisten Fragen hier geht es doch um solche einfachen Maschinen.
Deshalb würde ich es pauschal nicht als Fehler bezeichnen, volatile zu benutzen.
Zugegeben, die Codeoptimierung funktioniert mit volatile schlechter als mit einer Memory Barrier. Aber wenn es in der SW schon so kneift, dass diese schlechtere Codeoptimierung zu einem ernsten Problem führt, dann ist viel früher schon ein Fehler gemacht worden (HW/SW System design).
Ja, es ist eine Holzhammermethode aber ob es falsch ist, hängt von der Architektur ab.
Bei der Entscheidung, welchen Weg man beim Interrupt-Handling gehen
möchte, ist es das Beste, sich nicht so sehr von Aussagen, die die
Wörter "immer" oder "niemals" enthalten, beeinflussen zu lassen, sondern
sich selber vor Augen zu führen, was die entsprechenden Hilfsmittel wie
volatile, Memory Barriers und die Dinge in stdatomic.h (sofern
verfügbar) genau tun, um darauf basierend eine dem jeweiligen Kontext
bestmöglich angepasste Lösung zu finden.
Und sich zusätzlich vielleicht noch den jeweils generierten Assembler-Output anzuschauen. Dann wird man tatsächlich schlau wie dein Beispiel gut zeigt.
Im Sinne eines language-lawyer muss man natürlich sagen, das auch auf
dem simplen AVR ohne eine ISR-Sperre keine happens-before Relation
zwischen f() und ISR() gerantiert werden kann. Daher muss man die
gemeinsame Variable v als _Atomic (in C), std::atomic (in C++)
deklarieren, andernfalls ist es formal undefined-behaviour.
Wilhelm, du behauptest also ernsthaft, dass folgendes AVR-Beispiel ohne eine ISR-Sperre:
1
volatile uint8_t flag = 0, data;
2
ISR(USART_RXC_vect) {
3
if (!flag) {
4
data = UDR;
5
flag = 1;
6
}
7
}
8
void f () {
9
if (flag) {
10
uint8_t mydata = data;
11
flag = 0;
12
process(mydata);
13
}
14
}
laut C Standard undefined-behaviour beinhaltet? Dass also auf die hier gezeigte Weise mit volatile uint8_t flag keine happens-before-relationship zwischen ISR und f() für den Zugriff auf data hergestellt werden kann?
Da würde ich von dem language lawyer jetzt aber gerne mal die Paragraphen des C Standards genannt haben, die die Behauptung des undefined-behaviour für dieses Beispiel belegen ...
Eine andere Möglichkeit besteht darin, die Variable nicht als volatile
zu deklarieren und sie stattdessen nur an den Stellen, wo man den
Speicherzugriff erzwingen möchte, in volatile umzucasten:
*(volatile uint8_t *)&flag
Wie ist das, wenn ich dem Flag einen Wert zuweisen möchte? Linksseitiges Casten geht nicht, oder?
Zugegeben, die Codeoptimierung funktioniert mit volatile schlechter als
mit einer Memory Barrier.
Das muss ich nach dem Beispiel von Yalu
Beitrag "Re: ISR Aussetzer durch Volatile-Variablen-Polling ?"
widerrufen ;)
Das hängt natürlich ganz stark vom Anwendungsfall ab. Ich wollte mit dem
Beispiel nur zeigen, dass Memory Barriers nicht /immer/ die bessere
Lösung sind.
Eine andere Möglichkeit besteht darin, die Variable nicht als volatile
zu deklarieren und sie stattdessen nur an den Stellen, wo man den
Speicherzugriff erzwingen möchte, in volatile umzucasten:
*(volatile uint8_t *)&flag
Wie ist das, wenn ich dem Flag einen Wert zuweisen möchte? Linksseitiges
Casten geht nicht, oder?
Der obige Ausdruck ist ein L-Value, d.h. du kannst ihn links oder rechts
vom Zuweisungsoperator benutzen: