Tipps und Tricks zum C-Compiler (gcc) für den AVR

    english page is missing - you can help the project with a translation!

Optimierungen

    Die Prozessoren für Decoder sollten natürlich möglichst billig sein und deswegen versucht man, mit möglichst wenig Speicher auszukommen. Um nun den Compiler zu besonders kompakten Code zu veranlassen, sollte man ein paar Dinge beachten:
    1. Einstellungen des Compilers: der WinAVR (gcc) Compiler wird mit -Os auf "optimize for size" eingestellt. Weiters wird mit -fshort_enums eine kompakte Darstellung von Aufzähltypen erreicht.
    2. Der Compiler führt Berechnungen und logische Operationen generell als 16-Bit Integer durch, erst das Endergebnis wird dann der Zielvariablen oder dem if-then-else-Statement zugeführt. Man kann den Compiler zu kleinerem Code zwingen, wenn man Zwischenergebnisse der Berechnung auf eine lokale Variable vom Typ unsigned char casted. Dadurch kann der Optimizer unnötige 16-Bit Zwischenschritte erkennen und wegoptimieren.

      Ein Beispiel: das bisher empfangene Byte soll um eins nach links geschoben werden und das neue Bit hinten angefügt werden:
      • normaler Code:
                  if (DCC_INPUT == 1)
                     {
                        dccrec.accubyte = (dccrec.accubyte << 1) | 1;
                     }
                  else
                    {
                        dccrec.accubyte = dccrec.accubyte << 1;
                    }
            
      • optimierter Code:
                unsigned char my_accubyte;             // lokale Zwischenvariable
                my_accubyte = dccrec.accubyte << 1;
                if (DCC_INPUT == 1)
                  {
                    my_accubyte |= 1;                  // alle Operationen auf der lokalen Variablen
                  }
                dccrec.accubyte = my_accubyte;         // wieder zurückspeichern
            
        Das sieht auf den ersten Blick umständlicher aus, ist aber kleiner und schneller!
    3. Oft benutzte globale Variablen kann man in unbenutzten IO-Registern des AVR ablegen. Diese sind sowohl im Zugriff schneller, als auch per Bitbefehle manipulierbar; die Bits sind auch direkt abfragbar. Speziell GPIOR0 ... 2 sind für diese Zwecke gedacht.

      Beispiel:
              #define Recstate GPIOR1              // use GPIOR1 as state variable
      
              #define RECSTAT_WF_PREAMBLE  0       // define bit positions for states
              #define RECSTAT_WF_LEAD0     1
              #define RECSTAT_WF_BYTE      2
              #define RECSTAT_WF_TRAILER   3
          
      Die Zuweisung des Status erfolgt so:
              Recstate = 1<<RECSTAT_WF_LEAD0;
          
      Und die entsprechende Abfrage lautet:
              if (Recstate & (1<<RECSTAT_WF_PREAMBLE))            // wait for preamble
               {
               }
          
      Der Compiler generiert daraus folgenden, äußerst effektiven Code:
                  sbis        0x14, 0
                  rjmp        .+20
           
    4. Ganz "harte Nüsse" kann man mit inline-Assembler knacken. Hier ein Beispiel, wie man in einer Interruptroutine einen Wert auf ein IO-Register schreiben kann:
      #define ISR_INT0_OPTIMIZED
      #ifdef ISR_INT0_OPTIMIZED
      
          #define ISR_NAKED(vector) \
              void vector (void) __attribute__ ((signal, naked)); \
              void vector (void)
      
          ISR_NAKED(INT0_vect)
            {
               __asm__ __volatile
                (
                  "push r16"  "\n\t"
                  "ldi r16, %1"  "\n\t"
                  "out %0, r16" "\n\t"
                  "pop r16"  "\n\t"
               :                         // no output section
               : "M" (_SFR_IO_ADDR (TCCR1B)),
                 "M" ((0 << ICNC1)       // start timer1
                    | (0 << ICES1)
                    | (0 << WGM13)
                    | (1 << WGM12)       // Mode 4: CTC
                    | (0 << CS12)        // clk 1:1
                    | (0 << CS11)
                    | (1 << CS10))
                );
              asm volatile ( "reti" );
            }
      #else
          ISR(INT0_vect)
            {
              TCCR1B = (0 << ICNC1)       // start timer1
                     | (0 << ICES1)
                     | (0 << WGM13)
                     | (1 << WGM12)       // Mode 4: CTC
                     | (0 << CS12)        // clk 1:1
                     | (0 << CS11)
                     | (1 << CS10);
            }
      #endif
      
      Mit dem Attribute "naked" zwingt man gcc, den Funktionsprolog und -epilog wegzulassen. Man muß dann allerdings *alles* selbst machen, inkl. des RETI am Ende der ISR. Sinnvollerweise sollte inline-Assembler immer parallel zu einer C-Implementierung abgelegt werden, damit der Code möglichst kompatibel bleibt.

Besondere Probleme

