Forum: Mikrocontroller und Digitale Elektronik Seltsame Probleme mit Arduino Sketch - Out of Memory?


von Joerg P. (pleumann)


Lesenswert?

Tag zusammen,

ich arbeite an einem Arduino-Uno-Projekt, das eine fremde Library, eine 
eigene Library und den Code des eigentlichen Sketches beinhaltet. 
Eigentlich sind es sogar mehrere Sketches, aber für dieses Posting 
relevant ist einer, der Stresstests für die Library durchführt. Die 
Library steuert einen MCP 2515 CAN Controller an. Für den Stresstest 
setze ich den in den Loopback-Modus.

Ich stoße immer wieder auf seltsame Probleme, die sich (für mich) nicht 
einfach erklären lassen. Manchmal hängt der Code. Meistens treten aber 
nur an bestimmten Stellen fehlerhafte Daten auf, die es so eigentlich 
nicht geben dürfte. Die Fehler sind reproduzierbar. Kleine Änderungen am 
Code verschieben sie möglicherweise an eine andere Stelle. Änderungen am 
Timing (also Delays) haben keine Auswirkung.

Ich hatte erst Speichermangel im Verdacht. Allerdings dürfte das nach 
meinen bisherigen Erkennissen nicht der Fall sein. Der ATMega328 hat 32K 
Flash, von denen ich 8K nutze. Von den 2K RAM sind laut folgender 
Methode
1
int freeRam () {
2
  extern int __heap_start, *__brkval; 
3
  int v; 
4
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
5
}

an verschiedenen Stellen des Programms eigentlich immer 1.5K frei. 
malloc/free verwende ich gar nicht, d.h. alle Variablen liegen entweder 
statisch im Datensegment oder werden auf dem Stack angelegt. Auf die 
Verwendung riesiger Datenstrukuren, Arrays oder Strings habe ich gezielt 
verzichtet. Deshalb bin ich etwas ratlos.

Was mich zusätzlich irritiert, ist die Ausgabe von avr-size:

   text    data     bss     dec     hex filename
      0    8266       0    8266    204a UnitTests.cpp.hex

Sollten die 8K nicht eigentlich größtenteils im Codesegment liegen?

Ich wäre froh über Tipps zum weiteren Vorgehen, vor allem zu 
Debugging-Möglichkeiten. Bislang bin ich mit Serial.println() 
durchgekommen. Brauche ich jetzt gdb und einen JTagger, oder wie läuft 
das? Gibt es andere Strategien, um solche Fehler einzugrenzen? Oder 
übersehe ich etwas Offensichtliches?

Viele Grüße
Jörg

von Jürgen S. (jurs)


Lesenswert?

Joerg Pleumann schrieb:
> Manchmal hängt der Code. Meistens treten aber
> nur an bestimmten Stellen fehlerhafte Daten auf, die es so eigentlich
> nicht geben dürfte. Die Fehler sind reproduzierbar. Kleine Änderungen am
> Code verschieben sie möglicherweise an eine andere Stelle. Änderungen am
> Timing (also Delays) haben keine Auswirkung.

Da werden wohl vermutlich im regulären Programm unbeabsichtigt 
irgendwelche Speicherbereiche fremder Variablen überschrieben.

In die Falle tappe ich mit C und Arduino auch immer wieder, als alter 
Turbo Pascal und Delphi Programmierer. Mir fehlen bei der 
C-Programmierung die strengen Typprüfungen und die Range-Checks von 
Pascal.

> Brauche ich jetzt gdb und einen JTagger, oder wie
> läuft das? Gibt es andere Strategien, um solche Fehler einzugrenzen?

Assertations mit "assert()".
Weißt Du wie das in einem Arduino-Sketch funktioniert oder brauchst Du 
eine Anleitung?

von Karl H. (kbuchegg)


Lesenswert?

Joerg Pleumann schrieb:

> nicht geben dürfte. Die Fehler sind reproduzierbar. Kleine Änderungen am
> Code verschieben sie möglicherweise an eine andere Stelle.

Wenn Datenfehler scheinbar hüpfen, wenn man kleine Codeänderungen macht, 
dann hat man in der Mehrzahl der Fälle irgendwo einen Array-Overflow 
gebaut.

