Forum: Compiler & IDEs volatile und Arduino


Announcement: there is an English version of this forum on EmbDev.net. Posts you create there will be displayed on Mikrocontroller.net and EmbDev.net.
von Jürgen (derkleinemuck)


Lesenswert?

Ich bin gerade dabei volatile zu erklären. Auf Microchip Studio klappt 
mein Beispiel genau wie gewünscht und im Assembler sieht man auch warum. 
Die nicht-volatile Variable wird in der ISR inkrementiert und die 
Hauptroutine bekommt das nicht mit.

Jetzt wollte ich das auch mit der Arduino IDE zeigen und siehe da: es 
ist egal, wie ich die Variable deklariere - es funktioniert. Also das 
beispiel von Arduino ausprobiert: 
https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
Und auch dem ist egal, was ich mit volatile mache.

Warum??? Wo liegt mein Denkfehler oder vermutlich eher: was macht die 
IDE da komisches (mit einer der Compileroptionen?)

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Jürgen schrieb:
> Und auch dem ist egal, was ich mit volatile mache.

Wenn ich mich recht erinnere, sind bei Arduino die Optimierungen 
standardmäßig ausgeschaltet. Ich meine mich zu erinnern, dass man bei 
späteren Installationen gefragt wurde, ob man Optimierungen einschalten 
will oder nicht.

Bei abgeschalteter Optimierung ist es in der Regel so, dass der Compiler 
ziemlich genau das macht, was im Quelltext steht. Dabei liest er 
Variablen jedesmal neu ein statt sie in Registern zu vorhalten - 
zumindest über einige Programmabschnitte.

: Bearbeitet durch Moderator
von Jürgen (derkleinemuck)


Lesenswert?

So sieht der Aufruf der IDE aus:
1
...7.3.0-atmel3.6.1-arduino7/bin/avr-gcc" -Os -g -flto -fuse-linker-plugin -Wl,--gc-sections -mmcu=atmega328p -o "C:....elf" "....cpp.o" "...\\core.a" "-LC:\\Windows\\..." -lm

"-Os" ist doch die Standardoptimerung? Zumindest nutzt Studio die auch

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Jürgen schrieb:
> Die nicht-volatile Variable wird in der ISR inkrementiert und die
> Hauptroutine bekommt das nicht mit.
>
> Jetzt wollte ich das auch mit der Arduino IDE zeigen und siehe da: es
> ist egal, wie ich die Variable deklariere - es funktioniert. Also das
> beispiel von Arduino ausprobiert:
> 
https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/

Das da?
1
void loop() {
2
  digitalWrite(ledPin, state);
3
}
4
5
void blink() {
6
  state = !state;
7
}

digitalWrite() ist wohl (effektiv) sowas wie
1
* (volatile uint8_t*) ADDR = state;
Und das ist nach den Strict Aliasing Rules im gleichen Alias Set wie 
state.

Falls digitalWrite zu einem Funktionsaufruf compilert, dann muss state 
auch immer wieder neu gelesen werden (es sei denn die Funktion ist 
const, was sie nicht ist wegen dem volatile Zugriff).

Hinzu kommt noch das loop(), falls nicht geinlinet, ebenfalls ein 
Funktionsaufruf darstellt.  Also muss state auch immer wieder neu 
gelesen werden.

: Bearbeitet durch User
von Jürgen (derkleinemuck)


Lesenswert?

Sorry. Nein.
Ich habe den falschen Link zum Beispiel geschickt. Das ist ja nur eine 
IRS.
Ich meine das Beispiel: 
https://www.arduino.cc/reference/de/language/variables/variable-scope-qualifiers/volatile/ 
:
1
// Die LED wird angeschaltet, wenn der Interrupt-Pin den Status ändert.
2
volatile byte changed = 0;
3
4
void setup() {
5
  // Deklariere den LED-Pin als Output-Pin
6
  pinMode(LED_BUILTIN, OUTPUT);
7
  // Setze einen Interrupt
8
  attachInterrupt(digitalPinToInterrupt(2), toggle, CHANGE);
9
}
10
11
void loop() {
12
  if (changed == 1) {
13
    // toggle() wurde vom Interrupt aus aufgerufen!
14
    // changed auf 0 zurücksetzen
15
    changed = 0;
16
    // LED für 200 ms blinken lassen
17
    digitalWrite(LED_BUILTIN, HIGH);
18
    delay(200);
19
    digitalWrite(LED_BUILTIN, LOW);
20
  }
21
}
22
23
void toggle() {
24
  // Invertiere den Status, dass die LED blinkt
25
  changed = 1;
26
}

