Utilisation de la PSRAM

Cet article explique comment utiliser la RAM externe supplémentaire (PSRAM) présente sur certaines cartes ESP32 ( uPesy ESP32 Wrover DevKit , TinyPICO ) avec le langage Arduino et sur le Framework ESP-IDF.

Présentation et utilité

Puisque la RAM interne des microcontrôleurs est assez faible, on peut ajouter une RAM supplémentaire externe pour en avoir encore plus. Même, si l’ESP32 est un microcontrôleur qui possède beaucoup de RAM, elle peut ne pas être suffisante, surtout quand on veut manipuler de gros fichiers (JSON, HTML …)

La PSRAM est une RAM supplémentaire externe de 4 Mo qui est présente dans certains modules ESP32:
  • ESP32-WROVER-B

  • ESP32-WROVER-I

L’ESP32 communique avec la PSRAM par le protocole SPI. C’est pourquoi elle est aussi appelée SPI RAM . Le 3ème bus SPI de l’ESP32 est utilisé pour communiquer avec la mémoire flash (qui contient le programme) et avec la PSRAM.

../_images/psram.jpg

Aperçu interne du module ESP-WROVER-B

Note

Il y a une ambiguïté sur la quantité de mémoire réelle de la PSRAM. La capacité réelle de la PSRAM (sur la puce de silicium) n’est pas de 4 Mo mais de 8 Mo. Par contre, seulement 4 Mo peuvent être utilisés facilement dans nos programmes. Pour utiliser les 8 Mo de la PSRAM , c’est assez complexe. Use 8 MB of PSRAM Ceci dit 4 Mo de RAM en plus est déjà énorme (8 fois la RAM interne)!

L’intéret d’une RAM externe est de pouvoir soulager la RAM interne pour le stockage temporaire de données importantes.

On peut par exemple:
  • Manipuler de gros fichiers Json

  • Manipuler des pages WEB entières

  • Faire un serveur web performant

  • Créer d’énormes tableaux

  • Manipuler les données de gros fichiers

Comment l’utiliser ?

Sur Arduino IDE, pour pouvoir utiliser la PSRAM, il faut sélectionner une carte compatible, comme par exemple la carte ESP32 Wrover Module qui marchera pour toutes les cartes ESP32 qui possèdent une PSRAM.

../_images/psram0.fr.png

Si vous utilisez une carte présente dans la liste, comme par exemple pour la carte TinyPICO , vérifiez que la PSRAM est bien activée.

../_images/psram1.fr.png

Avertissement

La PSRAM n’apparaît pas dans la table des mémoires de l’ESP32, c’est donc normal que la taille de la RAM indiquée sur Arduino IDE ou Plateformio soit toujours de 327 Ko . Il y a une séparation claire entre la RAM interne de l’ESP32 (327 Ko) et la RAM externe (4 Mo).

../_images/psram2.fr.png

Pour utiliser la PSRAM, on peut soit utiliser des librairies qui prennent en charge la PSRAM ou faire soi même des allocations dynamiques pour créer des tableaux, des buffers, télécharger des fichiers HTLM, JSON …

Librairies Arduino / ESP32

Pour éviter de se prendre la tête avec des allocations dynamiques, certaines librairies Arduino (compatibles avec l’ESP32) prennent en charge la PSRAM. La plus connue est ArduinoJson qui permet de manipuler très facilement les fichiers JSON sur Arduino et ESP32.

../_images/arduinoJson.svg

ArduinoJson

Si vous ne connaisez pas encore ArduinoJson, regarder ce tutoriel pour installer et apprendre à utiliser la librairie . Pour utiliser la PSRAM avec ArduinoJson, il faut créer un JsonDocument spécifique que l’on place avant la fonction setup() :

struct SpiRamAllocator {
        void* allocate(size_t size) {
                return ps_malloc(size);

        }
        void deallocate(void* pointer) {
                free(pointer);
        }
};

using SpiRamJsonDocument = BasicJsonDocument<SpiRamAllocator>;

Ensuite on utilise le SpiRamJsonDocument comme un DynamicJsonDocument classique :

SpiRamJsonDocument doc(100000); //100 KB here
deserializeJson(doc, input);

Voici un exemple, qui permet de télécharger un fichier JSON de 65 Ko. Le JSON contient les résultats de la recherche « ESP32 json » sur Google.

Exemple

#include <WiFi.h>
#include <HTTPClient.h>
#define ARDUINOJSON_DECODE_UNICODE 1
#include <ArduinoJson.h>

//WiFi
const char *ssid = "Nom box WiFi";
const char *password = "mdp WiFi";

const char *url = "https://raw.githubusercontent.com/uPesy/ESP32_Tutorials/master/JSON/bigJsonExample.json";

struct SpiRamAllocator {
        void* allocate(size_t size) {
                return ps_malloc(size);

        }
        void deallocate(void* pointer) {
                free(pointer);
        }
};

using SpiRamJsonDocument = BasicJsonDocument<SpiRamAllocator>;


void setup(){
        delay(500);
        psramInit();
        Serial.begin(115200);
        Serial.println((String)"Memoire disponible dans la PSRAM : " +ESP.getFreePsram());

        //Se connecter au WiFi

        WiFi.begin(ssid, password);
        while (WiFi.status() != WL_CONNECTED){
                delay(100);
                Serial.print(".");
        }

        Serial.print("\nConnecté au WiFi avec l'IP : ");
        Serial.println(WiFi.localIP());

        Serial.println("Téléchargement JSON");
        HTTPClient http;
        http.useHTTP10(true);
        http.begin(url);
        http.GET();

        SpiRamJsonDocument doc(100000); //Créer un document JSON de 100 Ko
        DeserializationError error = deserializeJson(doc, http.getStream());
        if(error){
                Serial.print("deserializeJson() failed: ");
                Serial.println(error.c_str());
        }

        http.end();
        Serial.println((String)"Mémoire utilisée par le JsonDocument : " + doc.memoryUsage());


        for (int i=0; i!=10;i++){
                Serial.println("\n[+]");
                Serial.println(doc["items"][i]["title"].as<String>());
                Serial.println("------------------------------");
                Serial.println(doc["items"][i]["snippet"].as<String>());
                Serial.println("------------------------------");
                Serial.println((String) "URL : " + doc["items"][i]["link"].as<String>());

        }
}

void loop(){
}

Moniteur série:

Memoire disponible dans la PSRAM : 4194252
..............
Connecté au WiFi avec l'IP : 192.168.43.7
Téléchargement JSON
Mémoire utilisée par le JsonDocument: 62515

[+]
Installing ESP32 in Arduino IDE (Windows, Mac OS X, Linux ...
------------------------------
Installing ESP32 Add-on in Arduino IDE Windows, Mac OS X, Linux open. Enter
https://dl.espressif.com/dl/package_esp32_index.json into the “Additional Board ...
------------------------------
URL : https://randomnerdtutorials.com/installing-the-esp32-board-in-arduino-ide-windows-instructions/

[+]
ESP32: Parsing JSON – techtutorialsx
------------------------------
Apr 26, 2017 ... The objective of this post is to explain how to parse JSON messages with the
ESP32 and the ArduinoJson library. Introduction The objective of ...
------------------------------
URL : https://techtutorialsx.com/2017/04/26/esp32-parsing-json/

...

Regarder mon Instructable , qui permet de faire des recherches sur Google et d’afficher le résultat dans le moniteur série. (Marche aussi sans PSRAM)

../_images/psram3.jpg

Voir aussi

Comment utiliser la RAM externe sur l’ESP32 sur le site officiel ArduinoJson

Framework Arduino

Pour stocker des variables dans la PSRAM, il faut faire des allocations dynamiques avec des fonctions spécifiques pour la PSRAM de l’ESP32.

Note

Il n’est pas nécessaire d’avoir des connaissances avancées en C/C++ pour utiliser la PSRAM . La connaissances des pointeurs, des fonctions d’allocations dynamiques comme malloc() ou calloc() est un plus, mais les exemples proposés se suffisent à eux-mêmes pour utiliser la PSRAM dans des projets d’électronique. Pour plus d’informations sur l’allocation dynamique, regardez L’allocation dynamique sur OpenClassroom .

Les fonctions principales à utiliser sont les suivantes psramInit() , ESP.getFreePsram() , ps_malloc() ou ps_calloc() et free() .

La fonction psramInit() permet d’initialiser la PSRAM, la fonction ESP.getFreePsram() renvoie la quantité de mémoire disponible dans la PSRAM. Les 3 autres fonctions servent pour l’allocation dynamique.

Exemples

Cette section propose des exemples d’utilisation de la PSRAM. Ils permettent de comprendre comment utiliser les foncions citées ci-dessus. Il n’est pas nécessaire de tout comprendre pour pouvoir utiliser la PSRAM, il suffit de modifier les valeurs dans les exemples pour les utiliser dans vos projets.

../_images/variable.svg
../_images/matrix.svg
../_images/string-text.svg
../_images/html.svg

Variables

Tableaux

String / Chaine de charactères

HTLM/JSON

Variables
  • Cet exemple montre comment créer des variables qui sont stockées dans la PSRAM:

//Créer un entier
int *var_int = (int *) ps_malloc(sizeof(int));
*var_int = 42;

//Créer un nombre flottant
float *var_float = (float *) ps_malloc(sizeof(float));
*var_float = 42.42;

Voici le sketch complet :

Exemple

void setup() {
// put your setup code here, to run once:
Serial.begin(115200);
//Initialisation
if(psramInit()){
        Serial.println("\nLa PSRAM est correctement initialisée");
}else{
        Serial.println("\nLa PSRAM ne fonctionne pas");
}

//Créer un entier
int *var_int = (int *) ps_malloc(sizeof(int));
*var_int = 42;

//Créer un nombre flottant
float *var_float = (float *) ps_malloc(sizeof(float));
*var_float = 42.42;

Serial.println((String)"var_int = " + *var_int);
Serial.print("var_float = ");
Serial.println(*var_float);
}

Sortie du terminal :

La PSRAM est correctement initialisée
var_int=42
var_float = 42.42
Tableaux

Pour les tableaux, la syntaxe est très similaire :

//Création d'un tableaux entiers de 1000 éléments
int n_elements = 1000;
int *tableau = (int *) ps_malloc(n_elements * sizeof(int)); //Créer un tableau d'entiers de n_elements
//On accéde aux éléments comme pour un tableau classique
tableau[0] = 42;
tableau[42] = 42;
tableau[999] = 42;
  • Voici le sketch complet qui permet de créer un tableau d’entiers de 1000 éléments stocké dans la PSRAM:

Exemple

int n_elements = 1000;

void setup(){
        Serial.begin(9600);
        //Initialisation
        if(psramInit()){
        Serial.println("\nLa PSRAM est correctement initialisée");
        }else{
        Serial.println("La PSRAM ne fonctionne pas");
        }

        //Création d'un tableau de n_elements
        int tailleInitiale = ESP.getFreePsram();
        Serial.println((String)"Mémoire disponible PSRAM (octets): " + tailleInitiale); // Affiche le nombre d'octets encore disponible dans la PSRAM

        int *tableau = (int *) ps_malloc(n_elements * sizeof(int)); //Créer un tableau d'entiers de n_elements
        tableau[0] = 42;
        tableau[999] = 42; //On accéde aux éléments comme pour un tableau classique

        int taillePSRAM = ESP.getFreePsram();
        Serial.println((String)"Mémoire disponible PSRAM (octets): " + taillePSRAM); // L'espace libre de la mémoire a diminuée
        int tailleTableau = tailleInitiale - taillePSRAM;
        Serial.println((String)"Taille du tableau dans la mémoire en octets : " + tailleTableau);

        //Suppression du tableau
        free(tableau); //La memoire alloué est libérée.
        Serial.println((String)"Mémoire disponible PSRAM (octets): " +ESP.getFreePsram());
}

void loop() {
}

Sortie du terminal :

La PSRAM est corectement initialisée
Mémoire disponible PSRAM (octets): 4194252
Mémoire disponible PSRAM (octets): 4190236
Taille du tableau dans la mémoire en octets : 4016
Mémoire disponible PSRAM (octets): 4194252

Note

La taille en octets du tableau est de 4016 pour un tableau de 1000 entiers (int). Le type int est codé sur 4 octets. Il y a donc 4 * 1000 octets occupés par le tableau, les 16 octets restants contiennent des informations sur le bloc mémoire (taille, flags).

  • Pour des tableaux d’autres types de variables :

char *tableau1 = (char *) ps_malloc(n_elements * sizeof(char)); //Créer un tableau vide de n_elements caractères
float *tableau = (float *) ps_malloc(n_elements * sizeof(float)); //Créer un tableau de nombres avec virgules
  • On peut aussi créer des tableaux qui sont remplis de zéros avec ps_calloc() :

int n_elements = 20;
Serial.println((String)"Mémoire disponible PSRAM (octets): " +ESP.getFreePsram());
Serial.println("Tableau d'entiers initialisés à 0");
int *tableau = (int *) ps_calloc(n_elements, sizeof(int));
Serial.print("[tableau] : ");
for(int i=0; i!= n_elements;i++){
        Serial.print((String)tableau[0] + " ");
}
Serial.println((String)"\nMémoire disponible PSRAM (octets): " +ESP.getFreePsram());

Sortie du terminal :

Mémoire disponible PSRAM (octets): 4194252
Tableau d'entiers initialisés à 0
[tableau] : 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Mémoire disponible PSRAM (octets): 4194156
  • Voici 2 manières pour créer des tableaux à 2 dimensions :

Exemple

int n_lignes = 10;
int n_colonnes = 20;


void setup(){
        Serial.begin(9600);
        //Initialisation
        if(psramInit()){
                Serial.println("\nLa PSRAM est corectement initialisée");
        }else{
                Serial.println("La PSRAM ne fonctionne pas");
        }

        //Création d'un tableau de n_elements
        int tailleInitiale = ESP.getFreePsram();
        Serial.println((String)"Mémoire disponible PSRAM (octets): " + tailleInitiale); // Affiche le nombre d'octets encore disponible dans la PSRAM

        //Création d'un tableau à deux dimensions avec un tableau à une dimension
        int *tableau2D = (int *) ps_malloc(n_lignes * n_colonnes * sizeof(int)); // Création d'un tableau de n_lignes x n_colonnes d'entiers codés sur 4 octets
        //Pour accéder à la case situé à la ligne 5 à la colonne 10
        int ligne_i = 5;
        int colonne_j = 10;
        tableau2D[colonne_j * n_colonnes +ligne_i] = 43;

        //Création d'un tableau rempli de zero à deux dimensions avec un tableau de pointeurs
        int **tableau2Dbis = (int **) ps_calloc(n_lignes, sizeof(int *));
        for (int i =0; i!= n_colonnes;i++){
                tableau2Dbis[i] = (int *) ps_calloc(n_colonnes , sizeof(int)); //Créer un tableau de pointeurs
        }
        //Pour accéder à la case situé à la ligne 5 à la colonne 10
        tableau2Dbis[ligne_i][colonne_j] = 42;


        int taillePSRAM = ESP.getFreePsram();
        Serial.println((String)"Mémoire disponible PSRAM (octets): " + taillePSRAM); // L'espace libre de la mémoire a diminuée
        int tailleTableau2D = tailleInitiale - taillePSRAM;
        Serial.println((String)"Taille du tableau dans la mémoire en octets : " + tailleTableau2D);

        //Suppression du tableau
        free(tableau2D); //La memoire alloué est libérée.
        free(tableau2Dbis);
        Serial.println((String)"Mémoire disponible PSRAM (octets): " +ESP.getFreePsram());
}

void loop() {
}
String / Chaine de charactères

Puisqu’un String est une chaine de charactères, il s’agit juste d’un tableau de type char :

int n_elements = 20;
char *str = (char *) ps_calloc(n_elements, sizeof(char)); //Créer un tableau de n_elements caractères null ('\0')
str[0] = '4';
str[1] = '2';

Note

L’utilisation de ps_calloc() est plus intéressante car elle permet à l’ESP32, de directement détecter la fin de la chaine de charactères.

Voici un sketch d’exemple :

void setup() {
        Serial.begin(115200);
        if(psramInit()){
                Serial.println("\nLa PSRAM est correctement initialisée");
        }else{
                Serial.println("\nLa PSRAM ne fonctionne pas");
        }

        int n_elements = 20;
        char *str = (char *) ps_calloc(n_elements, sizeof(char)); //Créer un tableau de n_elements caractères null ('\0')
        for(int i = 0; i < 10;i+=3){
                str[i] = '4';
                str[i+1] = '2';
                str[i+2] = '_';
        }
        Serial.println(str);
}

Sortie du terminal :

La PSRAM est correctement initialisée
42_42_42_42_
HTLM/JSON

Voici un exemple qui permet de télécharger n’importe quel type de fichiers textes comme des pages HTML ou des fichiers JSON (de moins de 4 Mo) et de le stocker dans la PSRAM.

#include <WiFi.h>
#include <HTTPClient.h>
#define TIMEOUT 5000

const char *ssid = "Nom Box WiFi";
const char *password = "mdp";
const char *url = "https://fr.wikipedia.org/wiki/ESP32";

void setup() {
        Serial.begin(115200);

        //Connect to WiFi
        WiFi.begin(ssid, password);
        while (WiFi.status() != WL_CONNECTED){
        delay(100);
        Serial.print(".");
        }
        Serial.print("\nConnecté au WiFi avec comme IP : ");
        Serial.println(WiFi.localIP());

        char *page = (char *) ps_malloc(sizeof(char));
        int lengthHtml = 0;
        HTTPClient http;
        http.begin(url);
        http.useHTTP10(true); //Permet d'essayer d'avoir un Content-Length dans le header en utilisant HTTP1

        Serial.println((String) "Try GET "+ url);
        int httpCode = http.GET();
        if (httpCode > 0){
                // Affiche le réponse le code HTTP de la requete GET
                Serial.printf("[HTTP] GET... code: %d\n", httpCode);

                // Si le fichier existe (HTML, JSON, ...)
                if (httpCode == HTTP_CODE_OK){ // HTTP_CODE_OK = 200

                // Essaye de récupérer la taille du ficher ( -1 s'il n'y a pas de Content-Length dans l'header)
                int tempLength = http.getSize();
                Serial.println((String) "Content Length :" + tempLength);

                //Stockage de la page dans la PSRAM
                Serial.printf("Adresse mémoire de la page : %p \n", page);

                // Récupère le stream TCP
                WiFiClient *stream = http.getStreamPtr();

                //Initialisation position du buffer dans la PSRAM
                int position = 0;

                uint32_t currentTime = 0;
                uint8_t timeoutArboted = 1;

                // Récupère toute les données du fichier
                while(http.connected() && (tempLength > 0 || tempLength == -1)){

                        // Récupère les données disponibles (les données arrivent packets par packets)
                        size_t size = stream->available();
                        if (size){
                                page = (char*) ps_realloc(page, position + size + 1);
                                stream->readBytes(page+position, size);
                                position += size;

                                if (tempLength > 0){
                                        tempLength -= size;
                                }

                                timeoutArboted = 1;
                                }else{
                                        //Si on ne connaît pas la taille du fichier, on suppose que toutes les données sont recues avant le timeout
                                        if(timeoutArboted){
                                                //Lance le timer
                                                currentTime = millis();
                                                timeoutArboted = 0;
                                        }else{
                                                if(millis()-currentTime > TIMEOUT){
                                                        //On a atteind le Timeout
                                                        Serial.println("Timeout reached");
                                                        break;
                                                }
                                        }
                                }
                        }

                        *(page+position) = '\0';

                        lengthHtml = position;
                        Serial.println((String)"Downloaded " + lengthHtml + " Octets");
                }
        }
        else{
                Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
        }
        //Ferme le client HTTP
        http.end();
        delay(1500);
        Serial.println(page); // Pour afficher la page HTML entière

}

Sortie du terminal :

.............
Connecté au WiFi avec comme IP : 192.168.43.7
Try GET https://fr.wikipedia.org/wiki/ESP32
[HTTP] GET... code: 200
Content Length :66295
Adresse mémoire de la page : 0x3f800024
Downloaded 66295 Octets
<!DOCTYPE html>
<html class="client-nojs" lang="fr" dir="ltr">
<head>
<meta charset="UTF-8"/>
<title>ESP32 — Wikipédia</title>
...
...

Note

Vous pouvez remarquer que c’est beaucoup plus simple d’utiliser la librairie ArduinoJson pour télécharger des fichiers JSON.

Liste détaillée des fonctions

psramInit()
Renvoie

true si la PSRAM s’est bien initialisée, false sinon

Type renvoyé

booléen

Cette fonction s’assure que la PSRAM est utilisable en faisant différents tests, puis l’initialise.

ESP.getFreePsram()
Renvoie

La mémoire disponible en octets de la PSRAM.

Type renvoyé

int

Cette fonction permet de connaître la mémoire disponible dans la PSRAM. La taille maximale est de 4 194 252 octets .

ps_malloc(size_t tailleBloc)
Paramètres
  • tailleBloc – nombre total d’octets que l’on veut réserver dans la RAM.

Renvoie

Un pointeur du type donné en argument qui contient l’adresse du bloc mémoire.

Type renvoyé

void *

Cette fonction permet d’allouer un bloc de mémoire dans la PSRAM.

ps_calloc(size_t nElements, int tailleElement)
Paramètres
  • nElements – Nombre d’éléments que l’on veut réserver dans la RAM.

  • tailleElement – Taille en octets d’un seul élément.

Renvoie

Un pointeur du type donné en argument qui contient l’adresse du bloc mémoire.

Type renvoyé

void *

Cette fonction permet d’allouer un bloc de mémoire de nElements , chacun de taille tailleElement dans la PSRAM et initialise tous ces octets à la valeur de 0 .

ps_realloc(void * adresseBloc, nouvelleTailleBloc)
Paramètres
  • adresseBloc – Adresse mémoire du bloc de mémoire à réallouer.

  • nouvelleTailleBloc – Nouvelle taille en octets du bloc mémoire que l’on veut réserver dans la RAM.

Renvoie

Un pointeur du type donné en argument sur la zone mémoire réallouée.

Type renvoyé

void *

Cette fonction permet de réallouer un bloc de mémoire préalablement alloué avec ps_malloc() ou ps_calloc() . Si l’espace mémoire libre qui suit le bloc à réallouer est suffisamment grand pour la nouvelle taille du bloc, le bloc de mémoire d’origine est simplement agrandi. Par contre si l’espace libre n’est pas suffisant, un nouveau bloc de mémoire sera alloué, le contenu de la zone d’origine recopié dans la nouvelle zone et le bloc mémoire d’origine sera libéré automatiquement.

free(void *pointer)
Paramètres
  • pointer ( *int, *char, *double, *long... ) – L’adresse du bloc de mémoire à désallouer

Cette fonction libère un bloc de mémoire alloué dynamiquement avec ps_malloc() ou ps_calloc() .

Framework ESP-IDF

Les fonctions utilisées sur le framework Arduino sont basées sur celles du Framework ESP-IDF. Ainsi les fonctions décrites ci-dessous sont utilisées implicitement sur le Framework Arduino-ESP32.

Les fonctions à utiliser sont les suivantes esp_spiram_init() , heap_caps_get_free_size() , heap_caps_malloc() ou heap_caps_calloc() et free()

Voir aussi

Pour des informations plus poussées, consulter la documentation officielle sur la PSRAM