Seltsames Problem mit ATTiny10 + avr-gcc: in ISR verwendeter Zähler durch globale Variablen beschädigt?

Ich habe ein sehr seltsames Problem einer möglichen Speicherbeschädigung bei der Verwendung globaler Variablen und eines Timer-Überlauf-Interrupts auf dem ATTiny10 (mit avr-gcc 4.9.2). Ich kann keinen Sinn daraus machen, habe es aber geschafft, es auf die Reproduktion mit einem sehr einfachen Programm einzugrenzen:

#include <avr/io.h>

/* Timer overflow counter */
volatile unsigned int ovrf = 0;

/* Some global variables used in main() */
/* (MOVING THESE INTO main() FIXES THE ISSUE) */
unsigned long foo;
unsigned int bar;

int main(void) {
  /* Fast PWM 8 Bit Mode */
  TCCR0A |= _BV(WGM00);
  TCCR0B |= _BV(WGM02);
  /* Enable Timer Overflow Interrupt */
  TIMSK0 |= _BV(TOIE0);
  /* /8 prescaler */
  TCCR0B |= _BV(CS01); // 
  /* PB0 as output */
  DDRB |= _BV(PB0);
  /* Enable interrupts */
  sei();

  for (;;) {
    /* Some random code that uses the global vars */
    /* (REMOVING THIS FIXES THE ISSUE) */
    if (foo > bar) {
      foo = 0;
    }
  }
}

ISR(TIM0_OVF_vect) {
  ovrf++;

  /* Toggle LED (about once per second) */
  if ((ovrf / 500) % 2 == 0) {
    PORTB &= ~(_BV(PB0));      
  } else {
    PORTB |= _BV(PB0);
  }
}

Alles, was es tut, ist Folgendes:

  • Richtet den Timer und eine Timer-Überlauf-ISR ein, die einen Zähler (globale Variable ovrf) erhöht und eine LED basierend auf dem Wert dieses Zählers ein- und ausschaltet.
  • Die Hauptschleife greift nur auf zwei andere globale Variablen zu (schreibt nicht einmal irgendwo hin).

Ich würde erwarten, dass die LED regelmäßig blinkt, was beweist, dass der Interrupt funktioniert und der Zähler korrekt inkrementiert wird. Aber es schaltet sich nicht ein -- oder wenn das Programm leicht modifiziert wird, zB mehr Code oder main()mehr Variablen hinzugefügt werden -- blinkt es unregelmäßig oder mit einer sehr schnellen Rate. Daraus gehe ich nach vielen Tests und dem Versuch, jede andere Erklärung auszuschließen, davon aus, dass der Zähler ( ovrf) irgendwie von der Hauptschleife beschädigt wird.

Ich habe mehrere Änderungen gefunden, die das Problem verschwinden lassen können:

  • Verschieben der beiden globalen Variablen ( foound bar) inmain()
  • Entfernen des Codes, der auf sie zugreift
  • Ändern des Typs von fooinint
  • Deaktivieren Sie alle Optimierungen mit -O0(der Standardwert war -Os), aber das macht den Code ~1,6x größer.

Aber ich kann immer noch keine Erklärung für die tatsächliche Ursache sehen. Übersehe ich etwas Offensichtliches völlig ...? Mir gehen die Ideen aus und mir fällt nichts anderes ein als ein Compiler-Fehler, aber das ist sehr unwahrscheinlich, da dieses Beispiel so einfach ist.

AKTUALISIEREN

Basierend auf dem Vorschlag von @MarkU habe ich versucht, mit verschiedenen Optimierungseinstellungen zu spielen, um die genaue Option zu finden, die das Problem verursachen könnte:

  • Auch probiert -O1, hilft aber auch nicht

  • Ich fand, dass das -Os -fno-toplevel-reorder auch das Problem behebt! -- Ich vermute jedoch, dass dies nur ein zufälliger Effekt sein könnte:

  • In meinem ursprünglichen Programm (sehr ähnlich dem obigen vereinfachten Beispiel), in dem ich das Problem gefunden habe, hilft keines der oben genannten Dinge (nicht einmal -O0). Dort habe ich eine weitere globale Variable (a bool), und das einzige, was zu helfen scheint, ist, eine anfängliche Zuweisung (z. B. "bool ledOn = true;" --> "bool ledOn;") zu entfernen.

