Forum: Mikrocontroller und Digitale Elektronik AVR GCC: Problem mit Variable in Main und ISR


von Sven (Gast)


Lesenswert?

Hallo,

beim Programmieren meines Atmega16 ist mir folgendes aufgefallen, was 
mir Kopfzerbrechen bereitet:
1
#define F_CPU 16000000UL
2
#include <avr/io.h>
3
#include <util/delay.h>
4
#include <avr/interrupt.h>
5
6
7
uint8_t y=0;
8
9
int main(void)
10
11
{
12
  
13
  DDRD=0x03;
14
  PORTD=0x00;  
15
16
  sei();  
17
  TCCR1B = 0x01;
18
  TIMSK|=(1<<TOIE1);  
19
  
20
  while (1) 
21
  {
22
23
    y=1;
24
25
  }
26
27
}
28
    
29
ISR(TIMER1_OVF_vect)
30
{
31
  
32
  if (y==1){
33
    
34
    PORTD=(1<<PORTD0);  
35
  
36
  }  
37
          
38
}

In der While-Schleife wird der Variable y der Wert 1 übergeben. In der 
ISR befindet sich eine Verzweigung, die den PIN0 von PORTD auf 1 setzt, 
sobald y den Wert 1 hat. In der Praxis wird dieser PIN jedoch nie 1, 
obwohl längst y=1 sein müsste. Das Programm funktioniert nur dann, wenn 
ich in die While-Schleife weitere Befehle wie z.B. "_delay_ms(100)" 
schreibe oder die PINs von anderen PORTS manipuliere.

Hat jemand eine Idee, was hier Sache ist und wo mein Denkfehler liegt?

MfG
Sven

von Hoschti (Gast)


Lesenswert?

Der Compiler ist so schlau und merkt, dass Du "y" in der main nicht 
weiter benutzt/veränderst. Daher kann er diese Variable in einem 
Register halten. Deklariere sie als "volatile". Dann wird sie nach jeder 
Veränderung wieder ins RAM geschrieben.

Noch eine Anmerkung: Die Zuweisung "uint8_t y=0;" braucht's nicht. Im 
C-Standard werden alle globalen Variablen per Definition mit "0" 
vorbelegt.

von Stefan F. (Gast)


Lesenswert?

Ja, volatile ist richtig.

Bei Variablen, die größer als ein Byte sind, muss man noch etwas mehr 
aufpassen:

1) Im Hauptprogramm lesen:

Weil die CPU die Bytes mit mehreren CPU befehlen vom RAM in Register 
kopiert, könnte eine Interruptroutine dazwischen funken und 
inkonsistente Daten erzeugen. Wenn die Variable zum Beispiel den Wert 
255 hat und die ISR sie zwischendurch  um 1 erhöht, dann sieht das 
Hauptprogramm folgenden Wert:

1. Low-Byte kopieren: 255
2. High-Byte kopieren: 1 (weil zwischendurch inkrementiert wurde)

Macht zusammen: 511, richtig wäre aber 256.

2) Im Hauptprogramm schreiben:

Weil die CPU die Bytes mit mehreren CPU befehlen vom Register ins RAM 
kopiert, könnte eine Interruptroutine dazwischen kommen und 
inkonsistente Daten lesen. Gleiches Problem wie oben, das würde sich 
aber leicht durch Sperren von Interrupts mit sei() und cli() umgehen 
lassen.

Beim Fall 1) ist es mit cli() und sei() nicht so einfach getan, denn es 
könnte sein, dass die ISR zu diesem Zeitpunkt bereits läuft. Dann nützt 
die Sperre gar nichts.

Dazu gibt es lange Aufsätze, wo die Vor- und Nachteile diverser 
Lösungsansätze erklärt werden. Stichwort: Semaphoren und Locking.

von Dr. C (Gast)


Lesenswert?

Hoschti schrieb:
> Der Compiler ist so schlau und merkt, dass Du "y" in der main nicht
> weiter benutzt/veränderst. Daher kann er diese Variable in einem
> Register halten. Deklariere sie als "volatile". Dann wird sie nach jeder
> Veränderung wieder ins RAM geschrieben.

