EEPROM Teil 3 – externe SPI EEPROMs

Über den Beitrag

Mit diesem dritten Beitrag werde ich meine Reihe über EEPROMs abschließen. In den beiden vorherigen Beiträgen hatte ich über die internen EEPROMs der AVR Boards und die externen, I2C-basierten EEPROMs berichtet. Hier geht es nun um die externen, SPI basierten EEPROMs.

Warum noch ein separater Beitrag zu SPI EEPROMs? Lässt sich die schnellere SPI Übertragungsrate bei den langsamen EEPROM Schreibvorgängen überhaupt nutzen? Um es vorwegzunehmen: Die hohe SPI Geschwindigkeit fällt nicht so sehr ins Gewicht. Jedoch macht sich bei der Übertragung größerer Datenmengen „am Stück“ bemerkbar, dass hier die 32 Byte Beschränkung der I2C Übertragung nicht vorhanden ist. Dadurch könnt ihr die gesamte Page Size des EEPROMs nutzen.

Eigenschaften

Von der Bezeichnung her erkennt ihr die SPI EEPROMs daran, dass sie eine 25 im Namen tragen. Die Zahl (2n) hinter den Buchstaben bezeichnet die Speichergröße in Kilobit. Hier ein paar Beispiele:

SPI EEPROMs: 25LC512 (512 kbit), 25LC640A (64 kbit), 25A512 (512 kbit)
SPI EEPROMs: 25LC512 (512 kbit), 25LC640A (64 kbit), 25A512 (512 kbit)

Auf die allgemeinen Grundlagen der EEPROMs gehe ich nicht noch einmal ein. In den Teilen 1 und 2 dieser Reihe habe ich dazu schon einiges geschrieben.

Pinout

In der Regel besitzen die SPI EEPROMs die folgenden Pins:

Pinout der SPI EEPROMs
Pinout der SPI EEPROMs
  • CS: Chip Select, LOW-aktiv.
  • SO: Slave Out, zu verbinden mit MISO.
  • WP: Write Protect, LOW-aktiv.
    • Funktioniert nur im Zusammenspiel mit dem WPEN Bit (anders als bei der 24er Reihe)
  • GND (VSS) / VCC: Spannungsversorgung.
    • „LC/C“ Varianten i.d. Regel bis 5.5 Volt, „A“ Varianten i.d. Regel bis 3 Volt.
  • Hold: Hold-Pin, LOW-aktiv, pausiert eine Datenübertragung.
  • SCK: SPI Clock, zu verbinden mit SCK.
  • SI: Slave Input, zu verbinden mit MOSI.

Anschluss an den Mikrocontroller

Als Beispiel zeige ich hier, wie ihr den SPI basierten EEPROM, sofern er 5 Volt verträgt, mit einem Arduino Nano verbindet:

Anschluss eines SPI EEPROMs: 25LC512 am Arduino Nano
Ein 25LC512 am Arduino Nano

Den Hold-Pin verwende ich nur in einem Beispiel. Da wahrscheinlich wenige von euch die Funktion nutzen werden, habe ich ihn mit VCC verbunden. Falls ihr Mangel an Pins haben solltet, dann könnt ihr eventuell auch den WP-Pin einfach auf HIGH setzen.

Page Writing

Der Speicher eines EEPROMs ist in Pages segmentiert. Die Pages haben in der Regel eine Größe von 16, 32, 64 oder 128 Bytes. Den Wert erfahrt ihr aus dem Datenblatt.

Das Beschreiben von EEPROMs ist ein sehr langsamer Prozess. Schreibt ihr ein einzelnes Byte auf den EEPROM, benötigt ihr dafür die sogenannte „Write Cycle Time“, die meistens mit 5 Millisekunden angegeben wird. Um das Schreiben zu beschleunigen, besitzen die EEPROMs einen Pufferspeicher in der Größe der Pages. Diesen könnt ihr „in einem Rutsch“ (Burst Write) vollschreiben. Von diesem Pufferspeicher aus können die einzelnen Speicherzellen parallel beschrieben werden. Dadurch wird die Schreibgeschwindigkeit um einen Faktor erhöht, der maximal der Pagegröße (Wert in Byte) entspricht.

Die Grenzen der Pages sind fest und ihr dürft im Burst Write nicht über sie hinausschreiben. Ich bin darauf im letzten Beitrag recht intensiv eingegangen.

SPI EEPROMs ohne Bibliothek beschreiben und lesen

Jetzt geht es endlich los. Zunächst zeige ich, wie ihr SPI EEPROMs ohne Bibliothek beschreibt und lest. Wenn euch das nicht interessiert, könnt ihr diesen Abschnitt überspringen.

In diesem einfachen Beispiel schreiben wir zwei Integer-Werte an die EEPROM Adressen 4 und 6 und lesen sie danach aus.

Auf die SPI Funktionen werde ich in diesem Beitrag nicht eingehen. Falls notwendig, findet ihr hier etwas zu dem Thema.

Hier erst einmal der Sketch:

#include <SPI.h>
#define READ 0x03
#define WRITE 0x02
#define WREN 0x06  // write enable

const int csPin = 10;
const int wpPin = 9; // For this sketch you could also leave the wpPin unconnected
const int writeCycle = 5;

void setup(){
  pinMode(csPin, OUTPUT);
  digitalWrite(csPin, HIGH);
  pinMode(wpPin, OUTPUT);
  digitalWrite(wpPin,HIGH);
  SPI.begin();
  Serial.begin(9600);
 
  int intToWrite = 42;
  eepromWriteInt(4, intToWrite);
  intToWrite = -1111;
  eepromWriteInt(6, intToWrite);
  
  int intToRead = 0;
  intToRead = eepromReadInt(4);
  Serial.print("intToRead (Address = 4): ");
  Serial.println(intToRead);
  intToRead = 0;
  intToRead = eepromReadInt(6);
  Serial.print("intToRead (Address = 6): ");
  Serial.println(intToRead);
}

void loop(){}

void eepromWriteEnable(){
  digitalWrite(csPin, LOW);
  SPI.transfer(WREN); 
  digitalWrite(csPin, HIGH);
}

void eepromWriteInt(int addr, int value){
  eepromWriteEnable();
  // If you want to change the SPI clock speed, uncomment the following line and adjust
  //SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
  digitalWrite(csPin, LOW);
  SPI.transfer(WRITE);
  SPI.transfer((byte)(addr>>8));
  SPI.transfer((byte)(addr&0xFF));
  SPI.transfer((byte)(value>>8));
  SPI.transfer((byte)(value&0xFF));
  digitalWrite(csPin, HIGH);
  //SPI.endTransaction(); // Uncomment if you have uncommented SPI.beginTransaction()
  delay(writeCycle);
}

int eepromReadInt(int addr){
  byte MSB = 0;
  byte LSB = 0;
  int value = 0; 
  //SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
  digitalWrite(csPin, LOW);
  SPI.transfer(READ); 
  SPI.transfer((byte)(addr>>8));
  SPI.transfer((byte)(addr&0xFF));
  MSB = SPI.transfer(0x00);
  LSB = SPI.transfer(0x00);
  digitalWrite(csPin, HIGH);
  //SPI.endTransaction();
  value = (int)((MSB<<8)|LSB);
  return value;
}

 

Und hier die erwartete Ausgabe:

Ausgabe von spi_eeprom_without_lib.ino
Ausgabe von spi_eeprom_without_lib.ino

Ein paar Erläuterungen:

  • Die SPI EEPOMs „hören“ auf bis zu elf verschiedene Anweisungen. Der Sketch verwendet drei davon:
    • WREN  (Write Enable): Setzt das Write Enable Bit im Status Register des EEPROMs. Nach einem erfolgreichen Schreibprozess wird das Bit wieder zurückgesetzt.
    • WRITE: Leitet den Schreibvorgang ein.
    • READ: Leitet den Lesevorgang ein.
  • Nach dem Senden der zu schreibenden Daten, also jedes Mal, wenn der Chip Select Pin wieder auf HIGH geht, müsst ihr die Write Cycle Time abwarten.
  • Der Sketch funktioniert nicht, wenn ihr einen Integer-Wert auf zwei Pages verteilt. Probiert es aus: Wenn die Pagegröße eures SPI EEPROMs beispielsweise 128 Byte beträgt, dann schreibt mal ein Integer an die Adresse 127. Beim Auslesen erhaltet ihr einen falschen Wert.

Der letztgenannte Punkt ist nicht das einzige Manko an dem Sketch. Wenn Ihr anstelle von Integer-Werten andere Variablentypen wie Strings, Floats oder Arrays verwendet, müsst ihr ihn entsprechend anpassen. Außerdem gibt es noch eine Reihe von Features wie den Schreibschutz und die Holdfunktion, die hier noch nicht berücksichtigt wurden. All das lässt sich ergänzen, der einfachere Weg geht jedoch über eine Bibliothek. 

Bibliothek für SPI EEPROMs

Meine Bibliothek EEPROM_SPI_WE findet ihr hier auf GitHub oder ihr installiert sie über die Bibliotheksverwaltung der Arduino IDE. Anhand von ein paar Beispielsketchen stelle ich ihre Funktionen vor. Die Beispielsketche sind Teil der Bibliothek, d. h. ihr könnt sie auch direkt über die Arduino IDE aufrufen.

Lesen und Schreiben verschiedener Variablentypen

Wenn ihr euren SPI basierten EEPROM mit dem Mikrocontroller eurer Wahl verbunden habt, dann nehmt den folgenden Sketch und passt ihn gegebenenfalls hinsichtlich des CS- und des WP-Pins an. Für diesen Sketch ist der WP-Pin nicht erforderlich. Ihr könnt ihn auch unverbunden lassen. 