Es hat also definitiv etwas damit zu tun, wie Variablen zugewiesen werden, aber nicht nur mit ihrer Gesamtgröße. (Es gibt keine weiteren Abhängigkeiten, keine Funktionsaufrufe etc.)

AKTUALISIERUNG 2

Dem Rat von @Curd folgend, habe ich auch versucht, ovrf / 500durch zu ersetzen ovrf >> 9(ungefähr dasselbe, das genaue Timing interessiert mich hier sowieso nicht). Dadurch wurde der Code um 74 Bytes (!) reduziert – und dies behebt auch das Problem!

Ich habe mir den disassemblierten Code für die ISR angesehen: Diese Änderung reduziert die Anzahl der Bytes pushedam Anfang von 13 auf 7, was erklären könnte, warum es hilft!

(Dies ovrf / 500sollte nur ein schneller und einfacher Test sein, um zu überprüfen, ob die ISR funktioniert, aber ich wusste nicht, dass es in der tatsächlichen Implementierung überhaupt nicht so einfach ist! In meinem ursprünglichen Programm gibt es keine Division, ich behalte eine ungefähre Millis zählen, indem Sie einfach ovrfmit 2 multiplizieren.)

Ich habe auch den zerlegten Code für -Os("schlecht") und -Os -fno-toplevel-reorder("gut") verglichen, aber abgesehen davon, dass der Code am Anfang neu geordnet wird, main()scheinen sowohl der Inhalt von als auch die ISR gleich zu sein (gleiche Anzahl von Pop/Pushes usw.)

--

Anscheinend kann ich das Problem in diesem konkreten Beispiel mit einem der oben genannten Workarounds beheben, aber ich fühle mich immer noch unwohl, wenn ich die eigentliche Ursache nicht wirklich verstehe und nicht weiß, wie ich dies im allgemeinen Fall vermeiden kann. Und ich weiß nicht genug über Assembler, um den generierten Code zu analysieren.

Vielleicht sollte ich auch einige dieser Fragen stellen:

  • Ist diese Art von Trial-and-Error-Prozess "normal", wenn C für ATTiny10 verwendet wird? (Ich meine: nicht annähernd genug Ressourcen und / oder unzureichende Compiler-Unterstützung, um dies zuverlässig zu machen - erwarten Sie also nicht, dass es funktioniert, und kehren Sie einfach zur Assembly zurück, wenn dies nicht der Fall ist?)

  • Gibt es etwas, das generell vermieden werden sollte (z. B. keine Verwendung globaler Variablen oder Optimierung)?

AKTUALISIERUNG 3

Vielen Dank für all die Kommentare und Antworten, sie enthalten viele nützliche Vorschläge, die es wert sind, alle für alle zu überprüfen, die auf ein ähnliches Problem stoßen!

Ich hatte noch ein weiteres "Mysterium" mit meinem ursprünglichen Programm, bei dem das Ersetzen von a bool ledOn = true;durch bool ledOn;die einzige Lösung war.

Jetzt, da ich mehr verstehe, habe ich mir die generierte Assembly und die Speichernutzung noch einmal angesehen: Es stellt sich heraus, dass die Initialisierung den Compiler dazu bringt, ein .dataSegment zu produzieren und ein weiteres Byte im Speicher zugewiesen wird, was knapp über der Grenze liegt, um eine Kollision mit zu verursachen Stapel. Obwohl (glaube ich) am Ende ein Register für diese Variable verwendet wird, genau wie im Fall "keine explizite Initialisierung", sollte die zusätzliche Zuordnung nicht erforderlich sein. Ich denke, der Compiler hat einfach keine Optimierung für diesen Extremfall mit so wenig RAM.

