In diesem Beispiel möchte ich zeigen, wie mit Hilfe des NVM-Controllers lesend, bzw. schreibend auf das interne EEPROM zugegriffen wird. Das EEPROM wird immer dann gerne genutzt, wenn kleine Datenmengen (z. B. Programmparameter) nicht flüchtig im Mikrocontroller gespeichert werden sollen.
Sämtliche Schreibvorgänge des NVM-Controllers werden über einen internen Puffer durchgeführt, da der NVM-Controller sowohl für die Schreibvorgänge auf das EEPROM (Zugriff erfolgt byteweise), als auch für die Schreibvorgänge auf den Flash-Speicher (Zugriff erfolgt seitenweise) verantwortlich ist. Daher müssen die entsprechenden Datenbytes zuerst in den Puffer geschrieben werden. Anschließend wird die komplette Seite des Puffers in den Flash, bzw. das EEPROM kopiert.
Die Seitengröße des Puffers ist von Mikrocontroller zu Mikrocontroller unterschiedlich und muss im Datenblatt nachgeschaut werden. Bei einem XMega384C3 beträgt die Seitengröße für das EEPROM 32 Byte. Da der Zugriff auf das EEPROM seitenweise erfolgt, ist die Anzahl der vorhandenen Seiten wichtig. Diese ergibt sich aus der Seitengröße (32 Byte) und der Größe des EEPROMs (4k Byte), wodurch sich eine Anzahl von 128 Seiten ergibt.
Schreibzugriff auf das EEPROM:
Für das Beschreiben des EEPROMs sind folgende Schritte notwendig:
- EEPROM-Puffer mit Daten füllen
- NVM-Kommando LOAD_EEPROM_BUFFER in das CMD-Register schreiben.
- Schreibadresse in die ADDR-Register schreiben.
- Die Adresse besteht aus bis zu Bit. Die Aufteilung ergibt sich aus dem Datenblatt des Mikrocontrollers.
- Für das aktuelle Beispiel addressieren die Bits 4:0 den Byteoffset (E2BYTE) und die Bits 11:5 die Seite (E2PAGE) des EEPROMs.
- Das Datenbyte in das DATA0-Register schreiben. Dies startet den Schreibvorgang, wodurch das Byte in den internen Puffer kopiert wird.
- EEPROM Seite löschen
- EEPROM Seite schreiben
- Die Schritte 2 und 3 werden mit dem ERASE_WRITE_EEPROM_PAGE-Kommando ausgeführt.
- Das Kommando wird in das CMD-Register kopiert.
- Das ADDR-Register wird mit der Adresse gefüllt.
- Das CMDEX-Bit im CTRLA-Register wird gesetzt. Dieses Bit wird durch das CCP-Register geschützt, weshalb zuerst die korrekte Signatur in das CCP-Register geschrieben werden muss.
Der komplette Programmcode zum beschreiben des EEPROMs sieht folgendermaßen aus:
void NVM_EEPROMWriteByte(const uint8_t Page, const uint8_t Offset, const uint8_t Data) { if((Page > (EEPROM_SIZE / EEPROM_PAGE_SIZE)) || (Offset > (EEPROM_PAGE_SIZE - 0x01))) { return; } NVM_WaitBusy(); NVM.CMD = NVM_CMD_LOAD_EEPROM_BUFFER_gc; uint16_t Address = (uint16_t)(Page * EEPROM_PAGE_SIZE) | (Offset & (EEPROM_PAGE_SIZE - 0x01)); NVM.ADDR0 = Address & 0xFF; NVM.ADDR1 = (Address >> 0x08) & 0x1F; NVM.ADDR2 = 0x00; NVM.DATA0 = Data; NVM_ExecuteCommand(NVM_CMD_ERASE_WRITE_EEPROM_PAGE_gc); }
Zu Anfang wird geprüft ob die übergebene Seite und der übergebene Offset innerhalb des erlaubten Bereichs liegen. Die Konstanten EEPROM_SIZE
und EEPROM_PAGE_SIZE
werden vom AVRGCC mitgeliefert und beinhalten die entsprechenden Informationen des ausgewählten Mikrocontrollers.
Eine komplette Seite kann geschrieben werden, indem zuerst die Seitenadresse in das ADDR-Register geschrieben und anschließend der Inhalt der Seite in den Puffer geschrieben wird.
void NVM_EEPROMWritePage(const uint8_t Page, const uint8_t* Data) { if(Page > (EEPROM_SIZE / EEPROM_PAGE_SIZE)) { return; } NVM_WaitBusy(); NVM.CMD = NVM_CMD_LOAD_EEPROM_BUFFER_gc; uint16_t Address = (uint16_t)(Page * EEPROM_PAGE_SIZE); uint8_t Address0 = Address & 0xFF; NVM.ADDR1 = (Address >> 0x08) & 0x1F; NVM.ADDR2 = 0x00; for(uint8_t i = 0x00; i < EEPROM_PAGE_SIZE; i++) { NVM.ADDR0 = Address0 | i; NVM.DATA0 = *Data++; } NVM_ExecuteCommand(NVM_CMD_ERASE_WRITE_EEPROM_PAGE_gc); }
Da der Offset in diesem Fall variabel ist, muss der neue Wert, zusammen mit den niederwertigsten Bits der Seitenadresse, in das ADDR0-Register geschrieben werden.
Im nächsten Schritt wird dann geprüft, ob der NVM-Controller gerade ein Kommando abarbeitet, indem das NVMBUSY-Bit im STATUS-Register abgefragt wird:
static inline void NVM_WaitBusy(void) __attribute__ ((always_inline)); static inline void NVM_WaitBusy(void) { while(NVM.STATUS & NVM_NVMBUSY_bm); }
Sobald der NVM-Controller bereit ist, wird der erste Punkt abgearbeitet und die benötigten Register mit den nötigen Informationen gefüllt. Dazu wird mit Hilfe der übergebenen Seite und des übergebenen Offsets die EEPROM-Adresse berechnet und passend formatiert in die ADDR-Register des NVM-Controllers geschrieben. Nach dem Beschreiben des DATA0-Registers wird der Kopiervorgang gestartet. Von jetzt an befindet sich das übergebene Datenbyte im Puffer des NVM-Controllers.
Zum Schluss werden die letzten beiden Punkte abgearbeitet, indem das ERASE_WRITE_EEPROM_PAGE-Kommando in das CMD-Register kopiert und durch das Setzen des CMDEX-Bits ausgeführt wird. Da sich die Seitenadresse für das EEPROM bereits in den ADDR-Registern des NVM-Controllers befindet, kann dieser Punkt übersprungen werden.
Die Ausführung eines Kommandos durch das Setzen des CMDEX-Bits wird durch die nachfolgende Funktion gestartet.
static inline void NVM_ExecuteCommand(const uint8_t Command) { NVM.CMD = Command; asm volatile( "movw r30, %0" "\n\t" "ldi r16, %2" "\n\t" "out %3, r16" "\n\t" "st Z, %1" "\n\t" :: "r" (&NVM.CTRLA), "r" (NVM_CMDEX_bm), "M" (CCP_IOREG_gc), "i" (&CCP) : "r16", "r30", "r31" ); NVM.CMD = NVM_CMD_NO_OPERATION_gc; }
Zuerst wird das auszuführende Kommando in das CMD-Register kopiert. Damit das Kommando ausgeführt wird, muss das CMDEX-Bit im CTRLA-Register gesetzt werden. Bevor dieses Bit allerdings gesetzt werden kann, muss die Signatur 0xD8 in das CCP-Register kopiert werden. Nachdem die Signatur in das CCP-Register kopiert worden ist, hat die Software vier Taktzyklen Zeit um ein geschütztes Register, hier das CMD-Register, zu beschreiben.
Damit dieser Vorgang auch bei ausgeschalteter Optimierung funktioniert, habe ich die entsprechenden Codezeilen in Inline-Assembler programmiert. Zuerst werden die Adresse des CTRLA-Registers und die Signatur für das CCP-Register in den Registern r30 und r16 gespeichert. Anschließend wird die Signatur in das CCP-Register kopiert und direkt danach das CMDEX-Bit gesetzt. Abschließend wird das CMD-Register gelöscht, indem das Kommando NO_OPERATION in das Register geschrieben wird.
Jetzt kann die Anwendung das EEPROM beschreiben.
NVM_EEPROMWriteByte(0x00, 0x00, 0xCC); NVM_EEPROMWriteByte(0x00, 0x1F, 0xCC); NVM_EEPROMWriteByte(0x01, 0x02, 0xAA); uint8_t EEPROM_PageBufferOut[EEPROM_PAGE_SIZE]; for(uint8_t i = 0x00; i < EEPROM_PAGE_SIZE; i++) { EEPROM_PageBufferOut[i] = i; } NVM_EEPROMWritePage(1, EEPROM_PageBufferOut);
Nach der Ausführung des Programms kann das EEPROM z. B. mit einem JTAG-Programmiergerät ausgelesen werden, um zu überprüfen, ob die Werte auch tatsächlich in das EEPROM geschrieben worden sind.
:1000000001FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE :10001000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02ED :10002000FFFF03FFFFFFFFFFFFFFFFFFFFFFFFFFDC
Scheint alles geklappt zu haben. Dann schauen wir uns jetzt an, wie wir das EEPROM wieder löschen können.
Löschen des EEPROMs:
Damit das EEPROM gelöscht wird, müssen die folgenden Schritte durchgeführt werden:
- NVM-Kommando ERASE_EEPROM in das CMD-Register schreiben
- Das CMDEX-Bit im CTRLA-Register setzen
- Warten bis der NVM-Controller fertig ist
Die fertige Funktion zum Löschen des EEPROMs ist recht einfach gehalten. Da auch hier das CMDEX-Bit gesetzt werden muss, kann die Funktion NVM_ExecuteCommand
wiederverwendet werden.
void NVM_EEPROMErase(void) { NVM_WaitBusy(); NVM_ExecuteCommand(NVM_CMD_ERASE_EEPROM_gc); NVM_WaitBusy(); }
Direkt nach dem Aufruf der Funktion wird gewartet, bis der NVM-Controller alle offenen Aufträge abgearbeitet ist. Anschließend wird der ERASE_EEPROM-Befehl in das CMD-Register kopiert und ausgeführt. Sobald der NVM-Controller den Befehl abgearbeitet hat, wird die Funktion verlassen.
Als letztes wollen wir uns anschauen, wie die geschriebenen Daten wieder ausgelesen werden können.
Aus dem EEPROM lesen:
Für einen lesenden Zugriff auf das EEPROM müssen die folgenden Schritte abgearbeitet werden:
- NVM-Kommando READ_EEPROM in das CMD-Register schreiben
- Adresse in das ADDR-Register schreiben
- Das CMDEX-Bit im CTRLA-Register setzen
- DATA0-Register auslesen
Für eine entsprechende Lesefunktion sind bereits alle Komponenten vorhanden und müssen nur noch in der richtigen Reihenfolge aufgerufen werden.
uint8_t NVM_EEPROMReadByte(const uint8_t Page, const uint8_t Offset) { if((Page > (EEPROM_SIZE / EEPROM_PAGE_SIZE)) || (Offset > (EEPROM_PAGE_SIZE - 0x01))) { return -1; } NVM_WaitBusy(); uint16_t Address = (uint16_t)(Page * EEPROM_PAGE_SIZE) | (Offset & (EEPROM_PAGE_SIZE - 0x01)); NVM.ADDR0 = Address & 0xFF; NVM.ADDR1 = (Address >> 0x08) & 0x1F; NVM.ADDR2 = 0x00; NVM_ExecuteCommand(NVM_CMD_READ_EEPROM_gc); return NVM.DATA0; }
Der Ablauf ist nahezu identisch zum Schreibablauf, außer das im letzten Schritt das Datenbyte aus dem DATA0-Register ausgelesen und zurückgegeben wird. Für große Datenmengen empfiehlt es sich auch hier eine komplette Seite in einem Stück auszulesen.
void NVM_EEPROReadPage(const uint8_t Page, uint8_t* Data) { if(Page > (EEPROM_SIZE / EEPROM_PAGE_SIZE)) { return; } NVM_WaitBusy(); uint16_t Address = (uint16_t)(Page * EEPROM_PAGE_SIZE); uint8_t Address0 = Address & 0xFF; NVM.ADDR1 = (Address >> 0x08) & 0x1F; NVM.ADDR2 = 0x00; for(uint8_t i = 0x00; i < EEPROM_PAGE_SIZE; i++) { NVM.ADDR0 = Address0 | i; NVM_ExecuteCommand(NVM_CMD_READ_EEPROM_gc); *Data++ = NVM.DATA0; } }
Damit können die, im EEPROM gespeicherten, Datenbytes von der Software wieder ausgelesen werden.
uint8_t Data0 = NVM_EEPROMReadByte(0x00, 0x00); uint8_t Data1 = NVM_EEPROMReadByte(0x00, 0x1F); uint8_t Data2 = NVM_EEPROMReadByte(0x01, 0x02); uint8_t EEPROM_PageBufferIn[32]; NVM_EEPROReadPage(1, EEPROM_PageBufferIn);
→ Zurück
Vielen Dank für die Beschreibung, die sich auch leicht in Assembler umsetzen lässt.
Die Routinen funktionieren wie erwartet, haben aber auf Grund der schlechteren EEPROM-Integration im XMEGA notgedrungen den Nachteil, dass das Schreiben in eine beliebige zuvor nicht geladene Page etwa 2 µs (LOAD_EEPROM_BUFFER) + ca. 8,5 ms (ERASE_WRITE_EEPROM_PAGE) dauert. Im ungünstigsten Fall (Zugriff auf nur ein Byte pro Page über alle Pages eines 4K-EEPROMs) dauert das insgesamt über 1 Sekunde zum Schreiben von nur 128 Bytes! Beim ATmega waren das nur etwa 3,4 ms/Byte bei geringerer Taktgeschwindigkeit. Allerdings geht das Lesen über Page-Grenzen im Memory-Mapped-Modus des ATxmega sehr viel schneller (ca. 250 ns/Byte mit Overhead), als im Page-Modus. Das ist schon eine deutliche Verbesserung.
Leider scheint die XMEGA-Serie einen Bug im NVM-Controller zu haben, denn das Laden des Page-Buffers im Memory-Mapped-Modus sollte durch einen Schreibzugriff auf eine beliebige Page im mapped EEPROM vom NVM-Controller erkannt werden. XMEGA AU Manual, Seite 418:
„When EEPROM memory mapping is enabled, loading a data byte into the EEPROM page buffer can be performed through direct or indirect store instructions.“
Das führt jedoch zu der falschen Annahme, dass der richtige Page-Buffer beim „store“ zuvor auch automatisch geladen wird. Tatsächlich wird vom NVM ohne zuvor manuell ausgelöstes PAGE_LOAD aber immer nur Page0 verwendet. Das schränkt diesen Modus beim Schreibzugriff aber ohne wirklich nachvollziehbaren Grund deutlich ein. Es wäre für den NVM-Controller überhaupt kein Problem, dies dem Anwender (wenn auch mit einer LOAD_EEPROM_BUFFER-Verzögerung von lächerlichen 2 HALT-Zyklen) abzunehmen. Es wäre fantastisch, wenn er wahlweise zuvor die letzte geladene Page auch noch ins EEPROM schreiben könnte, dann aber mit 8,5 ms Verzögerung. Dann würde ein sequentielles Schreiben in die 4K des EEPROMs mit einem Zufallsmuster zwar immer noch etwa 1s dauern, jedoch würde ein LOAD_EEPROM_BUFFER mit einem ERASE_WRITE_EEPROM_PAGE über 4K nach jedem einzelnen Byte noch 128 Mal länger dauern!
Ansonsten wird die EEPROM-Handhabung im Datenblatt und der Applikationsschrift „AVR1315: Accessing the XMEGA EEPROM“ leider viel zu knapp und unvollständig (siehe oben) beschrieben. Ein paar Sätze mehr würden ein besseres Verständnis bringen, sowie weniger Nachfragen und eigene Experimente erfordern. In den Foren findet man dazu leider keinen wirklich verwertbaren Hinweis! Vielen Dank daher noch einmal für die ausführliche Darstellung, die mich erst darauf brachte, auch im Memory-Mapped-Modus zuvor die benötigte Page manuell zu laden.
Für meinen Anwendungsfall bleibt mir nur übrig einen Workaround zu programmieren, bei dem ein mehrfacher Schreibzugriff innerhalb einer Page durch ein delayed-write und einen eigenen Cache im Mittel reduziert werden muss. Die Begrenzung auf ca. 100000 EEPROM-Schreibzyklen pro Zelle ist nicht das Problem, sondern die gegenüber dem ATmega längere Ausführungsdauer pro Schreibzugriff.
Ob man vielleicht mit DMA-Zugriffen auf das EEPROM über Page-Grenzen etwas erreichen kann, habe ich noch nicht ausprobiert. Ich hatte gehofft (und falsch interpretiert), dass der Memory-Mapped-Modus mir das alles abnehmen würde. Vielleicht war es auch mal von Atmel so geplant, funktioniert hat es jedenfalls (bisher) nicht.
Atmel hatte wohl mehrere solcher Probleme, denn vor dem ATxmega waren die EEPROMs leicht zu korrumpieren und beim ATmega funktioniert das TWI-Interface auch nur unvollständig. Damit hatten auch Andere (https://www.robotroom.com/Atmel-AVR-TWI-I2C-Multi-Master-Problem.html) zu kämpfen. Nach der Übernahme durch Microchip werden diese Fehler wohl kaum noch berichtigt werden, da neue Wafermasken viel zu teuer sind. Schade!
Hi Thomas,
vielen Dank für diese ausführliche Darstellung! Die Probleme waren mir gar nicht so stark bewusst, aber ich habe das EEPROM auch eher seltener bis gar nicht benutzt. Deine Erkenntnisse sind aber durchaus spannend!
Gruß
Daniel