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).