Arduino: Multi-IO und EEPROM -[Teil 2] - AZ-Delivery

Teil 2 - EEPROM

Im ersten Teil dieser Reihe haben wir einen Eingangspin für mehrere Taster verwendet. Mit Widerständen haben wir einen Spannungsteiler gebaut, durch den wir für jeden Taster verschiedene Werte am Eingang erhalten.

Wir programmieren nun eine Funktion, die als Lernmodus dienen wird. Der Arduino lernt damit seine Taster kennen. Um aber diesen Modus nicht jedes Mal neu ausführen zu müssen, greifen wir auf den nichtflüchtigen EEPROM zurück. Die Daten in diesem Speicher bleiben erhalten, wenn der Arduino ausgeschaltet wird. Los geht's.

Benötigte Hardware 

Anzahl Bauteil Anmerkung
1 Verbindungskabel
1 Breadboard
1 Taster
1 Arduino Nano oder Uno
mehrere Widerstände 10 KOhm
PC mit Arduino IDE

Vorbereitung

Wir benötigen die Schaltung mit mehreren Tastern aus Teil 1:

EEPROM - oder "meine Daten sind nach dem Stromausfall noch da"

Der Arduino besitzt ein EEPROM (Electrically Erasable Programmable Read-Only Memory). Das ist ein kleiner Speicher, der erhalten bleibt, wenn der Mikrocontroller ausgeschaltet wird. Die Größe des Speichers ist abhängig von der verwendeten CPU. Im Fall des Arduino UNO oder NANO ist es der ATmega328P mit einer Größe von 1024 Bytes. Er kann auch nur byteweise beschrieben werden. In der Arduino IDE steht dafür die Bibliothek EEPROM.h zur Verfügung, die in den Quellcode inkludiert werden muss. Nähere Informationen findet man in der Arduino Reference.

Es könnte sich nun die Frage auftun, warum wir das für unser Programm brauchen. Abhängig von den Widerständen und der Anzahl der Taster müssen wir auf verschiedene Werte prüfen. Diese speichern wir in ein Array. Um diese Werte zu erhalten, müssten wir das Testprogramm auf den Arduino laden, die Werte ablesen, aufschreiben, das eigentliche Programm auf den Arduino laden, die Werte in das Array schreiben und eventuell Anpassungen durchführen, sollte sich die Anzahl der Taster verändern. Dann müssen die Grenzen für Schleifen etc. geändert werden.

Wir werden unser Programm so schreiben, dass das Array mit den Werten für die Taster dynamisch erzeugt und verwendet wird. Folglich brauchen wir einen Programmbereich, der nur zum "Anlernen" der Werte benutzt wird. Damit dieser ausgeführt wird, nutzen wir einen digitalen Eingang als Jumper. Wir setzen ihn nur am Anfang ein einziges Mal. Oder dann, wenn sich der Schaltungsaufbau verändert.

In diesem Programmbereich legt man fest, welche Taster in welcher Reihenfolge verwendet werden. Ich werde den Code so schreiben, dass, solange der Jumper gesetzt ist, der Lernmodus aktiv ist. Dann kann man jeden Button einmal betätigen, was durch das Aufleuchten der Onboard-LED bestätigt wird. Dann entfernt man den Jumper (oder schaltet einen Schalter um). Damit wird das normale Programm ausgeführt.

Die Werte für die jeweiligen Taster am analogen Eingang werden in das EEPROM geschrieben. Schaltet man den Arduino aus und wieder ein, ohne den Jumper zu setzen, werden die Werte aus dem EEPROM geladen und können wieder benutzt werden. Fügt man neue Taster hinzu und ändert die Widerstände, verbindet man den Jumper, betätigt wieder alle Taster in der gewünschten Reihenfolge und schon kann man das Programm wieder nutzen wie gewohnt.

Hinweis: Dabei ist natürlich trotzdem darauf zu achten, dass jeder weitere Taster mit Widerstand bedeutet, dass sich die nutzbaren Wertebereiche verkleinern. Eventuell muss dann auch die Variable RANGE verändert werden, die wir für das Abfangen der Schwankungen am analogen Eingang verwenden.

Arduino und EEPROM - Die Funktionen

Inkludiert man mit der Zeile:

#include <EEPROM.h>

die EEPROM.h Bibliothek, stehen einige Funktionen zur Verfügung, mit der man auf diesen kleinen Speicher zugreifen kann. 

