Forum: Mikrocontroller und Digitale Elektronik C++ | Sicherer Umgang mit Interrupts


von Dennis D. (schwebo)


Lesenswert?

Hallo zusammen,

ich habe eine Frage zum sicheren Umgang mit Interrupts.

Es geht um ein C++-Programm, das auf einem AVR-Mikrocontroller läuft und 
Daten asynchron über einen USART empfängt. Ich habe versucht, es auf die 
relevanten Teile zu reduzieren:
1
#include <util/atomic.h>
2
3
class SharedState {
4
public:
5
    SharedState();
6
7
    uint8_t buffer[255];
8
    uint16_t pos{};
9
};
10
11
SharedState::SharedState() : buffer {}, pos {0} {}
12
13
14
volatile SharedState sharedState;
15
16
17
class SerialDriver {
18
public:
19
20
    static SerialDriver& getInstance();
21
22
    // make sure no other instance can be created
23
    SerialDriver(SerialDriver const&) = delete;
24
    void operator=(SerialDriver const&) = delete;
25
26
    void doSomethingWithSharedState();
27
    void processIncomingByte();
28
29
private:
30
    USART_t& usart = USARTC0;
31
32
    SerialDriver();
33
};
34
35
SerialDriver& SerialDriver::getInstance() {
36
    static SerialDriver serialDriver;
37
    return serialDriver;
38
}
39
40
SerialDriver::SerialDriver() {
41
    // USART konfigurieren
42
    // ...
43
}
44
45
// lesender/schreibender Zugriff auf Shared State, aus main() aufgerufen
46
void SerialDriver::doSomethingWithSharedState() {
47
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {    
48
        // nur ein Beispiel!
49
        if (sharedState.pos >= 20000) {
50
            sharedState.pos = 0;
51
        }
52
    }
53
}
54
55
// lesender/schreibender Zugriff auf Shared State, aus ISR aufgerufen
56
void SerialDriver::processIncomingByte() {
57
    // z.B.
58
    sharedState.buffer[sharedState.pos] = usart.DATA;
59
    sharedState.pos++;
60
}
61
62
ISR(USARTC0_RXC_vect) {
63
    SerialDriver::getInstance().processIncomingByte();
64
}


Die Verbindung zwischen ISR und C++-Code habe ich über ein Singleton 
gelöst. Nicht schön, funktioniert aber und ich glaube alles andere wäre 
mit komplexerem Code verbunden.

Die Methode doSomethingWithSharedState() der SerialDriver-Klasse wird 
aus der main aufgerufen.
Die Methode processIncomingByte() der gleichen Klasse wird aus der ISR 
aufgerufen.
Beide greifen lesend und schreibend auf den Shared State zu, dessen 
Felder in einem C++-Objekt gekapselt sind.

Das Ganze funktioniert, ich bin mir wegen der lauernden Fallstricke aber 
unsicher, ob ich das richtig gemacht habe.
Konkret:

Verwende ich volatile hier korrekt?
Ist
1
volatile SharedState sharedState;
richtig, oder muss es
1
SharedState volatile sharedState;
heißen?

Funktioniert der Einsatz von:
1
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
2
        ...
3
}
wie erwartet? Oder hat das in C++ nichts verloren?


Ich freue mich auf eure Tipps.

Viele Grüße,
Dennis

von Heinz (Gast)


Lesenswert?

Hallo,

das Konzept, welches hier angewendet werden sollte, heißt Ringbuffer.
Der Ringbuffer wird im Interrupt beschrieben und im Hauptprogramm 
ausgelesen.
Eine Interruptsperre ist in diesem Anwendungsfall und bei korrekter 
Ringbuffer-Implementierung nicht notwendig. Es gibt viele 
Ringbuffer-Implementierungen, muss man nicht selbst entwickeln.

Es lohnt sich auch, das Konzept verstanden und angewendet zu haben, 
braucht man immer wieder mal in asynchronen Umgebungen

von foobar (Gast)


Lesenswert?

Das Betreten und Verlassen eines ATOMIC_BLOCK stellt eine Memory-Barrier 
dar - das volatile ist also nicht nötig.

Ob C++ da noch zusätzliche Auflagen hat?  Keine Ahnung - Exceptions würd 
ich zumindest innerhalb des Blocks nicht auslösen ...

von foobar (Gast)