Wie sieht der zerlegte Code aus?
"Ändern des Typs von foo in int" ist ein guter Ausgangspunkt. Es könnte sein, dass Ihr Vergleichsoperator Typumwandlungscode erzeugt, der benachbarte Daten durcheinander bringt, nachdem er "optimiert" wurde.
Untersuchen Sie den disassemblierten Code sowohl auf die -O0als auch -Osauf die Einstellungen - ich vermute, dass eines der Optimierungsflags (wie -fcaller-saves?) den Code verstopft, der die Status- oder Indexregister innerhalb der Interrupt-Serviceroutine speichert/wiederherstellt. Siehe Atmel ATtiny10 Datenblatt Abschnitt 5.8 Reset und Interrupt Handling. Siehe auch Optimierungsoptionen von gcc-4.9.2
Wie sieht Ihre Erinnerungskarte aus? ATtiny10 hat nur 32 Byte SRAM, mit einem unsigned, einem long und einem int (plus den Overhead, den die C-Bibliotheken und der Debugger behaupten), es muss überfüllt sein.
@IgnacioVazquez-Abrams: Danke! avr-objdump -DIch fürchte, ich bin mir nicht sicher, wonach ich suchen soll, ich weiß nicht viel über Assembler ... Ich habe mir die Ausgabe von und -Sfür die .hexund die .elfDateien angesehen , und ich kann Unterschiede erkennen, einige machen Sinn, aber ich kann dem Geschehen nicht wirklich folgen. (Ich wollte nicht die gesamte Ausgabe posten, mache das aber gerne - oder einen Teil davon, wenn das hilft!)
@Maple Wenn ich beide in longs ändere, ist das Problem immer noch da ... also ist es nicht das, aber wahrscheinlich etwas über den Zugriff auf diese Variablen in der Hauptschleife. (Ich bin jedoch verwirrt, da ich dort definitiv nichts anschreiben werde ...)
@MarkU Danke für den Vorschlag! Ich habe weitere Nachforschungen angestellt, siehe das Update oben. Memory Map: Entschuldigung, ich bin wirklich ein Anfänger auf diesem Gebiet, wo soll ich nach der Memory Map suchen ...? Ich bin mir nicht sicher, ob es einfach darum geht, zu viel Speicher zuzuweisen, da ich glaube, dass außer dem absoluten Minimum keine zusätzlichen Abhängigkeiten enthalten sind, und ich keinen Debugger verwende.
@Maple-Korrektur (kann meinen Kommentar oben nicht bearbeiten): mit "nichts schreiben" - ich meine, nichts schreiben, was mit der ISR geteilt wird - zumindest nicht absichtlich ...
@pdenes: Weißt du, wie viele Zyklen zum Auswerten benötigt werden ovrf / 500? Der ATtiny hat nicht einmal eine Anweisung zum Multiplizieren; ganz zu schweigen von der Teilung! Dieser Test kann deutlich mikrocontrollerfreundlicher implementiert werden. Möglicherweise dauert es mehr Zyklen, um die ISR zu verarbeiten, als Sie das Timer-Intervall konfiguriert haben. Übrigens: Ich sehe das Timer-Intervall nicht initialisiert!
@Curd: Sehr guter Punkt! Ich habe das getestet, siehe das zweite Update oben! Re-Timer-Intervall: Die Standard-CPU-Frequenz beträgt 1 MHz und mit einem /8-Prescaler und dem 8-Bit-Timer beträgt die Überlauffrequenz 1000000/8/256 ~ = 488 Hz, was mit dem übereinstimmt, was ich wann sehe es funktioniert richtig. (Ich glaube nicht, dass ich noch etwas initialisieren muss, aber ich habe vielleicht etwas falsch verstanden!)

Antworten (2)