Hinweis: Die Anzahl der Schreib- und Löschzyklen des EEPROM ist auf ca. 100.000 begrenzt. 

Wie kann man sich den Speicher bildlich vorstellen? Ich habe dazu folgendes Bild erstellt:

Stellen wir uns den Speicher in Feldern vor. Jedes Feld ist 8 Bit groß. 8 Bit sind 1 Byte. Die Bytes werden von 0 an aufwärts gezählt. In diesem Fall bis 1023. Das heißt, der Speicherplatz ist 1024 Bytes groß.

Jedes Feld hat eine Adresse, die durch diesen Zähler repräsentiert wird. Speichern wir nun Werte in das EEPROM, brauchen wir diese Adresse und die Größe des Datentyps. Denn ein Wert kann mehrere Felder einnehmen. Auf der rechten Seite kann man sehen, dass ein 16 Bit großer Integer-Wert zwei Felder einnimmt. Deklarieren wir uns einen eigenen Datentypen zum Beispiel mit einem
struct, kann dieser wie hier vier Felder, also 32 Bit umfassen.

Die nächste Variable ist dann wieder eine Ganzzahl mit 16 Bit. Wollen wir nun neue Werte direkt dahinter speichern, wäre das die Adresse mit dem Wert 8, denn davor wurden 8 Bytes von 0 bis 7 genutzt. Es ist also ratsam, einen Zähler zu nutzen, der die Größe aller bisher gespeicherten Datentypen enthält. So kann man auf den ersten freien Speicherplatz zugreifen.

Es gibt nun mehrere Möglichkeiten, durch die EEPROM.h Bibliothek Werte in den Speicher zu schreiben oder wieder auszulesen.

Dazu zählen die Funktionen write(addr, value) und read(addr). Nutzen wir write(addr, value), können wir nur einzelne Bytes an eine gewünschte Adresse schreiben. Die enthaltenen Werte dürfen damit nur zwischen 0 und 255 liegen. Sind meine Werte also größer als 255, muss ich deren Variable in Bytes aufsplitten, oder eine andere Lösung finden. Die Funktion read(addr) liest ebenso nur einzelne Bytes.

Die Funktionen put(addr, var) oder get(addr, var) sollten dann benutzt werden, wenn man über mehrere Speicherplätze Werte im EEPROM ablegen möchte. Sie kümmern sich automatisch darum, dass die einzelnen Bytes zusammenhängend ausgelesen werden. Ein weiterer Vorteil von put(addr, var) ist, dass der Wert nur dann gespeichert wird, wenn er sich von dem Wert unterscheidet, der sich vorher an dieser Stelle befunden hat. Dadurch können Schreibzyklen gespart und die Lebenszeit des EEPROM verlängert werden.

Der Funktion get(addr, var) übergibt man neben der Adresse eine Variable (hier var), in die der Wert gespeichert werden soll. Übergebe ich eine Variable vom Datentypen int, werden zwei Bytes (16 Bit) zusammenhängend ausgelesen.

Es gibt dann noch die Funktionen update(addr, value). Damit können ebenfalls wie mit write(addr, value) nur Werte von 0 bis 255 gespeichert werden. Allerdings wird hier auch nur dann geschrieben, wenn sich der neue Wert vom alten unterscheidet.

Wie bereits erwähnt, ist die Größe des EEPROM abhängig von der Art der CPU unterschiedlich. Möchte man nun alle Speicherplätze löschen (also auf 0 setzen), muss man mit einer Schleife über alle Adressen iterieren und an jeder dieser Adressen den Wert 0 mit write(addr, value) setzen.

Dazu gibt es auch ein Beispiel in der Arduino IDE. Möchte man sein Programm auf einen anderen Arduino portieren und sich nicht jedes Mal die Mühe machen und die Obergrenze der Schleife zum Löschen ändern, kann man die Funktion
length() benutzen. Sie gibt die Größe des Speichers zurück. Auch das ist in dem Beispiel "EEPROM clear" enthalten. 

Hinweis: Nutzt man einen ESP8266, muss man einige Dinge beachten. Diese sind auf GitHub dokumentiert. Es sind auch Beispiele dazu verfügbar.

Wann sollte man den EEPROM nutzen und wann nicht?

