Programmation & Électronique - réflexions en désordre

Journalisation à 8 kHz sur un ESP32-S3 en Arduino : exécution du programme depuis la PSRAM (XiP)

NNeil Bowen11 min de lecture

L'IMU du Jupiter passe à du SPI 10MHz sur la prochaine carte pour pouvoir enregistrer à 8kHz, contre les 2kHz qu'il gère actuellement sur I2C. Sur le Rocketry Forum, quelqu'un a soulevé une bonne question : un ESP32-S3 à 240MHz peut-il vraiment « enregistrer » à 8kHz, plutôt que de simplement lire le capteur à cette vitesse ? C'est une bonne question et bien que j'étais confiant que nous pourrions le faire, je ne l'avais pas encore vérifié. Donc, plutôt que de deviner, nous sommes allés le mesurer correctement. Voici ce que nous avons trouvé, et en bas il y a la bibliothèque Arduino reconstruite que nous avons obtenue, si vous voulez l'essayer dans vos propres projets.

Ce n'est vraiment pas la vitesse d'horloge

Ce qui arrête la plupart des projets ESP32 enregistrant rapidement, ce n'est pas le CPU, c'est la mémoire flash. Dans une compilation normale, le programme s'exécute directement depuis la même puce flash sur laquelle vous enregistrez, via un petit cache. Chaque fois que vous écrivez sur cette flash, l'ESP éteint le cache sur les deux cœurs jusqu'à ce que l'écriture se termine, donc tout code qui n'est pas déjà assis dans la RAM interne s'arrête simplement. Pas ralentit, s'arrête. Une écriture de page prend environ une milliseconde, et l'effacement d'un bloc de 64KB peut bloquer tout pendant 100 à 200ms. Pour un ordinateur de vol exécutant une boucle à taux fixe, c'est le vrai problème.

Ce que nous enregistrons

Chaque échantillon IMU est le gyroscope comme trois entiers 16 bits et l'accélération comme trois entiers 24 bits, les accéléromètres haute et basse g fusionnés en une lecture, donc 15 octets par échantillon.

Nous avions initialement prévu de stocker l'accélération fusionnée en tant que flottants, mais les entiers se sont avérés être le meilleur choix. L'unité est exactement 1/2048 g, qui est le pas propre de l'accéléromètre basse g sur sa plage 16g (le 0,488mg de ST est ce nombre arrondi), donc ses lectures correspondent à des nombres entiers et rien de ce que le capteur mesure n'est perdu. Le convertir en g est une division par 2048, qui est exacte, et 24 bits couvrent environ ±4000g, bien plus que les 256g que la plage haute g nécessite. C'est 3 octets par axe plutôt que 4, et contrairement à un flottant, il peut être codé en delta plus tard si nous le souhaitons (plus sur cela ci-dessous).

Tout ce que le Jupiter enregistre d'autre (l'état de vol 500Hz, baromètre, magnétomètre, GNSS, alimentation, etc.) ajoute environ 20kB/s quel que soit le taux IMU, donc plus vite nous allons, plus petite est sa part des données : 40% à 2kHz, un quart à 4kHz et seulement 14% à 8kHz.

Taux IMU Données IMU (kB/s) Autres données (kB/s) Total (kB/s) Avec delta (kB/s)
2kHz 30 20 50 42
4kHz 60 20 80 64
8kHz 120 20 140 108

Le chiffre des autres données est approximatif. La colonne avec delta est notre estimation du total écrit en flash pendant la poussée, où les vibrations du moteur rendent le codage delta moins efficace, seules les données IMU étant codées en delta. Sur le pas de tir et en planeur, ce serait encore moins. Il y a plus de détails sur le codage delta plus loin.