Mit nur 32 Byte Speicher (wie von MarkU in einem Kommentar erwähnt) ist der Speicher auf dem ATtiny10 unglaublich knapp. Der AVR-GCC-Compiler bietet keine Tools zur Stack-Überprüfung und generiert gerne Code, der den Stack überläuft. Hier ist zum Beispiel, was es für den Prolog zu Ihrer ISR generiert hat:

000000ba <__vector_4>:
  ba:   1f 93           push    r17
  bc:   0f 93           push    r16
  be:   0f b7           in      r16, 0x3f       ; 63
  c0:   0f 93           push    r16
  c2:   10 e0           ldi     r17, 0x00       ; 0
  c4:   4f 93           push    r20
  c6:   5f 93           push    r21
  c8:   6f 93           push    r22
  ca:   7f 93           push    r23
  cc:   8f 93           push    r24
  ce:   9f 93           push    r25
  d0:   af 93           push    r26
  d2:   bf 93           push    r27
  d4:   ef 93           push    r30
  d6:   ff 93           push    r31

Ich zähle 13 pushes drin. Dadurch wird der Stapel allein auf fast die Hälfte des Speichers Ihres Geräts erweitert. In Kombination mit einem anderen rcallim Hauptteil der ISR sowie ein paar pushes und rcalls im Prolog von mainwird der ISR-Stack mit dem Speicher kollidieren, der zum Speichern Ihrer globalen Variablen verwendet wird, und sie mit unerwarteten Daten überschreiben.

Der ATtiny10 ist kein gutes Ziel für einen C-Compiler. Wenn Ihre Anwendung einen etwas größeren Mikrocontroller unterstützen kann, ist möglicherweise ein Upgrade auf die tiny25/45/85-Familie gerechtfertigt. Ansonsten würde ich empfehlen, dieses Gerät mit Montage anzustreben.

Oder umsichtige und sorgfältige Verwendung von ISR_NAKED.
@IgnacioVazquez-Abrams Möglicherweise, obwohl Sie, wenn Sie nicht aufpassen, nur eine Reihe von Problemen (ISR verursacht Stapelkollision) gegen eine andere eintauschen (ISR beschädigt Register, die von verwendet werden) main().
Ich habe seinen Code simuliert und festgestellt, dass der Stack-Zeiger so niedrig wie die RAM-Adresse $46 war, als ich den Code aufrief, um ovrf/500 in der ISR zu berechnen, was der gleiche Ort wie ovrf war! Mit ovrf>>9 ging der Stackpointer nur auf $4E runter.
@BruceAbbott Macht Sinn. Die Division wird durch einen Aufruf einer udivdi-Funktion implementiert; Das Entfernen dieses Aufrufs verwandelt die ISR in eine Blattfunktion, was die Registerzuordnung verbessern könnte.
Danke @duskwuff! Ich denke, dies beantwortet auch meine "Follow-up" -Fragen zur Gültigkeit der Verwendung von C.
volatile unsigned int ovrf = 0;
unsigned long foo;
unsigned int bar;

+8 Byte RAM

int main(void) {

+2 Bytes oder RAM, Attribut verwenden ((OS_main))

ISR(TIM0_OVF_vect) {

Wenn Sie nichts angeben, wird ein Stackframe für einen Funktionsaufruf verschoben. Das werden ungefähr 14 Bytes sein, wenn ich mich richtig an meine ATTiny10-Kämpfe erinnere. Die Rücksendeadresse (2 Bytes) und einige Register.

Mit nur 32 Bytes können Sie einen vollständigen Funktionsaufruf durchführen. Wenn Sie mehr wollen, müssen Sie __attribute__((naked))Assembler verwenden und schreiben.

if ((ovrf / 500) % 2 == 0) {

Dies ruft wahrscheinlich Bibliotheksfunktionen auf, deren Stackframe Sie nicht speichern können.

Und es gibt auch nur 1024 Byte Programmspeicher. Dies ist sehr klein für C, ~100 Zeilen klein.