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
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?
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?
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
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.
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
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.
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.
|