Deutet folgende Befehlsfolge auf ein Problem im Bereich
Stack Pointer und Interrupts beim AVR-GCC hin ?
Vorweg eine Beschreibung ...
Die Compilerausgabedatei xxx.lss enthält zusätzlich zum
Projekt-C-Source
den übersetzten Assemblercode. Es folgt ein kleiner Auszug daraus:
a1c: 2d b7 in r18, 0x3d ; 61
a1e: 3e b7 in r19, 0x3e ; 62
a20: 24 5f subi r18, 0xF4 ; 244
a22: 3f 4f sbci r19, 0xFF ; 255
a24: 0f b6 in r0, 0x3f ; 63
a26: f8 94 cli
a28: 3e bf out 0x3e, r19 ; 62
a2a: 0f be out 0x3f, r0 ; 63
a2c: 2d bf out 0x3d, r18 ; 61
Ich verstehe das so:
a1c: // Stack Pointer (Low Byte) lesen und im r18 speichern
!!! Hier darf ein Interruptaufruf dazwischen kommen !!!
a1e: // Stack Pointer (HighByte) lesen und im r19 speichern
a20: // vom gelesenen Stack Pointer in r18 ein LowByte subtrahieren
a22: // vom gelesenen Stack Pointer in r19 ein HighByte subtrahieren
a24: // Status bzw. Flagregister SREG lesen und in r0 speichern
(retten)
a26: // alle Interrupts sperren (SREG Bit 7)
a28: // r19 ist das HighByte des veränderten Stack Pointers und
wird hier in den Stack Pointer zurückgeschrieben.
a2a: // r0 auf SREG zurückschreiben, um u.a. alten Interruptzustand
wieder herzustellen. Der Interrupt kann zugelassen sein.
!!! Hier darf aber niemals ein Interruptaufruf dazwischen
kommen,
weil der Stack Pointer auf eine ungültige Speicheradresse
zeigt !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
a2c: // r18 ist das LowByte des veränderten Stack Pointers und
wird hier eindeutig zu spät in den Stack Pointer
zurückgeschrieben.
Wie findet man solche Stellen ? Ganz einfach:
1. Datei xxx.lss mit Editor öffnen.
2. Mit der Suchfunktion (STRG F) nach dem Stichwort cli suchen und
weitersuchen.
Hinweis: Vergleichbare Befehlsfolgen kommen in der Compilerausgabedatei
xxx.lss auch vor, wobei r18 und r19 durch andere Register
ersetzt sind. Wenn ich das richtig erkannt habe, taucht die
Befehlsfolge bei Berechnungen mit Fliesskomma-Variablen und
beim <vfprintf> auf.
Die Befehlsfolge von a24: bis a2c: ist typischerweise
vorhanden.
Die Befehlsfolge von a1c: bis a22: (Stack Pointer
Manipulation)
kann auch eine andere Berechnung als eine Subtraktion
beinhalten.
Folgendes Problem sehe ich da:
Wenn zwischen Adresse a2a: und a2c: das laufende Programm durch einen
Interruptaufruf unterbrochen wird, schreibt der AVR seine
Rücksprungadresse auf den Stack und zwar dahin, wo der
16-bit-Stack Pointer hinzeigt. Da im genannten Beispiel der Stack
Pointer
undefiniert in den Speicher zeigt, - er kann wegen des Carry-Übertrags
der Berechnung um fast eine Page (256Byte) daneben liegen - , werden
also eventuell gültige Daten von der Rücksprungadresse überschrieben.
Der AVR findet zwar nach Abarbeitung der Interruptroutine an die
richtige
Adresse a2c: zurück, aber die überschriebenen gütigen Daten sind im
Speicher verändert und unbrauchbar, weil Variablen, andere Pointer,
Rücksprungadressen von Call-Aufrufen (egal - was auch immer) sind
verändert. Somit treten in anderen Programmteilen, die diese kaputten
Daten benutzen, unvorhersehbare beliebig geartete Folgefehler auf!
Wenn meine Theorie stimmt, ist der Absturz im wahrsten Sinne des Wortes
vorprogrammiert.
Ich weiss leider nicht, wie man dem Compiler das Problem abgewöhnen
kann.
Deshalb richtet sich dieser Hinweis an die Compiler-Experten und
Autoren.
Es sollte im Assemblercode besser folgende Reihenfolge eingehalten
werden:
1. 16-bit-Stack Pointer komplett lesen
2. 16-bit-Stack Pointer komplett manipulieren, wobei
das Ergebnis noch nicht in den Stack Pointer zurückgeschrieben
wird.
3. SREG-Zustand lesen (retten)
4. Interrupt sperren
5. 16-bit-Stack Pointer komplett jetzt zurückschreiben
6. geretteten SREG-Zustand ins SREG zurückschreiben, d.h. erst jetzt
erfolgt gegebenfalls eine Interruptfreigabe.
Mit dieser neuen Reihenfolge ist sichergestellt, dass kein Interrupt
mitten in das Setzen einer neuen Stack Pointer Adresse springen kann.
Der Interrupt wäre zwar über einige Prozessortaktzyklen länger
gesperrt,
jedoch hat die Sicherheit der Stack Pointer Definition oberste
Priorität.
Querverweis: Im AVR-Datenblatt wird der Stackpointer grundsätzlich
bei gesperrtem Interrupt gesetzt.
----------------------------------------------------------------------
Gedanklich hier noch vergleichbares anderes Beispiel, was aber kein
Compilerfehler, sondern einen denkbaren Programmierfehler in der
Anwendung aufzeigen soll, wenn ein Programmierer bei der Verwendung von
Interrupts nicht aufpaßt. Ich möchte ergänzend klarstellen, dass
meine Anwendung diesen Programmierfehler in C-Source nicht enthält.
Wer will, kann das folgende Beispiel als ergänzende Sichtweise zum
vorhandenen Thema im AVR-GCC-Tutorial sehen und verwenden.
Hier nun das Beispiel1 (richtig programmiert):
Wenn der Wert einer Mehrbyte-Variable, -Pointer, z.B. float,long oder
int
vom Hauptprogramm berechnet wird und vom Interruptprogramm gelesen
wird, muss der Interrupt im Hautprogramm während der Variablenzuweisung
gesperrt sein, damit der Interrupt die Zuweisung nicht unterbricht.
Die Zuweisung einer Mehrbyte-Variable besteht ja aus mehreren
Assemblerbefehlen, die einzeln Byte für Byte überschreiben. Das ist auf
einem 8bit-µC völlig normal.
Hier nun das Beispiel2 (falsch programmiert):
Wenn der Interrupt die Zuweisung unterbrechen darf, wäre die Folge,
dass ein Byte der Mehrbyte-Variable den alten Wert hat,
aber das andere Byte bereits den neuen Wert hat. Numerisch ergibt das
völlig einen unsinnigen Variablenwert. Wenn es z.B. den Exponenten
einer float-Variable trifft, ist das mindestens eine numerische
Katastrophe. Wenn damit das Interruptprogramm dann weiterechnet,
passiert in der Anwendung ein unberechenbarer Blödsinn.
Das gemeine ist, dass solche Laufzeitfehler selten auftreten, weil der
Interrupt ja zeitlich die Befehlsfolge der Mehrbyte-Zuweisung exakt
treffen muss. Aber man kann sich zu 100% darauf verlassen, dass der
Interrupt irgendwann garantiert ins Schwarze trifft, weil Interrupts
asynchron zur Softwarelaufzeit auftreten.
Dieses theoretische Beispiel ist gedanklich problemlos auf den
Stack Pointer übertragbar.
----------------------------------------------------------------------
Allgemeine Info zu meiner Anwendung:
Auf die Stack Pointer Problematik wurde ich aufmerksam, weil
gelegentlich
- etwa alle 10..15min - ein numerischer Softwareabsturz bei meiner
Anwendung auftaucht, den ich mir momentan nicht anders erklären kann.
Als Interruptquelle verwende ich einen netzsynchronen externen
100Hz-Interrupt. Zum Softwaredebugging von Variablen verwende ich den
printf des Compilers. Es ist zwar mein erstes Projekt auf dem ATMEL
AVR.
Jedoch habe ich seit 1979 auf den verschiedensten Prozessoren in
Assembler und anderen Sprachen programmiert.
----------------------------------------------------------------------
Gruss
Dietmar
Du brauchst dazu nicht erst umständliche Disassembler-Listings
bemühen, der generierte Compilercode steht dir offen.
Außerdem hättest du die Frage ziemlich kurz stellen können. (Sorry,
ich habe das Lesen des länglichen Artikels abgebrochen, nachdem ich
glaube erkannt zu haben, was dir nicht gefällt -- falls du danach noch
mehr Fragen gestellt hast, musst du dich nochmal melden.)
Nein, das ist kein Problem. Auch wenn es nirgends dokumentiert ist,
hat Atmel dem Compilerschreiber mal irgendwann bestätigt, dass die
Interruptfreigabe in der Tat und garantiert erst einen Befehl
verspätet erfolgt, d.h. der Befehl direkt nach einer Zuweisung an das
SREG, bei dem das I-Bit wieder gesetzt wird, wird selbst bei einem
anhängigen Interrupt noch ausgeführt.
Ich hoffe das beantwortet deine Frage (außer natürlich der nach deinen
sporadischen Softwareabstürzen, aber die musst du wohl oder übel
selbst finden ;-).
Hallo Dietmar,
ich habe das gleiche Problem wie du, dass meine printfs immer wieder
mal gestört werden, und auch manchmal der AVR eine reboot macht.
Nun meine Frage: Hast du dein Problem gelöst? War ein SW-Bug von dir,
oder hast du den Fehler immer noch?
Ich bin auf das gleiche Problem gestossen, und suche noch nach der
Lösung. Ein workaround war gestern Abend noch folgendes: Ich habe alle
Variable als staisch deklariert, bzw. ausserhalb den C-Funktionen. Dann
aht der Compiler keinen Stack manipulieren müssen.
Bisher konnte ich keinen Reboot mehr provozieren.
Nun bin ich mir nicht sicher, ob mein Programm ein problem hat, oder
doch vielelicht manchmal nach dem cli und dem SPL-setzten sich doch
nicht ein int dazwischen schummelt.
Vermutlich muss ich da mal einen Fang-Code machen, um das genau zu
untersuchen.
Gruß Erwin
Hallo allerseits,
Ich habe das lange Posting jetzt nicht ganz gelesen, jedoch kann ich
bestätigen wie Erwin es sagt, dass der AVRGCC bei Verwendung von
Strings im allgemeinen (egal ob nun bei printf oder was eigenem)
manchmal "fehlerhaften" Code produziert der zum "restart" des AVRs
führt.
Wahrscheinlich ein Sprung zu einer falschen adresse o.ä.
Also die Situation: Zunächst funktioniert das Programm wunderbar - kann
durchaus ein größeres Programm sein was bei -os etwa 4kB belegt.
Dann fügt man an einer - absolut unkritischen Stelle im Programm, z.B.
in der Main Funktion - einen Aufruf wie writeUART_P("ASDF"); hinzu.
Die Bedeutung dieser Funktion dürfte klar sein. Die Funktion selbst
wird an vielen Stellen im Programm verwendet und funktioniert
normalerweise problemlos.
Nachdem man diesen einen zusätzlichen Aufruf hinzugefügt hat,
funktioniert es aber auf einmal nicht mehr! Das programm stürzt dann
schon kurz nach dem Start ab und der AVR springt zum Anfang des
Programms zurück!
Das Lustige daran ist: Fügt man z.B. direkt nach oder vor diesem neuen
Aufruf nur ein einzelnes NOP hinzu - klappts oftmals wieder!
Das geht aber nicht immer - machmal muss man mehrere Nops auch ganz wo
anders hinzufügen...
So und nun kommts:
Schaltet man die Optimierung ab - also o0 - funktioniert es immer!
Aber egal ob o1, o2, oder os - dann geht es ab und zu nicht!
An meinem Code dürfte es also nicht liegen, denn der funktioniert in
99% der Fälle einwandfrei und bei abgeschalteter Optimierung sowieso.
(Nur passt dann das Timing an einigen stellen nicht mehr so gut und der
Code wird ziemlich aufgebläht)
Habe leider gerade keinen Code an dem ich das demonstrieren kann (darf
ich noch nicht veröffentlichen - kommt aber bald)
Vllt. mach ich demnächst mal ein kleines Beispiel dazu fertig.
Woran kann das liegen?
Nervt nämlich extrem!
Ich arbeite normalerweise mit PICs und dem C18 - da habe ich sowas
seltsames noch nie gehabt...
MfG,
Dominik
"mach ich demnächst mal ein kleines Beispiel dazu fertig"
Das wäre hilfreicher als allgemein gehaltene Anschuldigungen an die
Adresse des Compilers. Die erweisen sich nämlich fast immer als
Rohrkrepierer.
Das von Joerg skizzierte CLI Verhalten ist ganz offiziell in der
Instruction Set Reference dokumentiert. Und die ist deutlich älter als
Dietmars Klageschrift. Also muss er sich hier mangelnde Recherche
vorwerfen lassen, denn das ist ja wohl die erste Adresse in der man
suchen sollte.
Hallo zusammen,
ich ahbe mal eine etwas eigenwillige Untersuchung gemacht, um die
Aussage, dass es sich nicht ume inen Compilerfehler handeln kann, zu
unterstützen.
Hab einfach den sprintf wieder reingebaut, geschaut, ob Falschausgaben
kommen, und die kamen dann auch, zwar nicht reproduzierbar, aber sie
kamen, und dann habe ich im List-File alle cli gesucht, die
Befehlssequenzen im Hexfile umgepatched und noch einmal getestet.
Diesesmal sollte sich ja an der Funktionalität überhaupt nichts ändern,
denn selbst die Checksummen im Hexfile (ich hatte Glück, alle
cli-Befehle waren in der Mitte der Hexzeilen, so musste ich die
Checksummen nicht anpassen) waren ja gleich.
Nur die Ausführung von SPL = R31 und Status zurückschreiben waren in
der Reihenfolge vertauscht, also so, wie sie laut Lehrbuch auch sein
sollten, zuerst beide Register SPH und SPL umsetzen und dann den Int
wieder freigeben. Dann habe ich nochmals geschaut, und der Fehler war
noch da! Es liegt also an etwas anderem! Die Befehlssequenz mit dem cli
und dem Zurückschreiben ist es also definitiv nicht! So ein Bug wäre
sicherlich schon früher aufgefallen!
Jetzt muss ich eine Routine schreiben, die ein Stackmonitoring macht.
"Das von Joerg skizzierte CLI Verhalten ist ganz offiziell in der
Instruction Set Reference dokumentiert."
War mir doch gleich so, daß ich das irgendwo gelesen hab.
Es gibt z.B. auch die Folge:
1
cli();
2
// ... Freigabe des Aufwachinterrupts
3
sei();
4
sleep();
Damit ist garantiert, daß kein Interrupt vor dem Sleep zuschlagen kann
und man nie wieder aufwacht.
Peter
@Dominik
"An meinem Code dürfte es also nicht liegen, denn der funktioniert in
99% der Fälle einwandfrei und bei abgeschalteter Optimierung
sowieso."
In der Regel verbrennt man sich mit solchen Aussagen mächtig die
Zunge.
Solche Fehler sind typisch für ungültige Pointer (Schreiben über die
Feldgrenze hinaus, schreiben als falscher Typ, fehlende
Initialisierung).
Damit werden Returnadressen zerstört und manchmal kann der Sprung
trotzdem wieder im richtigen Kontext landen.
Typische Fehlerkandidaten sind scanf, itoa, usw.
Peter
Hallo,
der Thread ist zwar schon älter, aber ich möchte trotzdem einen Hinweis
loswerden.
Das beschriebene Verhalten ist bei mir ebenfalls aufgetreten, und zwar
im <__epilogue_restores__> von sprintf(), hier der Codeausschnitt mit
Kommentaren:
in r0, 0x3f ; SREG sichern
cli ; alle Interrupts sperren
out 0x3e, r29 ; SP_H setzen
out 0x3f, r0 ; SREG restore (auch Interrupts)
out 0x3d, r28 ; SP_L setzen
Sofort nach dem SREG Restore springt der Programmzeiger nach
18: f5 c1 rjmp .+1002 ; <__vector_12>
d.h. der anstehende Interrupt wird sofort ausgeführt, und nicht erst
einen Befehl später nach seiner Freigabe.
Aber: Diese Beobachtung habe ich im Simulator von VMLAB gemacht.
Wenn die von Jörg und Peter zitierte Aussage von Atmel richtig ist, was
wir wohl voraussetzen können, dann hat der VMLAB Simulator hier einen
Bug.
Möglicherweise auch andere Simulatoren ?
Gruß,
Peter H.
Ja, da hat offenbar VMLAB einen Bug. Wird aber leider nicht mehr
gepflegt, da das Projekt für Enrique nicht so zum Fliegen gekommen
ist, wie er sich das mal vorgestellt hatte.
>Aber: Diese Beobachtung habe ich im Simulator von VMLAB gemacht.
Richtig. VMLAB simuliert da falsch. Leider, dann das schränkt dessen
Nutzbarkeit doch ganz erheblich ein.
Andere Simulaturen sind andere Simulatoren - da besteht kein
Zusammenhang. AVRStudio z.B. macht es richtig.
Oliver
Es könnte natürlich mal jemand Enrique fragen, ob er nicht vielleicht
wenigstens diesen Bug fixen möchte. Sonst ist der Simulator ja
wirklich nicht schlecht, und das Freischalten der Interrupts um einen
Befehl zu verzögern kann eigentlich kein großer Aufwand sein. Beim
CLI musste er es ja ohnehin schon implementieren, denn dort ist es
auch so dokumentiert.
Ich hatte den Fehler vor einiger Zeit mal im VMLAB-Forum angesprochen
(als es das Forum noch gab), da gabs aber keine Reaktion.
Noch schöner wäre natürlich, den source-code als OpenSource-Projekt zu
veröffentlichen. Da gibt es nämlich schon noch ein paar Macken mehr, die
man sich auch mal ansehen könnte.
Oliver
Oliver wrote:
> Noch schöner wäre natürlich, den source-code als OpenSource-Projekt zu> veröffentlichen. Da gibt es nämlich schon noch ein paar Macken mehr, die> man sich auch mal ansehen könnte.
Wenn du dich mit Delphi rumschlagen willst... Kannst ja Enrique mal
fragen. Vielleicht gibt er dir den Code ja auch zur Pflege weiter,
ohne ihn gleich komplett opensource zu machen, wenn du dich im Gegenzug
verpflichtest, Bugfix-Releases zu machen?
Ich habe Enrique vor zwei Jahren treffen können, als ich dienstlich
in Madrid unterwegs war. Ist durchaus ein netter Mensch.
>Wenn du dich mit Delphi rumschlagen willst...
Nöööö.....
Wobei, den Simulationsfehler ausbügeln würde wohl noch gehen, wenn ich
denn Delphi überhaupt hätte, aber Windows-Oberflächen mit Delphi, nein
danke. Da müssten dann schon "Profis" ran.
Oliver
Hallo,
das wäre schon eine tolle Sache, wenn sich jemand findet, der VMLAB
weiterentwickelt und die Bugs beseitigt. Habe selber leider nicht
ausreichend Zeit dafür. Die Simulationsumgebung von VMLAB ist super,
insbesondere daß man eigene Komponenten hinzufügen kann. Ich habe meine
Temperaturmodule und Schieberegister auf diese Weise an den ATmega
rangehängt, und es funktioniert!
Für das diskutierte Problem mit der Interruptfreigabe im VMLAB Simulator
habe ich mir ein kleines Workaround gebastelt, und einen neuen Thread
dafür aufgemacht, damit es nicht mehr als Compilerbug auftaucht:
Beitrag "VMLAB Interrupt zu früh, Stackpointer falsch"
Viele Grüße,
Peter H.
ist es nicht die FAQ 9.9.28 aus dem avr-libc-user-manual, was die
Ursprungsfrage beantwortet:
9.9.28 Why are interrupts re-enabled in the middle of writing the stack
pointer?
Es interressiert mich wirklich, wie Dominik und andere angebliche
printf-Probleme gelöst haben. Mein sprintf_P() macht mich momentan
verrückt: Beitrag "gcc-avr: sprintf_P kracht (mit 3 oder mehr Parameter)"