Avr-gcc wird ohne Optimierungen nicht korrekt kompiliert, funktioniert aber (schlecht) mit -Os

Ich entwickle Software für attiny88 mit der avr-gcc-Toolchain. Dies ist ein billiger Mikrocontroller mit 8kB Programmspeicher, 512B SRAM und 64B nichtflüchtigem EEPROM-Datenspeicher. Das alles ist ausreichend für die Aufgabe, die es bewältigen soll. Ich habe keinen Debugger für diesen Chip.

Das Gerät fungiert als SPI-Slave und ermöglicht dem Master das Lesen/Programmieren des EEPROM und das Lesen des Gerätestatus (Zustand einiger analoger Eingänge und digitaler Ausgänge). Das Protokoll ist einfach - das erste Datenbyte trägt die Anweisung, bei der die höchsten zwei Bits die erforderliche Aktion codieren (00-nichts, 01-EEPROM schreiben, 10-EEPROM lesen, 11-Lesezustand) und der Rest ist Adresse. Das zweite Byte ist immer Null, das dritte Byte ist der zu lesende oder zu schreibende Wert, das vierte Byte ist immer Null.

Das Problem ist, dass ich ein seltsames Verhalten vom Compiler bekomme. Es ist schwierig, genau zu sagen, was vor sich geht, deshalb werde ich nur ein paar Beispiele geben. Am auffälligsten ist, dass das Gerät ohne -Os-Optimierung nicht über SPI antwortet. Dies hat keinen der offensichtlichen Gründe - das Programm passt in den Speicher und der Stack sollte nicht in .bss/.data-Abschnitte laufen (das Programm hat ~700B, SRAM ist zwischen 0x100 und 0x2ff abgebildet, wobei .bss_end bei 0x109 liegt; der Heap ist leer; es gibt weder verschachtelte Funktionsaufrufe noch verschachtelte Interrupts).

Wenn ich die -Os-Optimierung einschalte, reagiert das Programm wie vorgesehen. Hier ist der Arbeitscode zur Handhabung der ISR:

unsigned char state[8];
volatile unsigned char data;

ISR(SPI_STC_vect)
{
  switch(data>>6) {
  case 0: 
    data = SPDR;
    break;
  case 1:           /* write eeprom */
    while(EECR & (1<<EEPE));
    EECR = (0<<EEPM1)|(0<<EEPM0);
    EEARL = 0;
    EEDR = SPDR;
    EECR |= (1<<EEMPE);
    EECR |= (1<<EEPE);
    data = 0;
    break;
  case 2:               /* read eeprom */
    EEARL = data & 0x1f;    /* with 0x3f stops working (???) */
    EECR |= (1<<EERE);
    SPDR = EEDR;
    data = 0;
    break;
  case 3:           /* read state */
    SPDR = state[data&7];
    data = 0;
    break;
  }
}

Das Programm wird jedoch beschädigt, wenn es semantisch anders geschrieben wird:

  • Wenn ich die Zeile "EEARL = data & 0x1f;" ändere zu "EEARL = data & 0x3f;", was wünschenswert ist, da es ermöglichen würde, den gesamten EEPROM-Adressraum zu adressieren, das ERPROM-Schreiben/Lesen funktioniert nicht mehr (ich brauche keine vollen 64B, also habe ich es so gelassen)

  • Das Lesen des Status (Fall 3) wird unterbrochen, wenn ich die Zeile „SPDR = state[data&7];“ ersetze. mit Switch-Case-Konstrukt, das den Wert des PORTD/PORTB-Registers zurückgibt, wenn die Adresse 0 bzw. 1 ist (der aktuelle Workaround besteht darin, state[0] und state[1] mit PORTB/PORTD in der Hauptschleife synchron zu halten).

Übersehe ich etwas Wichtiges? Es scheint mir, dass der Compiler durcheinander bringt, aber ich habe keine Fehlerberichte für avr-gcc oder avr-libc (newlib?) gefunden, die in die Rechnung passen würden.

Die Toolchain wurde aus dem aktuellen gcc-avr-Paket in aptitude-Repositories installiert. Das Makefile ist ziemlich einfach:

all: main.hex main.s

main.elf: main.o
    avr-gcc -g -mmcu=attiny88 -o main.elf main.o

main.s: main.elf
    avr-objdump -d main.elf > main.s

main.hex: main.elf
    avr-objcopy -O ihex  main.elf main.hex

main.o: main.c
    avr-gcc -mmcu=attiny88 -Os -c main.c -Wall

UPDATE : Immer noch das gleiche Ergebnis mit den neuesten binutils, gcc und avr-libc (aus den Quellen neu kompiliert)

