Forum: Compiler & IDEs AVR-GCC: Variable wird im Interrupt gekillt / Math.h-Frage


von Jadeclaw D. (jadeclaw)


Lesenswert?

Versuche gerade mich in C zurechtzufinden.
Dabei kam nachfolgendes Programm heraus (s. u.)
Problem: Eine der Variablen wird konsequent abgeschossen,
d.h. auf 0 gesetzt.
Welche Variable es erwischt, hängt davon ab, wie diese
Variablen benannt werden, manchmal entscheidet auch die Reihenfolge,
in der die Variablen definiert werden.
Aber, heißt eine der Variablen 'Minuten', dann liegt die
Wahrscheinlichkeit bei 95%, daß es genau diese Variable erwischt.
Unabhängig davon, wie die anderen Variablen benannt werden.
Einige weitere Hinweise im Quelltext.
Frage an die Erfahrenen hier:
Was ist der Grund für diesen Effekt und wie kann man das abstellen?
Bei größeren Projekten sucht man sich bei sowas 'nen Wolf.

Und weil ich gerade dabei bin, ist es eigentlich normal,
daß bei Fließkomma-Arithmetik nur 6 Stellen Genauigkeit hinter
dem Komma erzielt wird? ( Math.h / libm.a / Variablen als Double dekl.)

GCC ist 4.1.2
AVR-LIBC ist 1.46
das Ganze aus dem letzten WinAVR-Paket und über AVRStudio bedient.
Alle Einstellungen auf Defaulteinstellungen,
Optimization steht auf -Os
Controller ist ein ATMega16 mit JTAG-Fuse aus und externem Quarz.
Die LCD-Routinen habe ich mal nicht mitgeliefert,
sind 1:1 aus dem AVR-GCC-Tutorial abkopiert.

1
// Testdatei
2
// CPU = ATMega16
3
4
#include <avr/io.h>
5
#include "lcd-routines.h"
6
#include <avr/pgmspace.h>
7
#include <stdio.h>
8
#include <avr/interrupt.h>
9
10
char StringImFlash[] PROGMEM = "FRG7-UniCounter"; // im "Flash"
11
volatile uint8_t Sekunde;
12
volatile uint8_t Minuten;   /* <-- das Hauptopfer  */
13
volatile uint8_t Minutes;
14
volatile uint8_t Minuten2;
15
volatile uint8_t Stunde;
16
volatile char Numout[2];
17
18
int main(void)
19
{
20
  DDRA = 0x00;
21
  DDRB = 0x00;
22
  DDRC = 0x3F;   /* Displayport */
23
  DDRD = 0xF0;
24
  PORTA = 0xFF;
25
  PORTB = 0xFF;
26
  PORTC = 0x0F;
27
  PORTD = 0xFF;
28
  
29
  lcd_init();
30
31
  set_cursor(0,1);
32
  lcd_string_p(StringImFlash);   /* Einschaltmeldung */
33
34
/* Hier werden die Variablen mit unterschiedlichen Werten initialisiert */
35
/* damit ich sehe, wen es erwischt hat.  */  
36
  Sekunde = 0;
37
  Minuten = 57;
38
  Minuten2 = 58;
39
  Stunde = 23;
40
  Minutes = 55;
41
42
/* Initialisierung von Timer2 und Interrupt, */
43
/* sowie Aktivierung vom 32kHz-Quarz an PC6 & 7 */
44
  ASSR |= (1<<AS2);
45
  TCCR2 |= (1<<WGM21) | (1<<CS20) | (1<<CS22) | (1<<COM20);
46
  OCR2 = 255;
47
  TCNT2 = 0;
48
  TIMSK |= (1<<OCIE2);
49
  sei();
50
51
52
53
  while(1)
54
  {
55
  }
56
57
  return 0;
58
}
59
60
ISR(TIMER2_COMP_vect)   /* Start Uhreninterrupt (Timer2 im Sekundentakt ) */
61
{
62
  Sekunde=Sekunde + 1;
63
  if (Sekunde > 59) /* Minute voll? */
64
  { 
65
    Sekunde = 0; 
66
    Minutes = Minutes + 1;    /* Dann eine Minute dazu */
67
    Minuten2 = Minuten2 + 1;  /* nochmal, damit ich sehe, */
68
                                          /* daß er auch hier durchläuft */
69
  /* weil der beim ersten Auftreten des Fehlers hier schon nicht */
70
        /* weiter kam. */
71
  }
72
  
73
  if (Minutes >= 60)    /* Stunde voll? */
74
  { 
75
    Minutes = 0;
76
    Minuten2 = 0;
77
    Stunde = Stunde + 1;     /* dann eine Stunde dazu */
78
  }
79
80
  if (Stunde >= 24)   /* Tag voll? */
81
  { 
82
    Sekunde = 0;    /*Dann alles von vorne. */
83
    Minutes = 0; 
84
    Stunde = 0; 
85
  }
86
87
88
  set_cursor(0,2);    /* Zweite Zeile vom 4-Zeilen-Display */
89
  
90
/* Nacheinander die Werte getrennt mit ':' ausgeben */
91
92
  sprintf(Numout, "%02d", Stunde);
93
  lcd_string(Numout);
94
  lcd_data(':');
95
96
  sprintf(Numout, "%02d", Minuten);
97
  lcd_string(Numout);
98
  lcd_data(':');
99
100
  sprintf(Numout, "%02d", Minutes);
101
  lcd_string(Numout);
102
  lcd_data(':');
103
104
  sprintf(Numout, "%02d", Minuten2);
105
  lcd_string(Numout);
106
  lcd_data(':');
107
108
  sprintf(Numout, "%02d", Sekunde);
109
  lcd_string(Numout);
110
  
111
}