Comment le Jupiter enregistre

  • L'IMU regroupe ses échantillons dans son propre FIFO, donc la boucle ne fait pas 8000 lectures par seconde. Elle s'exécute à 500Hz et extrait tout ce qui attend en une seule fois à chaque passage, ce qui est 16 échantillons par passage à 8kHz.
  • Chaque échantillon brut va dans des tampons en anneau en PSRAM. Rien n'est réorganisé, les anneaux sont des blocs de mémoire fixes avec des positions de lecture et d'écriture, et l'enregistreur marche simplement le long d'eux sur place. Cela signifie aussi que les données de pré-lancement sont déjà là quand le lancement est détecté.
  • Un petit tampon de 4KB en RAM interne regroupe les enregistrements et les écrit dans une partition flash brute dédiée. Cette partition est effacée avant le vol, donc en vol elle ne fait que des écritures de page, pas d'effacement et pas de système de fichiers en travers.
  • Après l'atterrissage, elle compresse tout le vol en un seul fichier, transmis en continu de sorte qu'il n'a jamais besoin de tout le vol en mémoire, quel que soit le taux auquel il a été enregistré.

Tout cela rencontre toujours le problème flash ci-dessus, c'est là que le prochain bit intervient.

Exécuter le programme depuis la PSRAM