Mit setPageSize() teilt ihr der Bibliothek die Pagegröße eures SPI EEPROMs mit. Die SPI Taktrate könnt ihr mit setSPIClockSpeed() anpassen. Ich habe 8 MHz voreingestellt. Allerdings gibt es ein Problem, wenn euer EEPROM nicht mit 8MHz verträglich ist. Deshalb könnt ihr die gewünschte SPI Taktrate auch beim Erstellen des EEPROM_SPI_WE Objektes übergeben.

Für die Erstellung der EEPROM_SPI_WE Objekte gibt es verschiedene Optionen. Wenn ihr die beiden verfügbaren SPI Schnittstellen eines ESP32 nutzen wollt, dann könnt ihr die SPI – Objekte an die EEPROM_SPI_WE Objekte übergeben. 

#include <SPI.h>
#include <EEPROM_SPI_WE.h>
const int csPin = 10; // Chip select pin
const int wpPin = 9;  // Write protect pin (optional)

/* There are different options to create your EEPROM_SPI_WE object:
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin, wpPin, spiClockSpeed); // e.g. uint32t spiClockSpeed = 4000000
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin, wpPin);
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin); 
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(&SPI, csPin, wpPin, spiClockSpeed); 
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(&SPI, csPin, wpPin);  
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(&SPI, csPin);  
 * If you don't define the wpPin and you connect it to GND or VCC,  
 * then protectStatusRegister() is the only function that won't work.
 * Passing the SPI object allows you, for example, to use both SPI 
 * interfaces on the ESP32.
 */
  EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin, wpPin);

void setup(){
  Serial.begin(9600);
  if(myEEP.init()){
    Serial.println("EEPROM connected");
  }
  else{
    Serial.println("EEPROM does not respond");
    while(1);
  }
  /* You can change the SPI clock speed also after you have created your 
   * object. The default is 8 MHz. Check the data sheet which clock speed is 
   * allowed. The frequency limit migh also depend on the voltage. 
   */
  //myEEP.setSPIClockSpeed(4000000); // use AFTER init()!
  
  /* Select the page size of your EEPROM.
   * Choose EEPROM_PAGE_SIZE_xxx,  
   * with xxx = 16, 32, 64, 128 or 256
   */
  myEEP.setPageSize(EEPROM_PAGE_SIZE_32);

  byte byteToWrite = 42; 
  myEEP.write(10, byteToWrite);  // write a byte to EEPROM address 10
  byte byteToRead = myEEP.read(10);
  Serial.print("Byte read: ");
  Serial.println(byteToRead);

  int intToWrite = -4242; 
  int intToRead = 0;
  myEEP.put(10, intToWrite); // write an integer to EEPROM address 10
  myEEP.get(10, intToRead);
  Serial.print("Integer read: ");
  Serial.println(intToRead);

  float floatToWrite = 42.42; 
  float floatToRead = 0.0; 
  myEEP.put(10, floatToWrite);
  myEEP.get(10, floatToRead);
  Serial.print("Float read: ");
  Serial.println(floatToRead);

  char charArrayToWrite[] = "This is no poetry, I am just a simple char array"; 
  myEEP.put(110, charArrayToWrite);  // write stringToWrite to address 110
  char charArrayToRead[60] = "";  // reserve sufficient space 
  myEEP.get(110, charArrayToRead);
  Serial.print("Char array read: ");
  Serial.println(charArrayToRead);

  String stringToWrite = "Hello, I am a test string";
  unsigned int nextAddr = myEEP.putString(200, stringToWrite);   // String objects need a different put function
  
  String stringToRead = "";
  myEEP.getString(200, stringToRead);
  Serial.print("String read: ");
  Serial.println(stringToRead);
  Serial.print("Next free address: ");
  Serial.println(nextAddr);

  int intArrayToWrite[20];
  int intArrayToRead[20];
  for(unsigned int i=0; i<(sizeof(intArrayToWrite)/sizeof(int)); i++){
    intArrayToWrite[i] = 10*i;
  } 
  myEEP.put(250, intArrayToWrite);
  myEEP.get(250, intArrayToRead);
  for(unsigned int i=0; i<(sizeof(intArrayToRead)/sizeof(int)); i++){
    Serial.print("intArrayToRead[");
    Serial.print(i);
    Serial.print("]: ");
    Serial.println(intArrayToRead[i]);
  }  
}
  
void loop(){}

 

Der Sketch erzeugt die folgende Ausgabe:

Output EEPROM_SPI_WE_basic_read_write.ino
Ausgabe EEPROM_SPI_WE_basic_read_write.ino

Ein paar Erläuterungen:

  • write() schreibt eine Variable vom Typ Byte.
  • Mit read() lest ihr eine Variable vom Typ Byte.
  • put() schreibt eine Variable an eine bestimmte (Start-)Adresse.
    • Die Funktion ist hinsichtlich der Variablentypen recht flexibel.
    • Ihr müsst euch um die Pages nicht kümmern. Page-übergreifendes Schreiben managed die Bibliothek im Hintergrund.
  • Mit get() lest ihr verschiedene Variablentypen vom EEPROM.
  • putString() / getString() sind die put/get Pendants für Strings.