Das ist nicht der Grund. Auf das Register kann ja sowohl in der Main als 
auch in der IRQ-Routine zugegriffen werden.
Der Compiler geht allerdings davon aus, dass die Variable y=0 ist, das 
sie ja global initialisiert und dann in der Main nicht mehr geändert 
wurde. Also spart er sich den Vergleich.

> Noch eine Anmerkung: Die Zuweisung "uint8_t y=0;" braucht's nicht. Im
> C-Standard werden alle globalen Variablen per Definition mit "0"
> vorbelegt.

Daher als Tip für Sven:
Erst mal ein C Buch lesen. Das sind nämlich Grundlagen.

von Stefan F. (Gast)


Lesenswert?

Der Compiler ist bestrebt, den Code so weit wie möglich zu reduzieren. 
Das kann er auch ausgesprochen gut, finde ich.

Nur was er nicht kann ist: Berücksichtigen, das Interruptroutinen 
jederzeit dazwischen funken können. Da muss man manuell nachhelfen.

Das Schlüsselwort volatile führt beim GCC dazu, dass die Variable im RAM 
liegt und bei jedem einzelnen Zugriff mit dem RAM synchronisiert wird. 
Also wenn du zweimal hintereinander "i++" schreibst:
1
volatile uint8_t i=0;
2
3
void foo()
4
{
5
    i++;
6
    i++;
7
}

Dann wird die Variable zwei mal aus dem RAM in ein Register kopiert, 
incrementiert und zurück ins RAM geschrieben.

Ohne Volatile würde der Compiler sie nur einmal aus dem RAM holen und 
erst zurück schreiben, nachdem sie 2x incrementiert wurde.

von Falk B. (falk)


Lesenswert?

Siehe Interrupt.

von Dr. Sommer (Gast)


Lesenswert?

Dr. C schrieb:
> Das ist nicht der Grund. Auf das Register kann ja sowohl in der Main als
> auch in der IRQ-Routine zugegriffen werden.

Der GCC macht aber keine Funktions-übergreifenden Registerzugriffe. Da 
jede Funktion Register anders benutzt, weiß man innerhalb einer ISR 
nicht, was jetzt in welchem Register steht.
Tatsächlich kennt C einfach keine ISR's, die sind gewissermaßen ein 
"Hack". Daher geht bei der automatischen Optimierung gerne mal was 
verloren, was für ISR's wichtig wäre, nämlich diese Zuweisung. Mit 
"volatile" wird diese Optimierung verboten. Globale Variablen landen 
beim GCC übrigens immer im RAM, nicht (nur) in Registern.

von Stefan F. (Gast)


Lesenswert?

Ich denke, Dr. C bezog sich auf die C Sprach-Spezifikation im 
allgemeinen. Und die erlaubt durchaus, dass eine Variable in einem 
Register zuhause sein kann. Wenn das dann so ist, ergibt seine 
zusätzliche Erklärung durchaus Sinn, denn er hat erklärt, warum auch 
Register-Variablen eventuell als volatile gekennzeichnet werden müssen.

Wir haben uns hier allerdings auf die konkrete Implementierung des 
avr-gcc bezogen.

von Dr. Sommer (Gast)


Lesenswert?

Stefanus F. schrieb:
> Ich denke, Dr. C bezog sich auf die C Sprach-Spezifikation im
> allgemeinen. Und die erlaubt durchaus, dass eine Variable in einem
> Register zuhause sein kann.

Ja... "Globale" Register-Zuordnungen sind aber tatsächlich ziemlich 
unüblich. Kennst du einen Compiler der so etwas macht?

von Stefan F. (Gast)


Lesenswert?

Dr. Sommer schrieb:
> Ja... "Globale" Register-Zuordnungen sind aber tatsächlich ziemlich
> unüblich. Kennst du einen Compiler der so etwas macht?

Der gcc kann es: 
https://gcc.gnu.org/onlinedocs/gcc-4.7.0/gcc/Global-Reg-Vars.html

Ist aber schon ein exotisches Szenario.

von Bernd K. (prof7bit)


Lesenswert?

Stefanus F. schrieb:
> ist es mit cli() und sei() nicht so einfach getan, denn es
> könnte sein, dass die ISR zu diesem Zeitpunkt bereits läuft.

Was? kannst Du mal kurz schlüssig erläutern wie nach einem cli noch 
ein Interrupt kommen können soll?

Auch würd mich mal die rätselhafte Bedeutung von "bereits läuft" 
interessieren auf einem Einkern-Prozessor, also wie die main noch 
gleichzeitig(?!) weiterlaufen soll wenn gerade in dem Moment ein 
Interrupt abgearbeitet wird.

von Stefan F. (Gast)


Lesenswert?

Bernd K. schrieb:
> Stefanus F. schrieb:
>> ist es mit cli() und sei() nicht so einfach getan, denn es
>> könnte sein, dass die ISR zu diesem Zeitpunkt bereits läuft.
>
> Was? kannst Du mal kurz schlüssig erläutern wie nach einem cli noch
> ein Interrupt kommen können soll?

1. ISR wird gestartet
2. Hauptprogramm sperrt erst jetzt Interrupts
äääähhhhh
3. Hauptprogramm liest das erste Byte der Variable
4. IST verändert die Variable
5. Hauptprogramm liest das zweite Byte der Variable

Sorry, ich habe gerade einen 2-Kern Rechner vor der Nase. Der AVR kann 
das ja gar nicht, da er nur einen Kern hat.

von c.K. (Gast)


Lesenswert?

So lange die CPU in der ISR ist, wird das Hauptprogramm nicht 
ausgeführt, und kann dem entsprechend die Interrupts nicht sperren

von Arno (Gast)


Lesenswert?

Stefanus F. schrieb:
> Bernd K. schrieb:
>> Was? kannst Du mal kurz schlüssig erläutern wie nach einem cli noch
>> ein Interrupt kommen können soll?
>
> Sorry, ich habe gerade einen 2-Kern Rechner vor der Nase. Der AVR kann
> das ja gar nicht, da er nur einen Kern hat.

Wobei es immer wieder mal Bugs in der Hardware gibt, durch die manche 
Flags erst verzögert wirken. Bei der recht einfachen Architektur der 
AVRs halte ich das aber für unwahrscheinlich, das wird tendenziell eher 
mit Cache und insbesondere Prefetching interessant.

MfG, Arno

von HildeK (Gast)


Lesenswert?

Hoschti schrieb:
> Der Compiler ist so schlau und merkt, dass Du "y" in der main nicht
> weiter benutzt/veränderst.

Das wird aber getan. Er initialisert mit '0' und ändert sie dann in der 
while(1) Loop auf '1'.
Irgendwann kommt der Interrupt und er müsste feststellen, dass sie '1' 
ist und den Port setzen.
Selbst wenn er optimiert, dann könnte er nur auf die '1' festlegen, und 
dann müsste PD0 auch gesetzt werden.

Dr. C schrieb:
> Der Compiler geht allerdings davon aus, dass die Variable y=0 ist, das
> sie ja global initialisiert und dann in der Main nicht mehr geändert
> wurde.

Sie wird doch in der main geändert - oder was übersehe ich da?

Sven schrieb:
1
 while (1)
2
   {
3
4
     y=1;
5
 
6
   }

von malsehen (Gast)


Lesenswert?

Sven schrieb:
> DDRD=0x03;
> PORTD=0x00;

sollte wohl DDRD=0x01 sein.

von malsehen (Gast)


Lesenswert?

malsehen schrieb:
> sollte wohl DDRD=0x01 sein.

Tschuldigung, hab Bumhug geschrieben.

Wird die ISR überhaupt aufgerufen?

von Norbert (Gast)


Lesenswert?

hab nur mal so ein bischen überflogen,
hat zwar mit deinem Problem glaub nichts zu tun,
aber ich würde sei() erst nach der initialisierung der timer register 
aufrufen

von A. S. (Gast)


Lesenswert?

Hoschti schrieb:
> Der Compiler ist so schlau und merkt, dass Du "y" in der main nicht
> weiter benutzt/veränderst. Daher kann er diese Variable in einem
> Register halten.

Wieso darf er das? Der Compiler kann nicht ahnen, welche anderen 
Translation Units dieses y benutzen.

@TO: Reden wir hier von C? oder C++?

Ist der Code original? Oder "aufgehübscht"?

Kannst Du den Assembler-Code posten? (nicht drüber nachdenken, den 
irgendwie zu bearbeiten, einfach posten).

von Dr. Sommer (Gast)


Lesenswert?

Achim S. schrieb:
> Wieso darf er das? Der Compiler kann nicht ahnen, welche anderen
> Translation Units dieses y benutzen.

Andere TU's können y aber nicht gleichzeitig nutzen. Höchstens während 
die main() eine Funktion in der anderen TU aufgerufen hat.

von Stefan F. (Gast)


Lesenswert?

Achim S. schrieb:
> Wieso darf er das? Der Compiler kann nicht ahnen, welche anderen
> Translation Units dieses y benutzen.

Kommt drauf an, wie man den Compiler aufruft. Es ist möglich (wenn auch 
unüblich), das gesamte Programm mit einem einzelnen gcc Aufruf zu 
compilieren. Dann ist so etwas machbar, weil der Compiler ganz genau 
weiß, was wann benutzt wird (außer Interrupts).

von Arno (Gast)


Lesenswert?

Achim S. schrieb:
> Hoschti schrieb:
>> Der Compiler ist so schlau und merkt, dass Du "y" in der main nicht
>> weiter benutzt/veränderst. Daher kann er diese Variable in einem
>> Register halten.
>
> Wieso darf er das? Der Compiler kann nicht ahnen, welche anderen
> Translation Units dieses y benutzen.

Was im C-Standard nicht vorgesehen ist, das ist nebenläufiger 
Programmablauf - egal ob durch IRQs oder Multi-Threading. Deswegen darf 
der Compiler davon ausgehen, dass y z.B. in der while(1)-Schleife nur 
durch Befehle innerhalb der Schleife verändert wird.

Und er kann erkennen, dass while(1) (ohne break) eine Endlosschleife ist 
und alles was danach kommt nie aufgerufen wird. Das kann also alles 
wegoptimiert werden. Und er kann erkennen, dass y nur bei der Zuweisung 
verwendet wird, das Ergebnis davon wird nie verwendet, er kann die 
Zuweisung also auch ganz weglassen.

Anders wäre es, wenn in der Schleife eine Funktion aus einer anderen 
Translation Unit aufgerufen werden würde - dann müsste vor dem 
Funktionsaufruf y wieder in den Speicher geschrieben werden, weil genau 
wie du schreibst, der Compiler ja nicht ahnen kann, ob die aufgerufene 
Funktion dieses y nicht vielleicht benutzt.

Anders wäre es auch, wenn die Schleife nicht endlos wäre. Dann müsste 
ggf. beim Verlassen der Schleife y wieder in den Speicher geschrieben 
werden, falls anschließend Funktionen aus anderen Translation Units 
aufgerufen werden. Oder falls wir nicht von main() sondern von einer 
anderen Funktion reden, die irgendwann zurückspringt.

MfG, Arno

von HildeK (Gast)


Lesenswert?

Arno schrieb:
> Und er kann erkennen, dass y nur bei der Zuweisung
> verwendet wird, das Ergebnis davon wird nie verwendet, er kann die
> Zuweisung also auch ganz weglassen.

Das dürfte der entscheidende Punkt sein! Und hat mir in meinen 
Überlegungen gefehlt.
Mit dem 'volatile' wird ihm dann mitgeteilt, dass er diese 
Schlussfolgerung ("Ergebnis wird nicht verwendet") nicht machen darf.

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Stefanus F. schrieb:
> Kommt drauf an, wie man den Compiler aufruft. Es ist möglich (wenn auch
> unüblich), das gesamte Programm mit einem einzelnen gcc Aufruf zu
> compilieren. Dann ist so etwas machbar, weil der Compiler ganz genau
> weiß, was wann benutzt wird (außer Interrupts).

Das ist so nicht korrekt. Es ist beim gcc vollkommen egal, ob Du zwei 
Übersetzungsmodule einzeln übersetzt oder beide zusammen.

Also:
1
$ cat a.c
2
int mult (int a, int b)
3
{
4
    return a * b;
5
}
1
$ cat b.c
2
#include <stdio.h>
3
4
extern int mult (int, int);
5
6
int main ()
7
{
8
    printf ("%d\n", mult (3, 4));
9
    return 0;
10
}

Einzeln übersetzt:
1
$ cc -O -c a.c
2
$ cc -O -c b.c
3
$ cc a.o b.o -o mult1

Zusammen übersetzt:
1
$ cc -O a.c b.c -o mult2

Ein Vergleich per
1
$ cmp mult1 mult2

ergibt, dass beide Executables identisch sind.

Warum das so ist, kannst Du auch gut daran erkennen, wenn Du
1
$ cc -O -v a.c b.c -o mult2

aufrufst. Der cc (resp. gcc) ruft nämlich den eigentlichen Compiler 
cc1 einzeln für a.c und b.c auf. Du hast damit also gar nichts 
gewonnen.

Aber ein klitzekleines Körnchen Wahrheit liegt doch noch in Deiner 
Aussage. Wenn Du nämlich LTO nutzt, dann kann der gcc modulübergreifend 
optimieren:
1
$ cc -flto -O a.c b.c -o mult3

mult3 ist dann ein paar hundert Bytes kleiner als mult1 bzw. mult2.

Aber auch hier ist es egal, ob Du beide zusammen oder beide einzeln 
kompilierst:
1
$ cc -flto -O -c a.c
2
$ cc -flto -O -c b.c
3
$ cc -flto a.o b.o -o mult4

mult4 ist danach identisch mit mult3.

: Bearbeitet durch Moderator
von guest (Gast)


Lesenswert?

Frank M. schrieb:
> Aber ein klitzekleines Körnchen Wahrheit liegt doch noch in Deiner
> Aussage. Wenn Du nämlich LTO nutzt, dann kann der gcc modulübergreifend
> optimieren:
1
-fwhole-program
gibt es auch noch

von Sven (Gast)


Lesenswert?

Ich hab die Lösung selbst gefunden:
In der while-Schleife müssen die Interrupts aktiviert werden. Wieso auch 
immer, diese wurden auf dem Weg zur while-Schleife deaktiviert:
1
sei();

von Harry L. (mysth)


Lesenswert?

Sven schrieb:
> diese wurden auf dem Weg zur while-Schleife deaktiviert:

Mit Sicherheit nicht!

von Apollo M. (Firma: @home) (majortom)


Lesenswert?

Stefanus F. schrieb:
> das würde sich
> aber leicht durch Sperren von Interrupts mit sei() und cli() umgehen
> lassen.

... das ist nicht optimal bzw. buggy ;-)

schaut mal bitte hier rein #include <util/atomic.h> und nutzt
besser die ATOMIC makros.

wenn nur sei/cli genutzt werden könnten status infos verloren gehen!


mt

von Harry L. (mysth)


Lesenswert?

Wenn man Interrupts global sperren muß, dann bitte so:
1
void foo(void)
2
{
3
uint8_t sreg;
4
5
  sreg = SREG;
6
  cli();
7
...
8
,,,
9
,,,
10
  SREG = sreg;  // der alte Zustand wird wieder hergestellt
11
}

Hintergrund:
da man beim Eintritt in die Funktion nicht weiss, ob das globale 
Interrupt-Flag gesetzt ist, sichert man das am Anfang, schaltet die 
Interrupts danach ab, und stellt am Ende das Flag wieder her.

So stehts auch in irgendeiner App-Note von Microchip. (bin gerade zu 
faul zum suchen)

: Bearbeitet durch User
von Apollo M. (Firma: @home) (majortom)


Lesenswert?

Sven schrieb:
> Ich hab die Lösung selbst gefunden:
> In der while-Schleife müssen die Interrupts aktiviert werden. Wieso auch
> immer, diese wurden auf dem Weg zur while-Schleife deaktiviert:
> sei();

