La mémoire EERAM 47L04/47C04/47L16/47C16

Posted on jeu. 12 septembre 2024 in Read The Datasheet

Rappel des objectifs de la sérieApprendre à lire et exploiter les fiches techniques des composants électroniques une étape après l’autre, en s’appuyant sur des exemples concrets

Prérequis Pour ce post, il est supposé que vous disposez de compétences de programmation « basiques » en C ou en C++. Si ce n’est pas le cas, je vous invite à lire le cours ZDS sur la programmation en C ou sur la programmation en C++.

Introduction

Les circuits EERAM 47L04/47C04/47L16/47C16 sont des puces de mémoire SRAM avec une petite particularité : en cas d’interruption de l’alimentation principale, les données peuvent être sauvegardées vers une seconde mémoire, EEPROM cette fois-ci.

En faisant cela, Microchip essaie de concilier les avantages des deux mondes.

  • Les mémoires SRAM classiques ont besoin d’être constamment alimentées pour maintenir les données en mémoire. Ici, en cas de perte d’alimentation, les données sont sauvegardées dans une mémoire EEPROM, et sont restaurées au retour de l’alimentation.
  • Les mémoires EEPROM ont des temps d’opération relativement lents, et un nombre de cycles d’écriture limité avant usure du composant. Les SRAM n’ont pas ces problèmes. Ici, on peut donc bénéficier de la vitesse d’exécution et de l’infinité d’écritures possibles en mémoire vive, et les seules opérations sur l’EEPROM auront lieu en cas de coupure d’alimentation.

Caractéristiques principales

Ces informations sont extraites de la fiche technique (PDF) de la puce.

  • Communication par le biais du bus I2C, avec 2×4 adresses disponibles : 0x50/0x52/0x54/0x56 pour les opérations de lecture/écriture sur la SRAM, et 0x18/0x1A/0x1C/0x1E pour les opérations affectant les registres de contrôle.
  • Stockage automatique vers l’EEPROM à la perte d’alimentation (en utilisant un condensateur externe qui joue le rôle de « tampon » énergétique) et rappel automatique des données au redémarrage.
  • Consommation : 200 µA (typ.) en activité, 40 μA (max.) en standby.
  • Disponible en packages TSSOP, SOIC et — surtout — PDIP-8. Ce dernier est, il faut bien le reconnaître, plus facile et plus flexible à utiliser pour le commun des makers (on peut notamment l’installer directement sur une breadboard).

Parcours de la datasheet : Electrical Characteristics et Pinout

Parmi les premiers éléments à chercher dans la datasheet, il y a le pinout — ou brochage (littéralement, quelle fonction est associée à chaque broche), et les Electrical characteristics (quelle tension d’alimentation, notamment).

Pour le brochage, la dernière figure de la page 2 nous donne les informations que l’on cherche. Ici, les 3 packages disponibles ont la même répartition.

*Pinout* des mémoires 47XXXX (source datasheet)

Le rôle des différentes broches est décrit plus en détail dans la section 3 « Pin descriptions » (page 25). On peut y lire que VCC correspond à l’alimentation électrique et VSS au ground. VCAP permet la connexion d’un condensateur pour l’utilisation de la fonction de sauvegarde automatique.

SCL et SDA sont des noms relativement standards pour les deux lignes du bus I2C, et leur nomenclature correspond par ailleurs à celles que l’on peut trouver sur les headers d’un Arduino UNO.

A1 et A2 sont des pins permettant de définir une adresse alternative pour la puce, suivant qu’ils soient à l’état haut (reliés à VCC) ou bas (reliés à VSS). S’ils ne sont pas connectés à quoi que ce soit, ils sont connectés — à l’intérieur de la puce — à VSS.

Enfin, le pin 7 HS est décrit comme étant un pin de forçage de sauvegarde : s’il est amené à l’état haut sous certaines conditions, la mémoire SRAM sera sauvegardée. S’il n’est pas connecté, il sera mis au niveau de VSS.

mise en route d’une 47C16 avec Arduino

Communiquer avec la puce

Les puces 47XXX communiquent avec le microcontrôleur via le bus I2C. Pour interagir avec, nous allons devoir utiliser la bibliothèque Wire du framework Arduino.

Définir une adresse

En page 10 de la datasheet, table 2-3 nous pouvons voir que ce composant dispose en réalité de 2 addresses différentes : l’une permet la configuration de la puce, l’autre l’accès à la mémoire.

Petites subtilités d’adressage Vous noterez que je parle de 2 adresses, mais que la table 2-3 en mentionne 4. La raison est simple : la différence se fait sur le dernier bit, qui est à 1 lorsque l’on veut lire la valeur d’un registre, et de 0 lorsqu’on veut y écrire. Ce système fait partie du standard du protocole I2C. D’un point de vue pratique, nous avons 2 addresses de 7 bits : 0b1010 A2 A1 0 pour l’accès à la SRAM et 0b0011 A2 A1 0 pour la configuration.

