Hallo,
Variablen die in der main gelesen und in einem Interrupt geschrieben
werden sollte man ja mit volatile kennzeichnen.
In einer ISR muss ja der Compiler die Variable eh immer aus dem RAM
holen und zurückschreiben, da er ja den Kontext des Aufrufs nicht kennt.
In der main könnte er folgern dass die Variable nicht beschrieben wird
(d.h. sich nicht geändert hat) und deswegen z.B. Vergleiche
wegoptimieren. Grund: er weiß ja nicht dass ein IR zuschlagen kann.
Wenn ich nun aber z.B. ein UART-Modul habe dass einen internen
Ringpuffer verwendet und per Header-Datei seine Schnittstellen zur main
bekanntmacht.
Der Compiler übersetzt ja die UART.c erstmal für sich. Er sieht aber
dass Funktionen die nicht static sind theoretisch von einem ihm zu
diesem Zeitpunkt unbekannten Modul aufgerufen werden können. D.h. er
kann eigentlich nichts in Registern halten, richtig?
Muss ich nun diesen Puffer auch volatile machen?
Viele Grüße,
Masl
Ja, die gibts natürlich, die schreibt ja das empfangene Byte in den
Puffer.
Daneben existiert eine Funktion die ein Byte daraus lesen kann.
Mein Gedankengang dahinter war aber, dass der Compiler in dieser
Funktion eigentlich immer den Puffer neu aus dem RAM laden muss, weil
die Funktion ja theoretisch von einem anderen Modul aufgerufen werden
kann, solang sie nicht static ist.
Oder woher soll der Compiler folgern dass sich die Register nicht
geändert haben? Er kennt ja nicht den kompletten Kontrollfluß über
mehrere c-Dateien.
Nochwas: was hat volatile mit dem von dir verlinkten Artikel zu tun?
Eine kritische Situation kann auch bei volatile noch entstehen, nämlich
wenn ich z.B. auf einem 8-Bitter einen 32-Bit Zugriff durchführe. Dieser
kann natürlich unterbrochen werden, volatile hin- oder her.
Um das zu vermeiden braucht man Mutexe oder Interruptsperren, aber das
ist imho eine ganz andere Geschichte.
Masl schrieb:> Oder woher soll der Compiler folgern dass sich die Register nicht> geändert haben? Er kennt ja nicht den kompletten Kontrollfluß über> mehrere c-Dateien.
Kommt auf die aktivierten Optimierungen an. Mit -flto hat er zum
Beispiel globale Kontrollfluß-Info — vorausgesetzt, LTO wird richtig
angewendt :-)
Wenn ich Dich richtig verstehe, meinst Du so etwas wie das hier
(vereinfacht):
1
staticuint8_tpuffer[100];
2
staticuint8_tindex=0;
3
uint8_tGetNextByte(void)
4
{
5
uint8_tx=puffer[index];
6
index++;
7
if(index>=100)index=0;
8
return(x);
9
}
Und jetzt überlegst Du, ob "puffer" volatile sein muß oder nicht, wo
doch GetNextByte() einfach ganz stur Speicheradressen ausliest und es da
nix zu optimieren gibt - richtig?
Wow, gute Frage!
Wahrscheinlich hilft es letztendlich nur, den Assembler-Code anschauen,
heutzutage können die Compiler ja wirklich unfaßbar gut optimieren.
Am sichersten dürfte es sein, wenn man sich das Programm konzeptionell
so vorstellt, dass alles in einer c-Datei steht und alle Funktionen
geinlint sind, es also nur noch die main-Funktion gibt. Alles, was sich
außerhalb dieses Kontrollflusses ändern kann, würde ich volatile setzen.
Bronco schrieb:> Wow, gute Frage!
Nicht wirklich .....
Bei volatile geht es tatsächlich nur um den Fall, wenn es einen 2.ten
Programpfad gibt bzw. eine andere Möglichkeit, wie sich der Inhalt einer
Variablen ändern kann, ohne dass es für den Compiler ersichtlich ist.
Um mehr gehts da nicht.
Und das ist, wenn man nur diese Funktion betrachtet, nun mal nicht der
Fall.
Da scheint ein Missverständnis bei Masl vorzuliegen.
Der Compiler kann sowieso nicht davon ausgehen, dass er zb index
einmalig in ein Register lädt und dann immer nur dieses Register
weiterverwendet. Eine Funktion ist aus Sprach-Sicht eine in sich
abgeschlossene Einheit. Die Funktion wird betreten und der Compiler darf
erst mal von nichts ausgehen sondern muss die Arbeitsumgebung
herstellen, die die Funktion braucht um arbeiten zu können. Die Funktion
wird irgendwann mal fertig und dann wird diese Arbeitsumgebung wieder
aufgelöst.
Das ist die Ausgangssituation auf der Optimizer aufbauen und unter
Umständen gewisse Dinge rauswerfen können. Aber immer gilt dabei 'As
if'. d.h. der Optimizer darf keine Veränderungen machen, die das
konzeptionelle Bild, so wie es sich aus der Sprachdefinition ergibt,
über den Haufen wirft. Das heißt, der Optimizer darf eine Optimierung
nur dann machen, wenn sichergestellt ist (er das nachweisen kann), dass
er an der Funktionalität nichts verändert.
volatile ist so gesehen eine Ausnahme davon, die man akzeptieren muss,
weil ein Verzicht darauf auf der einen Seite zu schwerwiegende
Einschränkungen im Optimizing-Prozess führen würde und auf der anderen
Seite das Einsatzgebiet von C zu stark einschränken würde.
> Der Compiler übersetzt ja die UART.c erstmal für sich. Er sieht aber> dass Funktionen die nicht static sind theoretisch von einem ihm zu> diesem Zeitpunkt unbekannten Modul aufgerufen werden können. D.h.> er kann eigentlich nichts in Registern halten, richtig?
Grundsätzlich gibt es keinen Passus im Sprachstandard, der dieses
verbieten würde, WENN es möglich wäre. Aber aus praktischen Gründen ist
das auf einer Architektur wie dem AVR nicht durchführbar. So ja. Er kann
sich den Wert nicht in einem Register bis zum nächsten Aufruf vorhalten.
Aber das ist nicht dein Problem. Das ist ein Problem, mit dem sich die
Compilerbauer rumschlagen müssen, ob und wenn ja wie sie ein derartiges
Feature implementieren könnten.
Karl Heinz Buchegger schrieb:> Bei volatile geht es tatsächlich nur um den Fall, wenn es einen 2.ten> Programpfad gibt bzw. eine andere Möglichkeit, wie sich der Inhalt einer> Variablen ändern kann, ohne dass es für den Compiler ersichtlich ist.
Bezieht sich das explizit auf "eine Variable"?
Wenn ich es richtig verstehe, geht es um folgendes:
Wir haben ein Array, also einen Speicherbereich, da schreibt die ISR
hinein und eine aus Main aufgerufene Funktion liest aus. Der
Lese-Zugriff erfolgt über einen Pointer (bzw. Array mit Index).
Die Frage ist: würde der Compiler ein solchen Speicher-Auslese-Zugriff
mit veränderlicher Adresse wegoptimieren, weil er denkt, der Speicher
ist eh immer konstant?
Wohl kaum. Also braucht man auch kein volatile, oder?
Fabian O. schrieb:> Am sichersten dürfte es sein, wenn man sich das Programm konzeptionell> so vorstellt, dass alles in einer c-Datei steht und alle Funktionen> geinlint sind, es also nur noch die main-Funktion gibt.
Das ist der sichere Ansatz.
In diesem Fall, wenn der Puffer 100 Byte ist, ist man theoretisch
sicher.
Aber wenn der Puffer z.B. nur 4 Byte ist, könnte sich der Compiler
sagen, ich halte ihn in 4 Registern und dann kracht es.
Korrekt müßte er also volatile sein.
Man kann aber auch nur den Mainzugriff casten. Das hat den Vorteil, daß
man beim Lesen immer darauf aufmerksam wird, daß hier ein Interrupt im
Spiel ist.
Peter Dannegger schrieb:> In diesem Fall, wenn der Puffer 100 Byte ist, ist man theoretisch> sicher.> Aber wenn der Puffer z.B. nur 4 Byte ist, könnte sich der Compiler> sagen, ich halte ihn in 4 Registern und dann kracht es.> Korrekt müßte er also volatile sein.
Er könnte die Zugriffe auf den Puffer auch komplett wegoptimieren. Mal
ein vollständiges Beispiel (der Code ist sinnlos, aber darum geht es ja
nicht):
1
// main.c
2
3
staticuint8_tpuffer[100]={0};
4
staticuint8_tindex=0;
5
6
staticuint8_tGetNextByte(void)
7
{
8
uint8_tx=puffer[index];
9
index++;
10
if(index>=100)index=0;
11
return(x);
12
}
13
14
staticvoidSetNextByte(uint8_tx)
15
{
16
puffer[index]=x;
17
index++;
18
}
19
20
voidmain(void)
21
{
22
while(1){
23
uint8_tx=GetNextByte();
24
putchar(x);
25
}
26
}
27
28
ISR()
29
{
30
setNextByte(HARDWARE_REGISTER);
31
}
Die Funktion GetNextByte() kann nicht von außerhalb aufgerufen werden
(da static) und sie wird nur einmal von main() aus aufgerufen, also wird
man sie inlinen:
1
voidmain(void)
2
{
3
while(1){
4
uint8_tx=puffer[index];
5
index++;
6
if(index>=100)index=0;
7
putchar(x);
8
}
9
}
Wann die ISR zuschlägt, weiß der Compiler nicht, also sieht er nur, dass
laufend puffer[0] bis puffer[99] ausgegeben wird. Das Array puffer wurde
mit 0 initialisiert und (ohne die ISR) nie beschrieben. Also kann er das
ganze optimieren zu:
1
voidmain(void)
2
{
3
while(1){
4
putchar(0);
5
}
6
}
Die ursprüngliche Frage war aber, wie es ist, wenn GetNextByte() und
main() in verschiedenen C-Dateien stehen. Dann weiß der Compiler beim
Compilieren von GetNextByte() nicht, ob und wann SetNextByte()
aufgerufen wird. Also wird er den Array-Zugriff nicht wegoptimieren. Das
gilt aber nur, wenn nach dem Compilieren der c-Dateien keine
modulübergreifende Optimierung des Gesamtprogramms mehr stattfindet. Und
darauf würde ich mich nicht verlassen, denn das hängt vom Compiler bzw.
dessen Einstellung ab (siehe Post von Johann L.).
Fabian O. schrieb:> Die ursprüngliche Frage war aber, wie es ist, wenn GetNextByte() und> main() in verschiedenen C-Dateien stehen. Dann weiß der Compiler beim> Compilieren von GetNextByte() nicht, ob und wann SetNextByte()> aufgerufen wird.
Das weiß er auch dann nicht, wenn die beiden Funktionen in einer Datei
stehen. Solange ihm nicht explizit per option oder per static gesagt
wird, daß das die einzige Sourcedatei ist, geht der immer davon aus, daß
es noch weitere Dateien geben kann.
Oliver
Dieses Beispiel trifft meine ursprüngliche Frage sehr gut :-)
Also kann man abschließend sagen dass es von einer Vielzahl an Faktoren
im Programmaufbau sowie Compileroptionen abhängt. Deswegen ist das
Deklarieren, oder zumindest das Casten durch volatile anzuraten.
Diese ominöse modul-übergreifend-optimieren-Option bringt mich aber nun
zu einer neuen Frage:
Beispiel: Ein Interrupt löst aus. Über eine Callback-Funktion melde ich
einer höheren Schicht (= anderes Modul) das ein bestimmtes Event
eingetreten ist.
Im Prinzip nur ein Funktionsaufruf und das Setzen eines Flags.
Bisher hab ich mich immer daran gestört dass bei so einem
Funktionsaufruf innerhalb einer ISR (die Funktion setzt nur ein Flag,
mehr nicht!) der Kontext komplett gesichert werden muss.
D.h. die ISR die eigentlich nicht viel macht ist hauptsächlich damit
beschäftigt, die Register erst alle auf den Stack zu pushen und später
wieder zu popen. (Jeweils 2 Takte => Flash und Laufzeit).
Würde dieses Verhalten durch diese Compiler-Option verhindert werden?
Masl schrieb:> Diese ominöse modul-übergreifend-optimieren-Option bringt mich aber nun> zu einer neuen Frage:> Beispiel: Ein Interrupt löst aus. Über eine Callback-Funktion melde ich> einer höheren Schicht (= anderes Modul) das ein bestimmtes Event> eingetreten ist.> Im Prinzip nur ein Funktionsaufruf und das Setzen eines Flags.>> Bisher hab ich mich immer daran gestört dass bei so einem> Funktionsaufruf innerhalb einer ISR (die Funktion setzt nur ein Flag,> mehr nicht!) der Kontext komplett gesichert werden muss.> D.h. die ISR die eigentlich nicht viel macht ist hauptsächlich damit> beschäftigt, die Register erst alle auf den Stack zu pushen und später> wieder zu popen. (Jeweils 2 Takte => Flash und Laufzeit).>> Würde dieses Verhalten durch diese Compiler-Option verhindert werden?
Ja.
Karl Heinz Buchegger schrieb:>> Wow, gute Frage!> Nicht wirklich .....
Anscheindend doch, denn
Fabian O. schrieb:> Wann die ISR zuschlägt, weiß der Compiler nicht, also sieht er nur, dass> laufend puffer[0] bis puffer[99] ausgegeben wird. Das Array puffer wurde> mit 0 initialisiert und (ohne die ISR) nie beschrieben. Also kann er das> ganze optimieren zu:
1
voidmain(void)
2
{
3
while(1){
4
putchar(0);
5
}
6
}
Wenn es stimmt, daß Compiler so optimieren darf, muß man den puffer doch
mit volatile kennzeichnen, oder PeDa's IVAR Ansatz gehen.