von Falk B. (falk)


Lesenswert?

Jürgen schrieb:
> Ich meine das Beispiel:

Mach's mal so
1
void loop() {
2
  while(1) {
3
    if (changed) {
4
      // toggle() wurde vom Interrupt aus aufgerufen!
5
      // changed auf 0 zurücksetzen
6
      changed = 0;
7
      // LED für 200 ms blinken lassen
8
      digitalWrite(LED_BUILTIN, HIGH);
9
      delay(200);
10
      digitalWrite(LED_BUILTIN, LOW);
11
    }
12
  }
13
}

von Johann L. (gjlayde) Benutzerseite


Lesenswert?

Jürgen schrieb:
>     digitalWrite(LED_BUILTIN, HIGH);

Ist auch wieder im selben Alias Set wie changed.

von Jürgen (derkleinemuck)


Lesenswert?

Wie kann ich das verhindern? Das Beispiel ist ja von Arduino und soll 
eigentlich genau das Phänomen demonstrieren.

von Markus F. (mfro)


Lesenswert?

Zuverlässig? Gar nicht.

Der Compiler garantiert (u.a), dass volatile Variablen bei jedem Zugriff 
gelesen (also nicht in Registern gehalten) werden.

Andersrum ist aber ganz und gar nicht garantiert, dass das Weglassen von 
volatile immer in einer Registervariable resultiert (d.h. ein 
vergessenes volatile führt nicht zuverlässig zu einem Fehlverhalten).

von Veit D. (devil-elec)


Lesenswert?

Hallo,

zum demonstrieren könnten 2 Zählvariablen miteinander bzw. 
"gegeneinander" zählen. Eine Variable ist volatile und die andere nicht 
volatile.
Beide werden im gleichen Interrupt inkrementiert, Timer Interrupt o.ä..
Das lässt man sehr zügig machen. Dann lässt man beide Werte bspw. nur 
aller 1s anzeigen. Die Chancen sollten gut stehen, dass man beobachten 
kann, dass beide Werte auseinander driften. Eine andere Idee habe ich 
nicht wie man das Fehlverhalten bewusst sichtbar machen könnte.

Und denke an das atomare "auslesen", sonst hat man andere Effekte. ;-)

: Bearbeitet durch User
von Oliver S. (oliverso)


Lesenswert?

Veit D. schrieb:
> Die Chancen sollten gut stehen, dass man beobachten
> kann, dass beide Werte auseinander driften.

Die Chancen stehen besser, das eine der Variablen sich gar nicht 
verändert.

Oliver

von Veit D. (devil-elec)


Lesenswert?

Hallo,

nö, gezählt wird, dass habe ich schon vorher gewusst. Was mich nur 
wundert ist, das sie nicht wie vermutet auseinander driften. Sowas ist 
leider schwer provozierbar. Timer 1 Normalmode und inkrementiert wird 
mit Overflow Interrupt.

von Εrnst B. (ernst)


Lesenswert?

Veit D. schrieb:
> Was mich nur
> wundert ist, das sie nicht wie vermutet auseinander driften.

Das wundert garnicht.
das "volatile" verhindert, dass der Compiler (falsche) Annahmen über den 
Inhalt der Variablen zur Optimierung verwendet.
Solche Annahmen kann er für die ISR sowieso nicht treffen, da sieht der 
Compiler nicht, wo sie aufgerufen wird, insofern macht innerhalb der ISR 
das "volatile" keinen Unterschied.

Beim Auslesen von Variablen innerhalb von main(), die auch von der ISR 
geändert werden, da macht es einen Unterschied.
Dort kann man dem Compiler mit memory/optimization-Barriers mitteilen, 
wo er eine evtl. geänderte Variable lieber neu aus dem RAM liest. Ist 
praktischerweise in sei(), ATOMIC_BLOCK usw. schon eingebaut.
Oder man macht die variable "volatile", wenn man eine einfache Lösung 
will ohne sich viel mit der Theorie herumzuschlagen.

