vendredi 10 janvier 2020

Utiliser une mémoire EEPROM I2C (Arduino)


Après avoir exploré l'utilisation de la mémoire EEPROM interne de l'Arduino,  voyons maintenant comment utiliser une mémoire EEPROM externe communiquant en I2C .



Je rappelle que le sigle EEPROM signifie "Electrically-Erasable Programmable Read-Only Memory". Il s'agit d'une mémoire non-volatile: les informations persistent après que l'EEPROM ait été mis hors-tension.

Une mémoire EEPROM externe: pourquoi?

Dans le dernier article, nous avons mentionné quelques utilisations pertinentes d'une mémoire EEPROM, comme sauvegarder les préférences d'un utilisateur ou les paramètres de calibration d'un capteur. On peut toutefois se demander à quoi pourrait bien servir une mémoire EEPROM externe alors que l'Arduino dispose déjà de sa propre mémoire EEPROM interne.

Quelques possibilités:
  • ajouter de la mémoire supplémentaire, si l'espace disponible dans l'EEPROM interne est insuffisant.
  • augmenter la durée de vie de l'EEPROM (la fiche technique des circuits intégrés que j'ai testés pour cet article garantit une durée de vie de l'ordre du million de réécritures, alors que celle de  l'EEPROM interne de l'Arduino est plutôt de 100 000 réécritures).
  • utilisation de l'EEPROM externe comme une clé de sécurité amovible qui serait nécessaire pour faire fonctionner l'Arduino, ou qui donnerait à son utilisateur des privilèges d'administrateur.
Les modèles d'EEPROM I2C testés

Pour les besoins de cet article, j'ai fait l'essai de 3 mémoires EEPROM  I2C:
Avec un peu de chance, les informations que vous trouverez ici vous serons également utiles pour d'autres modèles d'EEPROM  I2C.

Il est important de mentionner que la capacité des mémoires EEPROM I2C externes est donnée en kilobits et non en kilooctets. Ainsi, une mémoire EEPROM de 1K comporte 1024 bits (128 octets) et contient donc 8 fois moins d'information que la mémoire EEPROM interne de l'Arduino qui, elle, est de 1 kilooctet!

Connexions de l'EEPROM à l'Arduino

Les EEPROM I2C prennent la forme d'un circuit intégré de 8 broches.



Les 3 modèles de mémoire EEPROM que j'ai testés se branchent à l'Arduino de la même façon:


  • Les broches 1, 2 et 3 du CI ne sont pas branchées. Pour le modèle 24C64A, on peut relier certaines d'entre elles à 5 V pour modifier l'adresse I2C. Pour les deux autres modèles, ces 3 broches ne servent à rien!
  • La broche 4 du CI doit être branchée à la masse (GND).
  • La broche 5 du CI est responsable de l'échange des données (SDA). On la branche à la broche A4 de l'Arduino Uno, ainsi qu'à 5 V par l'entremise d'une résistance de tirage de quelques kiloohms (j'ai utilisé 4,7 kΩ).
  • La broche 6 du CI reçoit le signal d'horloge (SCL). On la branche à la broche A5 de l'Arduino Uno, ainsi qu'à 5 V par l'entremise d'une résistance de tirage de quelques kiloohms (j'ai utilisé 4,7  kΩ).
  • La broche 7 du CI (WP) doit être reliée à la masse (GND) pour qu'il soit possible d'écrire dans l'EEPROM. Si vous désirez ensuite empêcher que l'information de l'EEPROM puisse être modifiée, vous pouvez brancher cette broche à 5 V.
  • La broche 8 du CI doit être reliée à 5 V.

Exemples de sketch

Même si mes 3 modèles d'EEPROM se branchent exactement de la même façon et qu'ils ont la même adresse I2C (0x50), j'ai dû écrire un sketch légèrement différent pour chaque modèle!