Gruß
Jadeclaw.

von Klaus F. (kfalser)


Lesenswert?

Die Ansteuerung eines LCD Displays in der Interrupt Routine ist immer 
eine schlechte Idee.
ISR's sollen kurz sein, vielleicht ein paar Flags setzen und dann die HW 
neu aufsetzten.
So Funktionen wie lcd_data(), set_cursor() usw dauern sicher immer lang, 
und haben intern irgendwelche Warteschleifen in denen sie auf das busy 
Signal warten.
Dies gehört niemals in eine ISR.
Schreib dein Programm so um, dass die Ausgabe in der Hauptschleife 
erfolgt, am besten nur dann, wenn sich irgendetwas ändert, und die 
Timer-ISR macht nur das hochzählen.

von Jadeclaw (Gast)


Lesenswert?

Nunja, wie lange das Displayupdate dauert, ist hier relativ egal, da der 
Interrupt nur 1* pro Sekunde aufgerufen wird. Displayupdate ist aber 
auch schnell genug, daß der Interrupt 250 mal die Sekunde einlaufen 
darf, ohne daß es zu einem Rückstau kommt. Davon ab, zumindest wenn ich 
es in Assembler mache, habe ich die Wahl: Entweder ich packe die 
LCD-Ansteuerung komplett in die Interrupts oder ich verzichte ganz auf 
Interrupts. Weil, Interrupts und Displayupdate im Hauptprogramm führt 
dazu, daß das Display langsam aber sicher mit Müll gefüllt wird.

Gruß
Jadeclaw.
( not logged in )

von Michael Wilhelm (Gast)


Lesenswert?

Wie groß ist der Stack initialisiert?

MW

von Walter (Gast)


Lesenswert?

>Weil, Interrupts und Displayupdate im Hauptprogramm führt
>dazu, daß das Display langsam aber sicher mit Müll gefüllt wird.
die Aussage verstehe ich nicht


aber schreib doch mal
volatile char Numout[3];

das wird dein Problem lösen

Walter

von Gast (Gast)


Lesenswert?

Für den Fall das Du ein Scope hast:

Vielleicht solltest Du zum prüfen,
ob der IRQ wirklich nur 1 mal die Sekunde
kommt, am Anfang er Interrutroutine einen
Portpin togglen. Das schaust Du Dir dann mit dem
Scope an.

von jl (Gast)


Lesenswert?

wie Walter schon schrieb:
volatile char Numout[3];

in deiner print funktion willst du eine Dezimalzahl mit 2Stellen 
ausgeben. Im C gibt es aber immer eine 0x00 als Stringendekennzeichnung. 
Der braucht auch auch ein Byte Speicherplatz.

