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
classSharedState{
4
public:
5
SharedState();
6
7
uint8_tbuffer[255];
8
uint16_tpos{};
9
};
10
11
SharedState::SharedState():buffer{},pos{0}{}
12
13
14
volatileSharedStatesharedState;
15
16
17
classSerialDriver{
18
public:
19
20
staticSerialDriver&getInstance();
21
22
// make sure no other instance can be created
23
SerialDriver(SerialDriverconst&)=delete;
24
voidoperator=(SerialDriverconst&)=delete;
25
26
voiddoSomethingWithSharedState();
27
voidprocessIncomingByte();
28
29
private:
30
USART_t&usart=USARTC0;
31
32
SerialDriver();
33
};
34
35
SerialDriver&SerialDriver::getInstance(){
36
staticSerialDriverserialDriver;
37
returnserialDriver;
38
}
39
40
SerialDriver::SerialDriver(){
41
// USART konfigurieren
42
// ...
43
}
44
45
// lesender/schreibender Zugriff auf Shared State, aus main() aufgerufen
46
voidSerialDriver::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
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
volatileSharedStatesharedState;
richtig, oder muss es
1
SharedStatevolatilesharedState;
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
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
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 ...
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.
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.
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
volatileuint8_tfoo;
habe, ist klar was passiert: foo wird bei jedem Zugriff aus dem Speicher
geholt/dorthin zurückgeschrieben.
Wie sieht es bei
1
volatileSharedStatesharedState;
aus?
Ist nur die Referenz vor Optimierung geschützt oder auch
sharedState.buffer bzw. sharedState.pos?
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 ;-)