Fichiers .mpy de MicroPython¶
MicroPython définit le concept de fichier .mpy, un format de fichier conteneur binaire qui contient du code précompilé et qui peut être importé comme un module .py normal. Le fichier foo.mpy peut être importé via import foo, tant que foo.mpy peut être trouvé de la manière habituelle par le mécanisme d’importation. Habituellement, chaque répertoire listé dans sys.path est parcouru dans l’ordre. Lors de la recherche dans un répertoire donné, foo.py est recherché en premier et, s’il n’est pas trouvé, foo.mpy est ensuite recherché, puis la recherche se poursuit dans le répertoire suivant si aucun des deux n’est trouvé. Ainsi, foo.py a la priorité sur foo.mpy.
Ces fichiers .mpy peuvent contenir du bytecode qui est généralement généré à partir de fichiers source Python (fichiers .py) via le programme mpy-cross. Pour certaines architectures, un fichier .mpy peut également contenir du code machine natif, qui peut être généré de diverses manières, notamment à partir de code source C.
Le compilateur mpy-cross¶
mpy-cross est le compilateur croisé qui transforme un fichier source .py en un conteneur binaire .mpy prêt à être importé sur la caméra. Il fait partie de l’arborescence des sources de MicroPython (la même que celle utilisée pour compiler le micrologiciel de la caméra) et est également publié sous forme de paquet pip pour une utilisation côté hôte sans extraction complète du micrologiciel :
$ pip install --user mpy-cross
Ou via pipx :
$ pipx install mpy-cross
Une fois installé, invoquez-le sur un fichier source unique :
$ mpy-cross foo.py
Cela produit foo.mpy dans le répertoire courant, prêt à être copié sur le système de fichiers de la caméra à côté d’autres modules ou à être intégré dans une image ROMFS.
Les options de ligne de commande les plus utiles :
-o <path>– chemin de sortie pour le fichier.mpygénéré (par défaut, le nom du fichier d’entrée avec l’extension remplacée ;-o -écrit sur la sortie standard).-O<n>– niveau d’optimisation de0à3. La valeur par défaut0préserve les assertions et l’intégralité des emplacements source ;3supprime les assertions et les docstrings et réécrit les blocsif __debug__. Le niveau contrôle la même interfacemicropython.opt_levelque celle exposée par l’environnement d’exécution.-march=<arch>– architecture native cible pour les fonctions décorées par@nativeet@viper. Requis lorsque la source utilise ces décorateurs. La valeur doit correspondre à la classe de microcontrôleur de la caméra : choisissez-la dans la liste affichée parmpy-cross --help, ou lisez-la sur la caméra à l’exécution avecsys.implementation._mpy.-s <path>– chaîne de chemin source intégrée dans les informations de débogage du.mpy. Utile lorsque le chemin sur disque diffère du chemin d’importation sous lequel le fichier doit apparaître dans les traces de pile.-X emit=bytecode|native|viper– choisit l’émetteur par défaut pour l’ensemble du module (une alternative par fonction aux décorateurs@native/@viper).--version– affiche la version du format.mpyque ce binaire émet. Ce numéro doit correspondre à la version prise en charge par l’environnement d’exécution de la caméra (voir le tableau des versions ci-dessous), sinon l’importation lèveraValueError('incompatible .mpy file').
Exécutez mpy-cross --help pour la liste complète des options.
Le paquet pip expose également une petite API de module Python afin que les scripts de compilation puissent piloter le compilateur dans le processus plutôt que de lancer manuellement un sous-processus
import mpy_cross
mpy_cross.compile('foo.py', dest='build/foo.mpy', opt=3,
march=mpy_cross.NATIVE_ARCH_ARMV7EMSP)
mpy_cross.compile, mpy_cross.run et mpy_cross.mpy_version sont les trois points d’entrée ; mpy_cross.CrossCompileError transporte la sortie d’erreur du compilateur en cas de problème. Les constantes d’architecture (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP, etc.) correspondent aux chaînes acceptées par l’option -march.
Gestion des versions et compatibilité des fichiers .mpy¶
Un fichier .mpy donné peut être ou non compatible avec un système MicroPython donné. La compatibilité repose sur les éléments suivants :
Version du fichier .mpy : la version du fichier doit correspondre à la version prise en charge par le système qui le charge.
Sous-version du fichier .mpy : si le fichier .mpy contient du code machine natif, alors la sous-version du fichier doit correspondre à la version prise en charge par le système qui le charge. Sinon, s’il n’y a pas de code machine natif dans le fichier .mpy, la sous-version est ignorée lors du chargement.
Bits d’entier court : le fichier .mpy nécessitera un nombre minimal de bits dans un small integer et le système qui le charge doit prendre en charge au moins ce nombre de bits.
Architecture native : si le fichier .mpy contient du code machine natif, il spécifiera l’architecture de ce code machine et le système qui le charge doit prendre en charge l’exécution du code de cette architecture.
Si un système MicroPython prend en charge l’importation de fichiers .mpy, alors le champ sys.implementation._mpy existera et renverra un entier qui encode la version (les 8 bits de poids faible), les fonctionnalités et l’architecture native.
Tenter d’importer un fichier .mpy qui échoue à l’un des quatre premiers tests lèvera ValueError('incompatible .mpy file'). Tenter d’importer un fichier .mpy qui échoue au test d’architecture native (s’il contient du code machine natif) lèvera ValueError('incompatible .mpy arch').
Si l’importation d’un fichier .mpy échoue, essayez ce qui suit :
Déterminez la version .mpy et les indicateurs pris en charge par votre système MicroPython en exécutant
import sys sys_mpy = sys.implementation._mpy arch = [None, 'x86', 'x64', 'armv6', 'armv6m', 'armv7m', 'armv7em', 'armv7emsp', 'armv7emdp', 'xtensa', 'xtensawin', 'rv32imc', 'rv64imc'][(sys_mpy >> 10) & 0x0F] print('mpy version:', sys_mpy & 0xff) print('mpy sub-version:', sys_mpy >> 8 & 3) print('mpy flags:', end='') if arch: print(' -march=' + arch, end='') if (sys_mpy >> 16) != 0: print(' -march-flags=' + (sys_mpy >> 16), end='') print()
Vérifiez la validité du fichier .mpy en inspectant les deux premiers octets du fichier. Le premier octet devrait être un “M” majuscule et le deuxième octet sera le numéro de version, qui devrait correspondre à la version du système indiquée ci-dessus. S’il ne correspond pas, recompilez le fichier .mpy.
Vérifiez si la version .mpy du système correspond à la version émise par le
mpy-crossqui a été utilisé pour compiler le fichier .mpy, obtenue parmpy-cross --version. Si elle ne correspond pas, recompilezmpy-crossà partir du dépôt Git extrait au tag (ou au hash) indiqué parmpy-cross --version.Assurez-vous d’utiliser les bons indicateurs
mpy-cross, trouvés par le code ci-dessus, ou en inspectant la variable MakefileMPY_CROSS_FLAGSpour le port que vous utilisez.Si le bit #6 du troisième octet du fichier .mpy est activé, vérifiez si le vuint des bits d’indicateurs spécifiques à l’architecture encodés est compatible avec la cible sur laquelle vous importez le fichier.
Le tableau suivant montre la correspondance entre la version de MicroPython et la version .mpy.
Version de MicroPython |
version .mpy |
|---|---|
v1.23.0 et ultérieures |
6.3 |
v1.22.x |
6.2 |
v1.20 - v1.21.0 |
6.1 |
v1.19.x |
6 |
v1.12 - v1.18 |
5 |
v1.11 |
4 |
v1.9.3 - v1.10 |
3 |
v1.9 - v1.9.2 |
2 |
v1.5.1 - v1.8.7 |
0 |
Par souci d’exhaustivité, le tableau suivant montre le commit Git du dépôt principal de MicroPython auquel la version .mpy a été modifiée.
changement de version .mpy |
Commit Git |
|---|---|
6.2 vers 6.3 |
bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b |
6.1 vers 6.2 |
6967ff3c581a66f73e9f3d78975f47528db39980 |
6 vers 6.1 |
d94141e1473aebae0d3c63aeaa8397651ad6fa01 |
5 vers 6 |
f2040bfc7ee033e48acef9f289790f3b4e6b74e5 |
4 vers 5 |
5716c5cf65e9b2cb46c2906f40302401bdd27517 |
3 vers 4 |
9a5f92ea72754c01cc03e5efcdfe94021120531e |
2 vers 3 |
ff93fd4f50321c6190e1659b19e64fef3045a484 |
1 vers 2 |
dd11af209d226b7d18d5148b239662e30ed60bad |
0 vers 1 |
6a11048af1d01c78bdacddadd1b72dc7ba7c6478 |
version initiale 0 |
d8c834c95d506db979ec871417de90b7951edc30 |
Encodage binaire des fichiers .mpy¶
Les fichiers .mpy de MicroPython sont un format de conteneur binaire dans lequel les objets de code (bytecode et code machine natif) sont stockés en interne dans une hiérarchie imbriquée. Le code du module externe est stocké en premier, puis ses enfants suivent. Chaque enfant peut avoir d’autres enfants, par exemple dans le cas d’une classe ayant des méthodes, ou d’une fonction définissant une lambda ou une compréhension. Pour conserver des fichiers de petite taille tout en offrant une large plage de valeurs possibles, il utilise à de nombreux endroits le concept d’entier non signé à encodage variable (vuint). Comme l’encodage UTF-8, cet encodage stocke 7 bits par octet, le 8e bit (MSB) étant activé si un ou plusieurs octets suivent. Les bits de l’entier non signé sont stockés dans le vuint sous forme LSB.
Le niveau supérieur d’un fichier .mpy se compose de trois parties :
L’en-tête.
Les tables globales de qstr et de constantes.
Le raw-code de la portée externe du module. Cette portée externe est exécutée lorsque le fichier .mpy est importé.
Vous pouvez inspecter le contenu d’un fichier .mpy en utilisant mpy-tool.py, par exemple (à exécuter depuis la racine du dépôt principal de MicroPython)
$ ./tools/mpy-tool.py -xd myfile.mpy
L’en-tête¶
L’en-tête .mpy est :
taille |
champ |
|---|---|
octet |
valeur 0x4d (ASCII “M”) |
octet |
numéro de version majeure .mpy |
octet |
indicateurs de fonctionnalités, architecture native, numéro de version mineure (était les indicateurs de fonctionnalités dans les versions antérieures) |
octet |
nombre de bits dans un entier court |
Le troisième octet est divisé comme suit (MSB en premier) :
bit |
signification |
|---|---|
7 |
réservé, doit être 0 |
6 |
un vuint d’indicateurs spécifiques à l’architecture suit l’en-tête |
5..2 |
numéro d’architecture native |
1..0 |
numéro de version mineure |
Indicateurs spécifiques à l’architecture¶
Si le bit #6 de l’octet des indicateurs de fonctionnalités de l’en-tête est activé, alors un vuint contenant des informations spécifiques à l’architecture facultatives suivra l’en-tête. Le contenu de cet entier dépend de l’architecture native à laquelle le fichier est destiné.
Ceci est actuellement utilisé pour stocker quelles extensions de processeur RISC-V le fichier MPY nécessite pour fonctionner correctement, en plus de I, M, C et Zicsr. Les différentes variantes d’ArmV7 sont identifiées par leur numéro d’architecture native, mais réutiliser ce mécanisme compliquerait les choses pour RV32 et RV64.
Les fichiers MPY ciblant RV32 ou RV64 qui n’ont besoin d’aucune extension de processeur particulière n’ont pas besoin de fournir un entier d’indicateurs (en plus d’activer le bit approprié dans l’en-tête). L’absence d’une valeur d’indicateurs pour les fichiers MPY RV32 et RV64 est utilisée pour indiquer qu’aucune extension spécifique n’est nécessaire, et économise un octet dans le binaire de sortie final.
Voir aussi l’option de ligne de commande -march-flags dans mpy-tool.py et mpy-cross, ainsi que l’option de ligne de commande --arch-flags dans mpy_ld.py pour définir cette valeur lors de la création de fichiers MPY.
Les tables globales de qstr et de constantes¶
Un fichier .mpy contient une seule table de qstr et une seule table d’objets constants. Celles-ci sont globales au fichier .mpy, elles sont référencées par tous les objets raw-code imbriqués. La table de qstr fait correspondre le numéro de qstr interne (interne au fichier .mpy) au numéro de qstr résolu de l’environnement d’exécution dans lequel le fichier .mpy est importé. Cela relie le fichier .mpy au reste du système dans lequel il s’exécute. La table d’objets constants est remplie de références à tous les objets constants dont le fichier .mpy a besoin.
taille |
champ |
|---|---|
vuint |
nombre de qstrs |
vuint |
nombre d’objets constants |
… |
données qstr |
… |
objets constants encodés |
Éléments raw-code¶
Un élément raw-code contient du code, soit du bytecode, soit du code machine natif. Son contenu est :
taille |
champ |
|---|---|
vuint |
type, taille et présence d’éléments sub-raw-code |
… |
code (bytecode ou code machine) |
vuint |
nombre d’éléments sub-raw-code (uniquement si non nul) |
… |
éléments sub-raw-code |
Le premier vuint d’un élément raw-code encode le type de code stocké dans cet élément (les deux bits de poids faible), si ce raw-code a des enfants (le troisième bit de poids faible), et la longueur du code qui suit (la quantité de RAM à allouer pour celui-ci).
Après le vuint vient le code lui-même. Sauf si le type de code est du code viper avec relocalisations, ce code est constitué de données constantes et n’a pas besoin d’être modifié.
Si ce raw-code a des enfants (comme indiqué par un bit dans le premier vuint), après le code vient un vuint comptant le nombre d’éléments sub-raw-code.
Enfin, tous les éléments sub-raw-code sont stockés, de manière récursive.