Jörg

von Jadeclaw (Gast)


Lesenswert?

@Gast: Der kommt wirklich nur 1 mal pro Sekunde, es ist Timer2 im Output 
Compare Mode, da laß ich ihn OC2 (Pin21) toggeln.
Prescaler = 128, OCR2 = 255. Macht 32768 bei einem 32768Hz-Uhrenquarz.

Walter schrieb:
>> Weil, Interrupts und Displayupdate im Hauptprogramm führt
>> dazu, daß das Display langsam aber sicher mit Müll gefüllt wird.
> die Aussage verstehe ich nicht.

Das stellte sich so dar, daß während die normalen Daten ausgegeben 
wurden, auf den freien Bereichen des Displays zufällige Zeichen 
erscheinen, bis so langsam das ganze Display voll ist. Schalte ich 
Interrupts ab, bleibt das Display sauber, verteile ich die 
Displayupdates auf die Interrupts und fasse das Display nach der 
erstmaligen Initialiserung im Hauptprogramm nicht mehr an, bleibt das 
Display ebenfalls sauber. Getestet in Assembler auf ATMega8 und 
ATTiny2313.


Walter schrieb:
> aber schreib doch mal
> volatile char Numout[3];
> das wird dein Problem lösen

jL schrieb:
> wie Walter schon schrieb:
> volatile char Numout[3];
> in deiner print funktion willst du eine Dezimalzahl mit 2Stellen
> ausgeben. Im C gibt es aber immer eine 0x00 als Stringendekennzeichnung.
> Der braucht auch auch ein Byte Speicherplatz.

Je mehr ich darüber nachdenke, um so logischer erscheint mir das.
Mit anderen Worten: Der Compiler knallt mir das Terminierungsnullbyte in 
eine andere Variable rein, weil ich das Numout zu kurz gemacht habe. 
Gut, probiere ich das mal heute Abend aus, bin gerade außer Haus.

Bleibt noch die Frage nach der schlechten Genauigkeit im 
Fließkommabereich. Erwarte ich da zuviel?

Gruß
Jadeclaw.

von Karl H. (kbuchegg)


Lesenswert?

Jadeclaw wrote:
>> wie Walter schon schrieb:
>> volatile char Numout[3];
>> in deiner print funktion willst du eine Dezimalzahl mit 2Stellen
>> ausgeben. Im C gibt es aber immer eine 0x00 als Stringendekennzeichnung.
>> Der braucht auch auch ein Byte Speicherplatz.
>
> Je mehr ich darüber nachdenke, um so logischer erscheint mir das.
> Mit anderen Worten: Der Compiler knallt mir das Terminierungsnullbyte in
> eine andere Variable rein, weil ich das Numout zu kurz gemacht habe.
> Gut, probiere ich das mal heute Abend aus, bin gerade außer Haus.

Was du aus dieser Erfahrung mitnehmen solltest:
* Es ist meistens keine gute Idee, Character Arrays die Strings
  aufnehmen sollen, zu klein zu dimensionieren. Lieber ein
  paar Bytes mehr reservieren.
  Grund: Irgendwann dreht man mal am Format String (in diesem
  Fall) und wenn man dann keine Reserven mehr hat, dann knallts
  ganz gewaltig.

* Wenn irgendwie möglich, verwende keine Funktionen, die einen String
  erzeugen und in deren Argumentliste es keinen Parameter gibt, der
  die Größe des empfangenden Character Arrays angibt. Wenn es eine
  derartige Funktion gibt, dann sollte die Version ohne diesen Parameter
  tabu sein.

  Im gegenständlichen Fall: snprintf  verwenden!
1
  snprintf( Numout, sizeof( Numout), "%02d", Stunde );


>
> Bleibt noch die Frage nach der schlechten Genauigkeit im
> Fließkommabereich. Erwarte ich da zuviel?

Ja. Aus 4 Byte ist nunmal nicht mehr herauszuholen.

von Peter D. (peda)


Lesenswert?

Jadeclaw wrote:

> Das stellte sich so dar, daß während die normalen Daten ausgegeben
> wurden, auf den freien Bereichen des Displays zufällige Zeichen
> erscheinen, bis so langsam das ganze Display voll ist.