Verwendung kleiner EEPROMs

Kleine EEPROMs wie die 25LC0x0- oder ST950x0-Serie (mit x = 1, 2, 4) werden anders adressiert als ihre größeren Geschwister. Außerdem ist der Funktionsumfang geringer. Wenn ihr diese EEPROMS verwendet, dann müsst ihr im Sketch ein setSmallEEPROM() einfügen. Die Bibliothek enthält einen Beispielsketch.

Schreibvorgang beschleunigen mit continuousPut()

Nach einer write()-Anweisung, einer put()-Anweisung oder wenn die aktuelle Page voll ist, müsst warten, bis der Write Cycle vollendet ist. Die Bibliothek macht das im Hintergrund, indem sie das WIP-Bit (Write in Progress) im Statusregister des EEPROMs abfragt. Das ist etwas schneller als über ein delay() unter Anwendung der maximalen Write Cycle Time.

Stellt euch vor, ihr wollt die Messwerte eines Sensors in hoher Frequenz auf den EEPROM schreiben und macht das einzeln mithilfe der put() Anweisung. Dabei summieren sich die Write Cycle Times zu erheblichen Werten. Ihr könntet Abhilfe schaffen, indem ihr die Werte in eine Array zwischenspeichert und dieses auf den EEPROM übertragt. Je nach Größe des Arrays kostet das Arbeitsspeicher.

Ich habe deshalb eine Funktion continuousPut() implementiert, die den Schreibvorgang nicht abschließt. Ihr müsst sie mit continuousPutEnable() einleiten und mit continuousPutDisable() abschließen. Dazwischen könnt ihr so viele continuousPut() Anweisungen ausführen, wie ihr wollt. Die Write Cycle Time kommt nur beim Wechsel der Page oder bei Ausführung von continuousPutDisable() zum Tragen.

Und nun stellt euch vor, dass euer Sensor auch per SPI über dieselben MISO, MOSI und SCK angeschlossen wie der EEPROM angeschlossen ist. Um zwischen dem EEPROM und dem Sensor zu wechseln, müsste der Mikrocontroller die CS-Leitung zum EEPROM auf HIGH schalten. Damit würde der Schreibvorgang abgeschlossen und es wäre eine Write Cycle Wartezeit fällig. Hier kommt der Hold Pin ins Spiel. Auf LOW pausiert er den Schreibvorgang und schließt ihn nicht ab.

Der folgende Sketch erklärt den Gebrauch der Funktionen und zeigt den Zeitgewinn von continuousPut():

#include <SPI.h>
#include <EEPROM_SPI_WE.h>
const int csPin = 10; // Chip select pin
const int wpPin = 9;  // Write protect pin (optional)
const int holdPin = 5; // Hold pin, pauses SPI transaction if LOW

EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin, wpPin);