Also: erst mal ALLE Arrays überprüfen. Hauptaugenmerk dabei auf Strings 
legen! Ist wirklich überall das Array um 1 Element länger definiert als 
die Anzahl der erwarteten Buchstaben? Kann es passieren, dass die Länge 
überlaufen wird?

von Joerg P. (pleumann)


Lesenswert?

Danke für die beiden Antworten.

Die Idee mit den Assertions ist gut. Habe ich bei Arduino noch gar nicht 
ausprobiert. Gehen die "normalen" Assertions oder muss ich mir die per 
Makro selbst bauen?

Der Tipp mit den Arrays hat mich dann an den richtigen Stellen suchen 
lassen. Ich habe so zumindestens einen Teil meines Problems lösen 
können. Es gab ein einzelnes Bit in meinem struct, das ich nicht 
initialisiert habe. Meistens war es wohl automatisch 0 (was man als 
Java-Entwickler ja sowieso stillschweigend voraussetzt). Wenn es 
zufällig 1 war, wurde das Array anders behandelt, und ich hatte die 
fehlerhaften Daten. Die sind also weg.

Trotzdem bleibt ein Problem, aber es wird immer obskurer: Meine 
Datenstruktur hat intern u.a. ein 8-Byte-Array und ein Längenfeld. Wenn 
ich Längen bis maximal 6 verwende (also das Array bis dort fülle), dann 
kann ich im Test ohne Probleme 1000 Iterationen meines Tests 
durchführen. Wenn ich Längen von 7 oder 8 verwende und an einer sehr 
tiefen Stelle im Code (bevor die Sachen an den CAN gesendet werden) die 
Werte per Serial.println(a[i], DEC) ausgebe, dann hängt sich der gesamte 
Code relativ schnell auf. Jetzt kommt's: Lasse ich die Ausgabe weg oder 
nutze ich stattdessen Serial.println(a[i], HEX), dann geht's.

Der Zusammenhang mit der Länge deutet natürlich immer noch auf einen 
Array-Überlauf hin, aber ich kann absolut keinen im Code erkennen. Der 
Zusammenhang mit den verschiedenen Varianten von println() erschließt 
sich mir nicht wirklich. Kann sein, dass es Zufall ist und der 
eigentliche Root Cause die Länge ist. Kann sein, dass die Code-Pfade in 
der Serial-Bibliothek bei HEX und DEC so unterschiedlich sind, dass er 
im DEC-Fall einen Speicherüberlauf erzeugt. Ach ja, vergrößere ich mein 
Array oder lege ich dahinter ein Dummy-Feld an, hat das keine 
Auswirkung. vEs crasht immer noch. Ich bleibe also verwirrt.

Deshalb würde ich gern nochmal auf meine initiale Nachricht 
zurückkommen: Ist die Methode zur Bestimmung des freien Speichers 
brauchbar? Was ist von der Ausgabe von avr-size zu halten? Gibt es 
bekannte Speicherlecks mit Arduino (mit Google konnte ich nichts 
finden)? Und gibt es brauchbare Mittel, auf der Hardware zu debuggen und 
den Speicher zu untersuchen oder spricht da der verwöhnte 
(verweichlichte) Java-Entwickler aus mir? :)

Viele Grüße
Jörg

von Jürgen S. (jurs)


Lesenswert?

Joerg Pleumann schrieb:
> Die Idee mit den Assertions ist gut. Habe ich bei Arduino noch gar nicht
> ausprobiert. Gehen die "normalen" Assertions oder muss ich mir die per
> Makro selbst bauen?

So funktioniert assert mit Arduino, füge diesen Code in Dein Programm 
ein, um assert-Abbruchmeldungen auf die serielle Schnittstelle 
auszugeben (d.h. Serial muss auch initialisiert sein):
1
#define __ASSERT_USE_STDERR
2
#include <assert.h>
3
4
// handle diagnostic informations given by assertion and abort program execution:
5
void __assert(const char *__func, const char *__file, int __lineno, const char *__sexp) {
6
    // transmit diagnostic informations through serial link. 
7
    Serial.println(__func);
8
    Serial.println(__file);
9
    Serial.println(__lineno, DEC);
10
    Serial.println(__sexp);
11
    Serial.flush();
12
    // abort program execution.
13
    abort();
14
}