von Veit D. (devil-elec)


Angehängte Dateien:

Lesenswert?

Hallo,

ich habe innerhalb der ISR kein volatile.
Die beiden Zählvariablen sind global.

ATOMIC_BLOCK verwende ich zum auslesen, damit während des Auslesens kein 
Interrupt dazwischen funkt.

: Bearbeitet durch User
von Εrnst B. (ernst)


Lesenswert?

Veit D. schrieb:
> ich habe innerhalb der ISR kein volatile.

Doch. du verwendest dort die Variable
>> volatile unsigned long countVolatile;

Veit D. schrieb:
> ATOMIC_BLOCK verwende ich zum auslesen

Damit brauchst du das volatile nicht mehr. Der ATOMIC_BLOCK sorgt schon 
dafür, das alles passt.

von Veit D. (devil-elec)


Lesenswert?

Hallo,

du siehst aber schon das die Variablen global angelegt sind?
Würde lokal in der ISR sonst auch keinen Sinn machen.

> Damit brauchst du das volatile nicht mehr. Der ATOMIC_BLOCK sorgt schon
> dafür, das alles passt.

Ist das immer so? Für mich gehörte volatile und atomic immer zusammen 
damit nichts schief geht.

: Bearbeitet durch User
von Εrnst B. (ernst)


Lesenswert?

Veit D. schrieb:
> Für mich gehörte volatile und atomic immer zusammen
> damit nichts schief geht.

der ATOMIC_BLOCK erzwingt schon exakt den Effekt, den du mit volatile 
erreichen willst.

Falls du nachschauen willst, warum:
ATOMIC_BLOCK benutzt cli().

avr/interrupt.h definiert cli() als
1
# define cli()  __asm__ __volatile__ ("cli" ::: "memory")
das ::: "memory" ist dann die Magie:

https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
1
The "memory" clobber tells the compiler that the assembly code performs memory
2
reads or writes to items other than those listed in the input and output operands
3
(for example, accessing the memory pointed to by one of the input parameters).
4
To ensure memory contains correct values, GCC may need to flush specific register
5
values to memory before executing the asm. Further, the compiler does not assume
6
that any values read from memory before an asm remain unchanged after that asm;
7
it reloads them as needed. Using the "memory" clobber effectively forms a
8
read/write memory barrier for the compiler.

von Veit D. (devil-elec)


Lesenswert?

Hallo,

Danke für die Erklärung.

Gibt es Bsp. wofür man volatile alleine verwendet?
Also ohne cli, ATOMIC_BLOCK usw.
Für mich fällt da gerade jeder Grund weg.

von Εrnst B. (ernst)


Lesenswert?

Veit D. schrieb:
> Gibt es Bsp. wofür man volatile alleine verwendet?
1
(volatile) uint8_t flag =0;
2
3
ISR() {
4
  if (something) {
5
    flag=1;
6
  }
7
}
8
9
main() {
10
  print("Warte auf Flag...\n");
11
  while (!flag) {/* Warte...*/}
12
  print("Flag ist true\n");
13
}

Weil flag nur 8 Bit hat, braucht es kein Atomic Block um es sauber zu 
lesen. Aber der Compiler macht u.U. ohne volatile eine Endlosschleife 
aus dem while und optimiert den Rest von main() weg...

: Bearbeitet durch User
von Veit D. (devil-elec)


Lesenswert?

Hallo,

gut zu wissen mit der Optimiererei.
Danke.

von Peter D. (peda)


Lesenswert?

Volatile ist nur für Einzelzugriffe auf 8Bit Variablen sinnvoll.
Für Read/Write oder Mehrbytevariablen bzw. Structs/Arrays muß man Atomic 
benutzen.

Eine versteckte Falle sind 16Byte Register. Greift z.B. ein Interrupt 
auf OCR1A und das Main auf OCR1B zu, muß man im Main auch atomar 
kapseln, denn es wird das gemeinsame 8-bit temporary High Byte Register 
(TEMP) von T1 benutzt.

von Veit D. (devil-elec)


Lesenswert?

Hallo,

gute weitere Info. Danke.

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.