Die begrenzte Zahl der Speicher- und Löschzyklen wurden bereits weiter oben genannt. Daraus ergibt sich, dass man den Speicher nicht ständig nutzen sollte. Welche Daten nutzt man nicht ständig? Sensordaten aus dem Echtzeitbetrieb z.B. sollten nicht unbedingt in den EEPROM geschrieben werden. Möchte man z.B. Motoren nach dem Einschalten auf bestimmte Positionen bringen, oder bestimmte LEDs einschalten, dafür kann man das EEPROM gut nutzen. In unserem Fall sind es die Werte der Taster am analogen Eingang des Arduinos. 

Laut Arduino Reference dauert das Speichern in den EEPROM 3,3 ms. Auch das ist ein weiterer Grund, ihn nicht im Echtzeitbetrieb andauernd zu verwenden.

Werte der Taster im EEPROM speichern und auslesen

Kommen wir nun zu unserem Programm. Wir haben mehrere Taster, die durch verschiedene Werte am analogen Eingang voneinander unterschieden werden können. Diese Werte wollen wir eigentlich gar nicht kennen. Also speichern wir sie direkt in den EEPROM, und sollten wir sie brauchen, lesen wir sie wieder aus. Nämlich dann, wenn wir den Arduino einschalten. Ändern wir die elektronische Schaltung, ändern sich auch die Werte für die Taster. Wir müssen sie im EEPROM aktualisieren.

Der Wertebereich liegt zwischen 0 und 1023. Es reicht also nicht, wenn wir nur einzelne Bytes speichern (zur Erinnerung: 1 Byte = 8 Bit = Max 255). Wir sollten also den Datentypen
int nutzen und dementsprechend darauf vorbereitet sein, jeweils zwei Speicherplätze zu nutzen. Wie weiter oben beschrieben, kümmern sich die Funktionen put() und get() darum, dass die beiden 8 Bit großen Speicher zusammenhängend beschrieben bzw. ausgelesen werden.

Gehen wir davon aus, dass wir die Anzahl der Taster und deren Werte nur selten ändern. Wir müssen also den Modus für das "Anlernen" nicht ständig ausführen. Damit der Arduino erkennt, dass wir den Lernmodus ausführen möchten, setzen wir einen Jumper.

Wir nutzen einen digitalen Pin wie einen Schalter und verbinden ihn mit GND. Beachten dabei natürlich, dass wir den internen Pull-up-Widerstand im
setup() einschalten. Der Pin arbeitet dann active-low. Wir können gleich bei Programmstart an diesem Pin testen, ob der Zustand LOW ist. Dann springen wir in eine Funktion, die so lange ausgeführt wird, bis der Jumper wieder getrennt, also der Zustand an dem Pin HIGH wird. Das realisieren wir mit einer Schleife. 

Darin prüfen wir dann in jedem Durchlauf den analogen Pin, an dem unsere Taster angeschlossen sind. Wird ein Taster betätigt, wird sein Wert abgespeichert. Wir zählen dann einen Zähler hoch, mit dem wir die maximale Anzahl an Tastern festhalten. Wir gehen davon aus, dass wir nicht mehr als 255 Taster anschließen werden. Also reicht für diesen Zähler ein Byte, nämlich das nullte Byte gleich an der ersten Stelle des EEPROM.

Damit wir auch optisch sehen können, dass ein Taster erkannt wurde, nutzen wir die Onboard-LED des Arduinos. Wir lassen sie eine gewisse Zeit leuchten, bis der nächste Taster gedrückt werden kann. Wir sparen uns dadurch auch das Entprellen. Die Initialisierung der Taster ist nicht zeitkritisch, weswegen wir auf die delay()-Funktion zurückgreifen können.

Die Werte der Taster speichern wir vorerst in ein Array, bevor wir dessen Daten in den EEPROM übertragen. Damit man nicht durch mehrfaches Drücken der Taster den EEPROM unnötig füllt, achten wir darauf, ob die eingelesenen Werte bereits abgespeichert wurden. Wird ein Taster betätigt, wird der Wert mit allen anderen Werten im Array verglichen.

Dynamischer Speicher

Eine Lösung dafür sind die Funktionen malloc(), calloc() und realloc(). Damit ist es möglich, Speicher mit einer gewissen Größe zu allozieren, also zu reservieren. Mit realloc() ist es möglich, diesen Speicher zu erweitern. Wir können also jedes Mal, wenn ein weiterer Taster betätigt wurde, den Speicher vergrößern.

Quellcode

Setzen wir nun die Theorie in die Praxis um. Wir inkludieren zunächst die EEPROM.h Bibliothek:

#include <EEPROM.h>

 Dann löschen wir die Definition der Konstanten für die Anzahl der Taster:

#define BUTTONS 3