Ja, woher soll denn das arme Display wissen, ob die Daten nun vom 
Interrupt oder Main kommen.

Das Main schreibt was, nun haut der Interrupt dazwischen und schreibt 
auch was. Im günstigsten Fall hast du nun den halben Text des Main, dann 
den vom Interrupt und dann den Rest des Main. In der Regel hast Du aber 
Zeichensalat.
Ausgaben dürfen entweder nur vom Main oder nur vom Interrupt erfolgen.
Oder das Main sperrt die Interrupts für die komplette Dauer der Ausgabe.

Das gleiche gilt natürlich auch für alle anderen Arten Interfaces (UART, 
SPI, I2C).

Und auch Timerzugriffe beinhalten Stolperfallen, wenn der Interrupt 
nicht das temporäre High-Register sichert.


Wenn es sich nicht vermeiden läßt, eine Peripherie im Interrupt und Main 
gleichzeitig zu benutzen, immer dran denken, daß ein Interrupt das Main 
an jeder beliebigen Stelle unterbrechen kann.


Peter

von Jadeclaw (Gast)


Lesenswert?

Peter Dannegger schrieb:
> Ausgaben dürfen entweder nur vom Main oder nur vom Interrupt erfolgen.
Genau das ist der Fall.
Ausgabe erfolgt NUR über den Main, die Interrupts fassen das Display 
nicht an, nicht mal irgendeinen noch freien Pin am Displayport, dennoch 
füllt sich das Display mit Müll. Interrupts aus = alles Ok.
Ausgabe NUR über Interrupt, auch über verschiedene Vektoren = alles ok.

Gruß
Jadeclaw.
( not logged in )

von Karl H. (kbuchegg)


Lesenswert?

Jadeclaw wrote:
> Peter Dannegger schrieb:
>> Ausgaben dürfen entweder nur vom Main oder nur vom Interrupt erfolgen.
> Genau das ist der Fall.
> Ausgabe erfolgt NUR über den Main, die Interrupts fassen das Display
> nicht an, nicht mal irgendeinen noch freien Pin am Displayport, dennoch
> füllt sich das Display mit Müll.

Der LCD Funktion ist es sch...egal, wie gross du das Array
definiert hast, die interessiert sich ganz einfach nicht dafür.
Diese Funktion bekommt eine Startadresse im Speicher. Von dort
holt sie Byte für Byte und malt das entsprechende Zeichen auf
das Display. Solange bis das nächste Byte im Speicher den Inhalt
0 hat.
Du kannst dein char Array definieren wie du lustig bist,
interessiert die Ausgabefunktion nicht die Bohne. Die
hört erst dann mit der Ausgabe von Zeichen aus, bis sie ein
0 Byte im Speicher vorfindet. Wenn da kein 0 Zeichen ist,
dann gibt diese Funktion auch 2 Gigabyte aus (ok, auf einem
AVR nicht, weil der nicht genug Speicher hat :-), bis sie
dann das erste mal auf ein 0 im Speicher trifft.

Wie sich dein Müll auf dem LCD konkret aufbaut, hängt
nicht unwesentlich von der zeitlichen Abfolge der
Voorgänge ab. Wenn die lcd_ausgabe losläuft, noch
ehe der sprintf die Chance hatte, das 0 Byte zu schreiben,
tja, dann passiert sowas schon mal.

Was da konkret passiert, kann man nur vermuten wenn man
das konkrete Programm sieht. Aber irgendwo gibt es mit
ziemlicher Sicherheit eine Race-Kondition, so dass
die Ausgabefunktion das abschliessende 0 nicht sieht,
weil sie zu spät generiert wird.

In deinem fall kommt natürlich noch erschwerend dazu, dass
du zumindest im Original einen Buffer-Overflow hattest.

sprintf( NumOut, "%2d", Stunden );

schreibt nun mal 3 Zeichen und überschreibt damit eine Variable.
schlägt jetzt der Interrupt zwischen dem sprintf und dem
nachfolgenden lcd_string zu, so verändert die InterruptRoutine
diese Variable und zerstört dadurch das den String abchliessende
0 Byte. Woraufhin lcd_string auf der Suche nach dem 0 Byte
den halben Speicher ausdumpt.