Versuchen Sie, den generierten Maschinencode zu lesen/zu vergleichen.
Wo initialisiert man die dataVariable? Verlassen Sie sich auf die statische Initialisierung, um Ihnen "Zero-Out" zu geben? Das ist immer eine schlechte Idee, da Mikrocontroller dort oft vom C-Standard abweichen, um einen schnelleren Start zu ermöglichen.
Abgesehen davon ist der bei weitem häufigste Grund, warum Programme zwischen verschiedenen Optimierungsstufen nicht mehr funktionieren, dass Sie irgendwo in Ihrem Programm ein undefiniertes Verhalten haben. Das heißt, Sie haben wahrscheinlich einen schlafenden Fehler, der aufgetaucht ist, als Sie den Build geändert haben. Nicht unbedingt im geposteten Code.
@Lundin Ich initialisiere alles während der Initialisierung, sogar die avr-libc FAQ besagt, dass sie Variablen initialisieren ( nongnu.org/avr-libc/user-manual/FAQ.html#faq_varinit ), darauf verlasse ich mich nicht. Der Abschnitt .data ist leer.
Vor Jahren waren bestimmte Routinen in der avr-libc problematisch -O0. Ich frage mich, ob dies der Fall ist (siehe z. B. avrfreaks.net/forum/optimization-levels-avr-studio )
@PlasmaHH Danke für den Vorschlag, mir ist aufgefallen, dass ich die Disassemblierung unterscheiden kann, wenn ich nur eine Konstante von 0x1f auf 0x3f ändere, wie in der Frage besprochen. Die Unterschiede sind enorm - praktisch das ganze Programm ist anders. Scheint jedoch falsch zu sein - zum Beispiel ist die Anweisung cli (Interrupts deaktivieren) aus der Interrupt-Handler-Routine verschwunden
@Lundin Sie sehen den größten Teil des Programms, der Rest ist nur Init aller gemeinsam genutzten Variablen, Init von SPI und eine Endlosschleife. Das vollständige Programm sieht anders aus, aber ich teste es auf dieser Minimalversion, die die Fehler reproduziert. Ich lasse keinen Müll im Speicher und initialisiere alles manuell vor. Es ist schließlich ein einfaches Programm.
@Damien, das könnte erklären, warum es ohne -Os nicht funktioniert (obwohl ich gerne Details wissen würde, warum -O0 fehlschlägt), aber es erklärt immer noch nicht die von mir beschriebenen Fehler. Über das flüchtige Schlüsselwort - Daten sind flüchtig, wenn ich von der ISR darauf schreibe, der Status wird nur von der ISR gelesen, sodass sie nicht flüchtig sein müssen.
Zeitraubende Dinge (wie das Schreiben von EEPROM) in einem Interrupt-Handler (der sich an jedes SPI-Empfangsereignis erinnert) bringen definitiv Pech.
@carloc Das Lesen des EEPROM hält die CPU für 4 Zyklen an, das Schreiben erfolgt asynchron mit autonomen Automaten auf dem Chip - es blockiert die CPU nicht. Das Problem ist nicht da.
Zufällige Beobachtung Nr. 1: Sie müssen dies tun while(EECR & (1<<EEPE));, bevor Sie auch das Eeprom lesen . Beobachtung Nr. 2: Ich sehe nicht, wie das von Ihnen beschriebene 4-Byte-Protokoll mit Ihrem ISR funktioniert - Beispiel: eeprom write; das erste byte wird als befehl/adresse gelesen, das zweite (null) byte löst den eigentlichen schreibvorgang aus und schreibt 0 ins eeprom, das dritte byte (daten) wird wieder als befehl interpretiert. Das passt nicht zu deiner geschilderten Intention...
@marcelm Ich muss den Spinloop nicht machen, solange ich weiß, dass niemand das EEPROM programmieren wird, bevor er daraus liest. Der Spinloop beim Schreiben ist nur als Ausfallsicherung gegen Mem-Korruption da und sollte sich eigentlich nie drehen. Über das Programm - Beachten Sie, dass der Interrupt ausgelöst wird, wenn Sie die Übertragung eines Bytes beendet haben, während das nächste Byte bereits unterwegs sein kann. Deshalb gibt es die Polsterung und warum funktioniert sie. Es ist schnell, einfach und erfordert nur eine globale Variable.
@marcelm Entschuldigung, ich habe in dem Beitrag geschrieben, dass das Schreiben mit dem dritten Byte erfolgt, das ist falsch - das Datenbyte, das in das EEPROM geschrieben werden soll, ist das zweite. Gelesene Daten werden immer noch mit dem dritten Byte herausgeschoben.

Antworten (1)

Sie müssen die Ausgabe von überprüfen, avr-objdumpum zu sehen, welche genauen Anweisungen für Ihren Code generiert wurden. Übrigens wäre es hilfreich, Ihren C-Code per in die Disassemblierung aufzunehmen avr-objdump -S main.elf > main.s. 0x1FIch bezweifle, dass das ganze Programm anders wird, wenn Sie die Konstante durch ersetzen 0x3F. Ihr nächster Schritt wäre, Unterschiede in der Auflistung zu isolieren und sie sorgfältig zu analysieren.

Eine solche Analyse ist so weit, wie Sie ohne geeignete Entwicklungstools kommen können. Die Anschaffung eines Debuggers oder eines Simulators würde Ihnen viel Aufwand ersparen, der erforderlich ist, wenn Sie sich auf die Analyse statischer Listen beschränken.

PS: Ich gehe hier davon aus, dass Sie beim Kompilieren keine Compiler-Warnungen erhalten. Wenn Sie welche haben, sollte deren Behebung Ihre erste Priorität sein. Moderne Compiler leisten ziemlich gute Arbeit darin, Sie über subtile Fehler zu informieren, die sonst möglicherweise schwer zu finden sind.

Bei -Wall gibt es keine Warnungen; Simulavr/Debugger könnte von Nutzen sein, aber da ich eine Version der Software habe, die auf wiederholbare Weise funktioniert, ist dies auf dem Chip kein Problem. Zu den Unterschieden aufgrund der Änderung einer Konstante von 0x1f auf 0x3f: Diff markiert den größten Teil des Codes als neu geschrieben. Es läuft auf eine zusätzliche Variable im .bss-Abschnitt und Änderungen der Offsets im Programm hinaus, die die Adressierungsliterale ändern. Es ist seltsam, aber das Programm ist strukturell anders - es scheint, dass der Optimierer entscheidet, eine zusätzliche Variable zu verwenden, um die Sache mit der Konstante 0x3f zu beschleunigen.
@student versucht, Assembler-Listen vom Compiler zu erzeugen, dh avr-gcc -mmcu=attiny88 -Os -S main.c. Das sollte Adressliterale in symbolischer Form ( label_0001usw.) belassen, sodass das Erstellen eines Diffs einfacher sein sollte.