Les mémoires NAND flash ne sont pas très supportées par les principales librairies de microcontrôleurs

J’ai joué un peu avec une mémoire NAND flash et la librairie Adafruit_SPIFlash, sans savoir qu’elle n’était pas supportée. Premièrement, l’indice du JEDEC est décalé d’un octect, je ne sais pas pourquoi. En éditant le fichier Adafruit_SPIFlashBase.cpp à la ligne 114 (fonction begin), j’ai pu corriger cela et faire en sorte que le programme Flash_info fonctionne. J’ai ensuite essayé d’utiliser le programme sdfat_format, sans succès. Le programme a l’air de commencer, mais ne répond pas. Parfois, si je joue avec les câbles ou si j’approche ma main (il y a un effet capacitif étrange), le programme se termine avec l’erreur 13. En monitorant le courant consommé, je peux voir que la chip se met bien en mode active (~4mA de consommation, comparé à 0,2mA en sleep lorsque CS est mis à HIGH), mais qu’il n’y a pas d’écriture qui se produit (il faudrait un courant d’environ 25mA). J’ai abandonné et j’ai fait quelques recherches sur internet pour en savoir plus. Ce qui m’a mis la puce à l’oreille, c’est que toutes les puces définies dans la librairie d’Adafruit sont des NOR ou des FRAM.

Le thread ici parle du problème : https://community.particle.io/t/spiflash-support-for-w25n01gv/58283. Voir le commentaire : https://community.particle.io/t/spiflash-support-for-w25n01gv/58283/5 Voici la raison (entre autres) pourquoi la librairie SPIFlash ne fonctionne pas :

WIth NAND flash it’s always paged read and write with a weird page size of 2,112 bytes for the W24N01GV. So you always have to set the page and work with a page at a time, for both read and write. The SpiFlashRK library only works in linear mode and has no concept of working with paged mode.

Paul Stoffregen, le créateur des Teensy, a dit à deux endroit que les NAND flash ne sont pas supportés par sa librairie : https://forum.pjrc.com/index.php?threads/will-pauls-spiflash-library-work-with-these.31491/#post-92697 et https://github.com/PaulStoffregen/SerialFlash/issues/58#issuecomment-1814185087 « Just to state as clearly as possible, NAND chips are not supported by this library and probably never will be. »

Une possible solution serait d’utiliser la librairie LittleFS, tel que suggéré ici : https://protosupplies.com/product/w25n02g/. Il y a tout de même la gestion des bad blocks et des erreurs à garder en tête, mais ça me semble être la piste la plus réaliste pour faire fonctionner ces chips. https://github.com/PaulStoffregen/LittleFS

Interfaçage SPI d’une mémoire NOR Flash

J’ai compris quelques trucs dans l’utilisation de la librairie Adafruit_SPIFlash.

Premièrement, on peut créer une interface SPI avec les commandes suivantes :

SPIClass SPI_2(NRF_SPIM0, PIN_SPI_MISO, PIN_SPI_SCK, PIN_SPI_MOSI);

Le nrf52840 a 4 interfaces SPI, NRF_SPIM0-3. On peut déclarer les pins correspondantes, MISO, SCK et MOSI. Si tout est branché correctement, on peut envoyer la commande 9F qui permet d’imprimer le JEDEC ID (4 bytes suivant la commande) :

#include <SPI.h>

#define CS_PIN 6

SPIClass SPI_2(NRF_SPIM0, PIN_SPI_MISO, PIN_SPI_SCK, PIN_SPI_MOSI);

void setup() {
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH);
  Serial.begin(115200);
  while(!Serial) delay(100);

  SPI_2.begin();
  SPI_2.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0)); //10MHz
  digitalWrite(CS_PIN, LOW);
  Serial.println(SPI_2.transfer(0x9F)); //JEDEC ID
  Serial.println(SPI_2.transfer(0x00));
  Serial.println(SPI_2.transfer(0x00));
  Serial.println(SPI_2.transfer(0x00));
  Serial.println(SPI_2.transfer(0x00));
  digitalWrite(CS_PIN, HIGH);
  SPI_2.endTransaction();
  SPI_2.end();
}

void loop() {

}

Cela imprime les chiffres suivants :

159
239
64
24
0

239 = EFh, 64 = 40h, et 24 = 18h. Le JEDEC est donc 0xEF4018, ce qui correspond bien à celui du datasheet.

Pour utiliser la librairie Adafruit_SPIFlash, il faut se créer un nouveau device SPIFlash_Device. Le manufacturer ID correspond au premier byte du JEDEC, (EFh ici), ensuite la memory type au deuxième (40h ici) et finalement la capacité au troisième (18h ici). Les autres paramètres sont trouvés depuis le datasheet et à partir d’une chip similaire. J’utilise la SPIClass pour lui passer la bonne définition des pins de l’interface, avec le CS aussi. Voici le code complet :

#include <SPI.h>
#include <SdFat.h>

#include <Adafruit_SPIFlash.h>

#define CS_PIN 6

SPIFlash_Device_t const W25Q128JVSIQ{
  .total_size = (1UL << 24), /* 16 MiB */                                    
  .start_up_time_us = 5000, 
  .manufacturer_id = 0xef,                     
  .memory_type = 0x40, 
  .capacity = 0x18, 
  .max_clock_speed_mhz = 133,         
  .quad_enable_bit_mask = 0x02, 
  .has_sector_protection = false,              
  .supports_fast_read = true, 
  .supports_qspi = true,                         
  .supports_qspi_writes = true, 
  .write_status_register_split = false,        
  .single_status_byte = false, 
  .is_fram = false,                             
};

SPIClass SPI_2(NRF_SPIM0, PIN_SPI_MISO, PIN_SPI_SCK, PIN_SPI_MOSI);
Adafruit_FlashTransport_SPI flashTransport(CS_PIN, SPI_2);
Adafruit_SPIFlash flash(&flashTransport);

void setup() {
  Serial.begin(115200);
  while (!Serial) {
    delay(100); // wait for native usb
  }

  Serial.println("Adafruit Serial Flash Info example");

  Serial.println(flash.begin(&W25Q128JVSIQ, 1));

  Serial.print("JEDEC ID: 0x");
  Serial.println(flash.getJEDECID(), HEX);
  Serial.print("Flash size: ");
  Serial.print(flash.size() / 1024);
  Serial.println(" KB");
}

void loop() {
  // nothing to do
}

En utilisant cette définition, c’est possible d’utiliser tous les programmes de la librairie, comme SdFat, etc. et créer un système de fichier haut niveau sur la mémoire Flash, sans avoir à repasser par le low level du SPI et des commandes du registre de la mémoire.

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! 😀