Wir definieren uns nun eine Pinnummer für unseren Jumper:

#define JUMPER 3

Außerdem entfernen wir die Zeile, mit der wir das Array mit den Werten der Taster deklariert und definiert haben:

int buttonValues[BUTTONS] = {0, 509, 680};

Stattdessen brauchen wir nun Zeiger, die wir für den dynamischen Speicher verwenden, sowie Zähler für die Anzahl der Taster und um durch den EEPROM zu iterieren:

int buttonCounter = 0;
int eepromCounter = 1;
int *pButtonValues = NULL;
int *pTemp = NULL;

Der Zähler für den EEPROM beginnt bei 1, da an der nullten Stelle der Wert des Zählers gespeichert wird. Der soll beim Iterieren nicht berücksichtigt werden.

In unserer Funktion buttonRead(), die sich ja darum kümmert, die Werte in Tasternummern zu wandeln, müssen wir den Namen des Arrays durch den Namen des Pointers ersetzen.

Vorher:

int buttonRead(){
  int i = 0;
  int input = analogRead(BUTTON_PIN);
  for (i = 0; i <= BUTTONS - 1; i++) {    
    if (input < buttonValues[i] + RANGE && input > buttonValues[i] - RANGE) {
      return i;
    }
  }
  return -1; 
}
Nachher:
int buttonRead(){
  int i = 0;
  int input = analogRead(BUTTON_PIN);
  if (pButtonValues != NULL) {
    for (i = 0; i <= buttonCounter - 1; i++) {    
      if (input < pButtonValues[i] + RANGE && input > pButtonValues[i] - RANGE) {
        return i;
      }
    }
  }
  return -1; 
}

Hinweis: Wie man sieht, wird unser allozierter Speicher genau wie ein Array behandelt. Ein Array ist nichts anderes als ein Zeiger auf das erste Element des Arrays.

Wir hätten den alten Namen des Arrays auch beibehalten können. Damit man aber erkennt, dass es sich um eine Zeigervariable handelt, setzt man ein kleines p davor. Außerdem sollte man auch prüfen, ob der Zeiger nicht NULL ist, da es sonst zu Fehlverhalten kommen kann.

Im setup() setzen wir noch den digitalen Pin, den wir als Konstante definiert haben, als Eingang samt Pull-up-Widerstand:

pinMode(JUMPER, INPUT_PULLUP);

Außerdem prüfen wir nun ebenfalls im setup(), ob der Jumper gesetzt ist, oder nicht. Wenn ja, starten wir den Lernmodus, wenn nicht, lesen wir das EEPROM aus:

if (digitalRead(JUMPER) == LOW) {
    buttonInit();
  }
  else {
    buttonCounter = EEPROM.read(0);
    Serial.print("Buttons: ");
    Serial.println(buttonCounter);
    for (int i = 0; i <= buttonCounter - 1; i++) {
      pTemp = (int*) realloc (pButtonValues, i + 1 * sizeof(int));
      pButtonValues = pTemp;
      EEPROM.get(eepromCounter, pButtonValues[i]);
      
      Serial.print(i);
      Serial.print(" ");
      Serial.print(pButtonValues[i]);
      Serial.print(" ");
      Serial.println(eepromCounter);

      eepromCounter += sizeof(int);
    }    
  }

Die Funktion für das Anlernen nennen wir buttonInit(). Sie wird aufgerufen, wenn der Zustand des Jumper-Pins LOW ist, also eingeschaltet. Im anderen Fall wird das nullte Byte des EEPROM ausgelesen. Dort enthalten ist die Anzahl der Taster. Hier sind nun noch einige Ausgaben für den seriellen Monitor vorhanden, womit wir prüfen können, welche Werte ausgelesen werden.

Als Nächstes wird mit einer Schleife und dem Zähler für die Taster als Obergrenze durch das EEPROM iteriert. In jedem Schritt wird Speicher reserviert und die Werte aus dem EEPROM in diesen Speicher kopiert. Außerdem wird der Zähler für die Adresse im EEPROM hochgezählt. Es sind zwei verschiedene Zähler, da der Zähler für die Anzahl der Taster um 1 hochgezählt wird. Der Zähler für das EEPROM aber um die Größe eines Integer Datentypen, also 2.

Kommen wir nun zur eigentlich Funktion für das Initialisieren der Taster. Wie bereits erwähnt nennen wir sie buttonInit(). Darin läuft wie gesagt eine Schleife, solange der Jumper verbunden ist. Wird er getrennt, werden die Daten ins EEPROM übertragen und das eigentliche Programm wird ausgeführt.

