Interface avec une mémoire MRAM

Dans mon exploration des mémoires, je suis tombé sur la technologie MRAM. Elle est à ce jour plutôt dispendieuse (63$ pour la 16Mbit telle que testée (le prix est descendu à 60$, il y a espoir que ça devienne abordable)). Elle a la particularité d’être permanente (l’information reste dessus même lorsqu’elle n’est plus alimentée, comme un mémoire FLASH) et de supporter un nombre illimité de cycles d’écriture (min 10^14). Sa fréquence d’opération peut aller jusqu’à 108MHz. Son interface est SPI et fonctionne à 3,3V, ce qui fait qu’on peut l’utiliser avec n’importe quel microcontrôleur.

Dans mon exemple, je voulais tester un mode d’opération bien particulier. Dans un premier temps, le microcontrôleur initialise la mémoire et envoie le signal pour activer un write d’une durée indéterminée. Ensuite, un circuit externe (ici un micro PDM) envoie des données en continu. La mémoire se comporte comme un buffer circulaire, avec le pointer qui s’incrémente à chaque écriture de bit et revient automatiquement à 0 lorsqu’il atteint la fin de la profondeur. Cela fait en sorte que le circuit est autonome et ne dépend plus des commandes du microcontrôleur, ce dernier pouvant même entrer en sleep, ou gérer à 100% d’autres tâches. La seule autre mémoire que j’ai trouvée qui peut faire ça est la SRAM (article précédent). Cette propriété a le potentiel de simplifier certains designs digitaux, et de limiter la consommation de courant. C’était donc une solution intéressante à bien des égards, outre son coût prohibitif pour bien des applications commerciales.

Voici le code arduino complet :

#include <SPI.h>
int sram_CSn = A2;
int mic_sel = A0;
int mic_clk = 36;

void setup() {
  pinMode(mic_sel, OUTPUT);
  pinMode(sram_CSn, OUTPUT);
  pinMode(mic_clk, INPUT);
  digitalWrite(mic_sel, LOW); 
  digitalWrite(sram_CSn, HIGH);
  Serial.begin(2000000);
  analogWriteResolution(7);
  analogWriteFrequency(200000);

  //Enable write
  SPI.begin();
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); //10MHz
  digitalWrite(sram_CSn, LOW);
  SPI.transfer(0x06); //write enable command
  SPI.endTransaction();
  digitalWrite(sram_CSn, HIGH);
  SPI.end();

  //Test sequential pdm mic
  SPI.begin();
  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); //10MHz
  digitalWrite(sram_CSn, LOW);
  SPI.transfer(0x02); //write command
  SPI.transfer(0x00); //address (24 bits)
  SPI.transfer(0x00);
  SPI.transfer(0x00);
  /*
  //Signal triangulaire pour tester
  for (int i = 0; i < 2000000; i++){
    SPI.transfer(byte(i % 256));
    //SPI.transfer(0x00);
    //SPI.transfer(0x00);
  }*/
  for (int i = 0; i < 2097152; i++){ //les adresses vont de 0x000000 à 0x1FFFFF
    //SPI.transfer(0x0F);
    SPI.transfer(0x00);
  }
  SPI.endTransaction();
  SPI.end();
  //digitalWrite(sram_CSn, HIGH);

  //start le micro
  pinMode(mic_clk,OUTPUT);
  analogWrite(mic_clk, 64);
}

void loop() {
  int cmd = Serial.read();
    if (cmd == 114){
      //Ferme le micro et le write infini
      pinMode(mic_clk, INPUT);
      digitalWrite(sram_CSn, HIGH);
      delay(1);

      //Une seonde commande complète de write est nécessaire pour éviter de corrompre les données (fouille-moi pourquoi)
      SPI.begin();
      SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); //10MHz
      digitalWrite(sram_CSn, LOW);
      SPI.transfer(0x02); //write command
      // Magiquement, la mémoire garde son pointeur et écrit à l'adresse où elle était rendue, même si on input 000
      SPI.transfer(0x00); //address (24 bits)
      SPI.transfer(0x00);
      SPI.transfer(0x00);
      for (int i = 0; i < 1000; i++){ //marqueur pour la fin de l'échantillon
        SPI.transfer(0xFF);
      }
      SPI.endTransaction();
      SPI.end();
      digitalWrite(sram_CSn, HIGH);
      delay(1);
      
      //Read
      SPI.begin();
      SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); //10MHz
      digitalWrite(sram_CSn, LOW);
      SPI.transfer(0x03); //read command
      SPI.transfer(0x00); //address (24 bits)
      SPI.transfer(0x00);
      SPI.transfer(0x00);

      uint8_t charbuf[64];
      for (int i = 0; i < 65536; i++){ //lit la mémoire au complet (2Mo (62500*32octets))
        for (byte i = 0; i < 64; i = i + 2){
          charbuf[i] = SPI.transfer(0x00);
          charbuf[i+1] = 0x00;
        }
        Serial.write(charbuf, sizeof(charbuf));
      }

      SPI.endTransaction();
      SPI.end();
      digitalWrite(sram_CSn, HIGH);
      Serial.write(0xCC); //mmh il va falloir que je repense ce mécanisme
      Serial.write(0xCC); //send the stop bytes
    }

}

J’utilise le PWM pour simuler une clock qui alimente le micro PDM. Avec la commande analogWriteFrequency, je choisis une fréquence d’opération de 200kHz. Pour monter aussi haut, il faut jouer avec la résolution à l’aide de la commande analogWriteResolution, que je mets à 7. On active la clock avec la commande analogWrite(pin, 64), puisque la résolution est de 7 bits (0-128).

Pour initialiser la mémoire MRAM, il faut d’abord envoyer la commande 0x06 qui permet d’activer l’écriture dans la mémoire. On a seulement besoin d’envoyer cette commande une seule fois, c’est la protection en écriture. La commande write est 0x02, suivie de l’adresse de 24 bits. Je commence à 0x000000 pour l’adresse.

Pour faire mes tests, j’ai envoyé un signal triangulaire, ainsi qu’écrit la mémoire au complet à zéro pour m’assurer que le test est répétable.

Tant que la pin sram_CSn reste à LOW, le write se poursuit indéfiniment. La mémoire n’a pas besoin de rafraîchissement comme une PSRAM, ce qui fait qu’elle peut se comporter en buffer circulaire sans intervention externe.

Pour éviter que les données soient corrompues, je dois envoyer une seconde commande de write avant de lire la mémoire. Je n’ai pas investigué davantage pourquoi, c’est un bug qui m’a fait perdre beaucoup de temps et j’ai juste été content de le contourner. J’en profite pour écrire une série de 1000 points à la valeur 0xFF, ce qui me permet de distinguer la position de la fin de l’échantillon dans le buffer circulaire plus tard, en assumant que les vraies données ne contiendront jamais un tel signal saturé. C’est utile, puisque je n’ai pas trouvé de manière de savoir où le pointeur d’adresse est rendu lorsque la mémoire est opérée de manière autonome.

La commande de read est 0x03, suivie de l’adresse de 24 bits. Les données sont reçues lors d’un SPI.transmit(), le contenu du write est ignoré, je mets 0x00. Je crée un buffer de 64 octets pour transférer 32 octets de données à la fois dans un serial.print. Cela permet d’optimiser un peu la transmission usb. La fin de la transmission est signalée par le doublon 0xCC 0xCC, qui est détecté dans mon code python. Toutes les autres données sont sur le format 0x00 0xXX, le premier octet est vide et jeté lors de la lecture. Cette méthode est simple et me permet de transmettre 32 octets distincts par paquet, au prix d’une efficacité de 50%.

À 200kHz et même à 1MHz, la consommation en écriture est d’environ 9mA. Ce qui est plus élevé que la mémoire SRAM (1mA) mais plus bas que la mémoire flash (30mA et plus).

Installation du sandbox d’algèbre géométrique sur Fedora

Il y a de cela quelques années, j’avais assisté à une présentation sur l’algèbre géométrique qui m’avait beaucoup intéressée. Voulant en apprendre davantage, je me suis procuré le livre : Geometric Algebra for Computer Science, qui m’avait séduit par son approche pratique, étant un matheux qui préfère voir les choses (cf. Borra pour les GPH qui me lisent en ce moment 😉

Je n’avais jamais eu le temps de le lire, ayant d’autres priorités de lectures pour mes cours, mais maintenant que l’école est définitivement F.I.N.I.E je peux enfin lire des manuels de mathématiques obscures pour le plaisir! (oui ce genre de personne existe pour de vrai). En ce moment je viens de finir le chapitre 2, et j’ai tenté de faire les exercices de programmation, mais comme d’habitude avec tout ce qui touche à du C++, il faut passer des heures à installer toutes les librairies et comprendre comment les «linker» comme du monde.

Sur le site web du livre, on trouve le lien pour installer le sandbox dans la section Downloads. Pour avoir toutes les libraires, il faut installer les packages suivants (tel qu’énumérés sans spécification dans le fichier user_manual.txt) :
sudo dnf install freeglut freeglut-devel antlr antlr-C++

Puis peut-être d’autres choses qui était déjà installé sur mon système, genre OpenGL puis FLTK. Donc avec un terminal, on ouvre le dossier ga_sandbox-1.0.7 puis on écrit :
./configure
make

Ce qui va récursivement construire tous les fichiers C++, partout. Le plus important en fait c’est que la librairie «libgasandbox » se compile sans erreur.

Lorsque le make s’est rendu dans les dossiers des exemples, il m’a sorti l’erreur suivant à l’exemple 1 du premier chapitre (qui est présente dans tous les makefile de tous les exemples…) :

/usr/bin/ld: /usr/lib/gcc/x86_64-redhat-linux/7/../../../../lib64/libglut.so: référence au symbole non défini «glGetFloatv»
//usr/lib64/libGL.so.1: error adding symbols: DSO missing from command line

Après quelques heures de fouilles sur internet et d’essais pathétiques de gossage dans le makefile de l’exemple 1, j’ai finalement compris comment résoudre le problème : la librairie d’OpenGL n’était pas correctement liée. Il faut modifier la ligne 87 du makefile propre à l’exemple (GLUT_LIBS = -lglut) par : GLUT_LIBS = -lGL -lGLU -lglut , ce qui rajoute les deux librairies d’OpenGL GL et GLU nécessaires à la compilation du programme. Donc on enregistre le makefile, on exécute la commande make puis tadam! Ça compile enfin pour générer l’exécutable chap1ex1, que l’on peut ouvrir dans le terminal avec le classique « ./ » : ./chap1ex1 ce qui ouvre la fenêtre suivante avec les objets géométriques donnés en exemple, et la possibilité de faire tourner la vue en 3D.

Maintenant je peux enfin me concentrer à comprendre les maths et devenir un pro des espaces vectoriels à 5 dimensions! 😀