Dans chaque cas, j'ai écrit un premier sketch  qui écrit la valeur "100" à l'adresse numéro 5 de l'EEPROM, et un deuxième sketch qui lit la valeur enregistrée à chaque adresse de l'EEPROM.

Si l'EEPROM n'a jamais été utilisé auparavant, l'exécution du sketch de lecture affichera la valeur maximale de 255 pour chaque adresse:


Le sketch d'écriture enregistre la valeur "100" à l'adresse 5 de l'EEPROM.


On pourra ensuite exécuter à nouveau le sketch de lecture pour vérifier que l'adresse 5 contient maintenant la valeur 100:


Dans tous les cas, j'ai utilisé la bibliothèque Wire intégrée à l'IDE Arduino pour gérer la communication I2C.

Exemples de sketch pour le 24LC01B

Le 24LC01B (fiche technique) comporte 128 octets numérotés de 0 à 127. Chacun de ces 128 emplacements de mémoire peut contenir une valeur située entre 0 et 255.

Le sketch ci-dessous écrit la valeur "100" à l'adresse "5" de l'EEPROM. Cette écriture ne s'exécute qu'une seule fois, au démarrage du programme. L'essentiel du travail s'effectue dans la routine writebyte():

  • On débute la transmission en indiquant l'adresse I2C du 24LC01B:  Wire.beginTransmission(0x50);
  • On envoie au 24LC01B l'adresse de l'octet dont on désire modifier la valeur: Wire.write(5);
  • On envoie au 24LC01B la valeur qu'on désire enregistrer à cette adresse: Wire.write(100);
  • On clôt la transmission: Wire.endTransmission();

---
/*
Écriture d'une valeur dans une mémoire EEPROM 24LC01B
*/
#include <Wire.h> // bibliothèque i2c
#define adresse_EEPROM 0x50 //Addresse i2c de l'EEPROM
void setup(void)
{
// on veut écrire la valeur 100 à l'adresse 5
int adresse = 5; // max 127!
int valeur = 100; // max 255!
Serial.begin(9600);
Wire.begin();
delay (100);
writebyte(adresse_EEPROM, adresse, valeur);
// on avise l'utilisateur par le moniteur série:
Serial.print("La valeur ");
Serial.print(valeur);
Serial.print(" a été enregistrée en mémoire à l'adresse ");
Serial.println(adresse);
}
void loop() {
}
void writebyte(int adressei2c, unsigned int adresseMem, byte data )
{
Wire.beginTransmission(adressei2c); //adresse I2C de l'EEPROM
Wire.write(adresseMem); // numéro de l'octet à modifier
Wire.write(data); // nouvelle valeur que doit prendre l'octet
Wire.endTransmission(); // fin de la transmission I2C
delay(5);
}
---

Le sketch ci-dessous affiche dans le moniteur série la valeur enregistrée à chaque adresse de l'EEPROM. L'essentiel du travail s'effectue dans la routine readbyte():
  • On débute la transmission en indiquant l'adresse I2C du 24LC01B:  Wire.beginTransmission(0x50);
  • On envoie au 24LC01B l'adresse de l'octet dont on désire lire la valeur: Wire.write(5);
  • On clôt la transmission: Wire.endTransmission();
  • On demande au 24LC01B de nous envoyer l'information: Wire.requestFrom(0x50, 1);
  • On récupère l'information reçue: if (Wire.available()) {lecture = Wire.read();}
---
/*
Lecture de tout le contenu d'une mémoire EEPROM 24LC01B
*/
#include <Wire.h> // bibliothèque i2c
#define adresse_EEPROM 0x50 //Addresse i2c de l'EEPROM
#define adresseMax 128 // taille de l'EEPROM (nombre d'octets)
int adresse = 0;
void setup(void)
{
Serial.begin(9600);
Wire.begin();
delay(100);
Serial.println("Lecture des valeurs enregistrees sur l'EEPROM:");
for (int i = 0; i < adresseMax; i++) {
Serial.print("Adresse: ");
Serial.print(i);
Serial.print("\t");
Serial.print("Valeur: ");
Serial.println(readbyte(adresse_EEPROM, i), DEC);
}
}
void loop() {
}
byte readbyte(int adressei2c, unsigned int adresseMem )
{
byte lecture = 0;
Wire.beginTransmission(adressei2c); // adresse i2c du 24LC01B
Wire.write(adresseMem); // numéro de l'octet qu'on veut lire
Wire.endTransmission();
Wire.requestFrom(adressei2c, 1); // on demande au 24LC01B d'envoyer de l'info
delay(5);
if (Wire.available()) { // réception de l'info
lecture = Wire.read();
}
return lecture;
}
---