In Dein Programm kannst Du dann "assert" Aufrufe einbauen, falls die 
Bedingung der assert-Aufrufe wieder erwarten nicht true sondern false 
ist, stürzt das Programm ab und gibt als letzte Aktion noch eine 
Fehlermeldung über die serielle Konsole aus, die den Dateinamen, die 
Zeilennummer und den assert-Parameter enthält.

Allerdings ist die Zeilennummer nicht ganz korrekt, wahrscheinlich weil 
wohl ein Arduino "Sketch" nicht die vollständige CPP-Datei ist, die 
compiliert wird.

Bei mir zeigt die assert-Abbruchmeldung jedenfalls immer die 
Zeilennummer der Abbruchstelle um einige Zeilen zu tief an. Mal in einem 
Sketch 6 Zeilen zu tief, mal in einem anderen Sketch 8 Zeilen zu tief. 
Das Auffinden der richtigen Abbruchstelle aus einer 
assert-Abbruchmeldung sollte aber trotzdem einfach sein, bloß mal 6-8 
Zeilen höher schauen als angezeigt und die genaue assert-Bedingung wird 
ja auch angezeigt, die zum Abbruch geführt hat.

Vielleicht hilft's!

> Trotzdem bleibt ein Problem, aber es wird immer obskurer
> ...
> Deshalb würde ich gern nochmal auf meine initiale Nachricht
> zurückkommen: Ist die Methode zur Bestimmung des freien Speichers
> brauchbar?

Wenn Du Probleme mit überschriebenen Variablen hast, die nicht 
enthalten, was sie enthalten solllen, und im Sketch 
Interrupt-Behandlungsroutinen verwendet werden, dann würde ich auch 
unbedingt darauf achten, dass "unsicherer" Code aus allen 
Interrupt-Routinen rausfliegt und dass beim Aufruf von Code, bei dem 
Variablen durch den Interrupt-Aufruf verändert werden können, ohne als 
"volatile" deklariert zu sein, keine Interrupts ausgelöst werden können.

Beispiele für den Aufruf von unsicherem Code: Deine Methode zum Aufrufen 
des freien Speichers verändert Variablen, die während einer 
Interrupt-Routine verändert werden könnten. Ich kenne den Library-Code 
nicht: Sind diese "volatile" deklariert und können frei auch im 
User-Code verwendet werden? Falls nicht, Aufrufe dieser Funktion besser 
so kapseln:

cli(); //disable global interrupts
BytesFree = freeRam ();
sei(); //enable global interrupts

Anderes Beispiel: Innerhalb von Interrupt-Behandlungsroutinen keine 
Funktionen aufrufen, die entweder sehr lange laufen oder die Variablen 
verändern, die von anderen Funktionen verwendet werden und nicht 
"volatile" deklariert sind. Ein steter Quell der "Freude" sind da leider 
Debug-Meldungen über Serial.print. Falls Du Serial.print in 
Interruptroutinen hast: Schmeiße jedes "Serial.print" aus allen 
Interrupt-Behandlungsroutinen heraus!

> Was ist von der Ausgabe von avr-size zu halten?

Kenne ich nicht.

> Gibt es bekannte Speicherlecks mit Arduino (mit Google konnte
> ich nichts finden)?

Wenn Du selbst mit malloc und free keine Speicherlecks einbaust, sollte 
das System keine Speicherlecks haben. Mit Deiner freeRam-Routine kannst 
Du das ja prüfen. Mir sind Speicherlecks in den Libraries jedenfalls 
noch nicht untergekommen.

> Und gibt es brauchbare Mittel, auf der
> Hardware zu debuggen und den Speicher zu untersuchen oder spricht
> da der verwöhnte (verweichlichte) Java-Entwickler aus mir? :)

Nein, nicht innerhalb der Arduino-Entwicklungsumgebung.

Wenn Du Runtime-Debugging möchtest, mußt Du meines Wissens nach mit 
Atmel Studio programmieren und einen teuren Programmer nach "JTAG" 
Standard verwenden. Damit kenne ich mich überhaupt nicht aus.

von Joerg P. (pleumann)


Lesenswert?

Hallo Jürgen,

danke für die detaillierte Antwort. Die assert-Funktionalität werde ich 
definitiv ausprobieren. Das sieht sehr hilfreich aus, auch für künftige 
Projekte.

