Forum: Mikrocontroller und Digitale Elektronik Ganzes Objekt oder nur einzelne Attribute volatile?


von Keine A. (karabennemsi)


Lesenswert?

Wenn ich ein Objekt über einen Interrupt aufrufe sollte ich lieber das 
ganze Objekt volatile machen oder nur relevante Attribute in der 
Klassendefinition?

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Volatile ist kein magic bullet, dass alle Synchronisations-Probleme 
löst. Die Sprachen C und C++ geben Dir da auch wenig Garantien, die Du 
bräuchtest um sicher zwischen einem Interrupt Kontext und dem main 
thread zu kommunizieren.

Du brauchst die Garantie, dass bestimmte Operationen atomar sind und 
nicht in der Reihenfolge geändert werden. Zumindest für C++ gibt es da 
einen Teil der Standard Library, der das implementieren kann (<atomic>).

In der Praxis bekommst Du das mit `volatile` und Datentypen, die die CPU 
native handhaben kann (z.B. int).

Zu viel `volatile` in Deiner Software nimmt, dem Compiler die 
Möglichkeiten zu optimieren.

Antwort: nur die relevanten Attribute in der Klassen-Deklaration und 
klare Dokumentation, welche Funktionen sicher aus einem ISR Kontext 
aufgerufen werden können.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Möglicherweise ist beides nicht ausreichend. Wenn du aus der main() oder 
einem Thread aus mehrere Member/Attribute nacheinander bearbeitest, und 
zwischendurch ein Interrupt auftritt, sieht dieser ggf. einen 
inkonsistenten Zustand. Wenn der Interrupt selbst von anderen Interrupts 
aus unterbrochen werden kann, kann das ebenso im Interrupt zum Problem 
werden.

Die typische Lösung ist es, für die Dauer des Zugriffs die Interrupts zu 
sperren. Bei vielen Zugriffen ist das sowieso nötig (Read-Modify-Write). 
Wenn du zusätzlich beim Sperren/Entsperren eine Memory Barrier 
einsetzt, erübrigt sich das "volatile" komplett, weil die Barrier das 
schon automatisch impliziert. Beim AVR ist das bei sei()/cli(), beim 
Cortex-M bei __enable_irq()/__disable_irq() schon mit dabei, d.h. es ist 
nichts weiter zu tun. Bei anderen Controllern kann man eine solche 
Barrier beim GCC so ausführen (nach dem Sperren, vor dem Entsperren):
1
__asm__ volatile ("":::"memory");
Dies garantiert dass Zugriffe die davor/danach stattfinden nicht über 
die Barrier hinweg in einen einzelnen Zugriff zusammen gelegt werden.

Falls solche inkonsistenten Zustände nicht auftreten können, weil die 
Member des Objekts einzeln "für sich" stehen und keine 
Read-Modify-Write-Zugriffe erfolgen (also keine Inkrementation o.ä.), 
hast du Glück und "volatile" reicht. Ob das volatile an der Definition 
des Objekts oder der einzelnen Member steht ist im Prinzip egal, 
allerdings kann man ein Objekt, das "volatile" enthält, dann nicht mehr 
gut außerhalb von Interrupts nutzen, weil Zugriffe nicht mehr 
optimierbar sind. Das setzt allerdings voraus, dass die Datentypen "am 
Stück" geschrieben werden können. Beim AVR ist das nur bei 8bit-Typen 
der Fall, bei Cortex-M bei 8,18,32bit-Typen.

Eine alternative Lösung besteht in der Verwendung von atomics für die 
einzelnen Member, was aber nicht immer möglich ist und genaues Abstimmen 
der Zugriffe erfordert. Der Atomic-Mechanismus sorgt implizit dafür, 
dass die Zugriffe nicht wegoptimiert werden. Ein Vorteil ist, dass sich 
der Code dann sehr leicht auf Multithreading-Systeme (statt via 
Interrupts) portieren lässt, wo man ggf. nicht einfach so die 
Interrupts/Kontextwechsel sperren kann. Auch hier muss der Typ nativ "am 
Stück" geschrieben werden können. Siehe dazu auch 
std::atomic_is_lock_free / ATOMIC_xxx_LOCK_FREE, denn ein mittels Mutex 
implementiertes "nicht-natives" Atomic funktioniert natürlich nicht mit 
Interrupts.

: Bearbeitet durch User
von Keks F. (keksliebhaber)


Lesenswert?

Bitte Fehler korrigieren, aber meines Wissens nach hat "volatile" 
nichts, null, mit Multithreading bzw. Synchronization zu tun.
Es geht lediglich darum dem Kompiler mitzuteilen, dass die Variable in 
für den Kompiler evtl. nicht erkennbaren Wegen geändert wird, daher 
Zugriffe über Arbeitsregister vermieden werden sollen.
Der ISR ist als Vektor definiert, für den Kompiler ist der Aufruf bzw. 
dessen Konditionen gar nicht ersichtlich. Es wird lediglich gesagt, dass 
der ISR die Adresse haben muss und die Signatur/Calling Convention. Der 
Prozessor springt automatisch einfach dahin. Es ist ein Hardware- und 
kein Softwareinterrupt.