von Marvin M. (Gast)


Lesenswert?

Wenn das Display wirklich von unterschiedlichen Programmteilen, die 
teilweise im Interrupt laufen, mit Daten versorgt werden muss, dann 
nutze ich einen Buffer.
In einem Timer-Interrupt wird dieser Buffer regelmäßig zum Display 
geschickt, die einzelnen Programmteile greifen dann nur auf den globalen 
Buffer zu. Dadurch kann man Datensalat verhindern und man verhindert 
Delays in den Interrupts.

von Jadeclaw (Gast)


Lesenswert?

@Marvin: Ich hatte sowieso vor, Programm und die ganze Ausgabemimik 
voneinander zu trennen, damit das Ganze leichter wartbar wird und ein 
Displaytypwechsel schmerzlos vonstatten geht. Ich bin mit mir noch nicht 
ganz einig, ob es beim LC-Display bleibt. Das Ganze wird ein größeres 
Gerät, welches zum einen ein brauchbares Meßgerät abgeben soll, zum 
anderen einen vorhandenen KW-Empfänger um diverse Dinge erweitern soll 
(Frequenzanzeige, RTTY-Terminal, Beacon-Timer, Audiofiltersteuerung). 
Die Idee mit dem Displaybuffer erscheint mir auch am sinnvollsten, da 
ich dann am Rest nichts mehr umbauen muß, wenn ich das Display wechsle.

@Karl heinz Buchegger: Die Sache mit dem Müll im Display hat nichts mit 
dem GCC-Problem zu tun, sondern war eine frühere Beobachtung, bei der 
ich mir auch einen Wolf gesucht habe und habe ich als Antwort auf die 
Anmerkung von Klaus Falser von 8:25 Uhr erwähnt.

Gruß
Jadeclaw.
( not logged in )

von Falk B. (falk)


Lesenswert?

@ Marvin M. (Gast)

>Wenn das Display wirklich von unterschiedlichen Programmteilen, die
>teilweise im Interrupt laufen, mit Daten versorgt werden muss, dann
>nutze ich einen Buffer.

GENAU!

Interrupt

MFG
Falk

von Walter (Gast)


Lesenswert?

>Ausgabe erfolgt NUR über den Main, die Interrupts fassen das Display
>nicht an, nicht mal irgendeinen noch freien Pin am Displayport, dennoch
>füllt sich das Display mit Müll. Interrupts aus = alles Ok.

in deinem Programm erfolgt die Ausgabe aber im Interrupt!

von Jadeclaw D. (jadeclaw)


Lesenswert?

Walter schrieb:
> aber schreib doch mal
> volatile char Numout[3];
> das wird dein Problem lösen

jL schrieb:
> wie Walter schon schrieb:
> volatile char Numout[3];
> in deiner print funktion willst du eine Dezimalzahl mit 2Stellen
> ausgeben. Im C gibt es aber immer eine 0x00 als Stringendekennzeichnung.
> Der braucht auch auch ein Byte Speicherplatz.

Jupp, das war es auch.  >in die Tischplatte beiß<
Da hätte ich auch selbst drauf kommen müssen.

Karl heinz Buchegger schrieb:
>> Bleibt noch die Frage nach der schlechten Genauigkeit im
>> Fließkommabereich. Erwarte ich da zuviel?
>Ja. Aus 4 Byte ist nunmal nicht mehr herauszuholen.
Nur 4 Byte für double? Dann brauche ich mich ja nicht zu wundern.
Dann ist hier wohl Handarbeit angesagt.


Walter (Gast) schrieb:
> in deinem Programm erfolgt die Ausgabe aber im Interrupt!
Der Zumüllungseffekt hat mit diesem Programm nichts zu tun.
Das war eine andere Sache.

Gruß
Jadeclaw.

von Simon K. (simon) Benutzerseite


Lesenswert?

Jadeclaw Dinosaur wrote:
>>Ja. Aus 4 Byte ist nunmal nicht mehr herauszuholen.
> Nur 4 Byte für double? Dann brauche ich mich ja nicht zu wundern.
> Dann ist hier wohl Handarbeit angesagt.

Jep, sizeof(float) == sizeof(double) == 4 beim avr-gcc.

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.