Was den Rest angeht: Ja, ich verwende einen Interrupt Handler für den 
Fall, dass eine CAN-Botschaft ankommt. Der Handler schreibt die 
Nachricht in einen Ringpuffer. Eine andere Methode der Library liest sie 
dort heraus. Die Methode zum Lesen ist durch noInterrupts() und 
interrupts() geschützt. Der Interrupt Handler selbst sollte sowieso 
sicher sein, oder? Die meisten Variablen für den Puffer sind volatile. 
Einzig den Puffer (struct[]) selbst konnte ich nicht volatile 
deklarieren. Dann hat der Compiler gemeckert. Die Fehlerfälle 
(Serial.println) treten im Test nicht auf.
1
#define SIZE 32
2
#define ulong unsigned long
3
4
can_t _buffer[SIZE];
5
volatile int posRead = 0;
6
volatile int posWrite = 0;
7
volatile bool lastOpWasWrite = false;
8
9
void enqueue() {
10
  if (posWrite == posRead && lastOpWasWrite) {
11
    Serial.println("!!! Buffer full");
12
    return;
13
  }
14
15
  if (!can_get_message(&_buffer[posWrite])) {
16
    Serial.println("!!! No message");
17
    return;
18
  }
19
20
  posWrite = (posWrite + 1) % SIZE;
21
  lastOpWasWrite = true;
22
}
23
24
bool dequeue(can_t *p) {
25
  noInterrupts();
26
27
  if (posWrite == posRead && !lastOpWasWrite) {
28
    interrupts();
29
    return false;
30
  }
31
32
  memcpy(p, &_buffer[posRead], sizeof(can_t));
33
  posRead = (posRead + 1) % SIZE;
34
  lastOpWasWrite = false;
35
36
  interrupts();
37
38
  return true;
39
}

Ich werde später nochmal probieren, ganz auf den Ringpuffer zu 
verzichten. Für den Test sollte der nicht relevant sein, weil immer eine 
Nachricht gesendet und sofort empfangen wird. Das gäbe dann Hinweise 
darauf, ob die Pufferimplementierung das Problem verursacht.

Zu der Frage nach den Speicherlecks: Ich selbst verwende malloc/free gar 
nicht. Liegt also alles im Datensegment oder auf dem Stack. Ich frage 
mich manchmal, ob die String-(Klassen-)Implementierung von Arduino 
wirklich wasserdicht ist, weil mir Programme damit schon öfter um die 
Ohren geflogen sind. Oder ob die Arduino Libraries Funktionsaufrufe so 
tief schachteln, dass der Stack überquillt. Aber das würde mein Problem 
auch nicht erklären. Hier sieht es ja eher so aus, als würde der Stack 
von irgendwas überschrieben, nicht umgekehrt.

Später mehr...

Grüße
Jörg

von Jürgen S. (jurs)


Lesenswert?

Joerg Pleumann schrieb:
> Der Handler schreibt die
> Nachricht in einen Ringpuffer. Eine andere Methode der Library liest sie
> dort heraus.

Grundsatz: Wenn auf dieselben Variablen aus einer 
Interrupt-Behandlungsroutine UND aus dem normalem Programm heraus 
zugegriffen werden, dann müssen die betroffenen Variablen "volatile" 
deklariert sein. Sonst ist die Interrupt-Behandlungsroutine nicht 
sicher.

Ausnahme: Wenn Du während des Zugriffs außerhalb der Interruptbehandlung 
im normalen Programmablauf nur dann auf die Variablen zugreifst, während 
die Interrupts vorher gesperrt worden sind, ist es wieder sicher.

Also sind die Variablenzugriffe in Deiner dequeue-Funktion sicher (nach 
meinem Kenntnisstand). Soweit wohl kein Problem.

> Der Interrupt Handler selbst sollte sowieso sicher sein, oder?

Sofern Du mit Interrupt-Handler die Funktion "enqueue()" meinst, ist 
diese natürlich NICHT sicher in einer Interrupt-Behandlung aufrufbar, da 
Du darin Serial.print() verwendest. Das ist innerhalb einer 
Interrupt-Behandlung unsicher. Falls Du Fehlermeldungen über 
Serial.print anzeigen möchtest, die innerhalb der Interruptbehandlung 
auftreten, so darfst Du innerhalb der Interruptbehandlung nur ein 
Error-Flag setzen, das volatile deklariert ist.