Fehler sind Raceconditions, ja, dennoch ist es ein anderes Problem, denn 
auch atomare Operationen sind davon nicht ausgeschlossen.

: Bearbeitet durch User
von (prx) A. K. (prx)


Lesenswert?

Keks F. schrieb:
> Bitte Fehler korrigieren, aber meines Wissens nach hat "volatile"
> nichts, null, mit Multithreading bzw. Synchronization zu tun.

Interrupts und präemptives Multithreading sind in ihren Auswirkungen auf 
Variablen eng verwandt.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Keks F. schrieb:
> denn auch atomare Operationen sind davon nicht ausgeschlossen.

Ja, aber atomics machen solche Unterbrechungen sichtbar und ermöglichen 
so, die Operation falls nötig zu wiederholen. Geht natürlich nur wenn 
die Architektur das kann (Cortex-M ja, AVR nein).

von Keks F. (keksliebhaber)


Lesenswert?

(prx) A. K. schrieb:
> Interrupts und präemptives Multithreading sind in ihren Auswirkungen auf
> Variablen eng verwandt.

Ja, und das sage ich auch im weiteren Verlauf.

Niklas G. schrieb:
> Ja, aber atomics machen solche Unterbrechungen sichtbar und ermöglichen
> so, die Operation falls nötig zu wiederholen.

Das sind "aber" zwei verschiedene Dinge.
Eine atomare Operation ist eine Prozessorinstruktion, die innerhalb 
eines interruptfreien Bereiches geschieht.
Ein Atomic ist ein C Softwarekonstrukt und ist eine Ebene höher.

Dennoch hast du Recht in dem Nutzen der Anwendung hier.

: Bearbeitet durch User
von Wilhelm M. (wimalopaan)


Lesenswert?

Die Aspekte Atomarität und Memory-Barrier sind ja schon angesprochen 
worden.

Achtung AVR: Hier haben globale memory-barrier bzw. selektives 
"clobbern" einzelner Variablen einen Fehler im avr-gcc, der andere 
Optimierungen verhindert. Dass habe ich auch hier in einem Thread mal 
beschrieben.

Wenn Du mit volatile und einem Atomaritätskonzept arbeiten willst, danns 
würde ich nicht das Objekt bzw. die Komponenten volatile deklarieren, 
sondern immer nur den Zugriff. Du kannst auch eine all-static Klasse 
schreiben (oder Monostate-Klasse), die die ISR selbst als 
Elementfunktion beinhaltet, die geteilten Objekte private, non-volatile 
enthält und den externen Zugriff über volatile-qualifizierte Referenzen 
public exportiert, d.h. der externe Zugriff (aus Kontext main()) ist 
dann volatile, der Zugriff in der ISR ist non-volatile. Aber Achtung: 
nested-interrupts können weitere Maßnahmen notwendig machen.

Da wir sonst über Deine Struktur nichts wissen, bleiben die Antworten 
leider allgemein.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Keks F. schrieb:
> Eine atomare Operation ist eine Prozessorinstruktion, die innerhalb
> eines interruptfreien Bereiches geschieht.

Also eine gewöhnliche Instruktion wie INC, die zwischen CLI/SEI 
ausgeführt wird (AVR)? Das ist dann aber auch ein Software-Konstrukt. 
Atomare Operationen auf Speicher können nur wenige Prozessoren, weil 
kaum effizient umsetzbar.

Keks F. schrieb:
> Ein Atomic ist ein C Softwarekonstrukt und ist eine Ebene höher.

Also die Atomics aus der Standard-Bibliothek (_Atomic etc) benötigen 
spezielle Hardware-Unterstützung, bei ARM den "monitor".

von Peter D. (peda)


Lesenswert?

Ich nehme da einfach für das Main extra Zugriffsfunktionen, die sich um 
den atomaren Zugriff kümmern. Dann muß sich das Main nicht mehr um 
mögliche Konflikte sorgen.
Ich nehme auch immer die globale Interruptsperre, da sie am kürzesten 
dauert, d.h. die geringsten Seiteneffekte hat. Eine Sperre nur des 
betroffenen Interrupts kann nämlich das Zeitverhalten völlig auf den 
Kopf stellen (Prioritätsinversion).

von (prx) A. K. (prx)


Lesenswert?

Peter D. schrieb:
> Eine Sperre nur des
> betroffenen Interrupts kann nämlich das Zeitverhalten völlig auf den
> Kopf stellen (Prioritätsinversion).

