Hallo, ich habe ein Problem mit Software-Timern: In einem Interrupt incrementiere ich den Zählerstand. Dann gibt es Routinen, die den Zählerstand auslesen und andere Routinen, die den Zählerstand und / oder die Variable für die Referenzzeit verändern. Eine Timeout-Routine vergleicht Zählerstand mit Referenzzeit.' Die Variable ist uint16_t auf einem 32Bit-SAM.Die Kollision ist zwischen Lesen, incrementieren und zurückschreiben. Nach Recherche fand ich den Hinweis man soll die Bibliothek stdatomic verwenden. Da gibt es die atomic-Variablen, denen man aber eine memory_order angeben muss. Nur verstehe ich die Erklärungen überhaupt nicht. Was brauche ich?
https://en.cppreference.com/w/cpp/atomic/atomic Eigentlich reicht es, deine Zählvariable als std::atomic_uint16_t zu deklarieren. Oliver
:
Bearbeitet durch User
Du musst keinen Memory Order angeben. Der Standard ist erstmal std::memory_order_seq_cst.
@Oliver: Danke für die schnelle Antwort. ich programmiere in C unter Visual Studio. In Beipielen finde ich fast immer die atomic Lib. ist die stdatomic vorzuziehen? gibt es in der atomic Lib auch atomic-Variable?
In meiner stdatomic gibt es _Atomic short, _Atomic int, _Atomic _INT_FAST16_TYPE_ Weis jemand, was es damit auf sich hat? Wie ist die richtige Syntax?
Alex schrieb: > In einem Interrupt incrementiere ich den Zählerstand. > Dann gibt es Routinen, die den Zählerstand auslesen und andere Routinen, > die den Zählerstand und / oder die Variable für die Referenzzeit > verändern. > Eine Timeout-Routine vergleicht Zählerstand mit Referenzzeit.' > Die Variable ist uint16_t auf einem 32Bit-SAM.Die Kollision ist zwischen > Lesen, incrementieren und zurückschreiben. Vielleicht schätze ich die Situation falsch ein, aber irgendwie hört sich das für mich nach einem komischen Design an. Bei einem 8-bit System würde ich atomic Zugriffe auf eine uint16 Variable verstehen. Ich habe bei meinen Projekten auf dem SAM lediglich volatile verwendet und würde nie auf die Idee kommen, eine Variable zu verändern, die vom Interrupt inkrementiert wird. Ich würde dann eher eine Offset Variable nutzen die die Differenz zum Systemzähler enthält. Aber vielleicht habe ich da auch zu wenig Einblick in das Problem / den Aufbau.
>> ...und würde nie auf die Idee kommen, eine Variable zu verändern, die vom
Interrupt inkrementiert wird
Ich habe ein Array aus vielen Timern ("Eieruhren"), die mit 10kHz
Inkrementiert werden.
Mit Offset kann man machen, ist halt umständlicher. Und irgendwann muss
man noch Überlauf behandeln...
Deshalb würde ich es so gerade nicht machen. Und atomic ist doch genau
für solche Sachen da.
Alex schrieb: > ich programmiere in C Ok, dann gehts hier weiter: https://en.cppreference.com/w/c/language/atomic Da wärs dann der Datentyp atomic_ushort. Oliver
Alex schrieb: > Die Kollision ist zwischen > Lesen, incrementieren und zurückschreiben. Das würde ich vermeiden wollen. Es sollte immer nur eine Instanz geben, die die eine Variable schreiben darf. Ansonsten mußt Du außerhalb des Interrupts die gesamte Sequenz (Lesen, incrementieren und zurückschreiben) atomar kapseln. Ich rufe meine Softwaretimer in der Mainloop im 1ms Tick auf. Damit ist alles kollisionsfrei. Der Timerinterrupt setzt also nur das 1ms Flag für die Mainloop.
Peter D. schrieb: > Ansonsten mußt Du außerhalb des Interrupts die gesamte Sequenz (Lesen, > incrementieren und zurückschreiben) atomar kapseln. Und genau für diesen Zweck gibt es die atomic-Variablen...
Das Ganze funktioniert übrigens nur ab Cortex-M3, die M0 unterstützen das nicht. Es basiert auf den LDREX/STREX Instruktionen. CLREX wird tatsächlich nicht gebraucht, das machen die Cortex-M automatisch bei Interrupt-Eintritt. Atomics sind etwas fummelig, es ist natürlich nur der eine Zugriff (z.B. atomic_fetch_add()) in sich atomisch, weitere Zugriffe sind immer nur für sich zu betrachten.
>> Das Ganze funktioniert übrigens nur ab Cortex-M3, die M0 unterstützen
das nicht.
Vielen Dank für den Hinweis! Ich habe das gleiche Problem bei einem
SAMD21, der ja ein M0 ist.
Wie kann man das beim M0 und atomic hinbekommen?
Und ich hätte noch die Frage, ob das auch mit Array-Variablen funktioniert? Geht das so? volatile atomic_ushort eggtmr_ch_ticks[HELPER_EGGTIMER_MAX_CH_COUNT]; volatile atomic_ushort eggtmr_ch_times[HELPER_EGGTIMER_MAX_CH_COUNT];
Alex schrieb: > Wie kann man das beim M0 und atomic hinbekommen? Ganz klassisch mit ATOMIC_BLOCK(ATOMIC_RESTORESTATE){}
Alex schrieb: > Wie kann man das beim M0 und atomic hinbekommen? Interrupt disable ...do something... Interrupt enable Alex schrieb: > volatile atomic_ushort eggtmr_ch_times[HELPER_EGGTIMER_MAX_CH_COUNT]; Bin mir nicht sicher ob das bei dem M0 wirklich was bringt, da er ja keine exclusive lade/speicher Befehle hat.
Adam P. schrieb: > Bin mir nicht sicher ob das bei dem M0 wirklich was bringt, da er ja > keine exclusive lade/speicher Befehle hat. Da der Sinn einer Hochsprache für den Anwender u.a. darin besteht, die genaue Funktionalität der unterlagerten Hardware nicht kennen zu müssen, ist das ein Problem der Compilerbauer. Wenn es da gar keine Hardwareunterstützung für die Funktionalität gibt, muß es halt in Software passieren. So in der Art: Adam P. schrieb: > Interrupt disable > ...do something... > Interrupt enable Oliver
Alex schrieb: > ob das auch mit Array-Variablen > funktioniert? Ja, natürlich sind nur die einzelnen Variablen atomic. Also bei so etwas:
1 | volatile atomic_ushort eggtmr_ch_ticks[HELPER_EGGTIMER_MAX_CH_COUNT]; |
2 | |
3 | void myISR (void) { |
4 | assert (eggtmr_ch_ticks [0] == eggtmr_ch_ticks[1]); |
5 | }
|
6 | |
7 | int main () { |
8 | while (1) { |
9 | atomic_fetch_add (& eggtmr_ch_ticks[0], 1); |
10 | atomic_fetch_add (& eggtmr_ch_ticks[1], 1); |
11 | }
|
12 | }
|
Kann es schief gehen, d.h. die ISR kann ganz genau zwischen die beiden Operationen dazwischen funken. Ist ja auch logisch, es steht nirgendwo dass diese Operationen irgendwie verknüpft sein sollen. Oliver S. schrieb: > Da der Sinn einer Hochsprache für den Anwender u.a. darin besteht, die > genaue Funktionalität der unterlagerten Hardware nicht kennen zu müssen, Grundsätzlich ja, aber für Embedded-Systemsoftware auf dem Cortex-M0 gibt es für den Compiler keine vernünftige Möglichkeit dies umzusetzen. Der GCC generiert da ganz lapidar einen Aufruf an __atomic_fetch_add_4 o.ä., zu dem man dann eine undefined reference bekommt. Man könnte das selbst implementieren indem man die Interrupts sperrt, aber man kann nicht alle Exceptions sperren, z.B. den NMI (sagt ja schon der Name). Man müsste dann sicher stellen dass man in den nicht-sperrbaren Exception-Handlern bloß nicht auf atomics zugreift. Adam P. schrieb: > Interrupt disable > ...do something... > Interrupt enable Das ist grundsätzlich auch nicht schlecht, denn für einzelne Integer-Operationen ist das auch ziemlich effizient. Wenn der Interrupt erst 5 Takte später kommen kann ist das nicht unbedingt so tragisch. Etwas besser begreiflich ist es so allemal. Sobald aber ein RTOS im Spiel ist sollte man dann vermutlich doch besser atomics oder mutexe nutzen. Peter D. schrieb: > Ganz klassisch mit ATOMIC_BLOCK(ATOMIC_RESTORESTATE){} Das stammt aus der AVR-Libc und bei den üblich ARM-Umbegungen gibt es kein Äquivalent. Für die ARMs wurde das ganze Thema hier schon ausführlich diskutiert: Beitrag "atomic-lib für stm32" Der letzte Vorschlag dort, um ein ATOMIC_BLOCK für Cortex-M umzusetzen, ist schon sehr schön: Beitrag "Re: atomic-lib für stm32" Ich würde den noch ein kleines bisschen aufpolieren:
1 | #define ATOMIC_BLOCK(type) for( uint32_t __prim = (type) == 0 ? 0 : __get_PRIMASK(), \
|
2 | __cond = (__disable_irq(),1); __cond != 0; \
|
3 | (type) == 0 ? (__enable_irq(),0):(__set_PRIMASK(__prim),0),__cond=0)
|
4 | |
5 | |
6 | #define ATOMIC_FORCEON 0
|
7 | #define ATOMIC_RESTORESTATE 1
|
Dadurch erspart man sich den bedingten Sprung am Ende. Der Compiler erzeugt dann aus
1 | void testForceOn (void) { |
2 | ATOMIC_BLOCK (ATOMIC_FORCEON) { |
3 | __NOP (); |
4 | }
|
5 | }
|
6 | |
7 | void testRestore (void) { |
8 | ATOMIC_BLOCK (ATOMIC_RESTORESTATE) { |
9 | __NOP (); |
10 | }
|
11 | }
|
diesen Assemblercode:
1 | 00000014 <testForceOn>: |
2 | 14: b672 cpsid i |
3 | 16: 46c0 nop @ (mov r8, r8) |
4 | 18: b662 cpsie i |
5 | 1a: 4770 bx lr |
6 | |
7 | 0000001c <testRestore>: |
8 | 1c: f3ef 8310 mrs r3, PRIMASK |
9 | 20: b672 cpsid i |
10 | 22: 46c0 nop @ (mov r8, r8) |
11 | 24: f383 8810 msr PRIMASK, r3 |
12 | 28: 4770 bx lr |
Sowohl für Cortex-M0 als auch -M4 (bis auf das NOP...).
Niklas G. schrieb: > Sobald aber ein RTOS im > Spiel ist sollte man dann vermutlich doch besser atomics oder mutexe > nutzen. Du hast die Wahl zwischen zyklengenauen Interrupts, oder atomaren Variablenzugriff. Beides zusammen geht halt nicht. Weder vermutlich noch vielleicht. Sinnvoll ist es natürlich, bei einem RTOS die dafür vorgesehenen Mechanismen zu nutzen. Ändern tut das an der Tatsachen aber auch nichts. Oliver
:
Bearbeitet durch User
Oliver S. schrieb: > Beides zusammen geht halt nicht. Bei Cortex-M0 (ARMv6M) nicht, genau. Beim Cortex-M3/4/7 (ARMv7M) schon, aber halt nur bei einzelnen 8/16/32-Bit-Variablen, wobei allerdings der (unterbrechbare) Variablenzugriff selbst etwas langsamer wird, die Interruptlatenz aber nicht. Keine Arme, keine Kekse 😉🍪 Just for fun: Aus
1 | atomic_fetch_add (&acnt, 42); |
wird auf dem Cortex-M4 dann
1 | 2: f3bf 8f5b dmb ish |
2 | 6: e853 1f00 ldrex r1, [r3] |
3 | a: 312a adds r1, #42 @ 0x2a |
4 | c: e843 1200 strex r2, r1, [r3] |
5 | 10: 2a00 cmp r2, #0 |
6 | 12: d1f8 bne.n 6 <test+0x6> |
7 | 14: f3bf 8f5b dmb ish |
Das CMP und BNE brauchen auch im "Gut"-Fall (kein Interrupt dazwischen gekommen) 2-3 zusätzliche Takte. Die beiden DMB-Instruktionen können mehrere Takte brauchen, ARM sagt dass man sie auf Single-Core Systemen ohne Cache (also Cortex-M3/4) weglassen könnte, das weiß der GCC nur anscheinend nicht.
:
Bearbeitet durch User
Zuallererst sollte die Variable, die in der ISR inkrementiert wird, "volatile" qualifiziert werden. Dann ist für den Compiler klar, dass sich die Variable jederzeit ändern kann und er kommt nicht auf die Idee Optimierungen durchzuführen, die zu beschädigten Daten führen. "Volatile" garantiert aber keinen atomaren Zugriff. Ob der Zugriff atomar ist, hängt von einigen Randbedingungen ab. Am besten schaut man ins Disassembly. Wenn für das Lesen mehr als eine Instruction benötigt wird, ist der Zugriff nicht atomar. Um daher auf Nummer sicher zu gehen, sollten Interrupts vor dem Zugriff auf die Variable deaktiviert und nach dem Zugriff wieder zu aktiviert werden.
Peter D. schrieb: > Das würde ich vermeiden wollen. Es sollte immer nur eine Instanz geben, > die die eine Variable schreiben darf. > Ansonsten mußt Du außerhalb des Interrupts die gesamte Sequenz (Lesen, > incrementieren und zurückschreiben) atomar kapseln. Nicht unbedingt. Es gibt verschiedene Möglichkeiten wie man sowas umsetzen kann. Es kommt ganz auf die Zugriffsmuster an. Es gibt ja auch nicht nur Read-Modify-Write sondern auch nur "Write". Manchmal kann man z.b. es auch vom Wert abhängig machen, ala Zähler > 0 -> Wird im Interrupt runtergezählt. Zähler = 0 -> Userspace darf neuen Wert setzen. In anderen Fällen kann man den Zugriff aus dem Userspace auch solange wiederholen bis der gewünschte Wert drinnen steht (z.B. bei Timern) also ein Read-Modify gefolgt von Schleife aus Write-Read solange bis Read das gleiche wie Write ergibt. Da spart man sich Interruptlocks und macht sich somit die Interruptlatenz nicht kaputt. So werden oft Spinlocks implementiert.
P. S. schrieb: > Zuallererst sollte die Variable, die in der ISR inkrementiert wird, > "volatile" qualifiziert werden. Das ist bei Verwendung der "atomic" Typen unnötig. P. S. schrieb: > Dann ist für den Compiler klar, dass > sich die Variable jederzeit ändern kann und er kommt nicht auf die Idee > Optimierungen durchzuführen, die zu beschädigten Daten führen. Das weiß er bei Atomics auch, sofern man die richtige Memory-Order angibt, was standardmäßig der Fall ist. Sonst würde das auf großen Prozessoren überhaupt nicht funktionieren, und da braucht man auch kein volatile. P. S. schrieb: > "Volatile" garantiert aber keinen atomaren Zugriff. Ob der Zugriff > atomar ist, hängt von einigen Randbedingungen ab. Bei volatile alleine ist da gar nichts garantiert. Daher benutzt man atomics. P. S. schrieb: > Am besten schaut man > ins Disassembly. Das hilft aber nur bis zu dem Moment wo man neu kompiliert, und das möchte man sicherlich nicht jedes Mal prüfen. Besser man schaut in das Architecture Reference Manual welche Garantien es gibt, und in den Sprachstandard, wie man diese nutzt. Eben mit Atomics. P. S. schrieb: > Um daher auf Nummer sicher zu gehen, sollten Interrupts vor dem Zugriff > auf die Variable deaktiviert Nur auf Architekturen welche keine Atomics bieten, wie Cortex-M0. Andreas M. schrieb: > Read-Modify gefolgt von Schleife aus Write-Read solange bis Read das > gleiche wie Write ergibt. Da spart man sich Interruptlocks und macht > sich somit die Interruptlatenz nicht kaputt. So werden oft Spinlocks > implementiert. Atomics implementieren genau das, da ist automatisch eine Schleife drin.
Hallo an Alle, vielen Dank für eure ausführliche Diskussion zum Thema. Es gibt viele Wege das Problem zu lösen. Es kommt ganz auf die Erfordernisse an, welche Lösung man benutzen muss. Bei mir ist ein Timerjitter tolerierbar, solange der Timer nicht falsch geht. @Andreas M. Ja tatsächlich, die Technik mit dem Probelesen hatte ich auch schon umgesetzt. Ich habe es jetzt so umgesetzt, dass ich vor der Modifikation der Zählvariablen den Increment-Interrupt deaktiviere und danach wieder einschalte. Diese Routinen sind bei mir nicht sehr zeitkritisch. Aber ich werde auch die aufpolierte Version mal ausprobieren.
Andreas M. schrieb: > Manchmal kann man z.b. es auch vom Wert abhängig machen, > ala Zähler > 0 -> Wird im Interrupt runtergezählt. > Zähler = 0 -> Userspace darf neuen Wert setzen. Wie garantiert man hier dann, daß beim nicht atomaren Neusetzen das niederwertigste Byte (Word) als erstes geschrieben wird? Denn andernfalls könnte nach dem schreiben des höherwertigen Bytes (Word) der Interrupt dazwischen grätschen, mit runterzählen beginnen und das anschließende neu schreiben des niederwertigen Bytes macht daraus u.U. ein decrement um 256 statt um 1.
Michi S. schrieb: > Wie garantiert man hier dann, daß beim nicht atomaren Neusetzen das > niederwertigste Byte (Word) als erstes geschrieben wird? Der ARM liest bzw. schreibt Halfwords (16bit) und Words (32bit) immer als ganzes atomisch. Das ist von der Architektur garantiert. Ist auch logisch für einen 32bit-Prozessor. Problematisch ist das Read-Modify-Write, bei diesen 3 Schritten kann ein Interrupt dazwischen kommen.
Niklas G. schrieb: > Michi S. schrieb: >> Wie garantiert man hier dann, daß beim nicht atomaren Neusetzen das >> niederwertigste Byte (Word) als erstes geschrieben wird? > > Der ARM liest bzw. schreibt Halfwords (16bit) und Words (32bit) immer > als ganzes atomisch. Das ist von der Architektur garantiert. Ist auch > logisch für einen 32bit-Prozessor. Problematisch ist das > Read-Modify-Write, bei diesen 3 Schritten kann ein Interrupt dazwischen > kommen. Gilt das auch bei non-aligned access?
Uwe B. schrieb: > Gilt das auch bei non-aligned access? Nö. Unaligned Zugriffe sind bei ARMv6M (Cortex-M0) gar nicht möglich, bei ARMv7M (Cortex-M3 und höher) sind sie unterstützt (können per Software abgeschaltet werden), sind aber langsam. Der Prozessor setzt sie als 2 einzelne Zugriffe um, und damit sind sie nicht atomar, d.h. ein Interrupt kann dazwischen funken. Das gilt so für einzelne Lese/Schreibzugriffe. Versucht man einen exklusiven Zugriff (also für eine read-modify-write Operation, nicht nur einzelner Zugriff) auf unaligned Daten zu machen mittels der LDREX/STREX-Instruktionen (also über die "atomic" APIs in C/C++), stürzt der Controller ab ("alignment fault"). Eine unaligned atomic Variable in C/C++ anzulegen ist aber eigentlich sowieso nicht möglich/erlaubt. Eigentlich sind in C/C++ alle unaligned Zugriffe nicht erlaubt (undefined behavior), egal ob "atomic" oder nicht. Ich vermeide sie jedenfalls. Der Compiler darf davon ausgehen dass undefined behavior niemals auftritt, und könnte Codeteile die so etwas machen komplett wegoptimieren oder sonstwie verunstalten.
Niklas G. schrieb: > sind sie nicht atomar, d.h. > ein Interrupt kann dazwischen funken. Das gilt so für einzelne > Lese/Schreibzugriffe. DMA und 2. Core ja, aber wirklich auch Interrupts? Eine Unterbrechung innerhalb eines Befehls mit Fortsetzung gibt es bei ARM bei skalaren Befehlen nicht. Das gab es m.W. nur bei 68K. > Eigentlich sind in C/C++ alle unaligned Zugriffe nicht erlaubt > (undefined behavior), Per C/C++ allein ist das erlaubt. Und wenn Ereignisse wie Interrupts oder DMA reinspuken, interessiert das die Sprachdefinition nicht.
:
Bearbeitet durch User
(prx) A. K. schrieb: > Eine Unterbrechung innerhalb eines Befehls mit Fortsetzung gibt es bei > ARM bei skalaren Befehlen nicht Load/Store auf mehrere Adressen ist halt nicht skalar, daraus werden 2 Einzelzugriffe. (prx) A. K. schrieb: > Per C/C++ allein ist das erlaubt Nö. Dies verletzt die aliasing rules.
Michi S. schrieb: > Wie garantiert man hier dann, daß beim nicht atomaren Neusetzen das > niederwertigste Byte (Word) als erstes geschrieben wird? Denn > andernfalls könnte nach dem schreiben des höherwertigen Bytes (Word) der > Interrupt dazwischen grätschen, mit runterzählen beginnen und das > anschließende neu schreiben des niederwertigen Bytes macht daraus u.U. > ein decrement um 256 statt um 1 Ja, vor allem bei 8-Bittern muss man dass im Hinterkopf haben. Da klappt das nur mit 8-Bit Variablen. Generell sollte man bei solchen Konstrukten die Architektur im Hinterkopf behalten. Bei 32 Bit CPUs kann man davon ausgehen, das Einzelzugriffe auf "normale" 8/16/32 Bit Variablen atomar sind. (Unaligned sei hier mal ausgeschlossen) Aber auch auf 8 Bit CPUs kann man sowas anwenden, ja man kann im Prinzip sogar ganze Strukturen gegen Inkonsistente Zugriffe absichern indem man "Guards" einsetzt. D.h. es gibt nicht eine Variable, sondern zwei. (typischerweise als volatile gekennzeichnet) Der Userspace schreibt beide mit dem selben Wert, die ISR liest beide und tut nichts, wenn sie sich unterscheiden, oder dekrementiert halt beide. Und so funktioniert das auch mit Strukturen: Erste Variable verändern, Struktur aktualisieren, zweite Variable gleichziehen.
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.