Hallo zusammen,
angenommen man habe einen µC auf dem kein OS läuft. Es gibt ein
Hauptprogramm welches Berechnungen vornimmt und in bestimmten Variablen
abspeichert. Das Programm ist durch Interrupt unterbrechbar. Im
Interrupt werden die Variablen gelesen. Auch der Interrupt kann
Berechnungen vornehmen, die im Hauptprogramm gelesen werden. Weiterhin
nehme man an es gibt keinerlei Synchronisation zwischen Hauptprogramm
und Interrupt(Semaphore/Mutexe...) Was ist unter diesen Voraussetzungen
das schlimmste was passieren kann?
Gruß
Kevin
Dazu setzt man sogenannte Flags die anzeigen ob Berechnungen im Gange
sind oder abgeschlossen sind.
Sonnst :
Kevin schrieb:> Was ist unter diesen Voraussetzungen> das schlimmste was passieren kann?
Absolutes Chaos und undefinierte Ergebnisse ....;-)
Kevin schrieb:> Hallo zusammen,>> angenommen man habe einen µC auf dem kein OS läuft. Es gibt ein> Hauptprogramm welches Berechnungen vornimmt und in bestimmten Variablen> abspeichert. Das Programm ist durch Interrupt unterbrechbar. Im> Interrupt werden die Variablen gelesen. Auch der Interrupt kann> Berechnungen vornehmen, die im Hauptprogramm gelesen werden. Weiterhin> nehme man an es gibt keinerlei Synchronisation zwischen Hauptprogramm> und Interrupt(Semaphore/Mutexe...) Was ist unter diesen Voraussetzungen> das schlimmste was passieren kann?
Bei Assemblerprogrammierung einfach inkonsistente Daten.
Bei C/C++ bekommst Du UB noch dazu (was in diesem Fall aber mehr ein
weiterer formaler Aspekt ist).
Da Du ja scheinbar schon etwas von der Thematik verstanden hast, ist mir
der Hintergrund der Frage nicht ganz klar ...
Patrick L. schrieb:> Absolutes Chaos und undefinierte Ergebnisse ....;-)
Chaos klingt gut. Was ist mit undefiniert gemeint? Was wenn nur jeweils
lesend der Zugriff erfolgt? Also nicht auf die gelesene Variable
zurückgeschrieben wird. Was wäre hier der Worst Case?
Kevin schrieb:> Was ist unter diesen Voraussetzungen> das schlimmste was passieren kann?
Nichts. Außer, das Programm ist fehlerhaft, dann gibts Chaos. Das gilt
aber für alle fehlerhaften Programme.
Du musst halt selber dafür sorgen, daß sich ISR und Hauptprogramm nicht
gegenseitig verheddern. Die Stichworte dazu lauten volatile und atomic.
Oliver
Wilhelm M. schrieb:> Da Du ja scheinbar schon etwas von der Thematik verstanden hast, ist mir> der Hintergrund der Frage nicht ganz klar ...
Ich möchte verstehen unter welchen Bedingungen man eine Synchronisation
braucht und wann nicht, ohne jetzt ins Detail zu gehen wie z.b. so eine
Synchronisation aussehen muss.
Kevin schrieb:> Wilhelm M. schrieb:>> Da Du ja scheinbar schon etwas von der Thematik verstanden hast, ist mir>> der Hintergrund der Frage nicht ganz klar ...>> Ich möchte verstehen unter welchen Bedingungen man eine Synchronisation> braucht und wann nicht, ohne jetzt ins Detail zu gehen wie z.b. so eine> Synchronisation aussehen muss.
Eine Synchronisation brauchst Du immer bei nebenläufigem Zugriff, wenn
mindestens ein Schreiber dabei ist.
Je nach eingesetzter Sprache kann das dann unterschiedlich aussehen. Bei
C/C++ wurde schon volatile als "Optimierungssperre" genannt. Das hat
aber nichts mit Synchronisation zu tun. Weiterhin wird noch
wechselseitiger Ausschluss benötigt. Das kannst Du durch atomics oder
expliziter Syncho durch bspw. Mutexe erreichen.
Kevin schrieb:> Wilhelm M. schrieb:>> wechselseitiger Ausschluss>> benötigt man bei mehr als einen Schreiber, richtig?
Steht doch oben: mindestens einem Schreiber
Kevin schrieb:> Ich möchte verstehen unter welchen Bedingungen man eine Synchronisation> braucht und wann nicht,
Immer wenn auf die Daten, aus dem Hautprogramm und einer ISR zugegriffen
wird.
Das mag sich in einigen (seltenen) Fällen, von ganz alleine ergeben,
aber deine Aufmerksamkeit wird hier immer benötigt. Das ganze also mal
in ruhe, im Kopf durchspielen.
Kevin schrieb:> Was ist unter diesen Voraussetzungen> das schlimmste was passieren kann?
Dass du kaputte Zahlen verarbeitest.
Denn bei einem 32 Bit Integer würde ein (8 Bit) Mikrocontroller ganze 4
Scheibzugriffe nacheinander im RAM machen. Wenn du dazwischen liest,
bekommst du ein paar alte mit ein paar neuen Bits vermischt.
Der Interrupt kann über ein Flag signalisieren, daß neue Daten verfügbar
sind. Das Main muß sie dann atomar lesen, damit sie konsistent bleiben.
Sinnvoll legt man dazu ein Struct an.
Servus,
kritisch sind stets Schreib- und Lesezugriffe, welche mehr als einen
Assemblerbefehl benötigen.
Folgendes Szenario kann das vielleicht beschreiben:
Im Hauptprogramm wird eine 16-Bit Variable beschrieben (angenommen das
teilt sich 2 Assemblerbefehle auf, High- und Low-Byte schreiben) und
diese wird im Interrupt ausgelesen und "verarbeitet".
Es könnte folgendes passieren:
1. High-Byte wird geschrieben (Hauptprogramm)
2. Interrupt kommt
2.a. High-Byte (neu) wird im Interrupt gelesen
2.b. Low-Byte (alt) wird im Interrupt gelesen
2.c. Eine Mischung aus alt und neu wird verarbeitet - Möglicherweise
Unfug
2.d. Interrupt beendet
3. Low-Byte wird geschrieben (Hauptprogramm)
Das gemeine ist, daß dieser Fehler wahrscheinlich nur ab und zu auftritt
und dann auch nicht immer zu einem Fehler führt.
Die Lösung ist, sich das ganze sehr genau durchdenken. Allgemeine
Lösungen wie "immer volatile davorschreiben" helfen da nur sehr bedingt.
Es kommt immer auf den konkreten Fall und Architektur an.
Viel Erfolg
In dem Zusammenhang haben wir das oben genannte Volatile. Es verhindert
aber nicht Konflikte bei gleichzeitigen Zugriff auf Mehr-byte Variablen.
Da geht es um was anderes:
Man (auch der C Compiler) benutzt CPU Register so oft wie möglich, um
langsamere RAM-Zugriffe zu vermeiden. Beispiel:
1
intzaehler;
2
3
voidmachwas_hundert_mal()
4
{
5
for(i:=0;i<100;i++)
6
{
7
mach_was();
8
zaehler=zaehler+1;
9
}
10
}
Hier wird die Variable "zaehler" im RAM nicht hundert mal geändert,
sondern nur einmal nach dem Ende der Schleife. Sie wird auch nicht
hundert mal gelesen.
Wenn du jetzt den "zaehler" in einem anderen Thread (Interrupt) ausliest
während die for-Schleife läuft, bekommst du einen veralteten Wert. Wenn
du den Zähler im anderen Thread änderst, geht diese Änderung am Ende der
for-Schleife verloren, sie wird einfach überschrieben!
Volatile verhindert das.
1
volatileintzaehler;
Nun wird die Variable innerhalb der for-Schleife tatsächlich immer
wieder aus dem RAM gelesen, erhöht und wieder zurück ins RAM
geschrieben. Die Schleife läuft nun deutlich langsamer.
Dein "Problem" aus der Eingangsfrage ist übrigens weder Mikrocontroller
spezifisch noch hängt es von der Programmiersprache ab. Jedes PC
Programm ist ebenso betroffen.
Steve schrieb:> Kevin schrieb:>> Was ist unter diesen Voraussetzungen>> das schlimmste was passieren kann?>> Dass du kaputte Zahlen verarbeitest.>> Denn bei einem 32 Bit Integer würde ein (8 Bit) Mikrocontroller ganze 4> Scheibzugriffe nacheinander im RAM machen. Wenn du dazwischen liest,> bekommst du ein paar alte mit ein paar neuen Bits vermischt.
Das ist interessant. Daran hatte ich nicht gedacht. Kann man annehmen,
das auf einem 32Bit Controller der Schreibzugriff auf eine (8/16/32 Bit)
Variablen immer atomar erfolgt. Oder kann der Interrupt auch genau im
Schreiben erfolgen?
der_eine schrieb:> kritisch sind stets Schreib- und Lesezugriffe, welche mehr als einen> Assemblerbefehl benötigen.
Danke, beantwortet meine Frage siehe oben, schon ziemlich gut.
Kevin schrieb:> Kann man annehmen, das auf einem 32Bit Controller der Schreibzugriff> auf eine (8/16/32 Bit) Variablen immer atomar erfolgt.
Meistens, aber nicht immer. Denn die meisten 32 Bit CPUs können auch in
kleineren Einheiten auf das RAM zugreifen. Ich bin nicht sicher, ob man
sich darauf verlassen kann, dass 32 Bit Variablen immer am Stück gelesen
und geschrieben werden. Hängt sicher auch vom Compiler und dessen
Optionen ab, nicht nur von der CPU.
Peter D. schrieb:> Benutzt man das ATOMIC_BLOCK Macro wird der Zugriff auch nicht> wegoptimiert, d.h. volatile mit seinen Nachteilen ist unnötig.
Schaltet nur die Interrupts bei AVR ab. Trotzdem brauchst Du volatile.
Wilhelm M. schrieb:> Schaltet nur die Interrupts bei AVR ab.
nicht nur. ATOMIC_BLOCK enthält auch ein
_asm_ volatile ("" ::: "memory");
also eine memory barrier. d.H. alle Variablen, die im ATOMIC_BLOCK
geändert werden, sind vor dem Re-Aktivieren der ISRs auch wirklich
geschrieben.
Steve schrieb:> Kevin schrieb:>> Kann man annehmen, das auf einem 32Bit Controller der Schreibzugriff>> auf eine (8/16/32 Bit) Variablen immer atomar erfolgt.>> Meistens, aber nicht immer. Denn die meisten 32 Bit CPUs können auch in> kleineren Einheiten auf das RAM zugreifen. Ich bin nicht sicher, ob man> sich darauf verlassen kann, dass 32 Bit Variablen immer am Stück gelesen> und geschrieben werden. Hängt sicher auch vom Compiler und dessen> Optionen ab, nicht nur von der CPU.
Probleme kann es bei Misaligmment geben, in gepackten Strukturen und in
Bitfeldern.
Danke schonmal an alle.
Ich habe mal nachgelesen.FreeRtos z.b., nutzt zur Synchronisation
zwischen 2 Tasks Mutexe, jedoch zwischen Task und Interrupt Semaphore.
Vielleicht kann man sich da was abgucken, wenn es nicht zu komplex
implementiert ist.
Oje, nu kloppen sich die Spezis die Köppe ein. Dabei will der TO doch
nur Fahrradfahren lernen und nicht gleich mit nem Euro-Fighter, auf
seine Spatzen losgehen....
Kevin schrieb:> (prx) A. K. schrieb:>> Probleme kann es bei Misaligmment geben, in gepackten Strukturen und in>> Bitfeldern.>> Was meinst du mit Misalignment?
Wenn die 4 Bytes nicht an einer Adresse %4==0 liegen, die Maschine einen
solchen Zugriff nicht zulässt und der Compiler deshalb daraus mehrere
Zugriffe machen muss.
Kevin schrieb:> Danke schonmal an alle.>> Ich habe mal nachgelesen.FreeRtos z.b., nutzt zur Synchronisation> zwischen 2 Tasks Mutexe, jedoch zwischen Task und Interrupt Semaphore.> Vielleicht kann man sich da was abgucken, wenn es nicht zu komplex> implementiert ist.
Jetzt doch mit OS?
Wenn Du nicht in Assembler programmierst (also etwa C/C++), dann weißt
Du nicht, wie ein Objektzugriff gemacht wird. Du musst also vom
Schlimmsten ausgehen, und das ist dann eine RMW-Folge. Also etwas
nicht-atomares.
Daher hast Du zwei Aufgaben:
1) Du musst Atomarität herstellen durch
1a) atomics (s.a. C/C++-Bibliothek), oder
1b) explizite kritische Bereiche mit wechselseitigem Ausschluss
und
2) Du musst den Compiler für die gemeinsam genutzten Datenstrukten am
Optimieren hindern. Das macht man bei C/C++ durch volatile-Deklaration.
Bei den kritichen Bereichen musst Du noch zwischen bedingten und
unbedingten KB unterscheiden. Für bedingte KB brauchst Du dann noch
Bedingungsvariablen (ich meiner hier das Konzept, keine einfachen Flags
oder so).
Teo D. schrieb:> Oje, nu kloppen sich die Spezis die Köppe ein. Dabei will der TO doch> nur Fahrradfahren lernen und nicht gleich mit nem Euro-Fighter, auf> seine Spatzen losgehen....
Seine Fragen sind qualifiziert genug um ihn nicht als Spielkind zu
behandeln.
Wilhelm M. schrieb:> Jetzt doch mit OS?
Jetzt doch mit OS?
Nein, FreeRtos war in meiner Internetsuche nur recht weit oben ;-). Und
es ist recht interessant implementiert.
Wilhelm M. schrieb:> Bei den kritichen Bereichen musst Du noch zwischen bedingten und> unbedingten KB unterscheiden.
Was ist KB?
Εrnst B. schrieb:> Wilhelm M. schrieb:>> Schaltet nur die Interrupts bei AVR ab.>> nicht nur. ATOMIC_BLOCK enthält auch ein> _asm_ volatile ("" ::: "memory");>> also eine memory barrier. d.H. alle Variablen, die im ATOMIC_BLOCK> geändert werden, sind vor dem Re-Aktivieren der ISRs auch wirklich> geschrieben.
Nö.
1
static__inline__uint8_t__iCliRetVal(void)
2
{
3
cli();
4
return1;
5
}
6
7
#define ATOMIC_BLOCK(type) for ( type, __ToDo = __iCliRetVal(); \
Kevin schrieb:> Wilhelm M. schrieb:>> Jetzt doch mit OS?>> Jetzt doch mit OS?> Nein, FreeRtos war in meiner Internetsuche nur recht weit oben ;-). Und> es ist recht interessant implementiert.>> Wilhelm M. schrieb:>> Bei den kritichen Bereichen musst Du noch zwischen bedingten und>> unbedingten KB unterscheiden.>> Was ist KB?
Kritischer Bereich: ein Code-Abschnitt, in dem wechselseitiger
Ausschluss herrschen soll
(prx) A. K. schrieb:> Wenn die 4 Bytes nicht an einer Adresse %4==0 liegen, die Maschine einen> solchen Zugriff nicht zulässt und der Compiler deshalb daraus mehrere> Zugriffe machen muss.
Danke. Misalignment war mir schon klar, jetzt habe ich auch die
Verbindung warum es in meinem betrachteten Fall zum Problem werden kann.
Wilhelm M. schrieb:> Ja, aber nur dafür.
Das ist eine "destruktor"-Funktion, die immer ausgeführt wird, wenn die
Variable den scope verlässt.
Hat den Sinn, dass auch bei einem
1
ATOMIC_BLOCK(ATOMIC_FORCEON){
2
...
3
if(x)return;// oder break
4
...
5
}
das "return" ein "sei" auslöst. Sonst wären vorzeitigen Verlassen des
Blocks die Interrupts immer noch gesperrt.
Die darin deklarierte barrier gilt für den gesamten RAM, nicht nur die
eine Variable.
das cli beim Betreten hat keine solche optimization barrier.
d.H. der Compiler könnte zwar Berechnungen vor dem Atomic-Block in
diesen hineinziehen, aber nichts aus dem Block herausnehmen.
Nach Verlassen des Atomic Blocks ist jedenfalls sichergestellt, dass
alle Variablen sauber in den RAM zurückgespeichert wurden.
Εrnst B. schrieb:> Nach Verlassen des Atomic Blocks ist jedenfalls sichergestellt, dass> alle Variablen sauber in den RAM zurückgespeichert wurden.
Nö.
Wilhelm M. schrieb:> Nö.
Warum Nö? Dein Beispiel bestätigt doch 1:1 was ich geschrieben habe.
Nur dass der Compiler "x" eben ganz wegoptimiert hat. Darf er, wird ja
nie wieder gelesen.
Pack in dein Beispiel ans Ende noch ein "return x", dann siehst du
exakt, dass es zuerst in den RAM geschrieben wird, und für's return auch
wieder aus dem RAM gelesen wird.
1
main:
2
in r24,__SREG__
3
cli
4
sts x,__zero_reg__
5
out __SREG__,r24
6
lds r24,x
7
ldi r25,0
8
ret
Selben Effekt hast du auch, wenn "x" nicht static ist.
Εrnst B. schrieb:> Warum Nö? Dein Beispiel bestätigt doch 1:1 was ich geschrieben habe.> Nur dass der Compiler "x" eben ganz wegoptimiert hat. Darf er, wird ja> nie wieder gelesen.
Du sagst, dass im ATOMIC_BLOCK alle(!) dort zugegriffenen Variablen
automatich volatile wären. Wie Du siehst, stimmt diese Aussage nicht!
Wenn es volatile wäre, dürfte er den Zugriff auf x nicht wegoptimieren.
Du kannst Dich davon leicht überzeugen, indem Du in meinem Beispiel
einfach mal die volatile Deklaration einkommentierst.
Wilhelm M. schrieb:> Du sagst, dass im ATOMIC_BLOCK alle(!) dort zugegriffenen Variablen> automatich volatile wären. Wie Du siehst, stimmt diese Aussage nicht!
Das habe ich NIE behauptet. Es geht ja gerade darum, dass die
Variablen NICHT Volatile werden müssen, weil der Atomic-Block das
Rückschreiben erzwingt. Damit ist das übliche Problem von "Variable wird
in ISR und main verwendet" in den üblichen Fällen erschlagen, OHNE
dass man sich die anderen Nachteile von volatile einfängt.
Εrnst B. schrieb:> Wilhelm M. schrieb:>> Du sagst, dass im ATOMIC_BLOCK alle(!) dort zugegriffenen Variablen>> automatich volatile wären. Wie Du siehst, stimmt diese Aussage nicht!>> Das habe ich NIE behauptet. Es geht ja gerade darum, dass die> Variablen NICHT Volatile werden müssen, weil der Atomic-Block das> Rückschreiben erzwingt. Damit ist das übliche Problem von "Variable wird> in ISR und main verwendet" in den üblichen Fällen erschlagen, *OHNE*> dass man sich die anderen Nachteile von volatile einfängt.
Schau Dir folgendes an
1
#include<stdint.h>
2
#include<util/atomic.h>
3
4
static/*volatile */uint8_tx;
5
6
ISR(TIMER2_OVF_vect){
7
uint8_ty=x;
8
}
9
10
intmain(){
11
ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
12
x=0;
13
}
14
}
Ergibt:
1
__RAMPZ__=0x3b
2
__CCP__=0x34
3
.text
4
.typeTIMER2_OVF_vect,@function
5
TIMER2_OVF_vect:
6
__gcc_isr1;
7
__gcc_isr2;
8
reti
9
__gcc_isr0,r0
10
.sizeTIMER2_OVF_vect,.-TIMER2_OVF_vect
11
.section.text.startup,"ax",@progbits
12
.typemain,@function
13
main:
14
inr24,__SREG__;_1,MEM[(volatileuint8_t*)63B]
15
cli
16
out__SREG__,r24;MEM[(volatileuint8_t*)63B],_1
17
.L3:
18
rjmp.L3;
19
.sizemain,.-main
Atomarität und Optimierungssperre (ggf. durch volatile) sind zwei
unterschiedliche Dinge.
Wilhelm M. schrieb:> Wenn es volatile wäre, dürfte er den Zugriff auf x nicht wegoptimieren.
Dein Beispiel ist allerdings sehr speziell, da es keine Loop enthält.
Typisch wird bei MC-Anwendungen das Main nie verlassen. Ich wüßte
jedenfalls nicht, wie dann eine sinnvolle Anwendung zustande kommen
soll.
Außerdem sagst Du mit dem static, daß die Variable nirgends anders
zugegriffen werden kann.
Mach ne Loop ins Main, nimm das static weg, dann erfolgt auch die
Zuweisung.
Wilhelm M. schrieb:> Schau Dir folgendes an
Wieder ein Beispiel, was meine Aussagen belegt bzw. zumindest nicht
widerlegt.
In dem ASM-Code, den du zeigt, findet keinerlei nicht-synchronisierter
Zugriff auf "uint8_t x" statt. Exakt das, was der Atomic_block erreichen
soll.
QED.
Ich würde in der Interruptroutine keine Berechnungen oder andere
komplexe Aufgaben durchführen. Die Interruptfunktionen nur füe
IO-Operationen verwenden. Z.B. Wert von einem Port abholen und in ein
FIFO schreiben und dem Hintergrund mittels Flag signalisieren das etwas
im FIFO abholbar ist. Interrupt nur verwenden wenn die Gefahr besteht
etwas im Hintergrund zu verpassen.
Ist der Hintergrund fertig, dann kann er zum Energiesparen, den
Sleepmodus aktivieren. Dieser kann in einer Interruptroutine wieder
deaktiviert werden.
Kurz eine Bemerkung zum FIFO:
Die Interruptroutine hat alleinigen Zugriff zum Schreibzeiger, der
Hintergrund alleinigen Zugriff zum Lesezeiger. So kommen sich Interrupt
und Hintergrund nicht in die Quere.
Gerald K. schrieb:> Die Interruptroutine hat alleinigen Zugriff zum Schreibzeiger, der> Hintergrund alleinigen Zugriff zum Lesezeiger.
Beim Sende-FIFO dann umgekehrt.
Kevin schrieb:> Kann man annehmen,> das auf einem 32Bit Controller der Schreibzugriff auf eine (8/16/32 Bit)> Variablen immer atomar erfolgt.
Nein, das kann man nicht.
Es ist zwar der Regelfall, aber, wie bei jeder Regel: es gibt auch viele
Ausnahmen. Diese Ausnahmen sind sehr architekturspezifisch. Wobei hier
"Architektur" mehr umfasst, als das, was man üblicherweise darunter
versteht. Neben der CPU/MCU selber nämlich auch noch die konkrete
Schaltung, in der sie steckt.
c-hater schrieb:> Neben der CPU/MCU selber nämlich auch noch die konkrete Schaltung, in> der sie steckt.
Wobei eine Aufteilung auf mehrere Zugriffe im Businterface für
Interrupts innerhalb eines Cores atomar bleibt.
(prx) A. K. schrieb:> Wobei eine Aufteilung auf mehrere Zugriffe im Businterface für> Interrupts innerhalb eines Cores atomar bleibt.
Das kannst du für jede Architektur garantieren?