void setup(){
  Serial.begin(9600);
  pinMode(holdPin,OUTPUT);
  digitalWrite(holdPin, HIGH);
  if(myEEP.init()){
    Serial.println("EEPROM connected");
  }
  else{
    Serial.println("EEPROM does not respond");
    while(1);
  }
  /* You can change the SPI clock speed. The default of is 8 MHz */
  //myEEP.setSPIClockSpeed(4000000); // use AFTER init()!
  
  /* Select the page size of your EEPROM.
   * Choose EEPROM_PAGE_SIZE_xxx,  
   * with xxx = 16, 32, 64, 128 or 256
   */
  myEEP.setPageSize(EEPROM_PAGE_SIZE_128);

  unsigned int address = 0;
  unsigned long writeDuration = 0;
  unsigned long startTime = millis();

  /* Conventional way of writing: 
   * For each call of put() you have to wait the write cycle 
   * time (~4 ms).
   */
  myEEP.put(125,0);
  myEEP.put(127, 1111);
  myEEP.put(129, 2222);
  myEEP.put(131, 3333);
  myEEP.put(133, 4444);
  myEEP.put(135, 5555);
  myEEP.put(137, 6666);
  myEEP.put(139, 7777);
  myEEP.put(141, 8888);
  myEEP.put(143, 9999);
  writeDuration = millis() - startTime;
  Serial.print("Time needed for discontinuous writing [ms]: ");
  Serial.println(writeDuration); 
  
  /* Continuous writing:  
   * You only need to wait the write cycle time when a page is 
   * completed and when calling continuousPutDisable().
   * In this example the page is completed after writing to address 127.
   */
  address = 125;
  startTime = millis();
  myEEP.continuousPutEnable(address); // start address
  myEEP.continuousPut(9999);
  myEEP.continuousPut(8888);
  myEEP.continuousPut(7777);
  myEEP.continuousPut(6666);
  myEEP.continuousPut(5555);
  myEEP.continuousPut(4444);
  myEEP.continuousPut(3333);
  myEEP.continuousPut(2222);
  myEEP.continuousPut(1111);
  myEEP.continuousPut(0);
  myEEP.continuousPutDisable();
  writeDuration = millis() - startTime;
  
  Serial.print("Time needed for continuous writing incl. page change [ms]: ");
  Serial.println(writeDuration);
  /* Just to check that writing worked fine: */
  int intToRead;
  address = 125;
  for(unsigned int i=0; i<10; i++){
    myEEP.get(address, intToRead);
    address += sizeof(int);
    Serial.println(intToRead);
  }
 
  /* Another continuous writing:  
   * Here writing starts at address 0. If your page size is >= 32 you will write
   * everything into one page. So, only one write cycle waiting time is needed at 
   * the end. 
   */
  address = 0;
  startTime = millis();
  myEEP.continuousPutEnable(address);
  myEEP.continuousPut(0);
  myEEP.continuousPut(1111);
  myEEP.continuousPut(2222);
  myEEP.continuousPut(3333);
  myEEP.continuousPut(4444);
  myEEP.continuousPut(5555);
  myEEP.continuousPut(6666);
  myEEP.continuousPut(7777);
  myEEP.continuousPut(8888);
  myEEP.continuousPut(9999);
  myEEP.continuousPutDisable();
  writeDuration = millis() - startTime;
  Serial.print("Time needed for continuous writing without page change [ms]: ");
  Serial.println(writeDuration); 

  /* The next example shows how to use the hold pin */ 
  address = 0;
  myEEP.continuousPutEnable(address);
  myEEP.continuousPut(999);
  myEEP.continuousPut(888);
  myEEP.continuousPut(777);
  myEEP.continuousPut(666);
  digitalWrite(holdPin,LOW);
  /* With the hold pin low you can now use the SPI lines to control other 
     devices. The current SPI transaction with the EEPOM is just paused 
     (CS pin is still LOW). After the hold Pin is back to HIGH you can continue 
     the former transaction.
   */
  digitalWrite(holdPin,HIGH);
  myEEP.continuousPut(555);
  myEEP.continuousPut(444);
  myEEP.continuousPutDisable();
  /* Just to show that writing worked fine: */
  address = 0;
  for(unsigned int i=0; i<10; i++){
    myEEP.get(address, intToRead);
    address += sizeof(int);
    Serial.println(intToRead);
  } 
}
  
void loop(){}

 

Hier die Ausgabe:

Ausgabe von EEPROM_SPI_WE_continuous_put.ino
Ausgabe von EEPROM_SPI_WE_continuous_put.ino

Deutung

Der Sketch beinhaltet vier Schreibsquenzen. Da ich in den ersten beiden Schreibsequenzen die Startadresse 125 wähle, kommt es dort auf jeden Fall zu einem Pagewechsel. Bei der dritten Schreibsequenz (Start an der Adresse 0) gibt es keinen Pagewechsel, sofern ihr keinen EEPROM mit 16 Byte Pagegröße einsetzt. Das Ergebnis:

  • 45 Millisekunden für: 10 x put() + 2 Write Cycle Times
  • 8 Millisekunden für: 10 x continuousPut() + 2 Write Cycle Times
  • 4 Millisekunden für: 10 x continuousPut() + 1 Write Cycle Time

Die letzte Schreibsequenz zeigt nur, wie der Hold Pin verwendet wird. 

Schlafmodus und Schreibschutz

Mit dem folgenden folgen Sketch möchte ich zeigen, wie ihr den EEPROM in den Schlaf schickt und wie ihr dessen Speicher bzw. dessen Statusregister schreibschützt.

  • deepPowerDown() initiiert den Tiefschlaf des SPI EEPROMs.
    • Der Sketch zeigt, dass der EEPROM im Tiefschlaf nicht gelesen werden kann.
  • powerUpAndReadID() weckt den EEPROM und liefert dessen ID.
  • writeProtect(parameter) aktiviert den Schreibschutz des EEPROMs.
    • Ihr könnt ihn komplett schützen, nur die obere Hälfte des Adressraums oder das obere Viertel.
    • Mit dem Parameter PROTECT_NONE hebt ihr den Schreibschutz auf.
    • Den WP-Pin auf LOW zu setzen, bringt allein keinen Schreibschutz. Es muss zusätzlich das WPEN Bit (Write Protect Enable Bit) im Statusregister gesetzt werden.
  • protectStatusRegister(true/false); aktiviert den Schreibschutz für das Statusregister, allerdings gilt das nur für den WPEN Bit und die Bits zum Einstellen des zu schützenden Speicherbereiches.

Es ist etwas verwirrend zu verstehen, was protectStatusRegister() genau bewirkt. Ich hoffe, der folgende Sketch macht es klar.

#include <SPI.h>
#include <EEPROM_SPI_WE.h>
const int csPin = 10; // Chip select pin
const int wpPin = 9;  // Write protect pin (needed for this example sketch)