void buttonInit() {
  bool wasPressed = false;
  int input = 0;
  bool isThere = false;
  int i = 0;
  
  Serial.print("Alle Buttons nacheinander druecken und anschliessend den Jumper an Pin D");
  Serial.print(JUMPER);
  Serial.println(" entfernen");
  
  while(digitalRead(JUMPER) == LOW) {
    input = analogRead(BUTTON_PIN);
    if ((input < (1023 - RANGE)) && !wasPressed) {
      wasPressed = true;
      if (pTemp != NULL) {
        for (i = 0; i <= buttonCounter - 1; i++) {
          if (input < pButtonValues[i] + RANGE && input > pButtonValues[i] - RANGE) {
            isThere = true;
            break;
          }
        }
      }
      
      if (!isThere) {
        pTemp = (int*) realloc (pButtonValues, buttonCounter + 1 * sizeof(int));
        pButtonValues = pTemp;
        pButtonValues[buttonCounter] = input;
        buttonCounter++;
        digitalWrite(LED_BUILTIN, HIGH);
        delay(1000);
        digitalWrite(LED_BUILTIN, LOW);
      }
      else {
        isThere = false;
      }
    }
    
    else if (((analogRead(BUTTON_PIN)) > (1023 - RANGE)) && wasPressed) {
      wasPressed = false;
    }
    delay(100);
  }

  // Werte ins EEPROM uebertragen
  EEPROM.write(0, buttonCounter);
  for (i = 0; i <= buttonCounter - 1; i++) {
    Serial.print(i);
    Serial.print(" ");
    Serial.print(pButtonValues[i]);
    Serial.print(" ");
    Serial.println(eepromCounter);

    EEPROM.put(eepromCounter, pButtonValues[i]);
    eepromCounter += sizeof(int);
  }  
}

Gehen wir Schritt für Schritt durch den Quellcode. Wir deklarieren einige lokale Variablen. Dann wird die Schleife ausgeführt, solange der Jumper-Pin LOW ist. Dann lesen wir den analogen Eingang unserer Taster. Standardwert ist 1023. Ändert sich dieser Wert unterhalb unserer Grenze RANGE subtrahiert von 1023, wurde ein Taster betätigt.

Wir setzen dann die Variable wasPressed auf true, damit das Gedrückthalten des Tasters keine Auswirkungen hat. Sonst würde der Taster immer wieder neu initialisiert werden. Dann iterieren wir über die bisher eingelesenen Tasterwerte, um auszuschließen, dass der Taster bereits vorhanden ist und erneut gespeichert wird. Das dürfen wir natürlich nur, wenn der Zeiger, der auf das Array zeigt, nicht
NULL ist. Ist der Wert nicht vorhanden, wird der Speicher des Arrays (re-)alloziert (vergrößert) und der aktuelle Tasterwert hineingeschrieben.

Der Zähler für die Taster wird hochgezählt und die LED für eine kurze Zeit ein- und dann wieder ausgeschaltet. Danach wird dann noch das Loslassen des Tasters berücksichtigt, womit das erneute Drücken eines Tasters vorbereitet wird.

Entfernt man den Jumper, bzw. Schaltet den Schalter am Jumper-Pin aus (auf HIGH), wird die Schleife verlassen. Dann wird zuerst der Zähler für die Taster in den nullten Speicherplatz des EEPROM übertragen. Anschließend wird mit diesem Zähler als Obergrenze durch das Array mit den Werten der Taster iteriert. Diese werden Feld für Feld in das EEPROM gespeichert. Anschließend wird die Funktion verlassen.

Aufgerufen wurde sie im setup(). Das heißt, dass danach die loop() ausgeführt wird. Dort haben wir mit den Tastern den Zustand der LED verändert. Wir können nun den Jumper-Pin verbinden und den Arduino neu starten. Initialisieren wir nur zwei Taster, lösen die Verbindung des Jumper-Pins und starten den Arduino nochmal neu, sollte der dritte Taster ohne Funktion sein und wir können die LED nicht mehr blinken lassen.

Sie können nun mit den Widerständen rumprobieren, ohne deren Werte ständig notieren und in das Array eintragen zu müssen. Außerdem können sie die Reihenfolge der Taster verändern. Probieren Sie es aus. Sie sollten trotzdem ein Auge auf den seriellen Monitor werfen. Dort werden die Zähler und die Tasterwerte ausgegeben.

