Interrupt-Synchronisation

Gast #7192230
Lesenswert?

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
#7192241
Lesenswert?

Patrick L. schrieb:
> undefinierte Ergebnisse ...

https://www.mikrocontroller.net/articles/Interrupt#Interruptfeste_Programmierung


Patrick L. schrieb:
> Dazu setzt man sogenannte Flags die anzeigen ob Berechnungen im Gange
> sind oder abgeschlossen sind.

Meist reicht es, die Unterbrechungen zu unterbinden.
Was in der ISR berechnet wird, braucht halt ein Volatile, um nicht 
versehentlich, veraltete Registerinhalte zu verarbeiten.
#7192251
Lesenswert?

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 ...
#7192254
Lesenswert?

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
Gast #7192257
Lesenswert?

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.
#7192263
Lesenswert?

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.
#7192280
Lesenswert?

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.
Gast #7192300
Lesenswert?

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.
Gast #7192315
Lesenswert?

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
Gast #7192318
Lesenswert?

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
int zaehler;
2

3
void machwas_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
volatile int zaehler;

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.
Gast #7192322
Lesenswert?

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?
Gast #7192330
Lesenswert?

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.
#7192352
Lesenswert?

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.
Gast #7192357
Lesenswert?

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.
#7192377
Lesenswert?

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).
#7192385
Lesenswert?

Ε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
    return 1;
5
}
6

7
#define ATOMIC_BLOCK(type) for ( type, __ToDo = __iCliRetVal(); \
8
                         __ToDo ; __ToDo = 0 )
#7192386
Lesenswert?

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
Gast #7192396
Lesenswert?

(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.
#7192428
Lesenswert?

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.
#7192455
Lesenswert?

Εrnst B. schrieb:
> Nach Verlassen des Atomic Blocks ist jedenfalls sichergestellt, dass
> alle Variablen sauber in den RAM zurückgespeichert wurden.

Nö.
1
#include <stdint.h>
2
#include <util/atomic.h>
3
    
4
static /*volatile */ uint8_t x;
5

6
int main() {
7
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
8
        x = 0;
9
    }
10
}

ergibt
1
main:
2
in r24,__SREG__  ;  _1, MEM[(volatile uint8_t *)63B]
3
cli
4
out __SREG__,r24         ;  MEM[(volatile uint8_t *)63B], _1
5
ldi r24,0                ;
6
ldi r25,0                ;
7
ret
#7192471
Lesenswert?

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.
#7192477
Lesenswert?

Ε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.
#7192482
Lesenswert?

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.
#7192491
Lesenswert?

Ε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_t x;
5

6
ISR( TIMER2_OVF_vect ) {
7
    uint8_t y = x;
8
}
9

10
int main() {
11
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
12
        x = 0;
13
    }
14
}

Ergibt:
1
__RAMPZ__ = 0x3b
2
__CCP__ = 0x34
3
.text
4
.type   TIMER2_OVF_vect, @function
5
TIMER2_OVF_vect:
6
__gcc_isr 1      ;
7
__gcc_isr 2      ;
8
reti
9
__gcc_isr 0,r0
10
.size   TIMER2_OVF_vect, .-TIMER2_OVF_vect
11
.section        .text.startup,"ax",@progbits
12
.type   main, @function
13
main:
14
in r24,__SREG__  ;  _1, MEM[(volatile uint8_t *)63B]
15
cli
16
out __SREG__,r24         ;  MEM[(volatile uint8_t *)63B], _1
17
.L3:
18
rjmp .L3                 ;
19
.size   main, .-main

Atomarität und Optimierungssperre (ggf. durch volatile) sind zwei 
unterschiedliche Dinge.
#7192500
Lesenswert?

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.
#7192764
Lesenswert?

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.
Gast #7194416
Lesenswert?

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.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren