EEPROM Teil 2 – externe I2C EEPROMs

Ü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, I²C-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. Und genau darum geht es in diesem Beitrag.

Der Beitrag ist folgendermaßen gegliedert:

In einem separaten Beitrag behandele ich die externen, SPI-basierten EEPROMs. Sie unterscheiden sich von den I²C-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 von 64 kbit = 8 Kilobyte (kurz: kB oder kByte) hat. Eine Ausnahme ist der 24LC1025, dessen Speicherkapazität 1024 kBit beträgt. 

Ihr steuert die 24er-Reihe über I²C. 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.

EEPROM 24LC64
24LC64 EEPROM
EEPROM 24C256
24C256 EEPROM Modul

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:

EEPROM Pinout
EEPROM Pinout (24er Reihe)
  • 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: I²C-Anschlüsse, max. 400 kHz für 24LCxx-Reihe (Datenblatt prüfen!).

Anschluss an einen Arduino Nano

Beispielschaltung für einen 24LC256 an einem Arduino Nano:

EEPROM Schaltung am Arduino Nano
EEPROM Schaltung am Arduino Nano

Oft kann man auf die Pull-Up Widerstände verzichten. Probiert es aus.

Addressschema

Die Adressierung der LC24xx EEPROMs folgt – je nach Größe – verschiedenen Regeln. Die Basisadresse 0x50 (=0b1010000) ist jedoch für alle gleich.

Bei den EEPROMs der Größen 32 bis 512 kBit lassen sich mithilfe der unteren drei Bits insgesamt acht verschiedene I²C-Adressen einstellen, also 0x50 bis 0x57. Die Speicheradressen werden durch 2 Byte beschrieben. 

24LC32 bis 24LC512 Adressschema

Die kleinen EEPROMs der Größen 1 bis 16 kBit sind in Speicherblocks organisiert. Der 24L01 besitzt einen Block der Größe 128 Bytes. Der 24LC02 besitzt einen Block der Größe 256 Bytes. Die unteren 3 Bits der I²C-Adresse haben bei ihnen keine Bedeutung, das heißt, ihr könnt nur einen dieser Bausteine pro I²C-Schnittstelle nutzen. Die Speicheradressen „passen“ in ein Byte.

Der 24LC04 besitzt zwei Speicherblöcke mit 256 Byte. Die Blöcke werden mit verschiedenen I²C-Adressen angesprochen. Dafür ist das Bit B0 zuständig (s.u.). Die Speicheradresse innerhalb des Blocks kann dadurch immer noch durch ein Byte beschrieben werden.

Beim 24LC08 ist es genauso, nur dass er 4 Blöcke besitzt, die über B0 und B1 adressiert werden. Der 24LC16 besitzt 8 Blöcke, die über B0, B1 und B2 angesprochen werden.

24LC01 bis 24LC16 Adressschema

Beim 24LC1025 passen die Adressen nicht mehr in 2 Byte. Dieser EEPROM besitzt deswegen zwei Blöcke von je 64 kByte. Der Block wird, wie bei den kleinen EEPROMs, über die I²C-Adresse gewählt. Allerdings geschieht das hier über das Bit Nr. 3. Mit den unteren zwei Bits könnt ihr vier verschiedene I2C-Adressen ansprechen. 

24LC1025 Adressschema

Schließlich gibt es noch 2-MBit-EEPROMS wie den AT24CM02. Die Speicheradressierung benötigt 18 Bit (Bit 0 – Bit 17). Die obersten Speicheradressbits werden dabei über die unteren zwei Bits der I²C-Adresse übermittelt. Bit 3 der I²C-Adresse erlaubt euch, zwei dieser EEPROMs anzusprechen. Der AT24CM01 folgt im Prinzip demselben Schema, nur dass er mit dem MSB Address Bit A16 auskommt und 2 I2C-Adressbits hat. 

AT24CM02 Adressschema

Den EEPROM beschreiben und lesen

Mittelgroße EEPROMs – 24LC32 bis 24LC512

Im ersten, einfachen Beispiel schreiben wir vier 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 zwischen 32 und 512 kBit (= 4 bis 64 kByte) werden die Speicheradressen in zwei Bytes codiert. Dazu teilt ihr sie in ihr MSB (Most Significant Byte) und LSB (Least Significant Byte) und übergebt diese hintereinander.

Ihr beendet den Schreibvorgang mit Wire.endTransmission() ab. Das EEPROM benötigt jedoch noch etwas Zeit, um den Schreibzyklus abzuschließen. Vorher können Sie keine weiteren Daten in das EEPROM schreiben. Die „Schreibzykluszeit“ finden Sie im Datenblatt Ihres EEPROMs. In der Regel beträgt sie 5 Millisekunden.

Beim Beschreiben des EEPROMs müsst ihr über die Speicheradresse 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. Eine gelöschte Speicheradresse ist mit Einsen beschrieben. Wenn ihr sie als Byte lest, erhaltet ihr entsprechend 0b11111111 = 0xFF = 255.

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(115200);
  
  uint16_t address = 0;
  uint8_t byteVal_1 = 42;
  uint8_t byteVal_2 = 123;
  uint8_t byteVal_3 = 99;
  uint8_t byteVal_4 = 222;
  
  eepromByteWrite(address,byteVal_1);
  address++;
  eepromByteWrite(address,byteVal_2);
  address++;
  eepromByteWrite(address,byteVal_3);
  address++;
  eepromByteWrite(address,byteVal_4);
  
  for(address=0; address<4; address++){
    Serial.print("Byte at address ");
    Serial.print(address);
    Serial.print(": ");
    Serial.println(eepromByteRead(address));
  }
}

void loop(){}

void eepromByteWrite(uint16_t addr, uint8_t byteToWrite){
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((uint8_t)(addr >> 8));
  Wire.write((uint8_t)(addr & 0xFF));
  Wire.write(byteToWrite);
  Wire.endTransmission();
  delay(5); // important!
}

uint8_t eepromByteRead(uint16_t addr){
  uint8_t byteToRead;
  
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((uint8_t)(addr >> 8));
  Wire.write((uint8_t)(addr & 0xFF));
  Wire.endTransmission();
  Wire.requestFrom(I2C_ADDRESS, 1);
  byteToRead = Wire.read();
 
  return byteToRead;
}

 

Und so sieht dann die Ausgabe aus:

Ausgabe von ext_eeprom_byte_write_read.ino

Kleine EEPROMs 24LC01 bis 24LC16

Wie oben erklärt, wird die Speicheradresse bei den kleinen EEPROMs durch ein einzelnes Byte beschrieben. Da das ab dem 24LC04 (512 Speicheradressen) nicht mehr ausreicht, wird der Speicher in Blöcke von 256 Byte geteilt, die ihr über unterschiedliche I²C-Adressen ansprecht. Das Teilen in I²C-Adresse und Blockadresse übernehmen in dem folgenden Sketch die Funktionen eepromByteWrite() und eepromByteRead().

Um zu zeigen, dass die Blockzuordnung funktioniert, schreibt der Sketch die vier Bytes in die Speicheradressen 254 bis 257.

#include <Wire.h>
#define I2C_ADDRESS 0x50

void setup(){
  Wire.begin();
  Serial.begin(115200);
  
  /* choose smaller start address for 24lc02 or 24lc01 */
  const uint16_t startAddress = 254; 
  uint16_t address = startAddress;
  uint8_t byteVal_1 = 42;
  uint8_t byteVal_2 = 123;
  uint8_t byteVal_3 = 99;
  uint8_t byteVal_4 = 222;
  
  eepromByteWrite(address,byteVal_1);
  address++;
  eepromByteWrite(address,byteVal_2);
  address++;
  eepromByteWrite(address,byteVal_3);
  address++;
  eepromByteWrite(address,byteVal_4);
  
  for(address=startAddress; address<(startAddress+4); address++){
    Serial.print("Byte at address ");
    Serial.print(address);
    Serial.print(": ");
    Serial.println(eepromByteRead(address));
  }
}

void loop(){}

void eepromByteWrite(uint16_t addr, uint8_t byteToWrite){
  uint8_t i2cAddr = I2C_ADDRESS + addr/256;
  uint8_t blockAddr = addr%256;
  
  Wire.beginTransmission(i2cAddr);
  Wire.write(blockAddr);
  Wire.write(byteToWrite);
  Wire.endTransmission();
  delay(5); // important!
}

uint8_t eepromByteRead(uint16_t addr){
  uint8_t byteToRead;
  uint8_t i2cAddr = I2C_ADDRESS + addr/256; 
  uint8_t blockAddr = addr%256;
  
  Wire.beginTransmission(i2cAddr);
  Wire.write(blockAddr);
  Wire.endTransmission();
  Wire.requestFrom(I2C_ADDRESS, 1);
  byteToRead = Wire.read();
 
  return byteToRead;
}

 

Und das ist die Ausgabe:

Ausgabe von ext_eeprom_byte_write_read_smallsize.ino

Der Sketch funktioniert überraschenderweise auch auf einem 24LC01 mit derselben Ausgabe. Des Rätsels Lösung: Der EEPROM verwendet nur die unteren 7 Bits der Speicheradresse. Entsprechend werden byteVal_1 und byteVal_2 an die Adressen 126 und 127 geschrieben, byteVal_3 und byteVal_4 landen auf den Adressen 0 und 1. Und warum funktioniert das für byteVal_3 und byteVal_4, obwohl doch die I²C-Adresse auf 0x51 wechselt? Auch wenn die unteren 3 Bits der I²C-Adresse bei diesem EEPROM keine Rolle spielen, erhaltet ihr trotzdem mit einem I²C-Scanner acht I²C-Adressen (0x50 bis 0x57). Er „hört“ auf alle diese Adressen, aber schreibt immer in denselben Speicherblock. Prüfen könnt ihr das, indem ihr die Adressen 0 bis 127 auslest. 

Große EEPROMs 24LC1025 (und AT24CM01/02)

Beim 24LC1025 reichen die zwei Bytes die Speicheradressen nicht aus. Deswegen arbeitet er mit zwei Blöcken, die unterschiedliche I²C-Adressen haben. Aufgrund des Adressschemas sind das 0x50 und 0x54. Der folgende Sketch schreibt zwei Bytes an die Adressen 0xFFFE und 0xFFFF (65534 und 65535), also die letzten beiden Adressen des ersten Speicherblocks. Die anderen Bytes werden an die Adressen 0x10000 und 0x10001 geschrieben, also an die Adressen 0 und 1 des zweiten Blocks. 

#include <Wire.h>
#define I2C_ADDRESS_1 0x50
#define I2C_ADDRESS_2 0x54

void setup(){
  Wire.begin();
  Serial.begin(115200);
  
  uint32_t address = 0xFFFE;
  uint8_t byteVal_1 = 42;
  uint8_t byteVal_2 = 123;
  uint8_t byteVal_3 = 99;
  uint8_t byteVal_4 = 222;
  
  eepromByteWrite(address,byteVal_1);
  address++;
  eepromByteWrite(address,byteVal_2);
  address++;
  eepromByteWrite(address,byteVal_3);
  address++;
  eepromByteWrite(address,byteVal_4);
  
  for(address = 0xFFFE; address < 0x10002; address++){
    Serial.print("Byte at address 0x");
    Serial.print(address, HEX);
    Serial.print(": ");
    Serial.println(eepromByteRead(address));
  }
}

void loop(){}

void eepromByteWrite(uint32_t addr, uint8_t byteToWrite){
  uint8_t i2cAddr = I2C_ADDRESS_1;
  
  if (addr > 0xFFFF){
    i2cAddr = I2C_ADDRESS_2;
  }

  Wire.beginTransmission(i2cAddr);
  Wire.write((uint8_t)(addr >> 8));
  Wire.write((uint8_t)(addr & 0xFF));
  Wire.write(byteToWrite);
  Wire.endTransmission();
  delay(5); // important!
}

uint8_t eepromByteRead(uint32_t addr){
  uint8_t byteToRead;
  uint8_t i2cAddr = I2C_ADDRESS_1;
  
  if (addr > 0xFFFF){
    i2cAddr = I2C_ADDRESS_2;
  }

  Wire.beginTransmission(i2cAddr);
  Wire.write((uint8_t)(addr >> 8));
  Wire.write((uint8_t)(addr & 0xFF));
  Wire.endTransmission();
  Wire.requestFrom(i2cAddr, 1);
  byteToRead = Wire.read();
  
  return byteToRead;
}

 

Die Ausgabe ist:

Ausgabe ext_eeprom_byte_write_read_largesize.ino

Für den AT24CM01 und AT24CM02 müsstet ihr den Sketch modifizieren. Das spare ich mir jedoch.

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 vom EEPROM und geben es auf dem seriellen Monitor aus.

Bitte habt Verständnis, dass ich dieses und die folgenden Beispiele nur für die mittelgroßen EEPROMs schreibe. Es sollte euch aber nicht schwerfallen, sie, falls notwendig, anzupassen.

Hier zunächst der Sketch:

#include <Wire.h>
#define I2C_ADDRESS 0x50

void setup(){
  Wire.begin();
  //Wire.setClock(400000);
  Serial.begin(115200);
  uint16_t address = 0;
  uint32_t writeStart = 0;
  uint32_t writeDuration = 0;
  
  uint16_t arraySize = 100;
  int16_t intArray[arraySize];
  
  for(uint16_t i=0; i<arraySize; i++){
    intArray[i] = i*10;
  }
  writeStart = millis();
  writeIntArrayToEEPROM(address, intArray, arraySize);
  writeDuration = millis() - writeStart; 

  address = 0;
  for(uint16_t 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(uint16_t addr, int16_t *iArr, uint16_t arrSize){  
  for(uint16_t i = 0; i<arrSize; i++){
    Wire.beginTransmission(I2C_ADDRESS);
    Wire.write((uint8_t)((2*i + addr) >> 8));
    Wire.write((uint8_t)((2*i + addr) & 0xFF));
    Wire.write((uint8_t)(iArr[i] >> 8));
    Wire.write((uint8_t)(iArr[i]) & 0xFF);
    Wire.endTransmission();
    delay(5);
//    while(isBusy()){   // alternativ to delay(5).
//      delayMicroseconds(50);
//    }
  }
}
  
int16_t readIntFromEEPROM(uint16_t addr){
  int16_t intToRead;
  uint8_t msByte;
  uint8_t lsByte;
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((uint8_t)(addr >> 8));
  Wire.write((uint8_t)(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.
Ausgabe von ext_eeprom_int_array_write_read.ino

Das Schreiben des Arrays nahm 552 Millisekunden in Anspruch. Davon gehen 500 Millisekunden auf das Konto der „Write Cycle Time“. Sofern euer EEPROM Fast I²C beherrscht, könnt ihr die anderen 52 Millisekunden verringern, indem ihr die Zeile 6, Wire.setClock(400000), entkommentiert. Dadurch erhöht ihr die I²C-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 I²C 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 (sofern ihr ein Board mit 32 Byte Puffer und einen EEPROM mit einer Page Size von mindestens 32 verwendet):

#include <Wire.h>
#define I2C_ADDRESS 0x50

/* choose a greater number if the i2c buffer is bigger, e.g. a number 
   greater than 128 for an ESP32 board */
uint8_t numOfValues = 64; 

void setup(){
  Wire.begin();
  Serial.begin(115200);
  uint16_t address = 0;
   
  for(uint16_t i=address; i<numOfValues; i++){
    eepromByteWrite(i,(uint8_t)i);
  }

  uint8_t byteArray[numOfValues];
  for(uint8_t i=0; i<numOfValues; i++){
    byteArray[i] = i*2;
  }
  eepromBytePageWrite(address, byteArray, sizeof(byteArray));
  
  address = 0;
  for(uint16_t i=address; i<sizeof(byteArray); i++){
    Serial.print("byteArray[");
    Serial.print(i);
    Serial.print("]: ");
    Serial.println(readEEPROM(i)); 
  }
}
void loop(){}

void eepromByteWrite(uint16_t addr, uint8_t byteToWrite){
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((uint8_t)(addr >> 8));
  Wire.write((uint8_t)(addr & 0xFF));
  Wire.write(byteToWrite);
  Wire.endTransmission();
  delay(5);
}

void eepromBytePageWrite(uint16_t addr, uint8_t *byteArrayToWrite, uint8_t sizeOfArray){
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((uint8_t)(addr >> 8));
  Wire.write((uint8_t)(addr & 0xFF));
  for(uint16_t i=addr; i<sizeOfArray; i++){
    Wire.write(byteArrayToWrite[i]);
  }
  Wire.endTransmission();
  delay(5);
}

uint8_t readEEPROM(uint16_t addr){
  byte byteToRead;
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((uint8_t)(addr >> 8));
  Wire.write((uint8_t)(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:

Ausgabe von ext_eeprom_wire_limit_test.ino

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:

  1. Die Page Size,
  2. der Wire.write() Puffer und
  3. es darf nicht über eine Page Grenze hinausgeschrieben 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:

100 Integers auf EEPROM mit 64 Bytes pro Page
100 Integers auf EEPROM mit 64 Bytes pro Page

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(115200);
  uint16_t address = 0;
  uint32_t writeStart = 0;
  uint32_t writeDuration = 0;
  
  uint16_t arraySize = 100;
  int intArray[arraySize];
  
  for(uint16_t i=0; i<arraySize; i++){
    intArray[i] = i;
  }
  writeStart = millis();
  writeIntArrayToEEPROM(address, intArray, arraySize);
  writeDuration = millis() - writeStart; 
  
  address = 0;
  for(uint16_t 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(uint16_t addr, int *iArr, uint16_t arrSize){  
  uint16_t noOfIntsStillToWrite = arrSize;
  uint16_t arrayIndex = 0;
  
  while((noOfIntsStillToWrite != 0)){
    uint16_t chunk = (WRITE_LIMIT / sizeof(uint16_t));  // max chunk in number of ints
    uint16_t positionInPage = (addr % PAGE_SIZE);  // current position in page
    uint16_t spaceLeftInPage = (PAGE_SIZE - positionInPage) / sizeof(uint16_t); // 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(uint16_t addr, int *iArr, uint16_t chunkSize, uint16_t arrIdx){  
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((uint8_t)(addr>>8));
  Wire.write((uint8_t)(addr&0xFF));

  for(uint16_t i=0; i<chunkSize; i++){
    Wire.write((uint8_t)(iArr[i+arrIdx]>>8));
    Wire.write((uint8_t)(iArr[i+arrIdx])&0xFF);
  }
  Wire.endTransmission();
  delay(5); 
}

int16_t readIntFromEEPROM(uint16_t addr){
  int16_t intToRead;
  uint8_t msByte;
  uint8_t lsByte;
  Wire.beginTransmission(I2C_ADDRESS);
  Wire.write((uint8_t)(addr>>8));
  Wire.write((uint8_t)(addr&0xFF));
  Wire.endTransmission();
  Wire.requestFrom(I2C_ADDRESS, 2);
  msByte = Wire.read();
  lsByte = Wire.read();
  intToRead = (int16_t)(msByte<<8 | lsByte);
  return intToRead;
}

 

Ihr 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:

Ausgabe von ext_eeprom_int_array_write_read.ino

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. Die Verwendung ist praktisch gleich.

Wenn ihr Strings auf den EEPROM schreiben oder von ihm lesen möchtet, dann verwendet ihr dafür die Funktionen putString() bzw. getString(). Der Rest sollte eigentlich selbsterklärend sein. Hier ein Beispielsketch:

/*
  Read and write settings and calibration data to an external I2C EEPROM
  By: Nathan Seidle
  SparkFun Electronics
  Date: December 11th, 2019
  License: This code is public domain but you buy me a beer if you use this
  and we meet someday (Beerware license).
  Feel like supporting our work? Buy a board from SparkFun!
  https://www.sparkfun.com/products/18355

  This example demonstrates how to read and write various variables to memory.

  The I2C EEPROM should have all its ADR pins set to GND (0). This is default
  on the Qwiic board.

  Hardware Connections:
  Plug the SparkFun Qwiic EEPROM to an Uno, Artemis, or other Qwiic equipped board
  Load this sketch
  Open output window at 115200bps
*/

#include <Wire.h>

#include "SparkFun_External_EEPROM.h" // Click here to get the library: http://librarymanager/All#SparkFun_External_EEPROM
ExternalEEPROM myMem;

void setup()
{
  Serial.begin(115200);
  //delay(250); //Often needed for ESP based platforms
  Serial.println("Qwiic EEPROM example");

  Wire.begin();

  //We must set the memory specs. Pick your EEPROM From the list:

  // 24xx00 - 128 bit / 16 bytes - 1 address byte, 1 byte page
  // 24xx01 - 1024 bit / 128 bytes - 1 address byte, 8 byte page
  // 24xx02 - 2048 bit / 256 bytes - 1 address byte, 8 byte page
  // 24xx04 - 4096 bit / 512 bytes - 1 address byte, 16 byte page
  // 24xx08 - 8192 bit / 1024 bytes - 1 address byte, 16 byte page
  // 24xx16 - 16384 bit / 2048 bytes - 1 address byte, 16 byte page
  // 24xx32 - 32768 bit / 4096 bytes - 2 address bytes, 32 byte page
  // 24xx64 - 65536 bit / 8192 bytes - 2 address bytes, 32 byte page
  // 24xx128 - 131072 bit / 16384 bytes - 2 address bytes, 64 byte page
  // 24xx256 - 262144 bit / 32768 bytes - 2 address bytes, 64 byte page
  // 24xx512 - 524288 bit / 65536 bytes - 2 address bytes, 128 byte page
  // 24xx1025 - 1024000 bit / 128000 byte - 2 address byte, 128 byte page
  // 24xxM02 - 2097152 bit / 262144 byte - 2 address bytes, 256 byte page

  // Setting the memory type configures the memory size in bytes, the number of address bytes, and the page size in bytes.

  // Default to the Qwiic 24xx512 EEPROM: https://www.sparkfun.com/products/18355
  myMem.setMemoryType(512); // Valid types: 0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1025, 2048

  if (myMem.begin() == false)
  {
    Serial.println("No memory detected. Freezing.");
    while (true)
      ;
  }
  Serial.println("Memory detected!");

  Serial.print("Mem size in bytes: ");
  Serial.println(myMem.length());

  //Yes you can read and write bytes, but you shouldn't!
  byte myValue1 = 200;
  myMem.write(0, myValue1); //(location, data)

  byte myRead1 = myMem.read(0);
  Serial.print("I read (should be 200): ");
  Serial.println(myRead1);

  //You should use gets and puts. This will automatically and correctly arrange
  //the bytes for larger variable types.
  int myValue2 = -366;
  myMem.put(10, myValue2); //(location, data)
  int myRead2;
  myMem.get(10, myRead2); //location to read, thing to put data into
  Serial.print("I read (should be -366): ");
  Serial.println(myRead2);

  float myValue3 = -7.35;
  myMem.put(20, myValue3); //(location, data)
  float myRead3;
  myMem.get(20, myRead3); //location to read, thing to put data into
  Serial.print("I read (should be -7.35): ");
  Serial.println(myRead3);

  String myString = "Hi, I am just a simple test string";
  unsigned long nextEEPROMLocation = myMem.putString(30, myString);
  String myRead4 = "";
  myMem.getString(30, myRead4);
  Serial.print("I read: ");
  Serial.println(myRead4);
  Serial.print("Next available EEPROM location: ");
  Serial.println(nextEEPROMLocation);
}

void loop()
{
}

 

Und so sieht dann die Ausgabe aus:

Ausgabe von Example1_BasicReadWrite.ino

Probiert am besten auch die anderen Beispielsketche der Bibliothek aus, insbesondere Example2_Settings.ino.

16 thoughts on “EEPROM Teil 2 – externe I2C EEPROMs

  1. Moin,
    für eine technische Anwendung sollen Werte extern auf EEPROM gespeichert werden. Das ist mir mit diesem Programm gelungen, vielen Dank für diesen Beitrag. Es ergeben sich trotzdem noch weitere Fragen:
    Der Versuch, statt eines Arduinos ein ESP-Modul zu verwenden, funktionierte leider nicht. Was müsste geändert werden?
    Wie ist zu verfahren, wenn mehr Daten gespeichert werden müssen als auf ein EEPROM passen? Ein zweites EEPROM kann mit einer anderen Adresse auch an I2C angeschlossen werden. Dazu müssten die Brücken an 1-3 sicher anders verschaltet werden? Was ist dabei zu beachten?

    Wenn ein zweites EEPROM angeschlossen ist, wird dann automatisch im zweiten gespeichert, sobald das erste gefüllt ist oder muss ich einen Wechsel vornehmen, sobald eine Überfüllung droht? Wie kann ich dann feststellen, wie viel Restspeicherplatz auf einem EEPROM noch vorhanden ist?

    Weiterhin ist mir aufgefallen, dass float-Variablen standardmäßig nur mit zwei Nachkommastellen abgespeichert werden. Die einzige Lösung dazu, die ich gefunden habe, funktionierte über Konvertierung in einen String mit Angabe der Anzahl der Nachkommastellen: String(variable, 6). Hiermit werden dann 6 Nachkommastellen abgespeichert. Der Speicherbedarf entspricht dann der Anzahl aller Stellen inklusive des Kommas. Gibt es dazu noch eine andere Lösung?

    Viele Fragen, die Sie mir hoffentlich beantworten können. Dafür vorab schon einmal vielen Dank!
    Mit freundlichen Grüßen

    1. Hallo Thomas,
      die vom mir im Artikel beschriebene Methode ohne Bibliothek dient eigentlich nur der Anschauung. Das soll zeigen, wie die Dinger im Prinzip funktionieren. Das Ganze wird aber auch schnell unhandlich.

      Ich weiß ja nicht, welchen Sketch du auf dem ESP32 probiert hast, aber was z.B. geändert werden muss, ist die Größe eines Integers. Die ist bei einem ESP32 für gewöhnlich 4 Byte. Vielleicht ändere ich die Sketche mal von int zu den definierten Größen int16_t bzw. uint16_t.

      Das Problem der Speichergröße hast du selbst in der Hand. Die maximale Adresse kannst du leicht berechnen. Beispiel: du hast einen 24lc512, d.h 512 kritisch, d.h. 32k x 8 und damit ist die maximale Adresse 32k = 32767. Schreibe einfach nicht über die Adresse hinaus. Und ziehe die aktuell höchste verwendete Adresse davon ab, dann hast du die frei Kapazität in Byte.

      Das Schema für die I2C Adressen habe ich beschrieben- noch unklar? Und dann arbeitest du am besten mit einer Bibliothek wie der von Sparkfun. Zwei Objekte mit zwei unterschiedlichen Adtessen erzeugen und getrennt ansprechen.

      Zu guter letzt: Float werden als 4 Byte Variablen gespeichert. Das gibt eine Präzision von 6 bis 7 Ziffern. Wie viele Nachkommastellen du ausgibst, steuerst du über den zweiten Parameter von Serial.print(), also z.B.:
      float f = 1.23456;
      Serial.print(f, 4);
      gibt 4 Nachkommastellen.

      VG, Wolfgang

      1. Moin,
        vielen Dank für Ihre Hilfe. Der Tipp mit der Speicherberechnung hat mir geholfen. Meine Vermutung war zunächst, dass es vielleicht einen Befehl gäbe, mit dem ich die Größe des Restspeicher/belegten Speichers abrufen kann. So geht es natürlich auch, denn mit der Speicheradresse der abgelegten Werte kann ich natürlich unmittelbar nachvollziehen, wieviel Speicher bereits belegt sind.
        Ich hatte Ihnen zu ungenaue Angaben gemacht, deshalb versuche ich das jetzt zu präzisieren:
        Mein Programm arbeitet wie im letzten Beispiel mit der Sparkfun-Bibliothek und läuft auf einem Arduino Mega kombiniert mit einem Uhrmodul so weit einwandfrei.
        Nochmals zu den Nachkommastellen: Ich meinte damit, wie eine float-Variable im EEPROM abgespeichert wird. Im Print-Befehl wird sie mit 6 Nachkommastellen angegeben. Abgespeichert wurde sie aber stets mit 2 Nachkommastellen. Daran änderte sich auch nichts, wenn ich den Speicherbereich vergrößerte. Erst nachdem ich die float-Variable in einen String mit dem Befehl String(variable, 6) umgewandelt habe, wurde sie korrekt mit 6 Nachkommastellen abgespeichert und wieder ausgegeben.
        Weiterhin blieb mein Versuch, das gleiche Programm wie im letzten Beispiel auf einem ESP32 zum Laufen zu bringen, völlig erfolglos. Nach dem Hochladen wurde nicht einmal einer der Print-Befehle dargestellt, der Monitor reagierte zwar, blieb aber völlig leer. Ich habe über einen I2C-Scanner die Adresse 0x50 gefunden und in den wire.begin-Befehl mit aufgenommen. Auch das änderte nichts.
        Hätten Sie dafür eine Erklärung?
        Vielen Dank!
        Thomas Fischer

        1. Moin, haben Sie einmal die Reset-Taste nach dem Hochladen gedrückt? Die ESP32 Boards brauchen nach dem Hochladen ein wenig, bis sie sich mit dem seriellen Monitor verbinden. Das hat dann den Effekt, dass man direkt nach dem Hochladen nichts sieht. Das wäre die einfachste Erklärung. Ich habe den letzten Sketch aus meinem Beitrag eben noch einmal probiert und er funktioniert mit einem ESP32 Board und einem 512 kBit EEPROM.
          Zurück zu den Nachkommastellen: Ich habe den Sketch eben noch einmal modifiziert:
          Zeile 41:
          float myValue3 = -7.351234;
          Zeile 46:
          Serial.println(myRead3, 6);
          Damit erhalte ich in der Ausgabe:
          I read: -7.351234

          Etwas anderes ist eigentlich auch nicht zu erwarten, da die put()-Funktion nichts von Nachkommatellen weiß. Ein Float ist ein Wert der in 4 Bytes gespeichert wird:
          Bit 1: Vorzeichen
          Bit 2-9: Exponent
          Bit 10-32: Mantisse
          Die Rechenvorschrift dahinter lautet: (−1)Sign⋅1.Mantisse⋅2(Exponent−127)
          Also recht komplex. Aber darum muss man sich nicht kümmern. Was ich damit nur verdeutlichen will ist, dass ein Float nach diesem Schema gespeichert wird. Egal, ob in einem EEPROM, im Flash oder im Arbeitsspeicher. Die Zahl der Nachkommastellen ist lediglich eine Formatierungsoption der print() Funktion.
          Wenn das noch nicht klar sein sollte, dann sagen Sie mir bitte, wie Sie den Float Wert definieren, wie sie ihn im EEPROM abspeichern, dann auslesen und schleißlich ausgeben.

          VG, Wolfgang Ewald

        2. Ich habe die Beispielsketche noch einmal so abgeändert, dass sie auch auf einem ESP32 Board laufen. Dabei habe ich den Artikel auch noch etwas verbessert und ergänzt.

  2. Hallo,
    was müsste ich den tuen wenn meine Zahl deutlich über den bisherigen Werten liegt.
    Konkret müsste ich Werte bis 35.000 speichern?

    1. Hallo, die Größe der zu speichernden Zahlen spielt nur insofern eine Rolle, als die Zahl zum Variablentyp passen muss. Auf einem AVR-basierten Arduino ist 35000 ein unsigned integer. Der signed integer Bereich endet bei 32767. Also usigned int als Typ wählen und ganz“normal“ auf den EEPROM speichern. Oder habe ich die Frage missverstanden?

  3. Achtung, mit Elementen der Klasse „String“ geht das Beispiel NICHT! Bei „String“ wird (vermutlich) die Adresse der String-Instanz im EEPROM gespeichert. Scheinbar funktioniert das Beispiel dann, da die Instanz, die zurückgelesen wird, auf die gleiche Adresse gesetzt wird wie die speichernde Instanz. Und damit den gleichen Inhalt hat. Der Inalt des Strings wird somit mitnichten im EEPROM gespeichert sondern steht halt noch im Speicher des Arduino.
    „String“ wird nicht von der Sparkfun-Bibliothek unterstütz, was auch schon der allererste Issue bemerkt: https://github.com/sparkfun/SparkFun_External_EEPROM_Arduino_Library/issues/1
    Die Lösung ist relativ einfach: char[] statt String benutzen.

    1. Hallo Achim,

      ich muss widersprechen bzw. präzisieren. Denn natürlich sind meine Beispielsketche grundsätzlich geprüft und funktionieren. Die Sparkfun Bibliothek nimmt auch Strings, nur nicht bei Verwendung von ESP32 und ESP8266 basierten Boards. Das liegt an der unterschiedlichen Implementierung von Strings auf AVR- und ESP-Boards. Wer sich für Details zu diesem Thema interessiert, mag hier schauen:

      https://wolles-elektronikkiste.de/sram-management#string_handling_in_sram

      VG, Wolfgang

      1. Hallo Wolfgang,
        ich habe in der Sparkfun-Bibliothek nachgeschaut, sie verwendet ein Template, um die übergebene Variable ins EEPROM zu schreiben. Wird hier nicht ein einfacher Datentyp wie int, float, … verwendet sondern eine Klasse, was String ist, werden die internen Klassen-Variablen gespeichert. Mit anderen Worten, das, was auf dem Stack ist. Dass die Klasse noch was auf dem Heap liegen hat, nämlich die eigentlichen Inhalt des Strings, kann die put()-Funktion nicht wissen. Der Inhalt des Strings wird somit nicht im EEPROM gespeichert.
        Testen lässt sich das ganz leicht, indem du die put() in deinem Beispiel auskommentierst und nur die get() stehen lässt. Und – das ist entscheidend – vor dem Ausführen des modifizierten Programms den Strom zum Mikrocontroller kurz unterbrichst. Dann ist nämlich auch der alte String auf dem Heap weg. Und dann kommt kein vernünftiger String mehr zurück. Nur eine mit Quatsch-Inhalt, aber der gleichen Länge wie der gespeicherte.

        Ausgabe beim ersten Ausführen, mit Abspeichern und Zurücklesen:
        I read:
        -7.35
        and:
        This is no poetry, I am just a simple test String

        Strom kurz abgeschaltet, nur Zurücklesen:
        I read:
        -7.35
        and:
        ▒▒▒▒*}▒j!▒߻▒▒▒▒$▒b▒▒▒m▒▒97▒▒7h
        ▒:

        Viele Grüße und auch noch vielen Dank für deine tolle Seite!

        Achim

      2. Hallo Wolfgang,
        ich habe in der Sparkfun-Bibliothek nachgeschaut, sie verwendet ein Template, um die übergebene Variable ins EEPROM zu schreiben. Wird hier nicht ein einfacher Datentyp wie int, float, … verwendet sondern eine Klasse, was String ist, werden die internen Klassen-Variablen gespeichert. Mit anderen Worten, das, was auf dem Stack ist. Dass die Klasse noch was auf dem Heap liegen hat, nämlich den eigentlichen Inhalt des Strings, kann die put()-Funktion nicht wissen. Der Inhalt des Strings wird somit nicht im EEPROM gespeichert.
        Testen lässt sich das, indem du die put() in deinem Beispiel auskommentierst und nur die get() stehen lässt. Und – das ist entscheidend – vor dem Ausführen des modifizierten Programms den Strom zum Mikrocontroller kurz unterbrichst. Dann ist nämlich auch der alte String auf dem Heap weg. Und dann kommt kein vernünftiger String mehr zurück. Nur einer mit Quatsch-Inhalt, was halt zufällig so auf den Heap liegt.

        Ausgabe beim ersten Ausführen, mit Abspeichern und Zurücklesen:
        I read:
        -7.35
        and:
        This is no poetry, I am just a simple test String

        Strom kurz abgeschaltet, nur Zurücklesen:
        I read:
        -7.35
        and:
        ▒▒▒▒*}▒j!▒߻▒▒▒▒$▒b▒▒▒m▒▒97▒▒7h
        ▒:

        Viele Grüße und auch noch vielen Dank für deine tolle Seite!

        Achim

        1. Hi, vielen Dank. Verstehe, und das mit der Stromunterbrechung macht Sinn! Ich muss mir das nochmal anschauen und das Beispiel modifizieren.

          VG, Wolfgang

        2. Und noch eine Ergänzung: Ich habe der Sparkfun Bibliothek eine putString() und eine getString() Funktion spendiert. Sparkfun hat die Ergänzung in die Bibliothek aufgenommen und veröffentlicht, sodass sie jetzt in der aktuellen Version verfügbar ist.

  4. 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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert