mercredi 18 mars 2020

Serveur ESP32 : implémentation



Serveur ESP32 : implémentation


Cet article a pour but de présenter plusieurs méthodes d'implémentation pour un serveur HTTP sur ESP32 ou ESP8266.

Dans ce tutoriel nous allons aborder :
  • formulaire
  • objets divers :
    • bouton, bouton-radio
    • champ numérique avec boutons + -
    • champ texte, simple ou multiligne
  • un peu de JAVASCRIPT 
  • récupération de l'heure avec NTP
  • requêtes GET ou POST
  • envoi d'une réponse avec snprintf() 
  • envoi d'une réponse avec String
  • envoi d'une réponse à partir du SPIFFS avec une template

1. La classe WebServer

Cette classe fait partie de la librairie WebServer du package standard ESP32. Elle a déjà été utilisée dans l'article précédent :
https://riton-duino.blogspot.com/2020/03/arduino-ethernet-esp32-comparons.html

Elle permet d'implémenter un serveur très simplement. Nous n'allons pas revenir dessus.

2. La classe AsyncWebServer

Cette classe fait partie de la librairie ESPAsyncWebServer qui peut être trouvée ici :
https://github.com/me-no-dev/ESPAsyncWebServer
Comme dit sur la page la librairie AsyncTCP est nécessaire :
https://github.com/me-no-dev/AsyncTCP
Il existe une version ESP8266 :
https://github.com/me-no-dev/ESPAsyncTCP

Elle a l'énorme avantage de proposer, en autres, la gestion de pages HTML templates, ce qui va bien nous arranger.
Ceci sera expliqué plus loin.

3. Méthode GET ou POST

Comme je l'ai déjà expliqué deux méthode d'envoi de requête existent : GET ou POST.

En méthode GET les arguments suivent l'URL et on les voit dans la barre d'adresse du navigateur lorsque l'on valide le formulaire.
Les arguments font partie de l'URL, qui elle-même fait partie de l'entête HTTP.

En méthode POST les arguments ne sont pas visibles dans la barre d'adresse du navigateur lorsque l'on valide le formulaire.
Les arguments sont envoyés après l'entête HTTP. Un double caractère '\n' signale la fin de l'entête et donc le début des arguments.

On utilise en général la requête POST dans un formulaire. La requête GET est employée principalement dans la gestion de boutons ou de liens hypertexte.

4. JAVASCRIPT

Certains s'étonneront de voir du code JAVASCRIPT dans l'exemple qui va suivre. Comment se fait-il que l'on puisse exécuter du code JAVASCIPT dans une application ESP32 ?

Tout simplement l'ESP32 n'exécute pas le JAVASCRIPT. Les lignes JAVASCRIPT sont envoyées par l'ESP32 au sein du code HTML (ou dans des fichiers .js) et elles sont exécutées par le navigateur.

Pour l'ESP32 le code HTML et JAVASCRIPT ne sont que du texte. Il pourrait de la même façon envoyer un bout de code écrit en C++ ou une liste de courses, simplement le navigateur ne saurait pas quoi en faire, à part afficher le texte.

5. Affichage de pages à contenu variable

Il est assez rare, dans un logiciel serveur d'afficher une page statique, à moins qu'il ne s'agisse d'une page d'aide ou d'une page destinée à afficher la version du serveur, la liste des modifications, etc.
Dans la plupart des cas, nous aurons un mélange de code HTML fixe et de code HTML variable.

6. Les différentes méthodes d'envoi de réponse

Nous allons maintenant aborder la partie qui nous intéresse : quand le navigateur envoie une requête HTTP, comment faire pour lui répondre de manière correcte et efficace.

6.1. Utiliser un objet String

En général la solution utilisée est celle-ci :

  String msg = "<!DOCTYPE html>\n";
  msg += "<html>\n";
  msg += "<head>\n";
  msg += "<meta charset=\"UTF-8\">\n";
  msg += "<title>My Server</title>\n";
  msg += "</head>\n";
  msg += "<body>\n";
  msg += timeString;

  msg += "<br><br><h1>TITLE</h1>\n";
  // etc.
  request->send(200, "text/html", msg);


Dans la réponse à la requête du navigateur on vient insérer une variable timeString (la date et l'heure courante).

On voit des caratères quotes " échapés à l'aide de backslash : \"
Cet échappement est obligatoire en C si l'on veut insérer une quote dans une chaîne de caractères.
Résultat : fastidieux et assez illisible.

6.2. Utiliser snprintf

On peut également utiliser la vénérable fonction snprintf() (elle devrait bientôt fêter son demi-siècle d'existence) :

#define RESPONSE_SIZE     1000
char msg[RESPONSE_SIZE];
 

const char *page = "<!DOCTYPE html>\n"
                              "<html>\n"
                              "<head>\n"
                              "<meta charset=\"UTF-8\">\n"
                              "<title>Subscribers</title>\n"
                              "</head>\n"
                              "<body>\n"
                              "%s<br><br><h1>SUBSCRIBERS</h1>\n"

                              // etc
  snprintf(msg, RESPONSE_SIZE, page, timeString, etc.
  request->send(200, "text/html", msg);

Nous remarquons dans l'utilisation de la fonction snprintf() qu'un large buffer est utilisé est que nous passons son adresse et sa taille à la fonction, ceci afin d'éviter les débordements qui pourraient arriver si l'on utilisait simplement sprintf().
snprintf() apporte une sécurité supplémentaire, et c'est important.

La variable timeString est également passée en argument et sera formatée selon une chaîne de format %s (c'est à dire une chaîne de caractère).

snprintf() est une fonction à nombre d'arguments variables. Il est donc possible de passer plusieurs arguments à formater si la page HTML comporte plusieurs champs variables, donc plusieurs chaînes de format :
  • %s : chaîne de caractère
  • %d : variable entière (int)
  • %x : variable entière (int) à formater en hexadécimal
  • %l : variable entière (long)
  • %f : variable flotante (float)
  • la liste est longue.
Le nombre d'arguments variables doit être le même que le nombre de chaînes de format. Le format doit être cohérent avec le type de variable. On ne formate pas une variable entière avec une chaîne de format convenant à une chaîne de caractères, et vice-versa.

Par rapport à la solution précédente, il est assez difficile de maintenir une cohérence lorsqu'un nombre important de chaînes de format sont noyées au sein d'une longue page HTML. Il faut être rigoureux et contrôler que le bon nombre d'arguments est passé à snprintf() et que le format convient.

L'utilisation des quotes avec échappement est identique à la méthode précédente.
Résultat : fastidieux et à peine plus lisible.

Par contre cette méthode offre une rapidité d'exécution sans équivalent.

6.3. Utiliser une template

La troisième méthode passe par un fichier HTML stocké dans la partition SPIFFS de l'ESP32 :

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Subscribers</title>
</head>
<body>
%PLACEHOLDER DATE%<br><br><h1>SUBSCRIBERS</h1>

etc.

On voit dans ce fichier un identifiant %PLACEHOLDER DATE% qui sera remplacé par la valeur d'une variable lors de l'envoi.

Le fichier HTML doit être créé dans un répertoire data du projet et sera chargé dans la partition SPIFFS de l'ESP32 grâce au chargeur :

Menu "Outils / ESP32 Sketch Data Upload"
Menu "Outils / ESP8266 Sketch Data Upload"

Il est nécessaire d'installer un plugin dans l'IDE ARDUINO :
https://github.com/me-no-dev/arduino-esp32fs-plugin
https://github.com/esp8266/arduino-esp8266fs-plugin

Voici le template processor :

const char *var_timeString;
const char *var_statusColor;

String templateProcessor(const String& var)
{
  if (var == "PLACEHOLDER DATE") {
    return var_timeString;
  }

  // etc.
  else if (var == "PLACEHOLDER STATUS_COLOR") {
    return var_statusColor;
  }
  return "?????";

}

Ensuite dans le code du handler on passe le nom du fichier HTML et l'adresse d'une fonction de traitement des PLACEHOLDERs :

  server.on("/index.html", HTTP_GET, [](AsyncWebServerRequest * request) {
    request->send(SPIFFS, "/index.html", "text/html", false, templateProcessor);
  });

A chaque fois qu'un identifiant encadré par deux caractères % sera rencontré la fonction templateProcessor sera appelée. Elle renvoie la valeur à insérer dans la page HTML envoyée.
Bien entendu si la variable est un nombre entier ou flottant il faudra la convertir en String :

int var_subscriberId;
    return String(var_subscriberId);


Cette méthode est largement utilisée dans le monde du développement WEB (Cheetah par exemple en PYTHON).

Résultat : le fichier HTML est clair, et l'on n'est même pas perturbé par la présence des identifiants PLACEHOLDER.

Même si cette solution est coûteuse en temps d'exécution, elle soulage grandement le développeur.

7. Exemple

Un serveur exemple est récupérable ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v1.0/esp32-subscriber/

Une directive de compilation permet de tester l'une des trois méthodes :

#define USE_SPRINTF       1
#define USE_STRING        2
#define USE_SPIFFS        3
#define METHOD            USE_SPIFFS


Il suffit de changer :

#define METHOD            USE_SPRINTF
#define METHOD            USE_STRING

Il s'agit d'un petit serveur de gestion d'abonnés :


Le formulaire comporte un certain nombre d'objets :
  • date et heure courante (obtenue par NTP)
  • bouton "Read media"
  • champ champ N° de l'abonné
  • champ identifiant de l'abonné
  • champ nom de l'abonné
  • champ crédits de l'abonné
  • 6 boutons radio
    • ajouter 1, 5 ou 10 crédits
    • mettre les crédits à zéro
    • interdire l'abonné
    • réautoriser l'abonné
  • champ historique des actions
  • champ status 
  • bouton SUBMIT
Le bouton "Read media" permet de lire le média de l'abonné. Celui-ci peut de divers types :
  • carte magnétique
  • carte à puce
  • RFID
  • code barre
  • carte SD
  • etc.
Le champ "Subscriber Number" permet de parcourir la liste des abonnés.

esp32-subscriber.ino
Le skectch est assez classique. Il gère une seule URL : /
La callback handleRoot fait tout le travail, qu'il s'agisse de requêtes GET ou POST.

subscriber.cpp
Comme on peut le voir dans ce source, tout est virtuel bien entendu.
Les données sont en RAM, et le bouton "Read media" se contente d'appeler un fonction qui tire aléatoirement un numéro.
Aucun besoin de matériel. Un ESP32 suffit.

Dans une vraie application il faudrait ajouter un lecteur de média et enregistrer les données historiques dans un fichier.
Ceci sera probablement le sujet d'un prochain tutoriel.

index.html
Le fichier HTML comporte deux petites fonction JAVASCRIPT qui permettent de gérer les boutons "Read media" et "N° de l'abonné" :
<body>
<script type="text/javascript">
function subscriberChanged(value){
document.location ="/?subscriber=" + value;
};
function readMedia(){
document.location ="/?action=read_media"
};
</script>

La fonction subscriberChanged() est associée au champ "N° de l'abonné" :
<input type="number" id="subscriber" name="subscriber" value="%PLACEHOLDER SUBSCRIBER%"
min="1" max="%PLACEHOLDER NSUBSCRIBERS%" onchange="subscriberChanged(this.value)">
A chaque changement de valeur la page est réaffichée :

  document.location ="/?subscriber=" + value;

L'URL / est donc demandée avec l'argument subscriber=numéro. Cet argument est traité par la callback handleRoot du sketch.

La fonction readMedia() est associée au bouton "ReadMedia" :

<button onclick="readMedia()">Read media</button>

A chaque click la page est réaffichée :

document.location ="/?action=read_media"

L'URL / est donc demandée avec l'argument action=read_media. Cet argument est traité par la callback handleRoot du sketch.

8. Le petit plus : NTP

L'application demande l'heure au démarrage au serveur NTP (Network Time Protocol) pool.ntp.org :

#include <time.h>

const char* ntpServer = "pool.ntp.org";

  configTime(3600, 0, ntpServer);

Ensuite il suffit de lire l'heure et de la formater selon le besoin :

  char timeString[50];
  struct tm timeinfo;
 

  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
    return;
  }
  else {
    strftime(timeString, sizeof(timeString), "%d/%m/%Y %H:%M", &timeinfo);
  }


C'est tout ? Oui
Pas la moindre librairie ? Non.

Pour une application sérieuse il serait souhaitable de demander l'heure périodiquement, ceci afin de mettre à jour l'heure de l'ESP32, car son oscillateur n'est pas d'une précision extraordinaire.

Une période d'une heure par exemple, ou même une journée, est suffisante. On peut utiliser un timer.

Vous trouvez cela manque de sérieux d'équiper un ESP32 d'un oscillateur qui dérive tellement qu'il faille réajuster l'heure aussi souvent ?
Votre montre est plus précise ?

Sachez-le : un ARDUINO dérive encore plus.

Avouez que grâce au NTP un circuit RTC (Real Time Clock) paraît absolument superflu.

9. Petits défauts

9.1. snprintf

L'utilisation de snprintf() impose quelques précautions quand à la taille du buffer d'émission. Dans cet exemple elle est de 1950 octets.
Si la taille était insuffisante, la réponse serait tronquée. Dans le code la taille est testée :

#define RESPONSE_SIZE     1950
char msg[RESPONSE_SIZE];
 

  size_t sz = strlen(msg);
  if (sz > RESPONSE_SIZE - 50) {
    Serial.println("WARNING");
    Serial.print("response size: "); Serial.println(sz);
    Serial.print("maximal size: "); Serial.println(RESPONSE_SIZE);
  }


Si le buffer est presque plein, ou tout à fait plein, un message est affiché sur le terminal :

WARNING
response size: 1912
maximal size: 1950


Les risques de plantage dû à un débordement du buffer sont donc inexistants.

9.2. String

L'utilisation de String est lente. Pourquoi ?
A chaque fois que l'on ajoute une chaîne de caractère à un objet String, cela provoque une ré-allocation de mémoire. Cela prend du temps.

9.3. AsyncWebServer

La classe AsyncWebServer n'est pas tout à fait aussi pratique que la classe WebServer dans la manipulation des arguments de la requête.
Dans cette petite application j'ai choisi de traiter les requêtes GET et POST dans la même fonction callback handleRoot (j'ai fait ce choix car cela m'arrange d'un point de vue factorisation du code) :

  int subscriberIndex = 1;
  if (request->hasArg("subscriber")) {                      // GET
    subscriberIndex = request->arg("subscriber").toInt();
  }
  if (request->hasParam("subscriber", true)) {         // POST
    subscriberIndex = request->getParam("subscriber", true)->value().toInt();
  }


Dans cet exemple, suivant qu'il s'agisse d'une requête GET ou POST, l'accès aux arguments diffère. Pourquoi les dévloppeurs de AsyncWebServer ont-ils fait ce choix ? mystère.

L'utilisation de la classe WebServer est plus simple :

  int subscriberIndex = 1;
  if (server->hasArg("subscriber")) {                      // GET ou POST
    subscriberIndex = server->arg("subscriber").toInt();
  }


Mais ce petit défaut de la classe AsyncWebServer est minime. Il suffit de connaître le problème.

10. Performances

Le temps d'envoi de la réponse est le suivant :
  • version snprintf() : 2ms
  • version String : 4ms
  • version SPIFFS : 5ms
La version SPIFFS n'est pas beaucoup plus pénalisante que la version String. Autant utiliser le SPIFFS et les templates.
La version snprintf() est la plus rapide.

11. Empreinte mémoire

En utilisant la classe WebServer la quantité de mémoire utilisée par l'exemple de l'article cité plus haut et la suivante :

Code: 754574 octets (57%) 
Variables: 45264 octets (13%)

La classe AsyncWebServer est un peu plus gourmande en code :

Code: 794582 octets (60%)
Variables: 43544 octets (13%)

Les deux codes sont assez comparables. On peut donc estimer que la différence est négligeable.

La version SPIFFS utilisant les templates est à peine moins économe en FLASH. Elle est même plus économe en mémoire RAM :

Code : 800790 octets (61%)
Variables: 41648 octets (12%)

11. Téléchargement

Cette version 1.0 est disponible ici :
https://bitbucket.org/henri_bachetti/webserver-form/src/v1.0/esp32-subscriber/

12. Liens utiles

La suite :
Serveur ESP32 : implémentation (2ème partie)
Serveur ESP32 : implémentation (3eme-partie)
Serveur ESP32 : implémentation (4eme-partie)
Serveur ESP32 : implémentation (5eme-partie)
Serveur ESP32 : tests automatisés
Serveur ESP32 : tests automatisés (2ème partie)
Serveur ESP32 : implémentation (6eme-partie)

13. Conclusion

La classe AsyncWebServer ouvre de belles perspectives pour l'écriture de pages formulaires évoluées.
Les templates apportent un gain de temps appréciable. J'irais même jusqu'à dire que sans elles cette classe AsyncWebServer a peu d'intérêt.
J'espère que ce petit divertissement vous aura plu.


Cordialement
Henri

jeudi 12 mars 2020

ARDUINO + Ethernet ou ESP32 : comparons



ARDUINO + Ethernet ou ESP32 : comparons


Certaines personnes s'imaginent à tort qu'une carte Ethernet est facile à utiliser et que la librairie Ethernet offre les mêmes facilités que la librairie ESP32 ou ESP8266.

Il n'en est rien. Récupérer par exemple les arguments d'une requête en méthode POST n'est pas du tout à la portée de l'amateur.

Mieux vaut être prévenu avant de s'engager dans une voie sans issue.

1. Les deux approches

Cet article a pour but de comparer deux approches dans l'écriture d'un serveur HTTP.

La première solution utilise une carte ARDUINO NANO + carte Ethernet :
La seconde un module WIFI ESP32 :

2. Méthode GET ou POST

La comparaison porte principalement sur la récupération des données d'un formulaire.
Lorsque l'on valide un formulaire la requête envoyée par le navigateur est formée d'une nouvelle URL suivie du caractères '?' puis d'une suite d'arguments nom=valeur séparés par des caractères '&'.

Imaginons un formulaire permettant de saisir une entrée de répertoire. L'URL pourrait ressembler à ceci :

http://form_contacts?name=dupont&age=25&tel=0102034455&email=dupont@orange.fr

Deux méthode d'envoi existent : GET ou POST.

En méthode GET les arguments suivent l'URL et on les voit dans la barre d'adresse du navigateur lorsque l'on valide le formulaire.
Les arguments font partie de l'URL, qui elle-même fait partie de l'entête HTTP.

En méthode POST les arguments ne sont pas visibles dans la barre d'adresse du navigateur lorsque l'on valide le formulaire.
Les arguments sont envoyés après l'entête HTTP. Un double caractère '\n' signale la fin de l'entête et donc le début des arguments.

3. La version ARDUINO + Ethernet

Le sketch est disponible ici :
ethernet.ino

Le sketch permet d'envoyer le formulaire avec la méthode GET ou POST :

#define FORM                  POST
ou
#define FORM                  GET

Avec la classe EthernetServer il faut lire et décortiquer la requête soi-même pour récupérer les informations :
  • méthode : GET ou POST ou autre
  • URL
  • protocole
  • adresse IP du client
  • arguments
  • etc.
Il faut également associer soi-même URL et fonction de traitement.

Des caractères échappés peuvent apparaître dans une URL:
  • %20 : espace
  • %40 : @
  • etc
Ceux-ci doivent être décodés par nos soins. La classe EthernetServer ne prévoit rien à ce sujet.

Parlons de la méthode POST.
Classiquement il faudrait lire l'entête complète pour pouvoir récupérer les arguments, ce qui réclamerait au minimum 500 à 600 octets de mémoire RAM.
Je me suis débrouillé pour que la requête soit lue partiellement, y compris en méthode POST, en gérant les arguments à part, ceci afin de consommer le moins de mémoire RAM possible :

Les variables globales utilisent 846 octets (41%) de mémoire dynamique, ce qui laisse 1202 octets pour les variables locales. Le maximum est de 2048 octets.

Y a t-il des limitations ?
Oui, étant donné le peu de mémoire disponible il faut limiter la taille de certains buffers :

#define REQUEST_MAX           150    // longueur maximale de la requête HTTP
#define URL_MAX                    100   
// longueur maximale d'une URL
#define ARGSTR_MAX              80     // longueur maximale des arguments
#define ARG_MAX                     5       // nombre maximal d'arguments

Ces valeurs limitent fortement les possibilités de notre application.
On pourra adopter une carte ARDUINO MEGA pour plus de souplesse.

Je n'ai pas utilisé la classe String pour des raisons évidentes de fragmentation :
https://riton-duino.blogspot.com/2020/02/arduino-la-fragmentation-memoire.html

4. La version ESP32

Le sketch est disponible ici :
esp32.ino

Je suis parti de l'exemple HelloServer.

Avec la classe WebServer ESP32 tout ce qui a été fait précédemment dans la version ARDUINO + Ethernet est déjà fait.On pourrait faire la même chose, avec la même facilité, avec un ESP8266 et la classe ESP8266WebServer.

Je n'ai pas utilisé de stockage des pages HTML en SPIFFS, afin de pouvoir comparer les deux solutions sur un pied d'égalité.

5. Mode d'emploi

Après le chargement les deux sketches affichent sur le terminal série l'adresse IP sur laquelle il faut se connecter.

Voici les différentes URL que le serveur accepte :

http://xxx.xxx.xxx.xxx/parser : affiche les arguments de l'URL

Exemple : http://xxx.xxx.xxx.xxx/parser?arg1=1234&arg2=4567

Cette requête affichera les informations suivantes :
  • l'entête HTTP
  • adresse et port local
  • adresse et port distant
  • arguments : arg1=1234 et arg2=4567
http://xxx.xxx.xxx.xxx/form : affiche un formulaire :



Après avoir renseigné les champs (l'adresse mail doit comporter un caractère @) et cliqué sur Send la page suivante affiche les informations du formulaire.

On peut imaginer toutes sortes de traitements :
  • stockage en SD 
  • ajout à une base de données
  • etc.
Le serveur ESP32 propose une page de plus :

http://xxx.xxx.xxx.xxx/sensor-form : affiche un formulaire enrichi :
Ce formulaire comporte un ensemble de cases à cocher, de boutons radios et de champs numériques bornés.

Après avoir renseigné les champs (les champs numériques comportent des valeurs minimales / maximales) et cliqué sur Send la page suivante affiche les informations du formulaire.

Un petit exemple pour apprendre à se servir de différents champs "input".

A remarquer : la technique de formatage d'une page WEB avec sprintf().

Il existe beaucoup de documentation sur le WEB :
https://developer.mozilla.org/fr/docs/Web/HTML/Element/Input

6. Comparaison des deux solutions

La version ARDUINO + Ethernet :
  • 1 journée de travail
  • 386 lignes de code
  • complexité importante
La version ESP32 :
  • 30 minutes de travail
  • 122 lignes de code (hors formulaire enrichi)
  • complexité moyenne
Le but était simple : démontrer la richesse de la classe WebServer ESP32 et sa nette supériorité sur la classe EthernetServer.
Je ne pense pas que qui que ce soit aurait la moindre préférence pour la solution Ethernet.

A moins d'être absolument obligé d'utiliser Ethernet, l'ESP32 ou ESP8266 reste la solution idéale.

6.1. Le DHCP

La librairie Ethernet ne permet pas la modification du HostName par contre celui-ci est composé du mot WIZnet + les 3 derniers caractères de l'adresse Mac. Il y a donc moyen d'avoir un HostName différent par carte.

Côté ESP32 une méthode WiFi.setHostname(name) existe. Malheureusement elle ne fonctionne pas.
Une inspection des paquets DHCP avec tcpdump révèle que le HostName n'est pas présent dans la requête.

Il y a une astuce ou plutôt un contournement à connaître. J'ai trouvé l'information ici :  Hostname not sent via DHCP request #2537

  char hostName[12];
  uint8_t mac[6];
  WiFi.mode(WIFI_STA);
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE); 

  WiFi.macAddress(mac);
  sprintf(hostName, "Esp32%x%x%x", mac[3], mac[4], mac[5]);
  Serial.printf("setting hostname %s: %d\n", hostName, WiFi.setHostname(hostName));
 

  Serial.print("Connecting to "); Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.printf("%s (%s): connected to %s", WiFi.getHostname(), WiFi.macAddress().c_str(), ssid);
  Serial.print("IP address: "); Serial.println(WiFi.localIP());


Le nom attribué est libre. J'ai choisi ESP32 + les 3 derniers caractères de l'adresse Mac, mais c'est juste mon choix.

Accessoirement il est à noter qu'une LiveBox n'affichera ces changements qu'après redémarrage.

Avec tcpdump (sous Linux) il est facile d'inspecter les paquets DHCP :

$ sudo tcpdump -i eno1 -vvv -s 1500 '((port 67 or port 68) and (udp[38:4] = 0x3c71bf47a5b0))'
tcpdump: listening on eno1, link-type EN10MB (Ethernet), capture size 1500 bytes
13:03:41.012470 IP (tos 0x0, ttl 255, id 0, offset 0, flags [none], proto UDP (17), length 336)
    0.0.0.0.bootpc > 255.255.255.255.bootps: [udp sum ok] BOOTP/DHCP, Request from 3c:71:bf:47:a5:b0 (oui Unknown), length 308, xid 0xf7aa125b, Flags [none] (0x0000)
      Client-Ethernet-Address 3c:71:bf:47:a5:b0 (oui Unknown)
      Vendor-rfc1048 Extensions
        Magic Cookie 0x63825363
        DHCP-Message Option 53, length 1: Discover
        MSZ Option 57, length 2: 1500
        Hostname Option 12, length 11: "Esp3247a5b0"
        Parameter-Request Option 55, length 12:
          Subnet-Mask, Default-Gateway, BR, Domain-Name-Server
          Domain-Name, Netbios-Name-Server, Netbios-Node, Netbios-Scope
          Router-Discovery, Static-Route, Classless-Static-Route, Vendor-Option
        END Option 255, length 0
        PAD Option 0, length 0, occurs 33


eno1 est le nom de l'interface Ethernet de mon PC. On voit bien passer le HostName dans la requête.

7. Liens utiles

Librairie Ethernet :
https://github.com/arduino-libraries/Ethernet

Un autre article sur le sujet ARDUINO + Ethernet:
https://riton-duino.blogspot.com/2019/02/un-web-server-sur-ethernet.html
Cet article a été mis à jour dernièrement. Le serveur accepte maintenant les requêtes POST.


Cordialement
Henri

8. Mises à jour

16/03/2020 : ajout d'un formulaire enrichi (ESP32)
06/04/2020 : ajout DHCP et HostName

mardi 10 mars 2020

ARDUINO : PRO MINI 5V ou 3.3V



ARDUINO : PRO MINI 5V ou 3.3V


J'ai déjà écrit un article sur cette carte :

arduino-pro-mini-basse-consommation.html

Ce nouvel article a pour but de présenter les différents modèles de cartes PRO MINI et d'avertir sur certains dangers liés à leur utilisation.

Ce modèle de carte ARDUINO est intéressant à plus d'un titre :

  • encombrement réduit
  • possibilité de basse consommation

Il existe plusieurs modèles :

  • 5V 16MHz
  • 3.3V 8MHz 

J'utilise de plus en plus ces cartes, en lieu et place de cartes NANO ou UNO, car elles offrent les mêmes possibilités dans un encombrement moindre. Le choix de la tension d'alimentation est fait en premier lieu en fonction des modules présents :

  • NRF24L01, RFM69 : 3.3V
  • DS18B20, MCP9808, SHT31D : 3.3V ou 5V
  • HDC2080, BME280 : 3.3V 
  • L293D : 5V
  • etc.

Je sais bien qu'il existe des circuits d'adaptation de niveau permettant d'utiliser un microcontrôleur 5V avec des périphériques 3.3V, mais je préfère m'en passer.

De même, un microcontrôleur 5V peut parfaitement fonctionner avec des périphériques tolérants au 5V sur leur entrées, alimentés sous 3.3V, mais le fonctionnement est fortement dépendant de la valeur exacte de tension d'alimentation 5V du microcontrôleur ou de 3.3V du périphérique.

Pour ces raisons je préfère toujours adopter la même tension d'alimentation pour le microcontrôleur et les périphériques.

Il y a plusieurs méthodes d'alimentation de ces cartes :

  • par la broche RAW
  • par la broche VCC

Un régulateur (MIC5205 en général) est implanté sur la carte et permet d'alimenter celle-ci par la broche RAW sous une tension maximale de 16V.

Ce régulateur est soit un modèle 5V soit un modèle 3.3V.

La broche VCC permet l'alimentation directe en 3.3V ou 5V.

Première remarque : un ATMEGA328P équipé d'un oscillateur 16MHz est incapable de fonctionner sous 3.3V.

Un ATMEGA328P équipé d'un oscillateur 8MHz est capable de fonctionner sous 3.3V, car cette fréquence lui permet d'accepter une tension inférieure.

Dans les deux cas il s'agit exactement du même microcontrôleur.

Deuxième remarque : l'ATMEGA 328P, même s'il est équipé d'un oscillateur 8MHz, peut supporter 5V ou même 6V. Mais le régulateur présent sur une carte 8MHz 3.3V n'appréciera pas je pense, de subir 5V sur sa sortie.

Cela reste faisable néanmoins si l'on supprime le régulateur.

Troisième remarque : la PRO MINI 8MHz et la PRO MINI 16MHz ont une consommation en mode veille assez semblable :

  • PRO MINI 8MHz : 1.5µA
  • PRO MINI 16MHz : 3µA

Cette consommation est obtenue en retirant la LED POWER et le régulateur 3.3V ou 5V de la carte :

Quand on pense basse consommation on pense forcément PRO MINI 3.3V 8MHz, mais la PRO MINI 16MHz n'est pas à exclure. 3µA n'est pas une consommation excessive. Le courant d'auto-décharge d'une batterie est souvent supérieur !

1. Différents modèles

En dehors de cette différence de fréquence et de tension de fonctionnement il en existe d'autres. La carte PRO MINI est assez peu standardisée.

Certaines cartes mettent à disposition ou non les broches A4, A5, A6, et A7 :

PRO MINI 3.3V (A4 + A5) + (A6 + A7) en bout de carte.

PRO MINI 3.3V (A4 + A5 + A6 +A7)

PRO MINI 3.3V (A4 + A5 + A6 +A7) avec strap

La présence ou l'absence de A6 et A7 et l'emplacement de ces broches ne sont pas les seules différences.

Comme on peut le voir sur ces trois images les broches de connexion du convertisseur USB / série en bout de carte sont inversées :

  • DTR TX RX VCC GND GND pour la première
  • BLK GND VCC RX TX GRN pour la seconde et la troisième

Si l'on utilise un connecteur DUPONT 6 points pour le chargement du logiciel il suffira d'inverser le connecteur ou non. GRN est équivalent à DTR.

On constate également sur la troisième carte la présence d'un strap (en haut à droite, à côté de la broche GND) permettant de mettre hors service la partie régulateur 3.3V et LED POWER, afin de gagner quelques précieux mA.

Bien entendu, si l'on désire utiliser A4 et A5 pour une liaison I2C par exemple, il faudra souder un connecteur mâle supplémentaire à deux broches sous la carte.

Par contre cette solution ne sera pas utilisable sur breadboard. Dans ce cas, il faudra placer le connecteur sur le dessus de la carte.

2. Différenciation

Il arrive quelquefois que les vendeurs chinois commettent des erreurs et envoient des PRO MINI 16MHz en lieu et place de PRO MINI 8MHz.

Il m'est même arrivé de recevoir un lot de 10 cartes panachées (4 cartes 16MHz et 6 cartes 8MHz), absolument semblables en apparence.

Sous la carte il y a généralement des cases à cocher :

On constate généralement qu'aucun marquage n'est présent, d'où une confusion possible, et un danger potentiel.

Si la carte est alimentée par sa broche RAW et que des modules sont alimentés par la broche VCC, en fonction du modèle de carte PRO MINI, la tension sur la broche VCC sera de 5V ou 3.3V.

Si les modules alimentés par la broche VCC ne supportent que 3.3V ou 3.6V il y a donc danger si la carte est une PRO MINI 5V.

Il serait dommage de s'apercevoir du problème trop tard et il me semble absolument indispensable de tester ces cartes avant de les utiliser.

Comment les différencier ?

2.1. Le régulateur

On peut tout d'abord les alimenter (en 9V ou 12V par exemple) par leur broche RAW et mesurer la tension sur la broche VCC.

Si elle est de 5V, c'est une carte 16MHz. Si elle est de 3.3V, c'est une carte 8MHz

2.2. L'oscillateur

Certains oscillateurs (le petit composant métallique) possèdent un marquage.

Si ce n'est pas le cas, on peut tester la fréquence d'horloge de la carte.

Il faudra bien entendu d'abord souder le connecteur de chargement 6 points et le relier à un convertisseur USB / série :

arduino-pro-mini-basse-consommation.html : paragraphe 5. Chargement

#define LED 13

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(115200);
}

void loop() {
  digitalWrite(LED, HIGH);
  delay(1000);
  digitalWrite(LED, LOW);
  delay(1000);
  Serial.println("OK");
}

Ce petit sketch permet de vérifier qu'une carte est un modèle 8MHz ou 16MHz.

Après avoir choisi "Arduino Pro or Pro Mini" dans le menu Outils, deux choix sont proposés dans le menu Outils / Processeur :

  • ATmega 328P (5V 16MHz)
  • ATmega 328P (3.3V 8MHz)

Après chargement du sketch la LED doit clignoter avec une période de 1s / 1s.

En ouvrant le moniteur série les textes "OK" doivent apparaître.

Si ce n'est pas le cas, recompiler après avoir choisi l'autre option dans le menu Outils / Processeur.

Ensuite il est prudent de marquer ces carte dans les cases appropriées avec un marqueur indélébile, ou de coller une petite étiquette sous la carte.

3. Lien utiles

Apparemment je ne suis pas le seul à avoir écrit un article sur le sujet :

https://bentek.fr/comment-reconnaitre-la-version-dun-arduino-pro-mini/

 

Cordialement

Henri

 

samedi 7 mars 2020

Février 2020 : actualité des Blogs du Mois


Actualité des Blogs du Mois


Sur le blog d'Yves Pelletier :
Sur Arduiblog
Sur  Framboise 314:
Sur  MCHobby:
D'autres blog (merci à Yves Pelletier) :

Cordialement
Henri

dimanche 1 mars 2020

ARDUINO : au fond de la pile


ARDUINO : au fond de la pile


Nous allons parler dans cet article de la gestion de la pile (stack en anglais) d'un ATMEGA328p ou d'un ATMEGA2560.
Ces explications sont parfaitement applicables à d'autres microcontrôleurs. Les informaticiens ne sont pas de grands génies. Il leur arrive rarement de réinventer la poudre ou l'eau tiède.
Certaines différences peuvent exister, par exemple pour un ESP8266, un ESP32, ou STM32, la taille d'une adresse mémoire est simplement différente : 32 bits au lieu de 16.

Le but de cete article est également de fournir des outils de diagnostic de la pile. Il est en effet possible de mesurer à un instant T la place disponible sur la pile avec des outils relativement simples.

1. La pile

Lors de l'exécution d'un logiciel, la pile est utilisée pour empiler les adresses de retour des fonctions (lors du retour d'une fonction il vaut mieux en effet savoir d'où l'appel a été fait), ainsi que les variables locales (appelées aussi variables automatiques).
La pile est gérée grâce à un registre spécial : le pointeur de pile.

Lorsque l'on appelle une fonction l'adresse de retour est placée sur la pile et le pointeur de pile est décrémenté de 2 octets (la taille d'une adresse ATMEGA).
Si la fonction appelée déclare une variable locale le pointeur de pile est décrémenté de la taille de la variable. Dans certains cas, le compilateur utilisera un registre, plus rapide.
Si cette fonction appelle une autre fonction, le pointeur de pile est encore décrémenté, de la même manière.

Si la valeur du pointeur de pile descend trop bas il y a un risque d'aller corrompre les adresses haute de la zone des variables globales. Si le logiciel utilise des objets String ou l'allocation dynamique, les adresses hautes du tas seront corrompues.

2. Les espaces mémoire

Les différents espaces mémoire d'un ATMEGA328p peuvent être représentés ainsi :

Le contenu de ces différentes zones est le suivant :
  • STARTUP : code de démarrage
  • CODE : code de l'application, y compris celui des librairies
  • LOAD_DATA : les valeurs des variables globales et statiques initialisées
  • REGISTERS : registres du microcontrôleur (GPIOs, TIMERS, etc.)
  • DATA : variables globales et statiques initialisées
  • BSS : variables globales et statiques non initialisées
  • STACK : la pile
Les notions de variables globales, statiques, initialisées ou non, sont décrites dans le paragraphe suivant.

Le tas (zone mémoire réservé à l'allocation mémoire dynamique) n'est pas représenté. Il a déjà été décrit dans l'article précédent :
https://riton-duino.blogspot.com/2020/02/arduino-la-fragmentation-memoire.html

Ici nous allons parler de la pile. Le tas, existant ou pas, ne modifie en rien ces explications.

2.1. Les variables

Nous allons commencer par faire la distinction entre les différents types de variables :

int globalData;
int globalInitialized = 1000;

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

void loop()
{
  int localData;
  int localInitialized = 1000;
  static int staticData;
  static int staticInitialized = 1000;

  Serial.print("globalData :"); Serial.println(globalData);
  Serial.print("globalInitialized :"); Serial.println(globalInitialized);
  Serial.print("localData :"); Serial.println(localData);
  Serial.print("localInitialized :"); Serial.println(localInitialized);
  Serial.print("staticData :"); Serial.println(staticData);
  Serial.print("staticInitialised :"); Serial.println(staticInitialized);
  delay(1000);
}


globalData est une variable globale, accessible depuis n'importe quelle fonction. Toutes les variables globales sont initialisées à ZÉRO au démarrage.
Elle aura donc la valeur ZÉRO dès le départ, y compris avant l'entrée dans la fonction setup().

globalInitialized est une autre variable globale, initialisée à la déclaration.
Elle aura la valeur 1000 dès le départ, y compris avant l'entrée dans la fonction setup().

localData est une variable locale, créée sur la pile, accessible seulement depuis la fonction dans laquelle elle est déclarée. Elle a une valeur indéterminée en entrant dans la fonction. Ce n'est pas parce que par chance elle vaut ZÉRO la première fois qu'elle va conserver cette valeur dans le temps.
Une variable locale est détruite en sortant de la fonction.

globalInitialized est une autre variable locale. Elle est initialisée à la valeur 1000 en entrant dans la fonction, et ceci à chaque fois que cette fonction est appelée.

staticData est une variable statique, Elle a les mêmes propriétés qu'une variable globale, sauf qu'elle est accessible uniquement depuis la fonction dans laquelle elle est déclarée.

staticInitialized est une autre variable statique, initialisée à la déclaration. Elle a les mêmes propriétés qu'une variable globale initialisée à la déclaration, sauf qu'elle est accessible uniquement depuis la fonction dans laquelle elle est déclarée.

2.2. Le startup

Ce morceau de code est celui qui est exécuté avant l'entrée dans l'application. Ses différents rôles sont :
  • initialiser la pile
  • initialiser à ZÉRO la zone BSS (variables globales)
  • initialiser la zone DATA (variables globales initialisées)
  • appeler les constructeurs d'objets statiques
  • appeler main()
La zone BSS est initialisée à ZÉRO par une fonction __clear_bss du startup. Comme cette opération est faite automatiquement, il est inutile de le faire manuellement :

int  data = 0;   // initialisation à ZERO inutile

La zone DATA est initialisée par une fonction __do_copy_data du startup.
Cette opération est réalisée par recopie de la zone LOAD_DATA en mémoire FLASH vers la zone DATA en RAM.
La zone DATA et la zone LOAD_DATA ont donc la même taille.

2.3. L'application

Le point d'entrée de l'application s'appelle main(). Voici son contenu dans la librairie standard ARDUINO :

int main(void)
{
    init();
    initVariant();
#if defined(USBCON)
    USBDevice.attach();
#endif
    setup();
    for (;;) {
        loop();
        if (serialEventRun) serialEventRun();
    }
    return 0;
}

On retouve les points d'entrée d'un sketch : setup(), appelé une seule fois, et loop(), appelé en boucle.

2.4. Les variables locales

Comme on l'a vu plus haut les variables locales sont déclarées sur la pile. Lorsque l'on utilise trop de variables locales à travers des appels de fonctions imbriqués (une fonction appelle une autre fonction qui en appelle une autre) il y risque de débordement de pile.

Comment mesurer tout ça et évaluer le risque ?

2.5. La mesure de la pile

Le code suivant comporte quelques fonctions utiles :

#define GLOBAL_SIZE        1211
char global[GLOBAL_SIZE + 1];
int  data = 1000;

extern char *__bss_end;
extern char *__bss_start;
extern char *__data_end;
extern char *__data_start;
extern char *__data_load_start;
extern char *__data_load_end;

uint16_t data_start;
uint16_t data_end;
uint16_t data_load_start;
uint16_t data_load_end;
uint16_t bss_start;
uint16_t bss_end;
uint16_t stack_addr;
int stack_size;

#define fillStack(addr) for (char *p = (char *)&__bss_end+1; p < addr ; p++) *p = 'U';

#define LOCAL_SIZE        100

void __attribute__ ((noinline)) func2(void)
{
  char local[LOCAL_SIZE + 1];
  memset(local, 'C', LOCAL_SIZE);
  local[LOCAL_SIZE] = 0;
  Serial.print(F("func2 STACK       = 0X0")); Serial.println((uint16_t)local, HEX);
}

void __attribute__ ((noinline)) func1(void)
{
  char local[LOCAL_SIZE + 1];
  memset(local, 'B', LOCAL_SIZE);
  local[LOCAL_SIZE] = 0;
  Serial.print(F("func1 STACK       = 0X0")); Serial.println((uint16_t)local, HEX);
  func2();
}

int getFreeStack(void)
{
  for (char *p = (char *)bss_end+1 ; p < RAMEND ; p++) {
    if (*p != 'U') {
      return p - bss_end+1;
    }
  }
  return 0;
}

unsigned char memByteRam(const void* x) {return *(char*)x;}
unsigned char memBytePgm(const void* x) {return pgm_read_byte(x);}

void dump(Print& out, void const*at, int sz, unsigned char (*memByte)(const void*)) {
  while(sz>0) {
    out.print("0x");
    out.print((unsigned long)at < 0x10 ? "000" : (unsigned long)at<0x100 ? "00" : (unsigned long)at<0x1000 ? "0" : "");
    out.print((unsigned long)at,HEX);
    out.print(": ");
    for(int c=0;c<16;c++) {
      if (c==8) out.write(' ');
      if (sz-c>0) {
        // Because ISO C forbids `void*` arithmetic, we have to do some funky casting
        void *memAddress = (void *)((int)at + c);
        unsigned char v = memByte(memAddress);

        out.write(v>=32/*&&v<='z'*/?v:'.');
      } else out.write(' ');
    }
    out.write(' ');
    for (int c=0; c<16 && sz; c++, sz--) {
      // Because ISO C forbids `void*` arithmetic, we have to do some funky casting
      unsigned char v=memByte(at);
      at = (void *)((int)at + 1);

      if (c==8) out.write(' ');
      out.print(v<16?"0":"");
      out.print(v,HEX);
      // out.write(v==0x97?'=':' ');
      out.write(' ');
    }
    out.println();
  }
}

void dumpRam(Print& out, void const*at,int sz) {return dump(out,at,sz,memByteRam);}
void dumpPgm(Print& out, void const*at,int sz) {return dump(out,at,sz,memBytePgm);}

void setup()
{
  char foo = 0xff;
  data_load_start = (uint16_t)&__data_load_start;
  data_load_end = (uint16_t)&__data_load_end;
  data_start = (uint16_t)&__data_start;
  data_end = (uint16_t)&__data_end;
  bss_start = (uint16_t)&__bss_start;
  bss_end = (uint16_t)&__bss_end;
  stack_addr = &foo;
  stack_size = stack_addr - bss_end+1;
  fillStack(stack_addr);
  Serial.begin(115200);
  memset(global, 'A', GLOBAL_SIZE);
  Serial.print(F("global ")); Serial.println(global);
  Serial.print(F("data ")); Serial.println(data);
  Serial.print(F("free_stack        = ")); Serial.println(getFreeStack());
  dumpRam(Serial, bss_end+1, stack_size);
  Serial.print(("fill stack        : 0X0")); Serial.print((bss_end)+1, HEX); Serial.print(F(" - 0X0"));   Serial.println(stack_addr, HEX);
  Serial.print(F("free_stack        = ")); Serial.println(getFreeStack());
  Serial.print(F("RAMEND            = 0X0")); Serial.println(RAMEND, HEX);
  Serial.print(F("TOP OF STACK      = 0X0")); Serial.println(stack_addr, HEX);
  Serial.print(F("STACK POINTER     = 0X0")); Serial.println(SP, HEX);
  Serial.print(F("STACK SIZE        = ")); Serial.println(stack_size);
  Serial.print(F("__data_start      = 0X0")); Serial.println(data_start, HEX);
  Serial.print(F("__data_end        = 0X0")); Serial.println(data_end, HEX);
  Serial.print(F("__data_load_start = 0X0")); Serial.println(data_load_start, HEX);
  Serial.print(F("__data_load_end   = 0X0")); Serial.println(data_load_end, HEX);
  Serial.print(F("__bss_start       = 0X0")); Serial.println(bss_start, HEX);
  Serial.print(F("__bss_end         = 0X0")); Serial.println(bss_end, HEX);
  Serial.print(F("__bss_size        = ")); Serial.println(bss_end - bss_start);
  func1();
  Serial.print(F("free_stack        = ")); Serial.println(getFreeStack());
  dumpRam(Serial, bss_end+1, stack_size);
}

void loop()
{
}


Tout d'abord il y a un certain nombre de variables externes :

extern char *__bss_end;
extern char *__bss_start;
extern char *__data_end;
extern char *__data_start;
extern char *__data_load_start;
extern char *__data_load_end;


Celles-ci sont créées par l'éditeur de liens (le linker en anglais) lors de la fabrication de l'exécutable. Elles représentent les adresses de début et de fin des trois zones BSS, DATA et DATA_LOAD.

uint16_t data_start;
uint16_t data_end;
uint16_t data_load_start;
uint16_t data_load_end;
uint16_t bss_start;
uint16_t bss_end;
uint16_t stack_addr;
int stack_size;


Ces quelques autres variables sont une recopie des précédentes, car il est plus facile de faire des calculs à partir de variables entières.

La macro fillStack() permet de remplir la pile de caractères 'U' (caractère '\x55'). Au fur et à mesure de l'utilisation de la pile d'autres caractères vont venir remplacer ceux-ci. Lorsque l'on désirera mesurer jusqu'à quelle adresse la pile a été utilisée, il suffira de compter le nombre d'octets 'U'.

getFreeStack() retourne le nombre d'octets non utilisés sur la pile. Plus ce nombre sera important plus la pile sera considérée comme étant en bon état de santé.

dump() permet d'afficher le contenu d'une zone mémoire :

0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 

Sur chaque ligne 16 octets sont affichés. L'adresse est affichée, puis 16 caractères en mode texte puis les 16 mêmes caractères en hexadécimal.

Pourquoi les fonctions func1 et func2 ont-elles été déclarées noinline ?
Parce que sinon, le compilateur, comme elles sont appelées une seule fois,  inclurait leur code directement dans la fonction appelante, ce qui n'est pas voulu dans cet exercice.

A l'exécution ce sketch affiche beaucoup d'informations :

global AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
data 1000
### STACK DUMP
free_stack        = 506 
0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06BB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06CB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06DB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06EB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06FB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x070B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x071B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x072B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x073B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x074B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x075B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x076B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x077B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x078B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x079B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07BB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07CB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07DB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07EB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07FB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x080B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x081B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x082B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x083B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x084B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x085B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x086B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x087B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x088B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x089B: UU.⸮.⸮.. >?.B.... 55 55 04 89 01 BC 06 0E  29 2E 08 38 00 01 00 00 
0x08AB: #⸮..⸮... ⸮...@.⸮. 23 A3 00 01 DA 00 00 08  E2 00 01 01 40 08 C1 08 
0x08BB: ⸮...⸮.⸮. ..⸮.⸮.⸮. DF 00 02 00 3E 02 A9 00  00 01 FA 02 A9 04 89 01 
0x08CB: ⸮..'(. . ⸮.⸮..57. BC 06 0E 00 04 01 30 04  89 00 C1 06 0E 21 25 08 
0x08DB: .....#⸮. .⸮.⸮.".. 20 00 01 00 00 23 A3 00  01 DA 08 E6 E6 16 00 0F 
0x08EB: .`.⸮... .......⸮ 03 A3 08 FA 00 00 7F 00  00 00 03 05 11 01 05 B7 
0x08FB: ⸮.                FF 00 
fill stack        : 0X06AB - 0X08FB
free_stack        = 500
RAMEND            = 0X08FF
TOP OF STACK      = 0X08FB
STACK POINTER     = 0X08FA
STACK SIZE        = 594
__data_start      = 0X0100
__data_end        = 0X0138
__data_load_start = 0X0DBE
__data_load_end   = 0X0DF6
__bss_start       = 0X0138
__bss_end         = 0X06AA
__bss_size        = 1394
func1 STACK       = 0X0892
func2 STACK       = 0X0829

### STACK DUMP
free_stack        = 300
0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06BB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06CB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06DB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06EB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x06FB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x070B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x071B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x072B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x073B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x074B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x075B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x076B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x077B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x078B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x079B: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07BB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
0x07CB: UUUUUUUU UU.⸮.⸮.. 55 55 55 55 55 55 55 55  55 55 04 89 01 BC 06 0E 
0x07DB: ...9.... #⸮..⸮... 0B 0C 08 39 00 03 00 00  23 A3 00 01 D9 00 02 08 
0x07EB: ....@.⸮. .....⸮U 1A 00 03 01 40 07 F9 08  17 7F 00 00 00 02 A9 55 
0x07FB: UUUUU.⸮. ⸮....... 55 55 55 55 55 04 89 01  BC 06 0E 0D 0E 01 0A 00 
0x080B: ...#⸮..⸮ .......@ 02 00 00 23 A3 00 01 DB  00 01 01 14 00 02 01 40 
0x081B: ....... .⸮.(.⸮CC 00 03 00 00 7F 00 00 00  02 CD 08 28 03 F2 43 43 
0x082B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x083B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x084B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x085B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x086B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x087B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x088B: CC..⸮..B BBBBBBBB 43 43 00 08 91 04 1E 42  42 42 42 42 42 42 42 42 
0x089B: BB.⸮.⸮.. ?..B.... 42 42 04 89 01 BC 06 0E  2A 2F 08 38 00 01 00 00 
0x08AB: #⸮..⸮... ⸮...@.⸮. 23 A3 00 01 DA 00 00 08  E2 00 01 01 40 08 C1 08 
0x08BB: ⸮...⸮.⸮. ..,.⸮.⸮. DF 00 02 00 3E 02 A9 00  00 01 2C 02 A9 04 89 01 
0x08CB: ⸮..(). . ⸮.⸮..68. BC 06 0E 01 05 01 30 04  89 00 C1 06 0E 22 26 08 
0x08DB: .....#⸮. .⸮.⸮.".. 20 00 01 00 00 23 A3 00  01 D7 08 E6 E6 16 00 0F 
0x08EB: .`.⸮... .......y 03 A3 08 FA 00 00 7F 00  00 00 03 05 11 01 06 79 
0x08FB: ⸮.                FF 00 


Nous allons expliquer tout cela.

Nous allons commencer à la ligne suivante :

fill stack        : 0X06AB - 0X08FB

La pile est emplie de caractères 'U' de 0X06AB à 0X08FB.

RAMEND            = 0X08FF

Il s'agit de l'adresse haute de la mémoire RAM. L'adresse de début (voir plus bas __data_start vaut 0x0100. La quantité de mémoire est donc de :

0x900 - 0x100 : 0x800 = 2048 octets, pile la taille de mémoire RAM de l'ATMEGA328p.

TOP OF STACK      = 0X08FB

Il s'agit de l'adresse de la première variable locale du sketch : foo
Il est normal que l'adresse soit proche de RAMEND, étant donné que le pointeur de pile est presque à son maximum au démarrage.

STACK POINTER     = 0X08FA

Il s'agit de l'adresse stockée dans le pointeur de pile.
Il est normal que sa valeur soit inférieure d'un octet à celle de l'adresse de la variable foo puisque la variable foo a une taille d'un octet. Après le déclaration de foo le pointeur de pile a été décrémenté de 1 octet.

STACK SIZE        = 594

Cette valeur est obtenue en soustrayant l'adresse RAMEND l'adresse de la variable foo. On obtient donc la taille de la pile puisque foo a été déclarée sur la pile.
Attention : le compilateur ordonne les variables sur la pile comme bon lui semble. S'il y avait plusieurs variables locales, leurs emplacements en mémoire seraient forcément voisins mais dans un ordre indéterminé.

__data_start      = 0X0100

Cette adresse est le début de la zone DATA.

__data_end        = 0X0138

Cette adresse est la fin de la zone DATA.

__data_load_start = 0X0DBE

Cette adresse est le début de la zone LOAD_DATA en mémoire FLASH.

__data_load_end   = 0X0DF6

Cette adresse est la fin de la zone LOAD_DATA.

Au démarrage LOAD_DATA sera recopiée dans DATA. Elles doivent donc avoir la même taille.

__data_end - __data_start = __data_load_end - __data_load_start

Est-ce vrai ? Oui, 56 octets dans les deux cas.

A quoi correspondent ces 56 octets ?
Premièrement à notre variable data :

int  data = 1000;

Le reste est occupé par d'autres variables globales ou statiques initialisées à la déclaration, dans la librairie ARDUINO.

__bss_start       = 0X0138

Cette adresse est le début de la zone BSS, les variables globales.

__bss_end         = 0X06AA

Cette adresse est la fin de la zone BSS.

__bss_size        = 1394

Cette valeur représente la quantité de variables globales. En sommes-nous responsables ?

Non, puisque nous avons déclaré :
  • un tableau global de 1211 octets
  • 8 entiers de 2 octets chacun
  • donc un total de 1227 octets
Il y a donc 1394 - 1227 = 167 octets déclarés quelque part dans la librairie ARDUINO.

func1 STACK       = 0X0892

Il s'agit de l'adresse d'une variable locale de 100 octets déclarée dans func1.

func2 STACK       = 0X0829

Il s'agit de l'adresse d'une variable locale de 100 octets déclarée dans func2.

Pourquoi est-elle beaucoup plus basse que la précédente ?
parce que func1 appelle func2 donc les variables locales s'empilent.

Il y a 105 octets de différence, ce qui correpond à la somme de :
  • la taille de la variable locale
  • l'adresse de retour de la fonction
  • quelques registres que le compilateur a décidé d'empiler par comodité
Il nous reste deux blocs marqués ### STACK DUMP à examiner.

Le premier est affiché juste après avoir affiché notre variable data.

### STACK DUMP
free_stack        = 506
0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55  
...
0x089B: UU.⸮.⸮.. >?.B.... 55 55 04 89 01 BC 06 0E  29 2E 08 38 00 01 00 00  

Une quantité de pile de 506 octets n'a pas été utilisée.
On voit plus bas dans le dump qu'à l'adresse 0x089D le 'U' a disparu.

Tout le reste a déjà été exploité par les différents appels de fonctions, en particulier Serial.begin(), Serial.print() et Serial.println() qui ont déclaré et utilisé des variables locales entre cet emplacement et la fin de la mémoire RAM.

On peut donc dire que 0x08FB - 0x089D = 94 octets ont été utilisés depuis le début de l'exécution, ce qui est assez conséquent. Serial.print() est gourmand.

Le deuxième dump est affiché juste après avoir appelé la fonction func1 (qui elle-même appelle func2). On devrait observer les traces des deux fonctions.

### STACK DUMP
free_stack        = 300
0x06AB: UUUUUUUU UUUUUUUU 55 55 55 55 55 55 55 55  55 55 55 55 55 55 55 55 
...
0x07CB: UUUUUUUU UU.⸮.⸮.. 55 55 55 55 55 55 55 55  55 55 04 89 01 BC 06 0E  

Une quantité de pile de 300 octets n'a pas été utilisée.
On voit plus bas dans le dump qu'à l'adresse 0x07D5 le 'U' a disparu.

On voit également les traces des variables locales des fonctions func1 et func2.
func1 a rempli un buffer de caractères 'B'.
func2 a rempli un buffer de caractères 'C'

0x088B: CC..⸮..B BBBBBBBB 43 43 00 08 91 04 1E 42  42 42 42 42 42 42 42 42 
0x089B: BB.⸮.⸮.. ?..B.... 42 42 04 89 01 BC 06 0E  2A 2F 08 38 00 01 00 00 
0x08AB: #⸮..⸮... ⸮...@.⸮. 23 A3 00 01 DA 00 00 08  E2 00 01 01 40 08 C1 08 


0x081B: ....... .⸮.(.⸮CC 00 03 00 00 7F 00 00 00  02 CD 08 28 03 F2 43 43 
0x082B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 
0x083B: CCCCCCCC CCCCCCCC 43 43 43 43 43 43 43 43  43 43 43 43 43 43 43 43 


Et les traces sont encore visibles aux adresses affichées précédemment :

func1 STACK       = 0X0892
func2 STACK       = 0X0829

Pourquoi les traces du travail de func1 ont-elles en partie disparu ?
Parce que depuis la sortie de func1, d'autres fonctions, en particulier Serial.print() et Serial.println() ont déclaré des variables au même endroit.

Après avoir examiné toutes ces données on peut conclure :

STACK SIZE        = 594
free_stack        = 300 au minimum

La pile a été utilisée à environ 50%, ce qui est un signe de parfaite santé pour notre application.

3. Conclusion

Comme on vient de le voir, le compilateur et l'éditeur de liens mettent à notre disposition toutes les variables nécessaire au diagnostic mémoire d'une application.
Il n'y a qu'à les utiliser.


Cordialement
Henri