/* There are different options to create your EEPROM_SPI_WE object:
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin, wpPin);
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin); 
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(&SPI, csPin, wpPin);  
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(&SPI, csPin);  
 * If you don't define the wpPin and you connect it to GND or VCC,  
 * then protectStatusRegister() is the only function that won't work.
 * Passing the SPI object allows you, for example, to use both SPI 
 * interfaces on the ESP32.
 */
  EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin, wpPin);

void setup(){
  Serial.begin(9600);
  if(myEEP.init()){
    Serial.println("EEPROM connected");
  }
  else{
    Serial.println("EEPROM does not respond");
    while(1);
  }
  /* You can change the SPI clock speed. The default of is 8 MHz */
  //myEEP.setSPIClockSpeed(4000000); // use AFTER init()!
  
  /* Select the page size of your EEPROM.
   * Choose EEPROM_PAGE_SIZE_xxx,  
   * with xxx = 16, 32, 64, 128 or 256
   */
  myEEP.setPageSize(EEPROM_PAGE_SIZE_128);

  int testInt = 42;
  myEEP.put(50, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);
  Serial.println("Going in deep power down mode...");
  myEEP.deepPowerDown();
  Serial.println("Trying to read testInt... ");
  myEEP.get(50, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);
  Serial.println("Releasing from deep power down  mode...");
  byte id = myEEP.powerUpAndReadID();
  Serial.print("testInt: ");
  myEEP.get(50, testInt);
  Serial.println(testInt);
  Serial.print("Device ID: 0x");
  Serial.println(id, HEX);
  Serial.println("");

/* You can write protect the device. The following options are available:
 * PROTECT_ALL              Complete write protection;
 * PROTECT_UPPER_QUARTER    Protect the upper quarter; 
 * PROTECT_UPPER_HALF       Protect the upper half;
 * PROTECT_NONE             No write protection
 */
  myEEP.writeProtect(PROTECT_ALL);
  Serial.println("Protecting Device, trying to overwrite testInt...");
  myEEP.put(50, 4321);
  myEEP.writeProtect(PROTECT_NONE);
  Serial.print("testInt: ");
  myEEP.get(50, testInt);
  Serial.println(testInt);
  Serial.println("Protection removed, trying to overwrite testInt again...");
  myEEP.put(50, 4321);
  myEEP.get(50, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);
  Serial.println("");

  Serial.println("Protecting status register...");
  myEEP.protectStatusRegister(true);  //protects the non-volatile bits of status register
  Serial.println("Trying to protect device...");
  myEEP.writeProtect(PROTECT_ALL);
  Serial.println("Now overwriting testInt...");
  myEEP.put(50, 1234);
  myEEP.get(50, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);
  Serial.println("Status register protection prevented changing the device protection!");
  myEEP.protectStatusRegister(false);
}
  
void loop(){}

 

Hier nun die Ausgabe:

Ausgabe von EEPROM_SPI_WE_sleep_and_protect.ino
Ausgabe von EEPROM_SPI_WE_sleep_and_protect.ino

Trotz des Aufrufes myEEP.writeProtect(PROTECT_ALL) kann testInt am Endes des Sketches überschrieben werden, da das Statusregister zuvor schreibgeschützt wurde. 

Löschfunktionen

Die größeren SPI EEPROMs haben Löschfunktionen. Im Zweifelsfall schaut ins Datenblatt, ob das für euren EEPROM zutrifft – oder probiert den nächsten Sketch aus.

  • erasePage(address) löscht die Page in der sich die Adresse „address“ befindet. Welche Adresse innerhalb der Page ihr angebt, spielt keine Rolle.
  • eraseSector(address) löscht den Sektor, in dem sich die Adresse befindet. Der 25LC512 hat beispielsweise 4 Sektoren.
  • eraseCompleteEEPROM() tut das, was der Name sagt: Der gesamte EEPROM wird gelöscht.

Was heißt eigentlich „löschen“? Irgendein Wert steht immer an einer bestimmt Adresse. Deswegen ist der Begriff etwas irreführend. Mit den hier beschriebenen Funktionen wird in jede Speicheradresse eine 255 (0xFF) geschrieben. Lest ihr einen Integerwert aus einem gelöschten Speicherbereich, dann liefert das 0xFFFF = -1.

#include <SPI.h>
#include <EEPROM_SPI_WE.h>
const int csPin = 10; // Chip select pin
const int wpPin = 9;  // Write protect pin (optional)

/* There are different options to create your EEPROM_SPI_WE object:
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin, wpPin);
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin); 
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(&SPI, csPin, wpPin);  
 * EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(&SPI, csPin);  
 * If you don't define the wpPin and you connect it to GND or VCC,  
 * then protectStatusRegister() is the only function that won't work.
 * Passing the SPI object allows you, for example, to use both SPI 
 * interfaces on the ESP32.
 */
  EEPROM_SPI_WE myEEP = EEPROM_SPI_WE(csPin, wpPin);

