Wenn ich ein Objekt über einen Interrupt aufrufe sollte ich lieber das ganze Objekt volatile machen oder nur relevante Attribute in der Klassendefinition?
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.
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
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
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.
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).
(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
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.
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".
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).
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
(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.
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
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
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.
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).
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.
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.
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
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.