Lesenswert?

Ich schrieb:
> das volatile ist also nicht nötig.

Genauer: wenn alle (nicht-ISR-)Zugriffe innerhalb eines ATOMIC_BLOCK 
stattfinden, ist das volatile nicht nötig.

von W.S. (Gast)


Lesenswert?

Dennis D. schrieb:
> // make sure no other instance can be created

Du scheinst die Befindlichkeiten, die du vom PC her gewöhnt bist, auf 
einen Mikrocontroller 1:1 übertragen zu wollen. Hmm...

Also es soll ein Treiber werden, der im µC den Datenverkehr per UART 
erledigt.

Nun, der sollte im Prinzip 2 Teile haben:
1. den Teil, der "nach draußen" also zu dem Rest der Firmware arbeitet, 
also letztlich von irgendwas, das von main() her kommt, benutzt wird.

Der 2. Teil ist die Interrupt-Serviceroutine. Und beide haben als 
Trennstelle je einen Ringpuffer für Sende- und Empfangszweig dazwischen.

So ein Ringpuffer besteht zunächst aus einem Array von chars und dazu 
zwei Zeigern oder Indizes, je nachdem was du lieber magst. Ich bevorzuge 
Indizes, denn deine Puffer werden gewiß nicht größer als 256 Zeichen 
sein. Eher deutlich kleiner, so um die 16 Empfangszeichen und 64 
Sendezeichen schätze ich. Man hat ja am µC nicht unendlich viel RAM.

Einer der Zeiger dient zum Füllen des Puffers und der andere zum Leeren. 
Und die Instanz, die den Puffer füllt, besitzt auch den Schreib-Zeiger, 
während die andere (die ihn wieder leert) den Lese-Zeiger besitzt. Jede 
Seite darf beide Zeiger lesen, aber nur diejenige, die ihn besitzt, darf 
ihn schreiben.

Das ist eigentlich alles. Damit kommt man sich niemals gegenseitig ins 
Gehege und da schlußendlich dieser Treiber ein Teil der Firmware sein 
wird, ist es auch ausgeschlossen, daß er mehrfach darin vorkommt. Und 
instantiieren ist auch nicht, das Ganze wird ja fest in den 
Programmspeicher gebrannt und wird nicht von der Festplatte in den RAM 
geladen.

Und ein Treiber tut auch nichts a la "doSomethingWith...", sondern wird 
prozedural aufgerufen und Auswertungen irgendwelcher Art finden woanders 
statt.

W.S.

von Dennis D. (schwebo)


Lesenswert?

Danke für eure Antworten!

Der Ansatz mit Ringbuffer ist spannend. Ich hatte sowas mal in den 
Application Notes für den Xmega gesehen und werde es versuchen.

Unabhängig vom Thema Datenempfang mit USART habe ich noch weitere 
Komponenten, für die ich mit Interrupts arbeiten muss. Daher 
interessiert mich generell, ob ich, so wie im Beispiel, volatile auf ein 
C++-Objekt anwenden kann.

Mein Verständnis von volatile ist, dass es verhindert, dass durch 
Optimierungen Zugriffe auf Register abgebildet werden, durch die 
Änderungen in einer ISR nicht im Hauptprogramm sichtbar sind oder 
andersherum.

Wenn ich
1
volatile uint8_t foo;
habe, ist klar was passiert: foo wird bei jedem Zugriff aus dem Speicher 
geholt/dorthin zurückgeschrieben.

Wie sieht es bei
1
volatile SharedState sharedState;
aus?
Ist nur die Referenz vor Optimierung geschützt oder auch 
sharedState.buffer bzw. sharedState.pos?

von Mombert H. (mh_mh)


Lesenswert?

Dennis D. schrieb:
> Ist nur die Referenz vor Optimierung geschützt oder auch
> sharedState.buffer bzw. sharedState.pos?

Das ganze Objekt. Genau wie das ganze Objekt schreibgeschützt ist, wenn 
es const statt volatile markiert ist.
Und in deinem Beispiel gibt es keine Referenz auf sharedState. Das ist 
kein Java/C#/Python. Objekte sind Objekte und keine Referenzen auf 
Objekte ;-)

von Dennis D. (schwebo)


Lesenswert?

Danke Dir, jetzt ergibt das alles schon mehr Sinn :)

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.