void setup(){
  Serial.begin(9600);
  if(myEEP.init()){
    Serial.println("EEPROM connected");
  }
  else{
    Serial.println("EEPROM does not respond");
    while(1);
  }
  /* You can change the SPI clock speed. The default of is 8 MHz */
  //myEEP.setSPIClockSpeed(4000000); // use AFTER init()!
  
  /* Select the page size of your EEPROM.
   * Choose EEPROM_PAGE_SIZE_xxx,  
   * with xxx = 16, 32, 64, 128 or 256
   */
  myEEP.setPageSize(EEPROM_PAGE_SIZE_128);

  int testInt = 42;
  myEEP.put(10, testInt);
  myEEP.get(10, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);
  Serial.println("Erasing page....");
  myEEP.erasePage(3);  // choose any address within the page you want to erase
  myEEP.get(10, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);
  Serial.println("");

  testInt = 42;
  myEEP.put(10, testInt);
  myEEP.get(10, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);
  Serial.println("Erasing sector....");
  myEEP.eraseSector(7); // choose any address within the sector you want to erase
  myEEP.get(10, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);
  Serial.println("");
  
  testInt = 42;
  myEEP.put(10, testInt);
  myEEP.get(10, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);
  Serial.println("Erasing complete EEPROM....");
  myEEP.eraseCompleteEEPROM();
  myEEP.get(10, testInt);
  Serial.print("testInt: ");
  Serial.println(testInt);

  Serial.print("testInt, MSB: ");
  Serial.println(myEEP.read(10), BIN);
  Serial.print("testInt, LSB: ");
  Serial.println(myEEP.read(11), BIN);
  Serial.println("Erasing means writing 0xFF to all addresses"); 
}
  
void loop(){}

 

Hier die Ausgabe:

Löschen von SPI EEPROMS - Ausgabe von EEPROM_SPI_WE_erase.ino
Ausgabe von EEPROM_SPI_WE_erase.ino

Referenz – SPI EEPROMs intern

Dieser Abschnitt ist für diejenigen, die besonders interessiert sind, aber keine Lust haben, das Datenblatt zu lesen.

Instruction Set

Die SPI EEPROMs verfügen über einen recht begrenzten Satz an Funktionen (Instruction):

SPI EEPROMs Instruction Set
SPI EEPROMs Instruction Set

Nicht jeder EEPROM hat alle diese Funktionen implementiert. Das gilt insbesondere für die Löschbefehle.

Statusregister

Statusregister der SPI EEPROMs
Statusregister der SPI EEPROMs

Das Statusregister ist das einzige Register des SPI EEPROMs. Das WPEN Bit (Write Protection Enable), und die beiden BP Bits (Block Protection) sind nicht volatil, d.h. die Werte bleiben auch nach einer Trennung von der Versorgungsspannung erhalten. Ich habe meine Bibliothek allerdings so eingestellt, dass bei einem Neustart alle Bits auf null gesetzt werden.

Wenn ihr den EEPROM beschreiben wollt, dann müsst ihr das WEL-Bit (Write Enable Latch) setzen. Es wird nach einem erfolgreichem Schreib- oder Löschvorgang, einem Neustart oder durch die WRDI Instruction wieder gelöscht.

Die BP Bits haben folgende Wirkung (in Kombination mit WPEN):

Wirkung der Block Protection Bits der SPI EEPROMs
Wirkung der Block Protection Bits

Schreibschutz

SPI EEPROMs: Schreibschutz für den EEPROM und dessen Statusregister
Schreibschutz für den EEPROM und dessen Statusregister

Die obige Tabelle beschreibt die Wirkung des WEL-Bits, des WPEN-Bits und des WP-Pins auf den Schreibschutz für den EEPROM und dessen Statusregister.

Danksagung

Das Variablenhandling der put()- und get()-Funktion habe ich der großartigen Sparkfun Bibliothek SparkFun_External_EEPROM_Arduino_Library für I2C EEPROMs entnommen. Ich hatte diese in meinem letzten Beitrag beschrieben.