Besser ist deshalb die Sperre aller Interrupts mit gleicher und 
niedrigerer Prio. Bei den Cortex M ist das ausdrücklich vorgesehen 
(BASEPRI/BASEPRI_MAX) und nicht komplizierter als eine totale Sperre mit 
Speicherung des vorigen Zustands.

: Bearbeitet durch User
von Peter D. (peda)


Lesenswert?

(prx) A. K. schrieb:
> Besser ist deshalb die Sperre aller Interrupts mit gleicher und
> niedrigerer Prio.

Klingt recht tricky. Woher weiß das Main, welche Priorität der 
entsprechende Interrupt haben würde?
Man müßte ein define anlegen. Es sind aber auch Abläufe denkbar, wo die 
Priorität eines Interrupts sich ändert.

Das kann auch nicht jede Architektur, z.B. beim 8051 kann man auf die 
interne Prioritätslogik nicht zugreifen.

von Robert G. (robert_g311)


Lesenswert?

Servus,

aber auch einfach ein
1
__cli();
2
... nicht unterbrechbarer Code
3
__sei();

in der main() ist keine Garantie, daß der nicht unterbrechbare Code 
genauso abgearbeitet wird wie erwartet. Compiler optimieren in hohen 
Optimierungsstufen gerne auch an der Reihenfolge der Befehlsabarbeitung, 
und im Assembler kann es Sinngemäß dann so aussehen:
1
__cli();
2
... nicht unterbrechbarer Code - Teil 1
3
__sei();
4
... nicht unterbrechbarer Code - Teil 2

Sicher ist es, den nicht unterbrechbaren code in eine Funktion 
auszulagern, und für diese die Optimierungen im Compiler auszuschalten.

Gruß

Robert

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Robert G. schrieb:
> Compiler optimieren in hohen
> Optimierungsstufen gerne auch an der Reihenfolge der Befehlsabarbeitung,
> und im Assembler kann es Sinngemäß dann so aussehen:

Wie erläutert ist das eben nicht der Fall:

Niklas G. schrieb:
> Wenn du zusätzlich beim Sperren/Entsperren eine Memory Barrier
> einsetzt, erübrigt sich das "volatile" komplett, weil die Barrier das
> schon automatisch impliziert. Beim AVR ist das bei sei()/cli(), beim
> Cortex-M bei __enable_irq()/__disable_irq() schon mit dabei

Der Block zwischen den CLI/SEI Aufrufen kann in sich durchaus 
umsortiert werden. Das sollte aber unerheblich sein, weil er ja nicht 
unterbrochen werden kann, und weil es für ein gewöhnliches Objekt im 
Speicher keine Rolle spielt.

Bei den Zugriffen auf IO-Register spielt die Reihenfolge durchaus eine 
Rolle, aber da diese sowieso schon "volatile" sind, werden die auch nie 
umsortiert/zusammengelegt.

: Bearbeitet durch User
von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Robert G. schrieb:
> Compiler optimieren in hohen
> Optimierungsstufen gerne auch an der Reihenfolge der Befehlsabarbeitung,


Dann ist Dein Compiler kaput. Es gibt bestimmte Signale, die einen 
Compiler davon abhalten, Codereihenfolgen zu ändern. Assembler (bzw. 
intrinsics) sollte in der Regel dazu gehören.

von Niklas G. (erlkoenig) Benutzerseite


Lesenswert?

Torsten R. schrieb:
> Assembler (bzw.
> intrinsics) sollte in der Regel dazu gehören.

Nur wenn der Inline-Assembly-Block einen memory-Clobber enthält, wie 
oben gezeigt (beim GCC).

von (prx) A. K. (prx)


Lesenswert?

Niklas G. schrieb:
> Das sollte aber unerheblich sein, weil er ja nicht
> unterbrochen werden kann, und weil es für ein gewöhnliches Objekt im
> Speicher keine Rolle spielt.

Es kommt vor, dass der Compiler eine teure Operation dort reinschiebt, 
weil der Wert erst dort benötigt wird.

von (prx) A. K. (prx)


Lesenswert?

Peter D. schrieb:
> Klingt recht tricky. Woher weiß das Main, welche Priorität der
> entsprechende Interrupt haben würde?

Eine Frage der Software-Organisation. Wenn man diesen Code im Main dem 
Modul zuordnet, zu dem der Interrupt gehört, ergibt sich das ziemlich 
natürlich.

von Robert G. (robert_g311)


Lesenswert?

Servus,

die folgenden Links streifen das Thema zwar nur etwas, gehören aber in 
den Gesamtkontext:

https://www.iar.com/knowledge/learn/programming/beyond-volatile-how-to-save-days-of-debugging-time/
https://www.iar.com/knowledge/support/technical-notes/compiler/safe-programming-with-ewavr/



Wenn es jemanden Hilft: Gerne geschehen

Wenn es nicht hilft: Nix für Ungut

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.