L'ESP32-S3 peut copier le programme en PSRAM au démarrage et l'exécuter à partir de là au lieu de la flash (Espressif l'appelle XIP depuis PSRAM). Une fois qu'il le fait, une écriture en flash n'a plus besoin du cache être éteint, donc la boucle continue pendant que la flash est écrite. Le problème pour les utilisateurs d'Arduino est que c'est un paramètre non utilisé dans les bibliothèques précompilées d'Espressif, donc ce n'est pas quelque chose que vous pouvez activer depuis votre sketch. Nous avons donc reconstruit les bibliothèques S3 pour le cœur Arduino avec celui-ci activé.

ECC, parce que les fusées chauffent

En même temps, nous avons activé l'ECC pour la PSRAM. Espressif évalue la PSRAM octale dans le S3R8 à 65C, et 85C avec l'ECC activé. 65C semble beaucoup, mais une fusée sombre assis sur un pas de tir au soleil devient surprenamment chaude à l'intérieur, donc pour un ordinateur de vol, ça semblait être le bon endroit pour commencer. Cela coûte un seizième de la PSRAM, que nous pouvons facilement épargner.
Il est utile de noter que c'est la cote de température ambiante, pas la cote de température interne de la puce. Et comme le Jupiter n'utilise pas de RF (Wifi/BT, etc.), il s'étendra probablement un peu plus loin.

Encore une petite : où vit le tampon d'écriture

Nous avons trouvé que le tampon d'écriture de 4KB a également de l'importance. S'il est en PSRAM, le pilote flash ESP-IDF le copie via un minuscule tampon de 32 octets et écrit chaque 32 octets comme une commande séparée vers la puce flash. S'il est en RAM interne, il le transmet directement, 64 octets par commande, ce que le contrôleur flash du S3 peut prendre au maximum en une seule fois. Chaque commande coûte à peu près la même chose quel que soit ce qu'elle porte, donc la moitié des commandes c'est presque deux fois la vitesse : à plein régime, nous avons mesuré 209kB/s depuis PSRAM et 335kB/s depuis la RAM interne. 4KB de RAM interne, ce n'est rien, donc ce changement était facile.

Le test

Nous avons ajouté un test au firmware qui écrit le tampon de vol de la même façon que le logger de vol le fait tandis que la boucle 500Hz continue de tourner, puis rapporte à quel point la boucle a été retenue. Il écrit à 55kB/s, puis 166kB/s, puis à plein régime, puis efface tout ce qu'il a écrit. Ces deux débits ont été définis à partir de notre plan flottant antérieur (18 octets par échantillon) pour l'enregistrement 2kHz et 8kHz, donc ils sont un peu au-dessus de ce que le format entier nécessite, ce qui rend le test un peu plus difficile que la réalité. Pour être clair sur une chose, l'IMU sur cette carte est toujours sur I2C à 2kHz, donc les phases plus rapides sont la charge flash d'un enregistrement plus rapide, pas un échantillonnage plus rapide. Même carte, même test, seules les bibliothèques Arduino échangées.

Chaque cellule est le cœur Arduino en stock en premier, puis nos bibliothèques reconstruites avec le programme en PSRAM et l'ECC activé. La boucle devrait s'exécuter à 500Hz, un passage tous les 2ms, et l'écart pire est le temps le plus long entre deux passages.

Pendant que la flash est Taux de boucle Écart pire Débordements FIFO
Écriture à 55kB/s 349 → 499Hz 15,8 → 5,4ms 42 → 0
Écriture à 166kB/s 193 → 499Hz 15,0 → 5,2ms 77 → 0
Écriture à plein régime 196 → 499Hz 12,9 → 4,6ms 24 → 0
Effacement (blocs 64KB) 17 → 499Hz 202 → 4,9ms 3 → 0

Sur le cœur en stock, la boucle s'effondre dès que la flash est occupée, elle ne pouvait même pas suivre la phase 166kB/s (elle a réussi 160), et l'effacement l'arrête pendant 200ms. Chaque débordement est le FIFO de l'IMU qui se remplit et des échantillons perdus. Avec le programme en PSRAM, il maintient 499Hz à travers tout, y compris l'effacement. L'écart pire d'environ 5ms est juste le passage le plus lent de sa propre boucle (il le fait sans aucune écriture en flash en cours), le FIFO de l'IMU l'absorbe, et rien n'est perdu.

Codage delta

Le codage delta signifie stocker combien chaque lecture a changé depuis la dernière plutôt que la lecture elle-même. À ces débits, un échantillon est très proche du suivant, donc la plupart des changements tiennent en un ou deux octets plutôt que deux ou trois. C'est là que les entiers font leurs preuves : le changement entre deux entiers est lui-même un petit entier, tandis que les bits d'un flottant ne rétrécissent pas quand la valeur bouge à peine. En pas de 1/2048 g, un changement d'environ 4g maximum entre les échantillons tient en 2 octets.

La quantité économisée dépend de ce que fait la fusée. Sur le pas de tir ou en planeur, la plupart des échantillons tomberaient à 6 à 8 octets, mais pendant la poussée avec le moteur secouant tout, c'est plus comme 11, et la poussée est ce qui compte car c'est là que le taux d'enregistrement est à son plus haut. C'est notre estimation approximative plutôt qu'une mesure pour l'instant. Le Jupiter n'encode pas actuellement le tampon de vol en delta, car il n'en a pas besoin pour la vitesse. Là où il aiderait, c'est l'enregistrement du temps : le tampon de vol de 7,8MB contient environ 58 secondes à plein 8kHz brut, ou environ 75 secondes encodées en delta, avant que l'enregistrement hybride ne réduit le taux.

Alors à quelle vitesse pourrait-il enregistrer ?

En mettant ensemble les vitesses flash mesurées avec les tailles de données ci-dessus, vous obtenez le maximum que chaque configuration pourrait suivre :

Configuration Flash, plein régime (kB/s) Taux max, brut / delta (kHz) Données 8kHz, brut / delta (kB/s et partage du flash)
Cœur en stock, tampon en PSRAM* 160 9 / 12 140 / 108
88% / 68%
Cœur en stock, tampon en RAM interne* 306 19 / 25 140 / 108
46% / 35%
Bibliothèques reconstruites, tampon en PSRAM 209 12 / 17 140 / 108
67% / 52%
Bibliothèques reconstruites, tampon en RAM interne (le Jupiter) 335 21 / 28 140 / 108
42% / 32%

* La flash suit sur le cœur en stock mais la boucle non. Elle est tombée sous 200Hz et a perdu des échantillons IMU à chaque taux que nous avons essayé, donc en pratique elle ne peut pas enregistrer proprement à aucun de ceux-ci.

Brut est 15 octets par échantillon, encodé en delta est notre estimation de poussée de 11 octets, les deux incluent les 20kB/s de tout le reste, et ce sont les vitesses à plein régime mesurées avec la boucle en cours d'exécution, arrondies vers le bas sans rien d'épargner. Le bus SPI 10MHz n'est la limite à aucun de ces points non plus, il pourrait transporter environ 59 000 échantillons par seconde.

Donc 8kHz nécessite environ 140kB/s par rapport aux 335kB/s que la flash prend à plein régime, environ 42% sans aucun codage delta du tout, ou environ 108kB/s (32%) avec celui-ci. Le CPU n'en travaille pas beaucoup non plus : à 2kHz, la plupart de chaque passage de boucle est dépensée en attente du bus I2C, et sur du SPI 10MHz la même lecture à 8kHz devrait prendre environ 0,3ms. L'IMU lui-même plafonne à 8kHz de toute façon, donc le reste est de la marge. C'est toujours des essais sur banc jusqu'à ce que la carte SPI arrive, et les paramètres par défaut seront de toute façon plus bas, l'enregistrement réduisant son taux après la poussée, mais le S3 semble bien à la hauteur.

Essayez-le vous-même

Puisque nous avons fait le travail difficile, les bibliothèques ESP32-S3 reconstruites sont dans les téléchargements en bas de ce post pour quiconque utilisant Arduino. Elles sont pour le cœur esp32 3.3.12 du Boards Manager uniquement, et elles remplacent ce dossier :

Windows  %LOCALAPPDATA%\Arduino15\packages\esp32\tools\esp32s3-libs\3.3.12
Mac      ~/Library/Arduino15/packages/esp32/tools/esp32s3-libs/3.3.12
Linux    ~/.arduino15/packages/esp32/tools/esp32s3-libs/3.3.12

Sur Windows, fermez l'IDE Arduino, puis ouvrez une invite de commande (tapez cmd dans le menu Démarrer, pas PowerShell) et collez ceci dedans. Il trouve le zip dans votre dossier Téléchargements quel que soit le nom que le téléchargement a ajouté, et il conserve votre dossier original comme 3.3.12.stock :

cd /d "%LOCALAPPDATA%\Arduino15\packages\esp32\tools\esp32s3-libs"
ren 3.3.12 3.3.12.stock && for %f in ("%USERPROFILE%\Downloads\*esp32s3-libs-3.3.12-xip-ecc*.zip") do tar -xf "%f"
rmdir /s /q "%LOCALAPPDATA%\arduino\cores"

La dernière ligne efface la copie mise en cache du cœur compilé par l'IDE afin qu'il se récompile par rapport aux nouvelles bibliothèques. Pour revenir au stock :

cd /d "%LOCALAPPDATA%\Arduino15\packages\esp32\tools\esp32s3-libs"
ren 3.3.12 3.3.12.xip-ecc && ren 3.3.12.stock 3.3.12
rmdir /s /q "%LOCALAPPDATA%\arduino\cores"

N'utilisez pas l'option Extraire tout de l'Explorateur sur le zip, certains chemins à l'intérieur sont trop longs et il ignorera silencieusement les fichiers. La commande ci-dessus ou 7-Zip vont bien.

Quelques choses à savoir :

  • Nous l'avons testé sur un S3 avec 8MB de PSRAM octale (le S3R8), avec PSRAM défini sur OPI PSRAM dans le menu Outils. Nous ne l'avons pas essayé sur la PSRAM quad, et comme le programme s'exécute depuis PSRAM, il ne conviendra pas aux cartes sans aucune.
  • Le programme est copié en PSRAM au démarrage, vous perdez donc autant de PSRAM, plus un seizième pour l'ECC.
  • Il est construit avec l'esp32-arduino-lib-builder d'Espressif sur ESP-IDF 5.5.5, le même que les bibliothèques officielles 3.3.12, avec seulement deux paramètres ajoutés. Tout le crédit va à Espressif. C'est officieux et tel quel, alors gardez votre dossier stock.

Si vous préférez le construire vous-même, c'est l'esp32-arduino-lib-builder d'Espressif (Linux ou WSL), vérifié aux versions listées dans le fichier versions.txt dans le dossier des bibliothèques officielles 3.3.12, avec ces deux lignes ajoutées à configs/defconfig.esp32s3 avant d'exécuter la compilation :

CONFIG_SPIRAM_XIP_FROM_PSRAM=y
CONFIG_SPIRAM_ECC_ENABLE=y