Exemples de sketch pour le 24LC16B

Le 24LC16B comporte 8 blocs de 256 octets (donc un total de 2048 octets). Puisque 11 bits sont nécessaires pour exprimer les 2048 adresses, 3 de ces bits (ceux de poids fort) doivent être ajoutés à la fin de l'adresse I2C .

Nous devons donc faire appel aux opérations bit à bit...

Plutôt que simplement écrire Wire.beginTransmission(adressei2c), nous écrivons:

Wire.beginTransmission((int)(adressei2c | adresseMem >> 8));

L'expression "adressei2c | adresseMem >> 8" ajoute les bits de poids fort de l'adresse mémoire à la fin de l'adresse  I2C du 24LC16B.

Et plutôt que simplement écrire Wire.write(adresseMem), nous écrivons:

Wire.write((int)(adresseMem & 0b11111111));

L'expression "adresseMem & 0b11111111" ne conserve que les 8 bits de poids faible de l'adresse mémoire (ceux de poids forts ayant déjà été transmis en même temps que l'adresse  I2C.

Voici donc le sketch qui écrit la valeur 100 à l'adresse 5 du 24LC16B:

---
/*
Écriture d'une valeur dans une mémoire EEPROM 24LC16B
*/
#include <Wire.h> // bibliothèque i2c
#define adresse_EEPROM 0x50 //Addresse i2c de l'EEPROM
void setup(void)
{
// on veut écrire la valeur 100 à l'adresse 5
int adresse = 5; // max 2047
int valeur = 100; // max 255
Serial.begin(9600);
Wire.begin();
delay (100);
writebyte(adresse_EEPROM, adresse, valeur);
Serial.print("La valeur ");
Serial.print(valeur);
Serial.print(" a été enregistrée en mémoire à l'adresse ");
Serial.println(adresse);
}
void loop() {
}
void writebyte(int adressei2c, unsigned int adresseMem, byte data )
{
// adresse i2c + bits de poids fort de l'adresse mémoire:
Wire.beginTransmission((int)(adressei2c | adresseMem >> 8));
// bits de poids faible de l'adresse mémoire:
Wire.write((int)(adresseMem & 0b11111111));
Wire.write(data);
Wire.endTransmission();
delay(5);
}
---

...et voici le sketch qui lit chacune des 2048 valeurs enregistrées dans le 24LC16B:

---
/*
Lecture de tout le contenu d'une mémoire EEPROM 24LC16B
*/
#include <Wire.h> // bibliothèque i2c
#define adresse_EEPROM 0x50 //Addresse i2c de l'EEPROM
#define adresseMax 2048 // taille de l'EEPROM
int adresse = 0;
void setup(void)
{
Serial.begin(9600);
Wire.begin();
delay(100);
Serial.println("Lecture des valeurs enregistrees sur l'EEPROM:");
for (int i = 0; i < adresseMax; i++) {
Serial.print("Adresse: ");
Serial.print(i);
Serial.print("\t");
Serial.print("Valeur: ");
Serial.println(readbyte(adresse_EEPROM, i), DEC);
}
}
void loop() {
}
byte readbyte(int adressei2c, unsigned int adresseMem )
{
byte lecture = 0;
// adresse i2c + bits de poids fort de l'adresse mémoire:
Wire.beginTransmission((int)(adressei2c | adresseMem >> 8));
// bits de poids faible de l'adresse mémoire:
Wire.write((int)((adresseMem & 0b11111111)));
Wire.endTransmission();
// adresse i2c + bits de poids fort de l'adresse mémoire:
Wire.requestFrom((int)(adressei2c | adresseMem >> 8), 1);
delay(5);
if (Wire.available()) {
lecture = Wire.read();
}
return lecture;
}
---

Exemples de sketch pour le 24C64A

Le 24C64A comporte 8192 octets, et deux octets sont nécessaires pour écrire une adresse. Plutôt qu'envoyer une partie de l'adresse mémoire en même temps que l'adresse I2C , on sépare les 16 bits de l'adresse mémoire en deux octets:

Wire.write((int)(adresseMem >> 8));   // bits de poids fort de l'adresse mémoire
Wire.write((int)(adresseMem & 0xFF)); // bits de poids faible de l'adresse mémoire

Voici le sketch qui écrit la valeur 100 à l'adresse 5 du 24C64A:

---
/*
Écriture d'une valeur dans une mémoire EEPROM 24C64A
*/
#include <Wire.h> // bibliothèque i2c
#define adresse_EEPROM 0x50 //Addresse i2c de l'EEPROM
void setup(void)
{
// on veut écrire la valeur 100 à l'adresse 5
int adresse = 5; // max: 8191
int valeur = 100; // max: 255
Serial.begin(9600);
Wire.begin();
delay (100);
writebyte(adresse_EEPROM, adresse, valeur);
Serial.print("La valeur ");
Serial.print(valeur);
Serial.print(" a été enregistrée en mémoire à l'adresse ");
Serial.println(adresse);
}
void loop() {
}
void writebyte(int adressei2c, unsigned int adresseMem, byte data )
{
Wire.beginTransmission((int)(adressei2c)); // adresse i2c du 24C64A
Wire.write((int)(adresseMem >> 8)); // bits de poids fort de l'adresse mémoire
Wire.write((int)(adresseMem & 0xFF)); // bits de poids faible de l'adresse mémoire
Wire.write(data); // valeur qu'on désire enregistrer à cette adresse
Wire.endTransmission();
delay(5);
}
---

...et voici celui qui lit chacune des 8192 valeurs enregistrées dans le 24C64A:

---
/*
Lecture de tout le contenu d'une mémoire EEPROM 24C64A
*/
#include <Wire.h> // bibliothèque i2c
#define adresse_EEPROM 0x50 //Addresse i2c de l'EEPROM
#define adresseMax 8192 // taille de l'EEPROM
int adresse = 0;
void setup(void)
{
Serial.begin(9600);
Wire.begin();
delay(100);
Serial.println("Lecture des valeurs enregistrees sur l'EEPROM:");
for (int i = 0; i < adresseMax; i++) {
Serial.print("Adresse: ");
Serial.print(i);
Serial.print("\t");
Serial.print("Valeur: ");
Serial.println(readbyte(adresse_EEPROM, i), DEC);
}
}
void loop() {
}
byte readbyte(int adressei2c, unsigned int adresseMem )
{
byte lecture = 0;
Wire.beginTransmission((int)(adressei2c));
Wire.write((int)(adresseMem >> 8)); // bits de poids fort de l'adresse mémoire
Wire.write((int)(adresseMem & 0xFF)); // bits de poids faible de l'adresse mémoire
Wire.endTransmission();
Wire.requestFrom((int)(adressei2c), 1);
delay(5);
if (Wire.available()) {
lecture = Wire.read();
}
return lecture;
}
---


Yves Pelletier (TwitterFacebook)


2 commentaires:

  1. merci pour cette publication très explicite et simple pour la compréhension, c'est un model pour celui qui veut comprendre les mémoires 24c...

    RépondreSupprimer
  2. Merci beaucoup pour ces explications, c'est ce que j'avais besoin pour programmer ma première Eeprom 24C16 !

    RépondreSupprimer