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_ty=0;
8
9
intmain(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
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.
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.
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.
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
volatileuint8_ti=0;
2
3
voidfoo()
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.
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.
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.
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?
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.
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.
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
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:
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
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).
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.
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).
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
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.
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:
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:
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:
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
Wenn man Interrupts global sperren muß, dann bitte so:
1
voidfoo(void)
2
{
3
uint8_tsreg;
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)
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
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.
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.