Adresses disponibles pour communiquer avec la 47C16 (datasheet p. 10)

Nous pouvons donc commencer à initialiser notre sketch en incluant la bibliothèque Wire et en définissant les adresses avec lesquelles nous allons communiquer. Dans mon cas, je laisse A2 et A1 non connectés, ce qui équivaut à les connecter à VSS (Datasheet page 25), l’adresse de la SRAM sera donc, en hexadécimal, 0x50 (que l’on peut considérer comme l’adress « de base » de la puce) ; et celle des registres de configuration sera 0x18.

#include "Wire.h"

int config_address = 0b0011000;
int memory_address = 0b1010000;
/* On pourrait aussi écrire:
int config_address = 0x18;
int memory_address = 0x50; */

Les registres de contrôle

Avant de commencer à stocker des données, il peut être pertinent de s’intéresser à la configuration de la puce au travers des registres de contrôle.

Fonctionnement

Comme indiqué en page 10 de la datasheet, on peut accéder aux registres de contrôle avec l’adresse 0b0011 A2 A1 0.

La table 2-4 page 15 nous indique qu’il n’y a que deux registres de configuration, aux adresses 0x00 (registre STATUS) et 0x55 (registre COMMAND). Si on s’intéresse au paragraphe 2.4.1, on remarque que le registre STATUS est constitué de 8 bits, dont certains devront être configurés pour être sûrs que la puce fonctionnera comme on l’attend.

  • Les bits BP, qui contrôlent la protection en écriture de la puce. Pour le moment, je veux que toute la mémoire soit accessible, donc je dois les mettre tous trois à 0 (cf table 2-5).
  • le bit ASE détermine si la fonction de backup vers l’EEPROM est activée (bit à 1) ou désactivée (bit à 0). Dans un premier temps, je ne veux faire mes essais que sur la mémoire SRAM, donc ce bit sera à 0 et je le passerai à 1 plus tard.

Le registre COMMAND, pour sa part, permet de forcer un stockage en EEPROM (mise à 0011 0011) ou un chargement depuis l’EEPROM (mise à 1101 1101). La datasheet ne précise pas ce qu’il advient de ce registre une fois l’opération terminée, mais on peut supposer qu’un mécanisme est mis en place pour ne pas l’exécuter indéfiniment. Je n’ai pas besoin de cette fonctionnalité dans l’immédiat, mais je ferai en sorte de la tester.

Un petit test ?

Nous pouvons essayer de faire une première communication avec la 47C16, histoire de vérifier qu’elle fonctionne et commencer à prendre en main la bibliothèque Wire. Pour cela, nous pouvons demander à lire le contenu d’un des registres de contrôle, le modifier, et le lire à nouveau.

Pour lire le registre de configuration, notre code ressemble à ceci (je l’ai déjà mis sous forme de fonction, plus facile à appeler).

int read_status_register(void) {
  Wire.beginTransmission(config_address);
  Wire.write(0x00);
  Wire.endTransmission();
  Wire.requestFrom(config_address, 1);
  int c = Wire.read();
  return(c);
}

Pour faire simple : on commence une transmission I2C vers l’adresse de configuration, on écrit l’adresse du registre qui nous intéresse avec Wire.write(0x00), puis on termine immédiatement la transmission avec endTransmission(). Cette action a pour conséquence d’envoyer réellement les données écrites avec write() sur le bus I2C (avant cela, on peut considérer que ces données étaient stockées dans une sorte de « buffer »).
Enfin, avec requestFrom, on demande à ce que le périphérique nous envoie 1 byte (le 1 en second paramètre) en réponse, que l’on lit avec read().

Pour écrire une valeur dans un registre, le code est très similaire à la phase beginTransmission() ... endTransmission(), mais au lieu de n’envoyer qu’un seul byte, on va en envoyer autant que nécessaire avec plusieurs appels à write().

Testons en essayant d’activer la protection en écriture sur l’ensemble de la mémoire. Le tableau ci-dessous nous indique que, pour protéger l’ensemble de la SRAM en écriture, il faut mettre les bits 4-2 à 1.

Options de configuration des bits de protection en écriture

Voici donc le code qui permet la lecture / écriture / re-lecture.

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(5000);
  Serial.println("Essai lecture mémoire 47C16");
  delay(1000);
  Wire.begin();

  int c = read_status_register();
  Serial.println(c);
  delay(1000);

  Serial.println("Trying to setup software Write Protection");
  Wire.beginTransmission(config_address);
  Wire.write(0x00);
  Wire.write(0b00011100);
  Wire.endTransmission();

  Serial.println("Reading back configuration");
  c = read_status_register();
  Serial.println(c);
  delay(1000);
}

