Erfahrungen und Tipps zum ATXmega

    english page is missing - you can help the project with a translation!
    Im Folgenden gebe ich ein paar Erfahrungen wieder, die ich bei meinen Entwicklungen mit dem xmega gemacht habe.

Analogsystem

    Beim Gleisbesetztmelder wird der Strom des jeweiligen Abschnittes durch einen Widerstand (begrenzt durch anitparallele Schottky-Dioden) geleitet. Der entstehende Spannungsabfall im Bereich -0,4V bis +0,4V wird zur Messung verwendet.
    Der ATXmega bietet einen AD-Wandler, welcher auch ins 'Negative' messen kann. Das habe ich in einen Test vermessen:
  • Einstellungen:
      2 MHz Samplerate
      Single Measurement, Signed Mode, Resolution 12 Bit
      interne Bandgap Referenz mit 1V, Gain 1
      Chip mit Längsregler 3V3 versorgt
      Einspeisung der Meßspannung aus (längsgeregeltem) Labornetzteil via 1k
      Kontrolle der Spannung mit 4-1/2 stelligem DVM.
  • Ergebnisse:
      Rauschen: so ca. 4 Digit, d.h. 2mV.
      Meßbereich: 0 .. 1V, danach geht der Wandler bei Code 2047 sauber in die Sättigung;
      DC-Offset ca. 3mV, Nichtlinearität ca. 3mV.
      Negative Spannungen: linearer Meßbereich: 0 .. -340mV, dannach wird der Wandler nichtlinear und sättigt etwa bei -400mV
  • Bootloader, Flash schreiben

      Der Flashspeicher des xmega ist in Blöcken, sog. PAGES unterteilt. Beim Beschreiben des Flashes muß man immer eine ganze Page löschen, erst dann kann man schreiben. Für einzelne Bytes ist dieses Verfahren also nicht optimal geeignet.
      Im Bootloader geht man dann so vor, dass man eine komplette Page liest, die neuen Daten in dem gelesenen Teil überschreibt und abschließend die Page wieder zurückschreibt.
      Page lesen und schreiben geht nur mit den NVM-Controller, das ist ein Speicherkontroller, welcher über besondere Befehle angesprochen wird. Zusätzlich gilt noch die Einschränkung, dass Schreiben nur aus einen speziellen Adressbereich (Bootloaderbereich) heraus funktioniert. Doch dazu später mehr.

      Der Zugriff auf das Flash wird am besten mit dem von Atmel gelieferten Routinen (aus der Appnote AVR1605 bzw. AVR1316) bewerkstelligt. Hierzu muß aber diesen Routinen die Größe einer Page bekannt gemacht werden. Das geschieht über einen #define, je nach Version heißt dieser FLASH_PAGE_SIZE oder APP_SECTION_PAGE_SIZE. Atxmega128A1, Atxmega128D3 haben hier 512 Bytes, der Atxmega128A4 schlägt aus der Art und hat nur 256 Byte Pagegröße.
           Achtung: für Pagegrößen < 512 (also z.B. atxmega128A4) ist ein Bug in diesen Routinen, in SP_ReadFlashPage muß die Zeile
              ldi        r21, ((FLASH_PAGE_SIZE)&0xFF)   ; Load R21 with byte count.
      durch
              ldi        r21, ((FLASH_PAGE_SIZE/2)&0xFF)   ; Load R21 with word(!) count.
      ersetzt werden. Falls die Orginalversion verwendet wird, überschreibt die Routine benachbarten RAM-Speicher und der Programmcode stürzt ab. Stand 04/2014 ist dieser Fehler noch immer in den Appnotes enthalten.


      Wie oben schon erwähnt, braucht man zum Schreiben einen kleinen Teil in der Bootsection, das gilt auch für das Schreiben aus einer Applikation heraus. Wenn ein Bootloader vorhanden ist, dann kommen sich dieser Teil aus der Applikation und der Bootloader selbst in die Quere. Sinnvollerweise benutzt man den Schreibzugriff dann geteilt in beiden Programmen und legt ihn hierzu fest an eine fixe Stelle. Man muß also diesen Abschnitt (.section .BOOT) beim Linken entsprechend verschieben.

      Das geschieht durch hinzufügen des Parameters -Wl,-section-start=.BOOT=0x21fc0 (für 128k-Xmegas) zu den Linkparametern. Hierdurch kommt der Schreibzugriff auf das Flash an das Ende der Bootarea und kann sowohl von Bootloader als auch App benutzt werden.
      Natürlich muß man diesen Abschnitt aus dem Download der App entfernen (ist ja durch Bootloader bereits vorhanden), also ergänzt man den Hexconvertierprozess -avr-objcopy mit -R .BOOT .

      Da man von der App nicht sicher sein kann, ob wirklich ein Bootloader und diese Schreibroutine vorhanden ist, sollte man das beim Programmstart abprüfen:
      // Code zum Abpruefen des Bootloaders
      //
      // hier zuerst der Code aus Hexfile, als Referenz zum Vergleich (512 byte / block)
      #if (SPM_PAGESIZE == 256)
      const unsigned char sp_code_from_hex[] PROGMEM =
        {
          0xEE, 0x27, 0xFF, 0x27, 0x19, 0xBE, 0xDC, 0x01, 0x43, 0xE2, 0x40, 0x93, 0xCA, 0x01, 0x50, 0xE8,
          0x2D, 0xE9, 0x0D, 0x90, 0x1D, 0x90, 0x20, 0x93, 0x34, 0x00, 0xE8, 0x95, 0x32, 0x96, 0x5A, 0x95,
          0xC1, 0xF7, 0x11, 0x24, 0x08, 0x95, 0xFC, 0x01, 0x40, 0x93, 0xCA, 0x01, 0x2D, 0xE9, 0x20, 0x93,
          0x34, 0x00, 0xE8, 0x95, 0x11, 0x24, 0x3B, 0xBF, 0x08, 0x95
        };
      #elif (SPM_PAGESIZE == 512)
      const unsigned char sp_code_from_hex[] PROGMEM =
        {
          0xEE, 0x27, 0xFF, 0x27, 0x19, 0xBE, 0xDC, 0x01, 0x43, 0xE2, 0x40, 0x93, 0xCA, 0x01, 0x50, 0xE0,
          0x2D, 0xE9, 0x0D, 0x90, 0x1D, 0x90, 0x20, 0x93, 0x34, 0x00, 0xE8, 0x95, 0x32, 0x96, 0x5A, 0x95,
          0xC1, 0xF7, 0x11, 0x24, 0x08, 0x95, 0xFC, 0x01, 0x40, 0x93, 0xCA, 0x01, 0x2D, 0xE9, 0x20, 0x93,
          0x34, 0x00, 0xE8, 0x95, 0x11, 0x24, 0x3B, 0xBF, 0x08, 0x95,
        };
      #else
          #warning info: SPM_PAGESIZE = void
      #endif
      
      
      static unsigned char spm_is_available(void)
        {
          uint8_t i;
          uint32_t spm_code_addr = 0x10FE0L * 2;   // word-addr -> byte-addr
          const uint8_t *ref = sp_code_from_hex;
      
          for (i=0; i < sizeof(sp_code_from_hex); i++)
            {
              if (pgm_read_byte(ref++) != pgm_read_byte_far(spm_code_addr++))
                {
                  led_fire();led_fire();led_fire();led_fire();led_fire();
                  led_fire();led_fire();led_fire();led_fire();led_fire();
                  return(0);
                }
            }
          return(1);
        }
        

    Besondere Optimierungen

    Timer

      Beim ATXmega sind alle Timer gleich, jeder kann alles: 8 Bit, 16 Bit, Compareregister, Capture, usw. Zusätzlich sind ein paar Dinge eingebaut, welche bis data ziemliches Aufstand verursacht haben:
    • Wenn ein Waveform Generation Mode gewählt ist, kann man trotzdem noch auf die Pins zugreifen - nützlich z.B. einer H-Brücke, welche in der Richtung umgesteuert wird oder kurz aus dem PWM raus soll (z.B. für die Cutout von DCC BiDi).
    • Es gibt ein extra Register, um Timer-Ereignisse auszulösen. Man kann über dieses Register den Timer neu starten, einen Update provozieren (also z.B. mehrere Compareregister parallel umladen).

    Xmega und DCC

    DCC Dekodieren

      Der Xmega hat 8 interne Event-Leitungen, man kann frei definieren, wer da sendet und wer da empfängt. Das kann benutzen, um DCC elegant zu dekodieren:
    • Der DCC Eingang wird so programmiert, daß er auf Flanke (steigend oder fallend) reagiert. Diese Ereignis wird dann auf die Event-Leitung gelegt.
    • Ein Timer wird freilaufend programmiert, ein Ausgangs-Compare auf die Zeitspanne von 87µs (=DCC Period_1 * 0,75= programmiert. In Event-Aktions-Register des Timer wird nun 'RESTART' auf das obige Event eingetragen.
      Dadurch wird beim Flankenwechsel des DCC-Signals der Timer zurückgesetzt, und exakt nach 87µs erfolgt das Auslösen des Compares.
    • Nun kann man da entweder eine Interruptroutine anhängen und den Port einlesen oder wiederum ein Event auslösen, welches dann den DMA-Controller veranlaßt, den Port einzulesen und im Speicher abzulegen.
    • Events erlauben also sehr exaktes Timing und Abarbeiten von Abläufen ohne CPU-Interaktion.

    DCC erzeugen

      Bei DCC wird jedes Bit als eine Folge aus zwei Leitungszuständen (je ein Puls high, ein Puls low) kodiert, eine 1 wird dabei durch 58µs high + 58µs low, eine 0 durch 116µs high und 116µs low dargestellt.

      Jetzt kann man hier verschiedene Ansätze zum Erzeugen diese Signals verwenden, nochfolgend ist mal ein Ansatz dargestellt, welcher ohne strenge Interruptforderungen auskommt und nur sehr geringe CPU-Last verursacht.

      Es wird ein Timer in Frequency Mode verwendet: hier läuft der Zähler bis zu einem einstellbaren Endwert hoch und beginnt dann wieder von vorne. Bei jedem Endwert wechselt der Ausgang die Polarität. Wenn man nun diesen Endwert bei jedem Zählerumlauf neu beschreibt, so kann man beliebige Pulsketten erzeugen. Das Beschreiben geht am Besten mit dem DMA Controller. Zu beachten ist, das Frequency Mode nur auf dem Compareregister A und dessen zugehörigem Ausgang verfügbar ist.

      Beginnen wir mit dem Timer. Der Vorteiler wird auf 64 gestellt, damit ergibt sich bei 32 MHz ein Zeitraster von 2µs. Des weiteren stellt man die Timer auf Bytemode, damit belegen alle Zeiteinstellungen nur 8 Bit, auch der DMA braucht dann nur je ein Byte nachladen.
            static void init_timer_d0(void)
              {
                TCD0.CNT = 0;
                TCD0.PER = 0;
                TCD0.CCA = TIME_DCC_HALF1;
                TCD0.CTRLE = (1 << TC0_BYTEM_bp);       // Byte mode: 1 run as 8-bit counter
                TCD0.CTRLA = ( TCD0.CTRLA & ~TC0_CLKSEL_gm ) | TC_CLKSEL_DIV64_gc;
                TCD0.CTRLB = (0 << TC0_CCDEN_bp)
                           | (0 << TC0_CCCEN_bp)
                           | (0 << TC0_CCBEN_bp)
                           | (1 << TC0_CCAEN_bp)        // enable A (dcc)
                           | TC_WGMODE_FRQ_gc;
                TCD0.INTCTRLA = ( TCD0.INTCTRLA & ~TC0_OVFINTLVL_gm ) | TC_OVFINTLVL_OFF_gc;
              }


      Dann muß man den DMA Controller aufsetzen. Hier werden die Kanäle 2 und 3 verwendet und dabei auch der DBUFMODE aktiviert: nach der Übertragung des Kanals 2 wechselt der DMA-Controller automatisch auf Kanal 3 und umgekehrt. Übertragen wird je ein Byte (wir haben ja den Timer auf Byte-Mode laufen), SRC wird inkrementiert, DEST bleibt immer gleich. Der Reload des SRC-Pointers soll je Block (bei uns eine DCC-Message) erfolgen.
           static void init_dma(void)
              {
                DMA.CTRL = (1 << DMA_ENABLE_bp)
                         | (0 << DMA_RESET_bp)
                         | DMA_PRIMODE_CH0123_gc
                         | DMA_DBUFMODE_CH23_gc;
                DMA.CH2.REPCNT = 0;                        // run forever
      
                DMA.CH2.CTRLA = (0 << DMA_CH_ENABLE_bp)    //  Channel Enable
                              | (0 << DMA_CH_RESET_bp)   // Channel Software Reset
                              | (1 << DMA_CH_REPEAT_bp)   // Channel Repeat Mode bit position.
                              | (0 << DMA_CH_TRFREQ_bp)   // Channel Transfer Request bit position.
                              | (1 << DMA_CH_SINGLE_bp)   // Channel Single Shot Data Transfer bit position.
                              | DMA_CH_BURSTLEN_1BYTE_gc;
                DMA.CH2.CTRLB = DMA_CH_ERRINTLVL_OFF_gc
                              | DMA_CH_TRNINTLVL_LO_gc;  // transaction complete
      
                DMA.CH2.ADDRCTRL = DMA_CH_SRCRELOAD_BLOCK_gc  // Source address reload mode : _NONE, BLOCK, BURST, TRANSACTION
                                 | DMA_CH_SRCDIR_INC_gc       // Source addressing mode: FIXED, INC, DEC
                                 | DMA_CH_DESTRELOAD_NONE_gc  // Destination adress reload mode: NONE, BLOCK, BURST, TRANSACTION
                                 | DMA_CH_DESTDIR_FIXED_gc;   // Destination adressing mode: FIXED, INC, DEC
      
                DMA.CH2.TRIGSRC = DMA_CH_TRIGSRC_TCD0_CCA_gc;   // Counter D, comp. A
      
                src.as_uint32 = dcc_buffer_0;
                DMA.CH2.SRCADDR0 = src.low8;
                DMA.CH2.SRCADDR1 = src.high8;
                DMA.CH2.SRCADDR2 = src.upper8;
      
                dest.as_uint32 = &TCD0.CCAL;
                DMA.CH2.DESTADDR0 = dest.low8;
                DMA.CH2.DESTADDR1 = dest.high8;
                DMA.CH2.DESTADDR2 = dest.upper8;
      
                DMA.CH3.REPCNT = 0;
      
                DMA.CH3.CTRLA = (0 << DMA_CH_ENABLE_bp)    //  Channel Enable
                              | (0 << DMA_CH_RESET_bp)     // Channel Software Reset
                              | (1 << DMA_CH_REPEAT_bp)   // Channel Repeat Mode bit position.
                              | (0 << DMA_CH_TRFREQ_bp)   // Channel Transfer Request bit position.
                              | (1 << DMA_CH_SINGLE_bp)   // Channel Single Shot Data Transfer bit position.
                              | DMA_CH_BURSTLEN_1BYTE_gc;
                DMA.CH3.CTRLB = DMA_CH_ERRINTLVL_OFF_gc
                              | DMA_CH_TRNINTLVL_LO_gc;  // transaction complete
      
                DMA.CH3.ADDRCTRL = DMA_CH_SRCRELOAD_BLOCK_gc  // Source address reload mode : _NONE, BLOCK, BURST, TRANSACTION
                                 | DMA_CH_SRCDIR_INC_gc       // Source addressing mode: FIXED, INC, DEC
                                 | DMA_CH_DESTRELOAD_NONE_gc  // Destination adress reload mode: NONE, BLOCK, BURST, TRANSACTION
                                 | DMA_CH_DESTDIR_FIXED_gc;   // Destination adressing mode: FIXED, INC, DEC
      
                DMA.CH3.TRIGSRC = DMA_CH_TRIGSRC_TCD0_CCA_gc;   // Counter D, comp. A
      
                src.as_uint32 = dcc_buffer_1;
                DMA.CH3.SRCADDR0 = src.low8;
                DMA.CH3.SRCADDR1 = src.high8;
                DMA.CH3.SRCADDR2 = src.upper8;
      
                dest.as_uint32 = &TCD0.CCAL;
                DMA.CH3.DESTADDR0 = dest.low8;
                DMA.CH3.DESTADDR1 = dest.high8;
                DMA.CH3.DESTADDR2 = dest.upper8;
      
                dma_active_channel = 2;
      
                // and fire the DMA (we start with channel 2)
      
                DMA.CH2.CTRLA = (1 << DMA_CH_ENABLE_bp)     //  Channel Enable
                              | (0 << DMA_CH_RESET_bp)      // Channel Software Reset
                              | (1 << DMA_CH_REPEAT_bp)     // Channel Repeat Mode
                              | (0 << DMA_CH_TRFREQ_bp)     // Channel Transfer Request
                              | (1 << DMA_CH_SINGLE_bp)     // Channel Single Shot Data Transfer
                              | DMA_CH_BURSTLEN_1BYTE_gc;
              }


      Wenn ein DMA Durchlauf fertig ist, erfolgt ein Interrupt. In diesem Interrupt wird der Pointer auf die aktive DCC Nachricht um eins weitergeschoben und auch das Neuberechnen der Kurvenform für die nächste DCC-Nachricht angestoßen.
            ISR(DMA_CH2_vect)
              {
                DMA.INTFLAGS = DMA_CH2ERRIF_bm  | DMA_CH2TRNIF_bm;
      
                dma_active_channel = 3;
                actual_index++;
                actual_index &= 0x3;  // modulo 4
                isr_set_task_ready(TASK_DCC_GEN);
              }
      
            ISR(DMA_CH3_vect)
              {
                DMA.INTFLAGS = DMA_CH3ERRIF_bm  | DMA_CH3TRNIF_bm;
      
                dma_active_channel = 2;
                actual_index++;
                actual_index &= 0x3;  // modulo 4
                isr_set_task_ready(TASK_DCC_GEN);
              }


      Die in DMA-ISR freigegebene Task berechnet dann den Pulszug für das nächste DCC Signal:
         #define WRITE_1 {*dcc_buffer++ = TIME_DCC_HALF1;*dcc_buffer++ = TIME_DCC_HALF1;}
         #define WRITE_0 {*dcc_buffer++ = TIME_DCC_HALF0;*dcc_buffer++ = TIME_DCC_HALF0;}
      
         static void prepare_dma_buffer(t_dcc_message* dcc_message)
           {
             uint16_t size; uint8_t* dcc_buffer;
             uint8_t i,prebits; uint8_t my_xor = 0; // dcc checksum
      
             if (dma_active_channel == 2) dcc_buffer = dcc_buffer_1;
             else dcc_buffer = dcc_buffer_0;
      
             // ----- now fill this buffer with timing data
      
             if (dcc_message->type == is_prog) prebits = MAX_PREAMBLE_BITS;
             else prebits = NORMAL_PREAMBLE_BITS;
      
             for (i=0; i<prebits; i++)  {  WRITE_1; }                // preamble
             WRITE_0;                                                    // dcc starts
             for (i=0; i<dcc_message->size; i++)
               {
                 uint8_t current_byte = dcc_message->dcc[i];
                 my_xor ^= current_byte;
                 for (uint8_t mask=0x80; mask; mask >>= 1)  // msb first
                   {
                     if (current_byte & mask) { WRITE_1; }
                     else  { WRITE_0; }
                   }
                 WRITE_0;                                                // byte delimiter
               }
             for (uint8_t mask=0x80; mask; mask >>= 1)
               {
                 if (my_xor & mask) { WRITE_1; }
                 else  { WRITE_0; }
               }
             WRITE_1;                                                    // packet end bit
      
             if (dma_active_channel == 2)                              // ---- load to isr / dma
               {
                 size = dcc_buffer - dcc_buffer_1;
                 DMA.CH3.TRFCNT = size;
               }
      
             else
               {
                 size = dcc_buffer - dcc_buffer_0;
                 DMA.CH2.TRFCNT = size;
               }
           }
      Dieses Konstrukt verursacht eine Interruptlast von etwa 10µs alle 6ms, das Neuberechnen der Pulswerte dauert etwa 40µs (bei 32MHz Atxmega128A1), so dass dieser DCC-Generator eine Last kleiner 1% verursacht. Zudem ist der Interrupt von der Zeitanforderung her nicht kritisch.

    Xmega und DMX erzeugen

      DMX 512 ist ein sehr einfaches Protokoll, um Dimmer anzusteuern. Es handelt sich dabei um RS485 / RS422 mit 250kBaud, 8 Bit, keine Parity, zwei Stopbits (8N2). Es hat folgenden Aufbau:
    • Framebeginn: es wird ein Break (d.h. TX=Low) für die Dauer von mind. 88us gesendet.
    • Es folgt ein Mark after Break (d.h. TX=High) für die Dauer von mind. 8us.
    • Startbyte, für normale Datenübertragung wird 0x00 gesendet.
    • Schleife über alle DMX Byte, wobei jedes Byte exakt einem Kanal zugewiesen ist: erstes Byte - erster Kanal, zweites Byte - zweiter Kanal, usw. Die Daten sind nicht weiter kodiert oder gesichert, es können max. 512 Byte übertragen werden.
    • Das ist jetzt alles nicht weiter aufregend und kann mit dem UART erledigt werden. Einzig die Erzeugung des Breaks zu Beginn des Frames ist nicht so ohne weiteres mit dem UART darstellbar. Hier kann man entweder die Port vom UART abkoppeln, Pegel 'zu Fuß' einstellen und mittels delay_us() aktiv warten oder folgenden Trick verwenden:

      Zum Erzeugen des Break wird die Baudrate auf langsame 57600 Baud einstellt und ein Datum 0xC0 gesendet: bei 57600 ist ein Bit 17us lang, das entstehende Break ist also 121us lang: Stopbit und 6 LSB.

      DMX Ausgeben
      Start:UART auf 57600 einstellen.
      UART.DATA mit 0xC0 beschreiben.
      TXC-Interrupt freigeben (TX-complete)
      ISR(TXC):UART auf 250000 einstellen.
      Schleifenzähler = 0.
      UART.DATA mit 0x00 beschreiben.
      TXC-Interrupt sperren, DRE-Interrupt freigeben (Data Register Empty)
      ISR(DRE):UART.DATA mit dmx[Schleifenzähler] beschreiben.
      Schleifenzähler inkrementieren.
      Wenn Ende erreicht: DRE-Interrupt sperren
      Mit dieser Methode wird durch einmaliges Aufrufen der Startoperation der komplette DMX-Datensatz ausgegeben. Beispielcode für DMX512 Erzeugung mit dem Atxmega auf der Seite vom OneDMX-Projekt.

    Speichern von Daten im Flash bei atxmega

      Das permanente Speichern von Daten im Flash ist etwas vertrackt: Ins Flash kann man nur aus dem Bootloaderbereich schreiben (die SPM-Befehl sind von dort aus ausführbar), d.h. man braucht im Bootloaderbereich entsprechenden Code. Diesen Code muß man anspringen, von dort aus den Schreibvorgang durchführen und wieder zurückkommen.
      Dabei muß man beachten, dass der Bootbereich außerhalb der Reichweite der üblichen Calls bzw. Pointerzugriffe liegt: sowohl bei Call als auch bei Datenzugriff dorthin sind entsprechende Vorkehrungen zu treffen. (RAMPZ, EIND entsprechend umprogrammieren).
      Zudem wird in einer üblichen Installation im Bootbereich ein Bootloader liegen, d.h. beim FW-Update klammert man diesen Bereich aus, i.d.R. durch ein Hinzufügen von -R .boot zu den HEXFLAGS des Linkers im makefile.

      D.h. man muß da in den Bootbereich rein, kann es aber eigentlich nicht ... Ich habe das so gelöst, dass ich mir aus den Bootloader die Funktion 'write-Flash' rausgezogen habe, (Sektion .BOOT aus der sp_driver.c) die fest auf eine Adresse verlinkt habe (0x10Ef0) und diese Adresse dann aus der Applikation auch anspringe. (mit gesetztem EIND-Bit).
      Damit ich dabei nicht ins nirwana springe und mir der Prozessor 'abstürzt', prüfe ich beim Programmstart ab, ob passender Code an der Stelle liegt, an die ich hinspringen will. Wenn da nichts ist, dann werden schreibende Speicherzugriffe ins Flash aus der Applikation heraus unterbunden.
      Weiter muß man beachten, dass durch EIND und RAMPZ eventuelle die normale Adressierung des gcc daneben greift, sinnvoll ist hier ein Sperren der Interrupts vor der Speicherroutine und anschließende Wiederfreigabe.

    CRC-Schutz von Firmwarepaketen

      Beim Nachladen von Firmware hat man bei einem Mikrocontroller keine Möglichkeit, die erhaltenen Daten zwischenzuspeichern. Man muß also 'am lebenden Objekt' einspielen. Nur: wie stellt man sicher, dass dann alles korrekt drin ist?

      Also muß man eine Prüfsumme über den benutzen Flashspeicher bilden - diese wird beim Erzeugen der Firmware errechnet und zusammen mit der Firmware geladen. Der Code im Bootloader prüft dann vor Ausführung der Applikationsfirmware die Integrität und springt erst dann die Firmware an. Bei Fail kann der Bootloader 'gnädig' verfahren und den Bedarf für einen erneuten Download anzeigen. Das Problem dabei ist: man muß dem Bootloader bekannt machen, welche Bereiche jeweils zu prüfen sind.

      Folgendes Verfahren verwende ich:
    • An einer wohldefinierten Stelle im Flash wird die Länge der Firmware hinterlegt. Wohldefinierte Stellen sind leider nicht so leicht verfügbar: unten liegen die Vektoren, danach der Code; der Rest vom Flash könnte Daten enthalten. In der Bootarea wäre eine Möglichkeit, allerdings ist dann die Länge immer ein extra Anhängsel an das Hexfile - und Beschreiben der Bootarea aus dem Bootloader heraus auch nicht kugelsicher. Als Ort für die Länge verwende ich den Interruptvektor für 'Ausfall externer Oszillator' (OSC_XOSCF_vect), dieser liegt bei 0x0004 bis 0x0007 und damit zumindest an einer eindeutigen Stelle innerhalb der Firmware. Klar, diesen Interrupt darf man dann nie mehr verwenden.
    • Um die Länge dort abzulegen, benutze ich srec_cat. Srec_cat ein sehr universelles Tool, um Manipulationen an Hex-Dateien vorzunehmen:
      • Im ersten Schritt liest man das hex-file ein, nimmt dabei aber den Bereich von 0x0004 bis 0x0008 aus. (Parameter: -exclude 0x0004 0x0008)
      • Dann holt man sich die Länge im Format Little-Endian und deponiert das an der Stelle 0x0004: (Parameter: -MAximum_Little_Endian 0x0004 -fill 0x00 -over \( file.hex -I \)
      Jetzt liegt also eine gepatchte Hexdatei mit der Länge (in Bytes) statt des Interruptverktors vor.
    • Im nächsten Schritt wird nun an diese Datei eine CRC angehängt.
    • Da der Bootloader auf maximale Record-Längen von 16 Byte je Zeile beschränkt ist, muß srec_cat anweisen, nur kurze Zeilen zu erzeugen: Parameter -Output_Block_Size=16

    Erzeugen von Ansteuersignalen für WS2812-LEDs

      WS2812 RGB LEDs sind dreifarbige LEDs mit integriertem Controller, d.h. innerhalb der LED ist ein Controllerchip, eine rote, grüne sowie eine blaue LED installiert. Diese LED sind dann in einem 4- oder 6-poligen Gehäuse installiert, welches VCC und Masse sowie Data-In und Data-Out Anschluß nach außen führt.

      Die Ansteuerung erfolgt über eine einzelne Datenleitung mit einem asynchronen seriellen Protokoll, die Übertragungsgeschwindigkeit ist 800kBaud, d.h. die Dauer eines Bits 1,25µs. Die Daten sind 'return-to-zero'-kodiert, wobei die steigende Flanke jeweils den Beginn eines Bits definiert, die zeitliche Position der fallenden Flanke den Bitwert festlegt. Eine "0" wird über einen kurzen Puls, eine "1" über einen langen High-Puls kodiert.
      Der Datenstrom ist selbsttaktend und wird von LED zu LED durchgereicht, wobei jede LED die ersten 24 Bits entfernt und den Rest weiterreicht - so wie in der Schule die ausgeteilten Blätter: jeder nimmt eins runter und gibt den Rest weiter. Nach dem Datenstrom kommt eine Pause von mind. 50µs low ("reset code"), diese Pause ist das Signal zur Übernahme der Daten in die Ausgangsregister (PWM) des Treibers.
      Jede LED benötigt 24 Datenbits (Kodierung G8:R8:B8), jede Farbe wird LSB first übertragen.

      Die exakten Angaben zum Timing schwanken zwischen den Datenblätter, auch sind die Datenblätter of recht unprofessionell gestaltet (gerade hinsichtlich Revisionstand und Toleranzangaben). world-semi.com definiert im ws2811-Datenblatt T0 als 250ns (+- 75ns) high und 1000ns (+-75ns) low, T1 als 600ns high und 750ns low. Im Datenblatt des ws2812 (gleicher Chip) sind dann andere Werte.
      Datenblattangaben von LEDs sind typ 400ns / 850ns und 850ns / 400ns (je mit +/-150ns), auch die Periode ist mit +/- 600ns angegeben.
      Interessant in diesem Zusammenhang ein Beitrag auf hackaday.com: Dort wird als einzige relevante Größe die Breite des Highpulses genannt: unter 550ns wird es als 0 interpretiert, über 550ns als 1. Damit würde eventuelle längere Pausen in der Nullphase nicht ins Gewicht fallen.

      Es gibt diverse Publikationen, welches dieses Protokoll mit Softwarelösungen (also Bit-Wackeln) erzeugen, allen diesen Softwarelösungen ist gemeinsam, dass sie im Prinzip die CPU für Dauer der Datenübertragung komplett allokieren und zu 100% auslasten, ein anderes Kommunikationsprotokoll oder andere Interrupts sind nicht möglich. Das ist so nicht brauchbar.

      Lösung mit Timer und DMA
      Die nachfolgend vorgestellte Lösung benutzt die Peripherie des ATXmegas, insbesondere ein Zusammenspiel zwischen Timern, dessen Output-Comparemöglichkeiten und DMA. Die Grundgedanken:
      • Aufsetzen eines Timers mit Periodendauer = 1,25µs und Verwenden eines Comparekanals zur Implementierung der Kodierung.
      • Nachladen des Comparewertes für jedes Bit mittels DMA-Controller. Der DMA-Controller schiebt den Comparewert mittels Cycle-Stealing direkt vom Speicher ins Register und verursacht dadurch nur geringe Beeinflussung der CPU.
      • Nach Übertragen eines kompletten Ausgabestromes Einfügen einer Pause und Neuaufsetzen des DMA-Kanals.
      Der Vorteil dieser Lösung ist eine geringe CPU-Last (etwa 3%) und eine vernachlässigbare Beeinflußung des Interruptverhaltens der CPU. Auch arbietet die Lösung komplett autark: DMA und Timer nudeln die den Datenstrom samt Resetcode alleine raus, es ist keine weitere Interaktion erforderlich. Auch kurze Blockaden durch andere Interrupts oder globales Sperren der Interrupt sind kein Problem. Man muß nur die Werte im Speicher hinterlegen. Nachteilig ist der hohe Speicherbedarf, es müssen für jede LED 24 Bits, d.h. 24 Bytes an Comparewerten im Speicher abgelegt werden. Z.B. 64 LEDs erfordern einen Ausgabespeicher von 64 x 3 x 8 = 1536 Bytes.

      Achtung!
      Jede WS2812B verbraucht bis zu 18.5mA je LED, d.h. 55,5mA wenn alle 3 LEDs voll durchgesteuert sind (weiß). Das entspricht bei 5V 92mW je LED bzw. 277mW für den Chip. Ein längerer Streifen verbraucht also deutlich Strom: Sowohl die Leitungen als auch die Versorgung muß das liefern können! Und beim Einbau in ein Modell muß man fallweise auch die Entwärmung betrachten, die Energie muß auch wieder irgendwie raus. Also z.B. Kamine von Häusern wirklich als Abluft nützen ...
      Rechenbeispiele:
        - 9 LED's, volle Helligkeit: 500mA, 2,5W.
        - 18 LED's, volle Helligkeit: 1000mA, 5W.
        - 64 LED's, volle Helligkeit: 3500mA, 17,6W.

    Besonderheiten der Xmega E-Serie

      Der atxmega32e5 (oder atxmega16e5, atxmega8e5) ist eine preiswerte Variante der atxmega-Serie. Es gibt ein paar schicke Neuerungen (z.B. eine programmierbare Logik), aber auch sonstige Änderungen. Atmel hat wohl auch versucht, die Chipgröße zu verkleinern und den Baustein abgespeckt (oder soll man kastriert sagen?).
      Das führt bei der Portierung von vorhandenem Code zu diversen kleineren, aber lästigen Änderungen.

    NVM-Controller, EEPROM Zugriff

      Bei der Portierung bin über fehlendes NVM_CMD_READ_EEPROM 0x06 und NVM_CMD_LOAD_EEPROM_BUFFER 0x33 im header gestolpert. Blick ins Datenblatt: jo, ist so, gibt es nicht mehr. Neu: EEPROM Lesen geht jetzt über direkte oder indirekte Loadbefehle, dazu wird das EEPROM hinter dem normalen SRAM eingeblendet. Offset: MAPPED_EEPROM_START (= 0x1000); das ist eine Vereinfachung. EEPROM Schreiben geht jetzt ein Stück komplizierter: Es gibt einen vorgelagerten Pagebuffer (dessen Partitionierung man anhand E2PAGE und E2BYTE ermitteln muß), diesen muß man zu Fuß aus dem EEPROM befüllen (der NVM tut es nicht mehr, NVM_CMD_LOAD_EEPROM_BUFFER gibt es ja nicht mehr), dann kann man EEPROM erasen und neu schreiben. Oder: da das Schreiben ein Nullsetzen ist, einfach vorher flush und nur das Byte schreiben.

    UART

      Hier gibt es ein neues UART-Remapping. Hierzu gibt es in der PORT-struct ein neues REMAP Register. D.h. wenn man den UART auf Chan1 haben will, muss man den UART auf Channel 0 nehmen (auf chan1 gibt es keinen) und remappen:
      #if defined(__AVR_ATxmega8E5__) || defined(__AVR_ATxmega16E5__) || defined(__AVR_ATxmega32E5__)
          #if (MY_UART_CHAN == 0)
              #define MY_USART0_REMAP   0
          #else
              #undef MY_UART_CHAN
              #define MY_UART_CHAN      0         // take ISR from chan0
              #define MY_USART0_REMAP   1         // but introduce a remap flag
          #endif
      #else
          #define MY_USART0_REMAP   0
      #endif  
          #if (MY_UART_CHAN == 1)
              MY_PORT.DIRSET   = PIN7_bm;       // pin 7 (TXD1) as output
              MY_PORT.DIRCLR   = PIN6_bm;       // pin 6 (RXD1) as input
          #else
              #if (MY_USART0_REMAP == 1)
                  MY_PORT.DIRSET   = PIN7_bm;       // pin 7 (TXD1) as output
                  MY_PORT.DIRCLR   = PIN6_bm;       // pin 6 (RXD1) as input
                  MY_PORT.REMAP |= PORT_USART0_bm;
              #else
                  MY_PORT.DIRSET   = PIN3_bm;       // pin 3 (TXD0) as output
                  MY_PORT.DIRCLR   = PIN2_bm;       // pin 2 (RXD0) as input
              #endif
      

    Ports

    • Die Option PORT_SRLEN je Port ist entfallen, das entsprechende Bit ist mit 0 zu initialisieren. Dafür gibt es jetzt ein neues Register SRLCTRL, welches Slewrate-Control für den ganzen Port einstellt. Was genau da dann die Rise/Falltime mit und ohne Control ist, da schweigt das Datenblatt bzw. verweist auf das 'device' Datenblatt - wo dann auch nichts steht.
    • Es gibt nur noch einen Interrupt je PORT, aus PORT_INT0LVL_HI_gc wird PORT_INTLVL_HI_gc, gleiches natürlich auch bei INT0MASK. INTMASK
    • Neu ist das REMAP: man kann Funktionen aus dem unteren Nibble nach oben verschieben. Hilft beim Layout einer Schaltung.

    GPIO

      Es gibt nur 4 GPIOR Register: GPIOR0...GPIOR3

    Timer

      Diese sind stark geändert, insbesondere die Art und Wiese der Waveformerzeugung ist komplett überarbeitet. Atmel hat deswegen den Timer auch neue Namen gegeben: TC4 und TC5.
      Wichtig: im CTRLG Register gibt es jetzt ein STOP Bit, welches default auf 1 steht. Das muß man mit TCC4.CTRLGCLR = TC4_STOP_bm; weglöschen, damit der Timer losläuft.

      Auch scheinen auch die Predefines im header immer noch Änderungen unterworfen: einmal heißt es TC45_CLKSEL_DIV8_gc, in einer neueren Version des Headers dann TC_TC5_CLKSEL_DIV8_gc und in einer wieder neueren Version dann TC_CLKSEL_DIV8_gc. Ebenso haben sich die defines für die sonstigen Steuerbits geändert: aus TC_EVACT_RESTART_gc wird TC_TC5_EVACT_RESTART_gc. Ist aber nicht überall so: aus TC_CLKSEL_gm wird TC4_CLKSEL_gm.
      Da wird ein Update des Atmel Studios zur Lotterie und es freut den Programmierer ungeheuer, wenn man da jedesmal durch die Header durchwühlen muß.

    Bootloader in kleinen Chips

      Nur die Xmega größer 128k haben 8k Bootloader. Will man aus Kostengründen einen kleinen Chip einsetzen, hat man nur 4k für den Bootloader. Ein Bootloader paßt da eventuell dann nicht rein ...
      Eingefleischte Assembler-Programmier betrachten 4k als ausreichend groß ('da bau ich Dir locker einen XY-Bootloader rein'), wenn man aber keine Lust und Zeit hat, seinen Code mühsam auf Space zu minimieren, dann bietet sich folgender Weg an: man benutzt einfach Teile des Appspeichers auch für den Bootloader, einzig die Read/Write-Zugriffe müssen wirklich in der Bootarea liegen. Das Problem dabei: gcc legt zwingend die Vektortabelle hardcoded an den Beginn der .text section. Jetzt könnte man .text bei der Bootsection beginnen lassen und nur andere Teile verschieben. Dann muß man aber jeder Funktion ein section attribut für die Zielsection mitgeben - mühsam.

      Einfacher ist es, ein Trampolin an der Einsprungstelle abzulegen und die Section .text einfach tiefer beginnen zu lassen. Hier mal am Beispiel des Xmega32A4U gezeigt:
    • Es werden drei Bereiche definiert:
      • .text: das kommt nach 0x6000 bis 0x7FFF (belegt also die oberen 8k des Appflash). Dort soll der normale Code rein.
      • .boot_int_vectors: das kommt nach 0x8000 bis 0x81FF (belegt also den Beginn des Bootbereiches). Dort kommt das Trampolin rein.
      • .boot_rw: das kommt nach 0x8fc0: dort ist der Code für read/write aufs Flash drin.

    Inhalt Trampolin

      #define START_OF_BOOT_LOADER   0x06000
      void boot_vector_jumptable(void) \
           __attribute__((naked)) \
           __attribute__((section(".boot_int_vectors")));
      void boot_vector_jumptable(void)
      {
         //Adjust to the number of interrupts for your device / and destination
         __asm__ __volatile__ ( \
            "jmp 0x06000+0x000  \n\t" \
            "jmp 0x06000+0x004  \n\t" \
            "jmp 0x06000+0x008  \n\t" \
            ....
            "jmp 0x06000+0x1F4  \n\t" \
            "jmp 0x06000+0x1F8  \n\t" \
            "jmp 0x06000+0x1FC  \n\t" \
            ::                \
         );
      }
      

    Linkerspruch

      LDFLAGS += -Wl,-section-start=.boot_int_vectors=0x8000
      LDFLAGS += -Wl,-section-start=.boot_rw=0x8fc0
      LDFLAGS += -Wl,-section-start=.text=0x6000