Vorschau

Es ist nun unter anderem möglich, einen weiteren Taster hinzuzufügen. Wir können dann in die switch-case-Anweisung einen weiteren Zustand einfügen, der zum Beispiel noch weitere LEDs zum Leuchten bringt. Im nächsten Teil zeige ich nämlich, wie wir mehrere LEDs ansteuern und dabei so wenig Pins wie möglich verwenden. Ich verrate soviel: Mit drei Pins kann ich ein LED-Lauflicht bestehend aus sechs LEDs programmieren, die an ein bekanntes sprechendes Auto erinnern. Bis dahin.

Andreas Wolter

für AZ-Delivery Blog

Quellcode

Hier noch der komplette Quellcode des Programms:

#include <EEPROM.h>

#define BUTTON_PIN A1
#define RANGE 50
#define JUMPER 3

// State Machine
int state = 0;

// LED
const int ledPin = LED_BUILTIN;
bool ledState = LOW;
unsigned long previousMillis = 0;
const unsigned long interval = 500;
unsigned long currentMillis = 0;

// Entprell Timer
unsigned long prell_delay = 200;
unsigned long alte_zeit = 0;

// Input
int input = 0;
int input_alt = 0;

// Button
int buttonCounter = 0;
int eepromCounter = 1;
int *pButtonValues = NULL;
int *pTemp = NULL;

int buttonRead(){
  int i = 0;
  int input = analogRead(BUTTON_PIN);
  if (pButtonValues != NULL) {
    for (i = 0; i <= buttonCounter - 1; i++) {    
      if (input < pButtonValues[i] + RANGE && input > pButtonValues[i] - RANGE) {
        return i;
      }
    }
  }
  return -1; 
}

void buttonInit() {
  bool wasPressed = false;
  int input = 0;
  bool isThere = false;
  int i = 0;
  
  Serial.print("Alle Buttons nacheinander druecken und anschliessend den Jumper an Pin D");
  Serial.print(JUMPER);
  Serial.println(" entfernen");
  
  while(digitalRead(JUMPER) == LOW) {
    input = analogRead(BUTTON_PIN);
    if ((input < (1023 - RANGE)) && !wasPressed) {
      wasPressed = true;
      if (pTemp != NULL) {
        for (i = 0; i <= buttonCounter - 1; i++) {
          if (input < pButtonValues[i] + RANGE && input > pButtonValues[i] - RANGE) {
            isThere = true;
            break;
          }
        }
      }
      
      if (!isThere) {
        pTemp = (int*) realloc (pButtonValues, buttonCounter + 1 * sizeof(int));
        pButtonValues = pTemp;
        pButtonValues[buttonCounter] = input;
        buttonCounter++;
        digitalWrite(LED_BUILTIN, HIGH);
        delay(1000);
        digitalWrite(LED_BUILTIN, LOW);
      }
      else {
        isThere = false;
      }
    }
    
    else if (((analogRead(BUTTON_PIN)) > (1023 - RANGE)) && wasPressed) {
      wasPressed = false;
    }
    delay(100);
  }

  // Werte ins EEPROM uebertragen
  EEPROM.write(0, buttonCounter);
  for (i = 0; i <= buttonCounter - 1; i++) {
    Serial.print(i);
    Serial.print(" ");
    Serial.print(pButtonValues[i]);
    Serial.print(" ");
    Serial.println(eepromCounter);

    EEPROM.put(eepromCounter, pButtonValues[i]);
    eepromCounter += sizeof(int);
  }  
}

void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);
  pinMode(JUMPER, INPUT_PULLUP);
}

void loop() {
  input = buttonRead();

  // Entprellen
  // wenn Taster jetzt AN und vorher AUS und aktuelle Zeit - alte Zeit groesser, als vorgegebenes Delay
  if (input > -1 && input_alt != input && millis() - alte_zeit > prell_delay) {   
    state = input;
    Serial.println(state);
    alte_zeit = millis();
  }
  input_alt = input;

  switch (state) {
    case 0: {
      if (ledState != LOW) {
        ledState = LOW;
        digitalWrite(ledPin, ledState);
      }
    } break;
    case 1: {
      if (ledState != HIGH) {
        ledState = HIGH;
        digitalWrite(ledPin, ledState);
      }
    } break;
    case 2: {
      currentMillis = millis();
      if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        ledState = !ledState;
        digitalWrite(ledPin, ledState);
      }
    } break;
    default: break;
  }
}

 