Sprung nach 0x0000 (Restart)

    An diversen Stellen im Internet wird folgendes Konstrukt empfohlen, um einen Softwarereset auszulösen:
             void (*funcptr)( void ) = 0x0000;
             ...
             funcptr(); // Jump to Reset vector 0x0000
    
    Das hat bei mir nicht funktioniert, wie auch am Assemblerlisting (.lss) abzulesen war. Meine Lösung sieht so aus:
              __asm__ __volatile
                     (
                          "ldi r30,0"  "\n\t"
                          "ldi r31,0"  "\n\t"
                          "icall" "\n\t"
                     );
    

Selbsttest der Firmware

    Bei Updateprozessen, welche fallweise abgebrochen werden könnten, ist ein Selbsttest der Firmware sinnvoll. Hierzu verwendet man sinnvollerweise eine CRC-Berechnung über alle im Flashspeicher hinterlegten Daten. Hierzu bedarf es einer Reihe von Änderungen:
  • Längenvariable anlegen:
    Es ist nötig, in der Firmware selbst zu wissen, wie groß der benutzte und zu prüfende Flashspeicher ist. Also muß irgendwo (am besten an einer definierten Stelle) die Länge abgelegt sein. Im Flash kann man da ganz ans Ende gehen oder besser direkt nach den Vektoren und noch vor Initialisierung und Code.
    Der erste Schritt dazu ist das Freihalten eines Platzes. Man legt eine Secion an (z.B. mit Namen .length):
    const uint32_t ProgramLength __attribute__ ((section (".length"))) = 0x55555555;
  • Platz fest zuweisen:
    Jetzt muß man dafür sorgen, dass diese Section auch an den richtigen Platz kommt. Wenn man _in_ die .text Section hinein will, dann muß man leider das Linkerscript des GCC entsprechend anpassen. Hierzu holt man sich das Linkerscript für den verwendeten Prozessor und fügt dort im Bereich .text nach .vectors folgende Zeilen ein:
    .text   :
      {
        *(.vectors)
        KEEP(*(.vectors))
        *(.length)
        /* KEEP(*(.length)) - don't need to keep if it isn't defined */
        /* For data that needs to reside in the lower 64k of progmem.  */
        *(.progmem.gcc*)
        *(.progmem*) 
    Das Linkerscript findet man in /lib/ldscripts, für den atxmega128a4u war es avrxmega7.x. (Übersicht Prozessor-Modell, Übersicht Scripte)
    Dieses so modifizierte Linkerscript speichert man im Projektverzeichnis z.B. als avrxmega7_length.x . Damit der Linker dann auch damit arbeitet, braucht es einen Aufrufparameter:
    LDFLAGS += -T avrxmega7_length.x
    Jetzt sieht man im Hexfile direkt nach den Vektoren die oben definierten 55555555!
  • Richtigen Wert eintragen:
    Der nächste Schritt ist nun das Ersetzen dieses 55555555-Wertes mit der richtigen Länge. Hierzu verwendet man ein Tool, mit dem man in Hex-Dateien rummauscheln kann: z.B. srecord.
    Die Parameter -exclude 0x01FC 0x0200 -Little_Endian_Maximum 0x01FC sorgen dafür, dass srecord ein Loch von 0x01FC bis 0x0200 erzeugt, welches dann mit der Länge (Maximum) im Format little endian gefüllt wird.
    srec_cat $(PROJECT).org.hex -Intel -exclude 0x01FC 0x0200 -Little_Endian_Maximum 0x01FC -fill 0xff -over \( $(PROJECT).org.hex -I \) -Output $(PROJECT).max.hex -Intel
  • CRC anhängen:
    Nun kommt der eigentliche Teil, das Hinzufügen der CRC. Auch das leistet srecord:
    srec_cat $(PROJECT).max.hex -Intel -Little_Endian_CRC16 -max-addr \( $(PROJECT).max.hex -Intel \) -Cyclic_Redundancy_Check_16_XMODEM -Output $(PROJECT).crc.hex -Intel
    Abschließend bindet man das Hex nochmal zusammen und formatiert es 16 Hexzeichen breit, so wie es die Downloadtools wollen:
    srec_cat $(PROJECT).crc.hex -Intel -o $(PROJECT).out.hex -Intel --address_length=2 --line_length=44
    Jetzt liegt der Output als Hex mit eingetragener Länge und angehängter CRC vor und kann so auf den Baustein geladen werden.
  • Kontrolle:
    Auf dem Baustein muß nun noch die Überprüfung erfolgen.

  • Link: http://www.avrfreaks.net/forum/tutgccadding-crc-and-app-length-hex-files

Bootloader

    Wenn innerhalb von Bootloadern auf Bausteinen mit mehr als 64k Flash direkte Zugriffe ins Flash aus dem C-Compiler erfolgen, so werden diese als LPM statement erzeugt und greifen ins Leere! Betroffen sind davon Konstanten mit Attribut 'progmem' und auch Switch-Anweisung, die fallweise als Sprungtabelle im Flash abgelegt werden. Hier ist die Option -fno-jump-tables switch, if compiling a bootloader for devices with more than 64 KB of code memory.

Links