das ist definitive nicht "finden einer lsg." sondern trail and error 
bzw. blindes rumgestocher!

da geht niemals irgendwas verloren ... schon garnicht das globale 
interrupt enable.

zeige hier nochmals deinen jetzt aktualiesierten/verwendeten 
vollständigen source code und dann sag ich was dazu.


mt

von Sven (Gast)


Lesenswert?

Wenn ich das folgendermaßen programmiere, macht der uC das, was er soll:
1
#define F_CPU 16000000UL
2
#include <avr/io.h>
3
#include <util/delay.h>
4
#include <avr/interrupt.h>
5
6
int y;
7
uint32_t x;
8
9
int main(void)
10
11
{
12
  
13
  DDRC=0x03;
14
  PORTC=0x00;
15
16
  sei();
17
  TCCR1B = 0x01;
18
  TIMSK|=(1<<TOIE1);
19
  
20
  while (1)
21
22
  {
23
24
    y=1;
25
    sei();
26
  }
27
28
}
29
30
ISR(TIMER1_OVF_vect)
31
{
32
  
33
  if (y==1){
34
    
35
    PORTC=(1<<PORTC0);
36
    
37
  }
38
  
39
}

von Frank M. (ukw) (Moderator) Benutzerseite


Lesenswert?

Sven schrieb:
> Wenn ich das folgendermaßen programmiere, macht der uC das, was er
> soll:

Was soll er denn machen? Immer, wenn in der Main-Schleife y gesetzt 
wird, soll die ISR nach einem Timer-Overflow das Portbit setzen?

Eigentlich macht man das bei größeren Aktionen anders herum: Bei einem 
Timeroverflow setzt die ISR das Bit, welches in der main-Schleife 
gepollt wird. Da hier aber das Bit-Setzen in der ISR wirklich flott 
vonstatten geht, ist das okay so - auch wenn ich nicht ganz das Motiv 
hier verstehe.

Jetzt zu Deinem Source:

> int y;

Falsch: Da die Variable sowohl in ISR als auch in main() benutzt wird, 
muss die globale Variable als volatile definiert werden - spätestens 
dann, wenn Du den Optimierer für den Compiler einschaltest.

Schreibe also:

volatile int y;

While-Schleife:

>   while (1)
>   {
>     y=1;
>     sei();
>   }

Das sei() innerhalb der Schleife ist hyperfluid, da die Interrupts 
bereits vor der Schleife eingeschaltet wurden. Die Zeile kannst Du also 
löschen.

Jedoch solltest Du bei int-Variablen, die Du sowohl in ISR als auch in 
main() verwendest, vorsichtig sein. Bei den 8-Bit-AVRs geschieht das 
Lesen/Schreiben von 16-Bit-Integer-Variablen in 2 Schritten. Die Aktion 
y=1 ist also nicht atomar. Da Du sowieso nur ein lächerliches Bit der 
Variablen nutzt, solltest Du besser schreiben:

#include <stdint.h>
volatile uint8_t y;

Damit ist die Variable y nur noch 8 Bit breit und kann vom AVR atomar 
gelesen und beschrieben werden. Dadurch kommt es zu keinen 
Komplikationen, wenn gerade beim Schreiben von y die ISR die 
main-Schleife unterbricht.

: Bearbeitet durch Moderator
von Carl D. (jcw2)


Lesenswert?

AVRlibc zu sei():
"the macro also implies a memory barrier which can cause additional loss 
of optimization."

D.h. sei() bewirkt hier eher ein implizites volatile für y und ändert 
damit nicht das Lesen dieser in der ISR (die nie gesperrt war), sonder 
das Schreiben in der Main-Loop. Ohne wird y vermutlich nie beschrieben, 
da es ja in den Endlosschleife nicht mehr benutzt wird.

Statt zu raten sollte man sich in solchen Fällen einfach ein Listfile 
von obj-dump anlegen lassen. Da steht dann der tatsaachlich ausgeführte 
Code drin.

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.