volatile byte errorFlag = 0;
Dann kannst Du innerhalb der ISR-Behandlung dem Errorflag gefahrlos eine 
Fehlernummer zuweisen, und innerhalb der normalen Programmschleife (also 
ausserhalb der ISR-Routine) kannst Du gefahrlos auf das errorFlag prüfen 
und im Fall dass der Fehler aufgetreten ist, die Fehlermeldung über 
Serial.print anzeigen lassen und das errorFlag wieder löschen.

> Ich werde später nochmal probieren, ganz auf den Ringpuffer zu
> verzichten. Für den Test sollte der nicht relevant sein, weil immer eine
> Nachricht gesendet und sofort empfangen wird. Das gäbe dann Hinweise
> darauf, ob die Pufferimplementierung das Problem verursacht.

Die Variablenzugriffe scheinen OK zu sein, da Du auf die nicht volatile 
deklarierten Variablen nur in der ISR-Behandlung zugreifst oder die 
Interrupts während Zugriffen gesperrt sind. Das sollte OK sein.

Nicht OK ist die Verwendung von Serial.print in einer ISR-Routine.

> Zu der Frage nach den Speicherlecks: Ich selbst verwende malloc/free gar
> nicht. Liegt also alles im Datensegment oder auf dem Stack. Ich frage
> mich manchmal, ob die String-(Klassen-)Implementierung von Arduino
> wirklich wasserdicht ist, weil mir Programme damit schon öfter um die
> Ohren geflogen sind.

Die Strings in Arduino sind dynamische Objekte. Es gibt da zwar einige 
sehr schöne Komfort-Routinen zur Stringbehandlung, aber ich vermeide 
deren Verwendung und verwende nur die üblichen char-Arrays. Die 
Programme bleiben dann sehr viel kleiner als wenn String-Objekte 
verwendet werden. Mit "Strings" a la Arduino habe ich mich noch nicht 
gross beschäftigt.

Wenn man beides gleichzeitig, also char-Arrays und String-Objekte im 
selben Programm verwendet, muss man wohl auch sehr vorsichtig sein, dass 
man keine Funktionen für char-Arrays einfach auf einen String-Pointer 
anwendet, weil es dann wirklich unverhersehbar wird. Wie gesagt, ich 
vermeide String-Objekte und verwende nur Char-Arrays.

von Joerg P. (pleumann)


Lesenswert?

Hallo Jürgen,

Jürgen S. schrieb:
> Ausnahme: Wenn Du während des Zugriffs außerhalb der Interruptbehandlung
> im normalen Programmablauf nur dann auf die Variablen zugreifst, während
> die Interrupts vorher gesperrt worden sind, ist es wieder sicher.
>
> Also sind die Variablenzugriffe in Deiner dequeue-Funktion sicher (nach
> meinem Kenntnisstand). Soweit wohl kein Problem.

Dann müsste ich mir das volatile hier eigentlich sparen können, oder? 
Sicherzustellen, dass sich ISR und normaler Code nicht gegenseitig 
"unterbrechen" müsste reichen.

> Der Interrupt Handler selbst sollte sowieso sicher sein, oder?
>
> Sofern Du mit Interrupt-Handler die Funktion "enqueue()" meinst, ist
> diese natürlich NICHT sicher in einer Interrupt-Behandlung aufrufbar, da
> Du darin Serial.print() verwendest. Das ist innerhalb einer
> Interrupt-Behandlung unsicher. Falls Du Fehlermeldungen über
> Serial.print anzeigen möchtest, die innerhalb der Interruptbehandlung
> auftreten, so darfst Du innerhalb der Interruptbehandlung nur ein
> Error-Flag setzen, das volatile deklariert ist.

Verstanden. Erklärt auch möglicherweise mein Problem, obwohl ich ja 
zunächst angenommen hatte, dass die Serial.print()-Aufrufe nur im 
Fehlerfall zum Einsatz kommen würden. Innerhalb von can_get_message() 
finden sich aber gerade die Serial.print()-Aufrufe, von denen ich 
geschrieben hatte (DEC vs. HEX). Die passieren natürlich auch in der 
ISR. Wenn ich sie entferne, verschwindet mein Problem. Also, 
Arbeitshypothese: Es waren tatsächlich diese Ausgaben in der ISR.

Viele Grüße
Jörg

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.