À l’exécution de ce code, voici le retour produit par l’Arduino.

Sortie `Serial` de l’Arduino sur une lecture / écriture / re-lecture du registre de configuration

Si l’on regarde, 0b00011100 correspond bien à 28 en base 10. Notre code fonctionne et, comme on peut le déduire, la puce également.

Nous avons ici verrouillé l’intégralité de la mémoire en écriture. Si nous voulons pouvoir l’utiliser, il serait bon de lever cette protection. C’est chose faite avec ce code.

void remove_software_protection(void) {
  Wire.beginTransmission(config_address);
  Wire.write(0x00);
  Wire.write(0b00000000);
  Wire.endTransmission();
}

// À insérer avant l’accolade fermante de la fonction setup
remove_software_protection();

Nous repartons ainsi d’une copie vierge, qui pour le moment correspond aux paramètres définis pour les premiers essais : pas de protection en écriture, et pas de sauvegarde des données en cas de perte d’alimentation.

La mémoire SRAM

Maintenant que nous avons pu établir un premier contact avec la 47C16, il est temps de commencer à utiliser l’espace mémoire dont nous disposons.
Ce sont les pages 11 à 14 de la datasheet qui nous donnent les indications d’utilisation de la mémoire.

Fonctionnement en écriture

Comme indiqué au paragraphe 2.3.1 (information reprise sur la figure 2-4), lorsqu’une opération d’écriture est demandée, les deux premiers bits reçus par la puce seront interprétés comme « l’adresse » de la mémoire où seront stockées les données : l’Address Pointer (ou pointeur d’adressage).

Sur cette figure, on peut noter que les 5 premiers bits du Address High Byte sont notés X, et les 2 suivants sont notés Y. Cette notation est précisée en légende comme étant Dont't Care (X) et Don't Care for 47X04 (Y), ce qui signifie que les 5 premiers bits seront ignorés par toutes les puces 47XXX, et les 2 suivants par les puces 47X04.

Étant donné que nous utilisons ici une 47C16, seuls les 3 derniers bits du High Byte et le Low Byte dans son ensemble devront être configurés.

Il ne faudra cependant pas oublier ce point dans le code: nous devrons séparer l’adresse entre l’octet de poids fort et l’octet de poids faible nous-mêmes (avec les macros highByte et lowByte du framework Arduino par exemple).

Séquence I2C pour l’écriture d’un byte (source Datasheet)

Pour écrire plusieurs bytes, le schéma est identique (voir figure 2-5), mais au lieu de n’envoyer qu’un seul byte de données on peut en envoyer — théoriquement — une infinité. Le pointeur d’adresse est automatiquement incrémenté après chaque byte (à noter, ce sera important pour un des modes de lecture). Comme le dit la Datasheet à la fin du paragraphe 2.3.1.2 Sequential Write : * il n’y a pas de limite au nombre de bytes pouvant être écrits en une seule commande ; * si on envoie trop de bytes par rapport à la taille disponible de la mémoire après l’adresse d’écriture, le pointeur d’adressage reviendra à 0 (les bytes ainsi envoyés peuvent potentiellement effacer d’anciennes données).

Fonctionnement en lecture

La lecture d’informations depuis la mémoire peut se faire selon 3 modes : la lecture à l’adresse courante, la lecture dite aléatoire et la lecture séquentielle.

  • Lecture aléatoire Permet d’accéder à n’importe quelle zone de la mémoire sans devoir dérouler tout le contenu de celle-ci, pour récupérer le contenu d’un byte.

  • Lecture séquentielle Dérive de la lecture aléatoire et permet de lire un nombre arbitraire de bytes. La différence se fait au niveau du contrôleur qui, au lieu d’envoyer un signal STOP après le premier byte, envoie un ACK (ACKnowledge) à la puce, qui retourne alors le byte suivant. Le schéma se répète, tant que le contrôleur n’envoie pas de STOP.
    Heureusement, ce n’est pas à nous d’envoyer ce signal STOP, la librairie Wire s’en charge pour nous au travers de l’instruction requestFrom(adress, nb_bytes) : c’est ce nb_bytes qui va permettre d’envoyer « automatiquement » le signal STOP après avoir lu le nombre de bytes demandés.

  • Lecture à l’adresse courante De ce que j’en comprends pour le moment, la lecture à l’adresse courante retourne la valeur stockée à l’adresse pointée par le pointeur d’adresse au moment de la requête. Logiquement, si je lance l’écriture d’un tableau de bytes, cette lecture devrait me renvoyer la dernière valeur de mon tableau, puisque l’écriture séquentielle incrémente la valeur du pointeur.

Le Code

Dans un premier temps, essayons d’écrire un byte, puis de le re-lire.

void setup() {
  Serial.begin(9600);
  delay(5000);
  Serial.println("Essai lecture mémoire 47C16");
  delay(1000);
  Wire.begin();

  char write_byte = 200;
  Wire.beginTransmission(memory_address);
  //Pour cette première écriture, on va se placer à l’adresse 0.
  Wire.write(0b00000000);
  Wire.write(0b00000000);
  Wire.write(write_byte);
  Wire.endTransmission();

  char backread_byte = 0;
  Wire.beginTransmission(memory_address);
  Wire.write(0);
  Wire.write(0);
  Wire.endTransmission();
  Wire.requestFrom(memory_address, 1);
  backread_byte = Wire.read();

  if(write_byte==backread_byte) {
    Serial.println("Les 2 bytes sont identiques");
  } else {
    Serial.print("Les 2 bytes sont différents : W = ");
    Serial.print(write_byte);
    Serial.print(", R = ");
    Serial.println(backread_byte);
  }
}

Ce code donne, sur un Arduino UNO R3, la sortie suivante. Les 2 bytes, celui envoyé à la puce et celui lu, sont identiques. Notre opération d’écriture a donc à priori bien fonctionné.

-> Essai lecture mémoire 47C16
-> Les 2 bytes sont identiques

Essayons maintenant une écriture de plusieurs bytes, et d’en lire un au milieu.

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  delay(5000);
  Serial.println("Essai lecture mémoire 47C16");
  delay(1000);
  Wire.begin();

  char write_byte = 200;
  Wire.beginTransmission(memory_address);
  //Pour cette opération, on va se placer à l’adresse 0 et écrire 3 bytes.
  Wire.write(0b00000000);
  Wire.write(0b00000000);
  Wire.write(100);
  Wire.write(150);
  Wire.write(200);
  Wire.endTransmission();

  //On veut récupérer le second byte écrit (normalement égal à 150)
  int backread_byte = 0;
  Wire.beginTransmission(memory_address);
  Wire.write(0);
  Wire.write(1);
  Wire.endTransmission();
  Wire.requestFrom(memory_address, 1);
  backread_byte = Wire.read();

  Serial.print("Backread byte = ");
  Serial.println(backread_byte);
}

Ce qui nous donne :

-> Essai lecture mémoire 47C16
-> Backread byte = 150

On peut avoir confirmation du bon fonctionnement en regardant le graphe produit par un analyseur logique.

Sortie de l’analyseur logique pour une opération d’écriture d’array (`0x64`, `0x96` et `0xC8`) et de lecture de l’élément médian (`0x96`)
L’opération de lecture démarre peu après la marque « 0 ».

Gérer les erreurs

En page 9 de la datasheet, les tables 2-1 et 2-2 nous permettent de déterminer les retours que peut produire la puce sur certaines erreurs.

Retours possibles de la puce sur les opérations de lecture et d’écriture

On peut capturer ce retour et l’interpréter ensuite pour savoir si une erreur a eu lieu, au travers de la fonction endTransmission(). Celle-ci doit en effet renvoyer un int, dont la valeur nous renseigne sur le résultat de l’opération. Parmi les codes qui nous intéresse, on peut noter :

  • 0 : succès ;
  • 2 : NoACK reçu lors de l’envoi de l’adresse (l’adresse demandée est invalide) ;
  • 3 : NoACK reçu lors de l’envoi de la commande (commande invalide).

Cet article est déjà assez long, j’aborderai le sujet dans un prochain post.

Conclusion

Si nous revenons sur ce que nous avons vu dans cet article, nous avons balayé les sujets suivants : * les caractéristiques générales des puces mémoire de la série 47XXX ; * le brochage de ces composants ; * les caractéristiques d’alimentation et de consommation de courant ; * les différentes façons de communiquer avec la puce via le bus I2C, en théorie (description des adresses et des registres) et en pratique (avec un Arduino, au travers de la bibliothèque Wire) ; * la gestion des erreurs d’écriture.

Voilà qui a produit un article assez dense, mais qui était cependant nécessaire pour que — me concernant — je puisse mettre à plat ce que je comprenais à la lecture de la fiche technique, et que je puisse vous aider (du moins je l’espère) à moins appréhender la lecture d’une fiche technique.

Globalement, l’ensemble de ce que nous avons vu d’un point de vue fonctionnement / code gagnerait probablement à être rassemblé dans une bibliothèque de manière à ce que chacun puisse utiliser ces circuits de mémoire sans devoir repartir de zéro.
Et ça tombe bien, c’est ce que j’ai fait ! Cette bibliothèque est disponible au travers du gestionnaire de bibliothèques de l’IDE Arduino, sous le nom EERAM_47XXX.

Quelques références :