Für arduinoProjekte für anfänger

7 comments

Andreas Wolter

Andreas Wolter

Vielen Dank für die Blumen. Freut mich, wenn es Ihnen gefällt und ich Sie inspirieren kann.

@Stephan: das ist korrekt. Die externen FRAMs haben mehr Schreibzyklen. Es gibt diese Komponenten als I2C und SPI Ausführung. Alternativ kann man auch externe EEPROMs verwenden. @Jonathan entschuldigen Sie die späte Antwort. Mir ist bei der Suche auf die Antwort Ihrer Frage aufgefallen, dass ich einen Fehler gemacht habe. Die Zeile: pTemp = (int*) realloc (pButtonValues, buttonCounter + 1 * sizeof(int));

müsste eigentlich heißen:
pTemp = (int*) realloc (pButtonValues, (buttonCounter + 1) * sizeof(int));
Da fehlt ein Satz Klammern, denn Punktrechnung geht vor Strichrechnung. Ich muss prüfen, warum es trotzdem funktioniert. Was realloc angeht, verweise ich mal auf die C++ Referenz (auch wenn das C ist):
http://www.cplusplus.com/reference/cstdlib/realloc/
Die Funktion gibt void* zurück. Das heißt, man kann per Typcasting den Rückgabetypen an seine Wünsche anpassen. In meinem Fall ist das ein int*. Also muss die Rückgabe auch nach int* gecastet werden. Damit hat man einen Zeiger auf den neuen Speicherplatz bestehend aus int-Daten. Der erste Parameter ist ein Zeiger auf die alten Daten. Der zweite Parameter ist die neue zu reservierende Speichergröße. Wenn wir also bisher zwei int-Werte gespeichert haben, müssen wir nun z.B. 3 * sizeof(int) [drei mal sizeof(int), hier ist der Stern kein Dereferenzierer, also quasi kein Pointer, sondern ein Multiplikationsoperator] benutzen. Das Ergebnis speichert man in einen temporären Zeiger. Danach gibt man dem alten Zeiger den neuen erweiterten Speicherbereich und anschließend kann man das neu hinzugefügte Feld mit einem Wert füllen. Nutzt man jetzt für die Größe des zu erweiternden Speichers eine Zählvariable (also statt 3 sowas wie counter), dann muss man ein wenig aufpassen. Ist der counter zu Beginn 0 und man reserviert counter * sizeof(int), dann reserviert man 0. Das geht natürlich nicht. Im Beispiel der C++ Referenz wird der counter vorher hochgezählt. Er ist zur Zeit der ersten Reservierung auf jeden Fall größer als 0. Dafür muss man in einem der nächsten Schritte bei der Zuweisung in das Array 1 vom counter abziehen. Denn das erste Speicherfeld eines Arrays ist nicht arrayName1, sondern arrayName0. An der Stelle ist dort im Programm der Zähler schon auf 1. Um aber die nullte Stelle zu beschreiben, muss man arrayName[counter – 1] schreiben. Ich habe das anders gemacht (warum weiß ich nicht mehr genau). Mein counter ist zu Beginn 0, aber während der Reservierung wollte ich mit counter + 1 an dieser Stelle den Zähler erhöhen, damit dort beim ersten Durchlauf 1 * sizeof(int) steht. Ich habe nur leider die Klammern vergessen (Punktrechnung vor Strichrechnung. So konnte ich in der späteren Zuweisung arrayName[counter] ohne -1 schreiben, denn der Zähler ist beim ersten durchlauf 0, da er da noch nicht hochgezählt wurde. Wenn ich die Durchläufe einzeln durchgehe, wäre der reservierte Speicher: Durchlauf 1: 0 + 1 * 2 also 2 (was noch passt) Durchlauf 2: 1 + 1 * 2 also 3 Durchlauf 3: 2 + 1 * 2 also 4 Durchlauf 4: 3 + 1 * 2 also 5 Wenn man vier Buttons initialisiert, hat man nur 5 bytes reserviert, es müssten aber 8 bytes sein. mit Klammer wäre das nämlich: Durchlauf 1: (0 + 1) * 2 also 2 Durchlauf 2: (1 + 1) * 2 also 4 Durchlauf 3: (2 + 1) * 2 also 6 Durchlauf 4: (3 + 1) * 2 also 8 Es dürfte also nicht funktionieren, sobald die Werte am analogen Eingang 255 überschreiten. Ich werde den Quellcode korrigieren. Zur Frage, wie man andere Datentypen verwendet. Zweidimensionale Arrays sollten mit int** funktionieren. String wird schwierig, da String eine C++ Klasse ist und realloc ist C, das keine String Klassen kennt. Man muss dann auf char * zurückgreifen, was in C quasi Strings sind. Allerdings ist Arduino C++ und da muss man bei den Zuweisungen aufpassen. Es ist etwas kompliziert. Der Datentyp long (4 Byte) sollte mit long* statt int* funktionieren.

Grüße, Andreas Wolter

Jonathan

Jonathan

Hallo Herr Wolter,
Erst einmal ein großes Lob ! Toller Artikel, Sauberer Code, prima ! Besonders der Teil mit den Pointern und der Allokieren von Speicher fand ich sehr interessant. Leider kommt das Thema Pointer etwas kurz, weil ja das Haupt Thema EEPROM behandelt werden soll. Ich stolpere etwas über die Zeile: pTemp = (int*) realloc (pButtonValues, buttonCounter + 1 * sizeof(int)); Wie nutze ich Pointer und Memory Allokation im Zusammenhang mit Strings oder longints ? oder zweidimensionalen Arrays ? Wäre es möglich in einem Folgeblog von Ihnen etwas mehr von der Verwendung von Pointern und Adressreservierung mithilfe von malloc und Printern zu erfahren ? Besonders die praktischen Anwendungen währen interessant Vielen Dank

Rolf Krüger

Rolf Krüger

Hallo Herr Wolter,
ein interessanter Artikel und ein sauberer Programmcode.
Mein Respekt.
Herzliche Grüße
Rolf Krüger

Stephan

Stephan

Hallo zusammen,
interessanter bei häufigen Änderungen ist F-RAM.
Beispiel: Cypress FM24C64B (ca. 2,50 Euro)
■ 64-Kbit ferroelectric random access memory (F-RAM) logically
organized as 8 K × 8
❐ High-endurance 100 trillion (10^14) read/writes
❐ 151-year data retention
❐ NoDelay™ writes

Andreas Wolter

Andreas Wolter

@Thomas: ja durch Enums kann man es noch etwas lesbarer gestalten.

@FJH: es ist möglich, externe EEPROMs zu verwenden, die dann natürlich einfacher auszutauschen sind. Es kommt hier, wie so oft, auf den Anwendungsfall an.

In diesem Beispiel wird das EEPROM für den Lernmodus der Taster verwendet. Wenn man für ein Projekt die Anzahl der Taster nur sehr selten ändert, sollte das kein Problem darstellen.

Wird der Speicher für eine andere Anwendung wirklich oft überschrieben, sollte man auf eine andere Möglichkeit ausweichen. Wie die von Ihnen vorgeschlagene Variante des externen EEPROMs. Mit 100.000 Schreibvorgängen pro Sekunde hätte man das Maximum in ca. 27 Stunden erreicht.
Man kann also sagen: Habe ich ein Projekt, in dem ich ständig und andauernd Daten speichern muss, sollte ich nicht auf den integrierten nichtflüchtigen Speicher zurückgreifen.
Man nutzt ihn daher nur für Anwendungen wie Initialisierung oder Profildaten, die sich nicht oft ändern.

Thomas

Thomas

Vielen Dank für dieses sehr informative Tutorial und das gelungene Beispiel, das auch ein bisschen komplexer ist und zeigt wie man aus einer speziellen Problemstellung auch eine allgemeine, leicht erweiterbare Lösung entwickeln kann. Respekt!
Als Sahnehäubchen könnte man die Indices, die buttonRead() zurückliefert mit einem enum definieren und diese enum-Definitionen in den case-Anweisungen verwenden, um die magic nunbers -1, 0, 1, 2 zu vermeiden.

FJH

FJH

Guten Tag,
Wenn ein EEPROM nur Ca 100 000 mal beschrieben werden kann, könnte man dann nicht besser ein EEPROM extern vom AT Prozessor installieren und dieses beschreiben? Dann wäre ein Austausch möglich ohne den Prozessor zu ersetzen.
Gruß
F.-J.H.

Leave a comment

All comments are moderated before being published

Recommended blog posts

  1. ESP32 jetzt über den Boardverwalter installieren - AZ-Delivery
  2. Internet-Radio mit dem ESP32 - UPDATE - AZ-Delivery
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1 - AZ-Delivery
  4. ESP32 - das Multitalent - AZ-Delivery