Über den Beitrag
In meinem letzten Beitrag hatte ich über den internen EEPROM der AVR Mikrocontroller beziehungsweise der AVR basierten Arduino Boards berichtet. In diesem Beitrag widme ich mich nun den externen, I2C gesteuerten EEPROMs. Mit geeigneten Bibliotheken, wie der von Sparkfun, könnt ihr die EEPROMs sehr bequem beschreiben und auslesen. Für ein besseres Verständnis lohnt es sich aber auch nachzuvollziehen, wie die Ansteuerung ohne Bibliothek funktioniert.
Der Beitrag ist folgendermaßen gegliedert:
- Einführung
- Pinout und Anschluss an den Mikrocontroller
- EEPROM beschreiben und lesen
- Größere Datenmengen schreiben
- Page Write
- Sparkfun Bibliothek
In einem separaten Beitrag behandele ich die externen, SPI basierten EEPROMs. Sie unterscheiden sich von den I2C basierten EEPROMs, dass ich das nicht in einem einzigen Beitrag unterbringen konnte.
Einführung
Was ist ein EEPROM?
Die Abkürzung EEPROM steht für Electrically Erasable Programmable Read Only Memory, also einen elektrisch löschbaren, programmierbaren Speicher, der nur lesbar ist. Diese in sich etwas widersprüchliche Bezeichnung ist historisch gewachsen. Darauf bin ich in meinem letzten Beitrag kurz eingegangen. Kurz gesagt dient ein EEPROM als Speicher für Daten, die auch nach dem Ausschalten der Versorgungsspannung nicht verloren gehen sollen.
Welche Eigenschaften haben EEPROMs?
Zu den Vorzügen von EEPROMs gehört ihre kompakte Bauweise. Außerdem sind die Daten auf einem EEPROM vergleichsweise lange sicher aufgehoben. Teilweise geben die Hersteller eine Datenhaltbarkeit (Data Retention) von über 200 Jahren an. Ich werde das mal in 200 Jahren prüfen und gegebenenfalls reklamieren ;-).
Nachteilig ist hingegen die vergleichsweise langsame Schreibgeschwindigkeit der EEPROMs. Sie liegt im Bereich von Millisekunden für ein einzelnes Byte. Durch „Page Writing“ steigern die hier besprochenen Modelle ihre effektive Schreibgeschwindigkeit deutlich gegenüber den internen EEPROMs. Jedoch sind sie immer noch unvergleichlich viel langsamer als Flash-Speicher.
Ein EEPROM ist nicht unendlich oft wiederbeschreibbar. Allerdings ist dieser Nachteil oft von nur begrenzter Relevanz, denn die garantierten Schreibzyklen liegen für gewöhnlich bei über einer Million. Wenn ihr dieselbe Speicherzelle jede Sekunde neu beschreibt, hättet ihr die Lebensdauer entsprechend nach ~11.5 Tagen erreicht. Zu hoch sollte die Schreibfrequenz also nicht sein.
Welche EEPROMs ich bespreche
Ich beziehe mich in diesem Beitrag auf die EEPROMs der 24er-Baureihe. Diese EEPROMs werden nach dem Schema „24xxyy“ bezeichnet.
Das „xx“ im Namen codiert unterschiedliche Spannungsbereiche und Übertragungsgeschwindigkeiten. Oft trifft man „LC“-, „C“- und „A“-Typen an. Die abschließende Zahl gibt die Speicherkapazität in Kilobit (kurz: kb o. kbit) an. Unten links seht ihr beispielsweise einen „24LC64„, der also eine Kapazität 64 kbit = 8 Kilobyte (kurz: kB oder kbyte) hat.
Ihr steuert die 24er-Reihe über I2C. Daneben gibt es weitere Reihen. Die 25er-Reihe beispielsweise „hört“ auf SPI, die 11er-, 21er- und 28er-Reihen kommunizieren per One-Wire Techniken.
Weitere Kürzel im Anschluss definieren Bauformen, Temperaturbereiche und anderes.
Strombedarf
Die EEPROMs sind hinsichtlich ihres Strombedarfes recht genügsam. Während des Schreibens brauchen sie in der Regel 0.1 -1.0 Milliampere. Beim Lesen ist der Wert noch geringer. Im Standby liegt der Verbrauch maximal bei wenigen Mikroampere.
Pinout und Anschluss an den Mikrocontroller
Pinout der 24er EEPROM Reihe
Das Pinout Schema der EEPROMs der 24er-Reihe sieht in der Regel wie folgt aus:
- A0 / A1 / (A2): Adresspins, 4 bis 8 Adressen sind üblicherweise einstellbar.
- VCC / GND: Spannungsversorgung, z.B. 2.5 – 5.5 Volt für die 24LCxx-Reihe (Datenblatt prüfen!).
- WP: Write Protection (Schreibschutz);
- Inaktiv, wenn mit GND verbunden.
- Aktiv, wenn mit VCC verbunden.
- SDA / SCL: I2C-Anschlüsse, max. 400 kHz für 24LCxx-Reihe (Datenblatt prüfen!).
- Adressschema: 1 0 1 0 A2 A1 A0 mit GND = 0 und VCC = 1.
- Beispiel A1/A2/A3 an GND → Adresse = 0b1010000 = 80 = 0x50.
Anschluss an einen Arduino Nano
Beispielschaltung für einen 24LC256 an einem Arduino Nano:

Oft kann man auf die Pull-Up Widerstände verzichten. Probiert es aus.
Den EEPROM beschreiben und lesen
Im einem ersten, einfachen Beispiel schreiben wir drei Byte-Werte auf den EEPROM und lesen sie dann wieder aus. Ihr leitet den Schreibvorgang ein, indem ihr mittels Wire.beginTransmission()
die I2C-Adresse des EEPROMs übergebt.
Im nächsten Schritt übermittelt ihr mit Wire.write()
die Speicheradresse, an der ihr euren Wert speichern wollt. Bei Speicherkapazitäten bis 2kbit (=256 byte) könnt ihr die Adresse als ein einzelnes Byte übergeben. Einen so kleinen EEPROM werdet ihr aber nur in Ausnahmefällen einsetzen. Darüber hinaus, bis 512 kbit (= 64 kByte), passt die Adresse in eine Unsigned Integer Variable. Die Adressen teilt ihr in ihr MSB (Most Significant Byte) und LSB (Least Significant Byte) übergebt diese hintereinander.
Ihr schließt den Schreibvorgang mit Wire.endTransmission()
ab. Danach müsst ihr dem EEPROM genügend Zeit (die „Write Cycle Time“) geben, bevor ihr einen weiteren Wert speichern könnt. Die „Write Cycle Time“ findet ihr im Datenblatt eures EEPROMs. Typischerweise sind das 5 Millisekunden.
Ihr müsst beim Schreiben über die Adresse Buch führen. Es gibt keine Funktion, die euch warnen würde, dass ein Speicherplatz schon beschrieben ist. Genau genommen gibt es keine Unterscheidung zwischen beschriebenen und unbeschriebenen Speicherzellen. Irgendein Wert steht da auf jeden Fall. Daten können also nur überschrieben werden, um sie zu löschen.
Das Lesen funktioniert wie das Schreiben, nur mit der Wire.requestFrom()
Funktion. Zwischen den Leseoperationen müsst ihr keine Wartezeit einfügen.
#include <Wire.h> #define I2C_ADDRESS 0x50 void setup(){ Wire.begin(); Serial.begin(9600); unsigned int address = 0; byte byteVal_1 = 42; byte byteVal_2 = 123; byte byteVal_3 = 255; eepromByteWrite(address,byteVal_1); address++; eepromByteWrite(address,byteVal_2); address++; eepromByteWrite(address,byteVal_3); for(address=0; address<3; address++){ Serial.print("Byte at address "); Serial.print(address); Serial.print(": "); Serial.println(eepromByteRead(address)); } } void loop(){} void eepromByteWrite(unsigned int addr, byte byteToWrite){ Wire.beginTransmission(I2C_ADDRESS); Wire.write((byte)(addr>>8)); Wire.write((byte)(addr&0xFF)); Wire.write(byteToWrite); Wire.endTransmission(); delay(5); // important! } int eepromByteRead(unsigned int addr){ int byteToRead; Wire.beginTransmission(I2C_ADDRESS); Wire.write((byte)(addr>>8)); Wire.write((byte)(addr&0xFF)); Wire.endTransmission(); Wire.requestFrom(I2C_ADDRESS, 1); byteToRead = Wire.read(); return byteToRead; }
Und so sieht dann die Ausgabe aus:
Größere Datenmengen auf den EEPROM schreiben
Im nächsten Beispiel erzeugen wir zunächst ein Array aus einhundert Integer-Werten. Der Wert des i-ten Elements des Arrays ist 10 · i (willkürlich gewählt). Das Array schreiben wir auf den EEPROM, lesen es von EEPROM und geben es auf dem seriellen Monitor aus.
Hier zunächst der Sketch:
#include <Wire.h> #define I2C_ADDRESS 0x50 void setup(){ Wire.begin(); //Wire.setClock(400000); Serial.begin(9600); unsigned int address = 0; unsigned long writeStart = 0; unsigned long writeDuration = 0; unsigned int arraySize = 100; int intArray[arraySize]; for(unsigned int i=0; i<arraySize; i++){ intArray[i] = i*10; } writeStart = millis(); writeIntArrayToEEPROM(address, intArray, arraySize); writeDuration = millis() - writeStart; address = 0; for(unsigned int i=0; i<arraySize; i++){ Serial.print("intArray["); Serial.print(i); Serial.print("]: "); Serial.println(readIntFromEEPROM(address + 2*i)); } Serial.print("Time needed for writing [ms]: "); Serial.println(writeDuration); } void loop(){} void writeIntArrayToEEPROM(unsigned int addr, int *iArr, unsigned int arrSize){ for(unsigned int i = 0; i<arrSize; i++){ Wire.beginTransmission(I2C_ADDRESS); Wire.write((byte)((2*i+addr)>>8)); Wire.write((byte)((2*i+addr)&0xFF)); Wire.write((byte)(iArr[i]>>8)); Wire.write((byte)(iArr[i])&0xFF); Wire.endTransmission(); delay(5); // while(isBusy()){ // alternativ to delay(5). // delayMicroseconds(50); // } } } unsigned int readIntFromEEPROM(unsigned int addr){ int intToRead; byte msByte; byte lsByte; Wire.beginTransmission(I2C_ADDRESS); Wire.write((byte)(addr>>8)); Wire.write((byte)(addr&0xFF)); Wire.endTransmission(); Wire.requestFrom(I2C_ADDRESS, 2); msByte = Wire.read(); lsByte = Wire.read(); intToRead = msByte<<8 | lsByte; return intToRead; } bool isBusy(){ Wire.beginTransmission(I2C_ADDRESS); return Wire.endTransmission(); }
Ein paar Erläuterungen:
- Das Array wird der Schreibfunktion
writeIntArrayToEEPROM()
als Zeiger übergeben. Darauf bin ich in meinem letzten Beitrag eingegangen. - Ein Integer belegt zwei Bytes. Deshalb muss nicht nur die Adresse, sondern auch der zu schreibende Wert in MSB und LSB zerlegt werden.
Das Schreiben des Arrays nahm 552 Millisekunden in Anspruch. Davon gehen 500 Millisekunden auf das Konto der „Write Cycle Time“. Sofern euer EEPROM Fast I2C beherrscht, könnt ihr die anderen 52 Millisekunden verringern, indem ihr die Zeile 6, Wire.setClock(400000)
, entkommentiert. Dadurch erhöht ihr die I2C Frequenz von 100 auf 400 kHz. Damit habe ich den Schreibvorgang auf 519 Millisekunden reduzieren können.
Die fünf Millisekunden Wartezeit für die „Write Cycle Time“ enthalten einen gewissen Sicherheitspuffer. Solange der EEPROM mit dem Schreiben beschäftigt ist, lässt er sich per I2C nicht ansprechen. Das bedeutet, dass er kein Acknowledge sendet, wenn ihr ihn ansprecht. Diese Eigenschaft macht sich die Funktion isBusy()
zunutze.
Kommentiert mal die Zeile 43, delay(5)
, aus und entkommentiert die folgenden drei Zeilen. Mit dieser Maßnahme und dem Wechsel auf 400 kHz sank die benötigte Zeit zum Schreiben des Arrays auf 380 Millisekunden. Die wirkliche „Write Cycle Time“ liegt also bei ca. 3.5 Millisekunden.
EEPROM Page Write
Wie ihr eben gesehen habt, ist es möglich, mehrere Bytes „in einem Rutsch“ (Page Write), also ohne zwischenzeitliches Wire.endTransmission()
und delay()
auf den EEPROM zu schreiben. Ansonsten hätte der Schreibvorgang für das Integer-Array mindestens 1000 Millisekunden in Anspruch genommen.
Das funktioniert, weil der EEPROM in Speicherabschnitte (Pages) segmentiert ist und für die Pages einen Puffer besitzt. Die übertragenen Werte werden also schnell in den Puffer geschrieben und dann in aller Ruhe von da in den Speicher geschrieben. Die Größe der Pages (Page Size) könnt ihr dem Datenblatt entnehmen. Für einen 24LC64 beträgt die Page Size 32 Bytes, für einen 24LC256 beträgt sie 64 Bytes.
Die Pages beginnen und enden an festen Speicheradressen. Page Writing über das Ende einer Page hinaus funktioniert nicht.
Limitierung durch den Wire.write() Puffer
Für das Page Writing gibt es noch einen limitierenden Faktor auf der Arduino Seite. Und zwar ist das der Puffer für Wire.write()
. Das könnt ihr mit dem folgenden Sketch nachvollziehen:
#include <Wire.h> #define I2C_ADDRESS 0x50 void setup(){ Wire.begin(); Serial.begin(9600); unsigned int address = 0; for(unsigned int i=address; i<64; i++){ eepromByteWrite(i,(byte)i); } byte byteArray[64]; for(byte i=0; i<64; i++){ byteArray[i] = i*2; } eepromBytePageWrite(address, byteArray, sizeof(byteArray)); address = 0; for(unsigned int i=address; i<sizeof(byteArray); i++){ Serial.print("byteArray["); Serial.print(i); Serial.print("]: "); Serial.println(readEEPROM(i)); } } void loop(){} void eepromByteWrite(unsigned int addr, byte byteToWrite){ Wire.beginTransmission(I2C_ADDRESS); Wire.write((byte)(addr>>8)); Wire.write((byte)(addr&0xFF)); Wire.write(byteToWrite); Wire.endTransmission(); delay(5); } void eepromBytePageWrite(unsigned int addr, byte *byteArrayToWrite, unsigned int sizeOfArray){ Wire.beginTransmission(I2C_ADDRESS); Wire.write((byte)(addr>>8)); Wire.write((byte)(addr&0xFF)); for(unsigned int i=addr; i<sizeOfArray; i++){ Wire.write(byteArrayToWrite[i]); } Wire.endTransmission(); delay(5); } byte readEEPROM(unsigned int addr){ byte byteToRead; Wire.beginTransmission(I2C_ADDRESS); Wire.write((byte)(addr>>8)); Wire.write((byte)(addr&0xFF)); Wire.endTransmission(); Wire.requestFrom(I2C_ADDRESS, 1); byteToRead = Wire.read(); return byteToRead; }
Der Sketch beschreibt zunächst die ersten 64 Speicherplätze mit einem Wert, und zwar der Adresse selbst. Die Werte werden einzeln und mit 5 Millisekunden Pause geschrieben. Diese Aktion sorgt lediglich für einen definierten Ausgangszustand.
Danach schreibt der Sketch 64 neue Werte (Adresse multipliziert mit 2) auf den EEPROM. Dabei verwendet er die Page Write Methode. Und so sieht dann unerwarteterweise die Ausgabe aus:
Die ersten 30 Werte (0-29) sind durch das Page Write Verfahren ersetzt worden, danach treffen wir auf die alten Werte. Der Grund ist folgender: Bei aufeinanderfolgenden Wire.write()
Befehlen (also ohne zwischenzeitliches Wire.endTransmission()
) werden die zu schreibenden Werte Arduino-seitig in einen Puffer geschrieben, der auf 32 Byte beschränkt ist. Die Übermittlung der Adresse benötigt 2 Bytes, bleiben also noch 30 Bytes für die Daten. Alle zusätzlichen Wire.write()
Aufrufe laufen ins Leere.
Korrektes Page Writing
Beim Page Write Verfahren sind also drei Dinge zu berücksichtigen:
- Die Page Size,
- der Wire.write() Puffer, und
- es darf nicht über eine Page Grenze hinaus geschrieben werden.
Im nächsten Beispiel schreiben wir ein Array von 100 Integer-Werten (= 200 Byte) im Page Write Verfahren auf einen EEPROM mit einer Page Size von 64 Byte. Wenn wir bei der Adresse 0 starten, dann haben wir 64 Byte Platz. Der limitierende Faktor ist der Wire.write()
Puffer. Also beschreiben wir die Speicherplätze 0-29, dann 30-59. Danach müssen wir das Page Ende nach Adresse 63 beachten und können nur 4 Bytes am Stück schreiben. Dasselbe passiert in den folgenden zwei Pages. Die dann noch verbleibenden 8 Byte schreiben wir in die letzte Page. Schematisch sieht das so aus:

Als Sketch könnte das zum Beispiel so aussehen:
#include <Wire.h> #define I2C_ADDRESS 0x50 #define PAGE_SIZE 64 #define WRITE_LIMIT 30 void setup(){ Wire.begin(); Serial.begin(9600); unsigned int address = 0; unsigned long writeStart = 0; unsigned long writeDuration = 0; unsigned int arraySize = 100; int intArray[arraySize]; for(unsigned int i=0; i<arraySize; i++){ intArray[i] = i*1; } writeStart = millis(); writeIntArrayToEEPROM(address, intArray, arraySize); writeDuration = millis() - writeStart; address = 0; for(unsigned int i=0; i<arraySize; i++){ Serial.print("intArray["); Serial.print(i); Serial.print("]: "); Serial.println(readIntFromEEPROM(address + 2*i)); } Serial.print("Time needed for writing [ms]: "); Serial.println(writeDuration); } void loop(){} void writeIntArrayToEEPROM(unsigned int addr, int *iArr, unsigned int arrSize){ unsigned int noOfIntsStillToWrite = arrSize; unsigned int arrayIndex = 0; while((noOfIntsStillToWrite != 0)){ unsigned int chunk = (WRITE_LIMIT / sizeof(int)); // max chunk in number of ints unsigned int positionInPage = (addr % PAGE_SIZE); // current position in page unsigned int spaceLeftInPage = (PAGE_SIZE - positionInPage) / sizeof(int); // available storage space if(spaceLeftInPage < chunk){ chunk = spaceLeftInPage; } if(noOfIntsStillToWrite < chunk){ chunk = noOfIntsStillToWrite; } writeEEPROM(addr, iArr, chunk, arrayIndex); noOfIntsStillToWrite -= chunk; addr += (chunk * 2); arrayIndex += chunk; } } void writeEEPROM(unsigned int addr, int *iArr, unsigned int chunkSize, unsigned int arrIdx){ Wire.beginTransmission(I2C_ADDRESS); Wire.write((byte)(addr>>8)); Wire.write((byte)(addr&0xFF)); for(unsigned int i=0; i<chunkSize; i++){ Wire.write((byte)(iArr[i+arrIdx]>>8)); Wire.write((byte)(iArr[i+arrIdx])&0xFF); } Wire.endTransmission(); delay(5); } unsigned int readIntFromEEPROM(unsigned int addr){ int intToRead; byte msByte; byte lsByte; Wire.beginTransmission(I2C_ADDRESS); Wire.write((byte)(addr>>8)); Wire.write((byte)(addr&0xFF)); Wire.endTransmission(); Wire.requestFrom(I2C_ADDRESS, 2); msByte = Wire.read(); lsByte = Wire.read(); intToRead = msByte<<8 | lsByte; return intToRead; }
Ihr – beziehungsweise euer Sketch – müsst also immer wieder berechnen, wie viele Bytes ihr in einem Schritt auf den EEPROM schreiben könnt. Der Geschwindigkeitsgewinn durch die Page Write Methode ist erwartungsgemäß erheblich:
Auch hier könnt ihr die Geschwindigkeit durch Verwendung der isBusy() Funktion und durch Wechsel auf 400 kHz weiter steigern. Letzteres natürlich nur, wenn der EEPROM das beherrscht.
Sparkfun Bibliothek
Ihr könnt euch das Leben erheblich erleichtern, indem ihr eine Bibliothek verwendet. Ich habe die SparkFun_External_EEPROM_Arduino_Library getestet. Sie ist leicht zu bedienen und recht komfortabel. Ihr erhaltet die Bibliothek über die Bibliotheksverwaltung der Arduino IDE oder ihr ladet sie direkt von GitHub herunter (Link). Die get() und put() Funktionen erinnern an die entsprechenden Funktionen aus EEPROM.h für die internen EEPROMs der AVR Boards. Sie sind aber noch komfortabler, da sie auch String-Variablen schreiben und lesen können.
myMem.enablePollForWriteComplete()
bewirkt, dass anstelle einer Wartezeit für die Schreibvorgänge ein Funktion die Wiederverfügbarkeit abfragt. Also wie die oben beschriebene isBusy() Funktion. Der Rest sollte eigentlich selbsterklärend sein. Hier ein Beispielsketch:
#define EEPROM_ADDRESS 0x50 #include <Wire.h> #include "SparkFun_External_EEPROM.h" ExternalEEPROM myMem; void setup(){ Serial.begin(9600); delay(10); Serial.println("I2C EEPROM example"); Wire.begin(); myMem.setMemorySize(262144/8); // 256kbit = 2^18; 256kbit = 32kbyte myMem.setPageSize(64); //In bytes. Has 64 byte page size. myMem.enablePollForWriteComplete(); //Supports I2C polling of write completion myMem.setPageWriteTime(5); //5 ms max write time if (myMem.begin(EEPROM_ADDRESS) == false){ Serial.println("No memory detected. Freezing."); while (1); } Serial.println("Memory detected!"); float myFloat = -7.35; myMem.put(20, myFloat); float myReadFloat = 0.0; myMem.get(20, myReadFloat); //location to read, thing to put data into String myString = "This is no poetry, I am just a simple test String"; myMem.put(50, myString); String myReadString = ""; myMem.get(50, myReadString); Serial.println("I read: "); Serial.println(myReadFloat); Serial.println("and: "); Serial.println(myReadString); } void loop(){}
Und so sieht dann die Ausgabe aus:
Bisher kannte ich EEPROMS nur in den AVR-Controllern, die ich vor längerer Zeit auch programmiert habe.
Auch externe EEPROMS hatte ich über I2C-Bus angeschlossen, musste aber sämtliche Kommandos in Assembler programmieren.
Die Info, dass es zu den EEPROMS mit I2C Interface auch Bibliotheken gibt ist mir neu.
Danke und viele Grüße