15 thoughts on “EEPROM Teil 3 – externe SPI EEPROMs

  1. Hallo Wolfgang,

    ich habe herausbekommen, warum deine Bibliothek nicht mit dem EEPROM auf dem AVR-IoT Cellular Mini Board zusammenarbeiten wollte (siehe mein Kommentar bei dem Beitrag zur Arduino-IoT-Cloud). Die Bibliothek unterstützt keine 24-Bit-Adressen. Ich habe mal eine GitHub-Pull-Request erzeugt, den du akzeptieren müsstest.

    Beste Grüße
    Bernhard

      1. Hallo Bernhard,

        ich muss es vielleicht nicht, tue es aber gerne! Vielen lieben Dank. Wundert mich, dass es noch niemandem aufgefallen ist.

        VG, Wolfgang

    1. Kleines Update: Habe die 25LC020A bekommen und kann sowohl das Nicht-Funktionieren, wie auch den Fix (Adresse auf 1 Byte reduzieren) nachvollziehen. Es gibt nur eine kleine Komplikation bei 512 Byte EEPROMS. Ein Befehl hat bei diesen kleinen EEPROMs (25LC0xA mit x=1,2 oder 4) die Struktur: 0 0 0 0 A8 x x x A7 A6 A5 A4 A3 A2 A1 A0, wobei x x x die Instruktion (Lesen, Schreiben, Reset, etc) verschlüsselt und A0 bis A8 die Adresse. Für 512 Byte braucht man 9 Bit. Für den 256 Byte EEPROM funktioniert der „Quick-Fix“, für 512 nicht. Ich möchte die Änderung nur einmal machen und dafür richtig. Deswegen habe ich nochmal 512er bestellt. Mit anderen Worten: braucht noch ein bisschen!

      1. Wieso liefert der Reichelt bei Dir so schnell? 😉 Noch viel Spaß beim Programmieren!

        1. Reichelt hat noch einmal schnell geliefert. Ich habe die kleinen EEPROMs jetzt implementiert. Die Version 1.1.0 der Bibliothek ist jetzt auf GitHub verfügbar. In der Arduino IDE sind neue Version normalerweise innerhalb von 24 h verfügbar. Bei Verwendung der kleinen EEPROMs muss man ein setSmallEEPROM() hinzufügen. Es gibt auch einen Beispielsketch dazu.

          In der neuen Version habe ich das Handling von Strings überarbeitet. Zusätzlich zu put() und get() gibt es jetzt ein putString() und ein getString().

          Ich hoffe, bei Dir funktioniert auch alles! Viel Spaß & Erfolg!

          1. Danke, es funktioniert! Reife Leistung! Habs mit ESP32 und Arduino 2 getestet.
            Das Stringschreiben ist extrem nützlich, weil man da nicht mehr selber mit den Bytes herumschusseln muss. Aber wäre die Rückgabe der neuen Adresse nicht noch schöner?
            Man müsste dann nicht mehr selber rechnen.

            –>
            myEEP.putString(addr, string1);
            addr += string1.length() + 1;
            myEEP.putString(addr, string2);
            –>
            addr = myEEP.putString(addr, string1);
            addr = myEEP.putString(addr, string2);

            Beispiel EEPROM.h aus ESP32-Software:
            size_t writeString(int address, String value);

            1. Nette Ergänzung, stimmt. Habe ich auch noch gerade implementiert. Version: 1.1.1

              1. Diese Information aus dem Datenblatt ist auch nicht zu verachten:

                //Clock Frequency 25LC010A/25LC020A/25LC040A
                //— 5 MHz 4.5V..5.5V
                //— 3 MHz 2.5V..4.5V
                myEEP.setSPIClockSpeed(3000000); // use AFTER init()!

                1. Stimmt auch! Und ändere ich auch gerne. Vielen Dank! Bevor ich jetzt noch eine neue Version veröffentliche, werde ich aber mal warten, ob du noch etwas findest und dann alles in einem Rutsch implementieren.

                  1. Hi!
                    Ich glaube, es ist für den Anwender intuitiver, wenn der selber die Prom-Größe angibt, so wie das in der Sparkfun-Lib gemacht wird. Wer kann schon auf Anhieb sagen, was ein kleiner Prom ist? Anhand der Größe wird dann Small gesetzt und die korrekte Adressierung gemacht.

                    Sparkfun:
                    myMem.setMemorySize(512 * 1024 / 8); //In bytes. 512kbit = 64kbyte

                    WE:
                    myEEP.setSmallEEPROM();

                    1. Alles gut, aber es wäre wirklich einfacher für mich, wenn du alle Vorschläge sammeln könntest und zusammen meldest, wenn deine Arbeiten abgeschlossen sind.

  2. Hallo Herr Ewald,

    ich bin stolzer Besitzer eines 25LC020A SPI, 2k, page size=16 !!!
    Daher musste ich gleich Ihre Lib ausprobieren. Leider frisst die keine kleinen EEPROMS. Bei 2k reicht wohl eine 8 Bit-Adresse völlig aus. Bei einer 16 Bit-Adressierung schlägt das Schreiben fehl. Wenn ich folgende Zeilen ändere, dann funktioniert es.

    //_spi->transfer(static_cast(addr>>8));
    //_spi->transfer(static_cast(addr&0xFF));
    _spi->transfer(static_cast(addr));

    Ich weiß jetzt nicht, was da noch alles für Anpassungen nötig wären. Die page_size ist ja auch kleiner als üblich. Es wäre schön, wenn Sie ihre Lib auch noch für kleine, preiswerte SPI-EEPROMs erweitern könnten.

    Grüße
    R.S.

    1. Hallo, ich habe mir gerade solche EEPROMs bestellt. Ich werde dann mal schauen, wie komplex die Änderungen werden. Wenn es so einfach ist wie ich denke, dann werde ich es schnell implementieren können.
      Die Page Size 16 ist kein Problem, da schon vorgesehen. Das lässt sich einfach mit myEEP.setPageSize(EEPROM_PAGE_SIZE_16) einstellen.
      Danke für die Anregung!
      VG, Wolfgang

Schreibe einen Kommentar

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