MicroPython .mpy-Dateien¶
MicroPython definiert das Konzept einer .mpy-Datei, bei der es sich um ein binäres Containerdateiformat handelt, das vorkompilierten Code enthält und das wie ein normales .py-Modul importiert werden kann. Die Datei foo.mpy kann über import foo importiert werden, solange foo.mpy auf die übliche Weise vom Import-Mechanismus gefunden werden kann. Normalerweise wird jedes in sys.path aufgeführte Verzeichnis der Reihe nach durchsucht. Beim Durchsuchen eines bestimmten Verzeichnisses wird zuerst nach foo.py gesucht, und falls diese nicht gefunden wird, wird nach foo.mpy gesucht; anschließend wird die Suche im nächsten Verzeichnis fortgesetzt, falls keine von beiden gefunden wurde. Somit hat foo.py Vorrang vor foo.mpy.
Diese .mpy-Dateien können bytecode enthalten, der üblicherweise mit dem Programm mpy-cross aus Python-Quelldateien (.py-Dateien) erzeugt wird. Für einige Architekturen kann eine .mpy-Datei auch nativen Maschinencode enthalten, der auf verschiedene Weise erzeugt werden kann, insbesondere aus C-Quellcode.
Der mpy-cross-Compiler¶
mpy-cross ist der Cross-Compiler, der eine .py-Quelldatei in einen .mpy-Binärcontainer umwandelt, der bereit ist, auf der Cam importiert zu werden. Er ist Teil des MicroPython-Quellbaums (desselben, der zum Erstellen der Cam-Firmware verwendet wird) und wird außerdem als pip-Paket für die hostseitige Verwendung ohne vollständiges Firmware-Checkout veröffentlicht:
$ pip install --user mpy-cross
Oder über pipx:
$ pipx install mpy-cross
Nach der Installation rufen Sie es für eine einzelne Quelldatei auf:
$ mpy-cross foo.py
Dies erzeugt foo.mpy im aktuellen Verzeichnis, bereit, neben andere Module auf das Dateisystem der Cam kopiert oder in ein ROMFS-Image eingespeist zu werden.
Die nützlichsten Befehlszeilenoptionen:
-o <path>– Ausgabepfad für die erzeugte.mpy(standardmäßig der Eingabedateiname mit ersetzter Erweiterung;-o -schreibt nach stdout).-O<n>– Optimierungsstufe0bis3. Der Standardwert0behält Assertions und vollständige Quellpositionen bei;3entfernt Assertions und Docstrings und schreibtif __debug__-Blöcke um. Die Stufe steuert dieselbemicropython.opt_level-Schnittstelle, die die Laufzeit bereitstellt.-march=<arch>– native Zielarchitektur für mit@nativeund@viperdekorierte Funktionen. Erforderlich, wenn der Quellcode diese Dekoratoren verwendet. Der Wert muss zur MCU-Klasse der Cam passen: Wählen Sie ihn aus der Liste, diempy-cross --helpausgibt, oder lesen Sie ihn zur Laufzeit von der Cam mitsys.implementation._mpyaus.-s <path>– Quellpfad-Zeichenkette, die in die Debug-Informationen der.mpyeingebettet wird. Nützlich, wenn der Pfad auf der Festplatte vom Importpfad abweicht, unter dem die Datei in Tracebacks erscheinen soll.-X emit=bytecode|native|viper– wählt den Standard-Emitter für das gesamte Modul (eine modulweite Alternative zu den Dekoratoren@native/@viper).--version– gibt die.mpy-Formatversion aus, die diese Binärdatei erzeugt. Diese Nummer muss zur Version passen, die die Laufzeit der Cam unterstützt (siehe Release-Tabelle unten), andernfalls löst der ImportValueError('incompatible .mpy file')aus.
Führen Sie mpy-cross --help für die vollständige Liste der Flags aus.
Das pip-Paket stellt außerdem eine kleine Python-Modul-API bereit, sodass Build-Skripte den Compiler im selben Prozess ansteuern können, anstatt von Hand einen Unterprozess abzuspalten:
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 und mpy_cross.mpy_version sind die drei Einstiegspunkte; mpy_cross.CrossCompileError enthält die stderr-Ausgabe des Compilers, wenn etwas schiefgeht. Die Architekturkonstanten (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP usw.) entsprechen den Zeichenketten, die das -march-Flag akzeptiert.
Versionierung und Kompatibilität von .mpy-Dateien¶
Eine bestimmte .mpy-Datei kann mit einem bestimmten MicroPython-System kompatibel sein oder auch nicht. Die Kompatibilität beruht auf Folgendem:
Version der .mpy-Datei: Die Version der Datei muss zur Version passen, die das ladende System unterstützt.
Unterversion der .mpy-Datei: Wenn die .mpy-Datei nativen Maschinencode enthält, muss die Unterversion der Datei zur Version passen, die das ladende System unterstützt. Andernfalls, wenn kein nativer Maschinencode in der .mpy-Datei enthalten ist, wird die Unterversion beim Laden ignoriert.
Bits für kleine Ganzzahlen: Die .mpy-Datei erfordert eine Mindestanzahl von Bits in einer small integer, und das ladende System muss mindestens diese Anzahl von Bits unterstützen.
Native Architektur: Wenn die .mpy-Datei nativen Maschinencode enthält, gibt sie die Architektur dieses Maschinencodes an, und das ladende System muss die Ausführung von Code dieser Architektur unterstützen.
Wenn ein MicroPython-System den Import von .mpy-Dateien unterstützt, existiert das Feld sys.implementation._mpy und gibt eine Ganzzahl zurück, die die Version (untere 8 Bit), Funktionen und native Architektur kodiert.
Der Versuch, eine .mpy-Datei zu importieren, die einen der ersten vier Tests nicht besteht, löst ValueError('incompatible .mpy file') aus. Der Versuch, eine .mpy-Datei zu importieren, die den Test der nativen Architektur nicht besteht (sofern sie nativen Maschinencode enthält), löst ValueError('incompatible .mpy arch') aus.
Wenn der Import einer .mpy-Datei fehlschlägt, versuchen Sie Folgendes:
Ermitteln Sie die von Ihrem MicroPython-System unterstützte .mpy-Version und -Flags, indem Sie Folgendes ausführen:
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()
Überprüfen Sie die Gültigkeit der .mpy-Datei, indem Sie die ersten beiden Bytes der Datei inspizieren. Das erste Byte sollte ein großgeschriebenes ‚M‘ sein und das zweite Byte die Versionsnummer, die mit der oben ermittelten Systemversion übereinstimmen sollte. Falls sie nicht übereinstimmt, erstellen Sie die .mpy-Datei neu.
Prüfen Sie, ob die .mpy-Version des Systems mit der von
mpy-crosserzeugten Version übereinstimmt, das zum Erstellen der .mpy-Datei verwendet wurde, ermittelt übermpy-cross --version. Falls sie nicht übereinstimmt, kompilieren Siempy-crossneu aus dem Git-Repository, das auf dem vonmpy-cross --versiongemeldeten Tag (oder Hash) ausgecheckt ist.Stellen Sie sicher, dass Sie die korrekten
mpy-cross-Flags verwenden, die durch den obigen Code ermittelt werden oder durch Inspektion der Makefile-VariablenMPY_CROSS_FLAGSfür den von Ihnen verwendeten Port.Wenn im dritten Byte der .mpy-Datei Bit #6 gesetzt ist, prüfen Sie, ob die kodierten architekturspezifischen Flag-Bits (vuint) mit dem Ziel kompatibel sind, auf dem Sie die Datei importieren.
Die folgende Tabelle zeigt die Zuordnung zwischen MicroPython-Release und .mpy-Version.
MicroPython-Release |
.mpy-Version |
|---|---|
v1.23.0 und höher |
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 |
Der Vollständigkeit halber zeigt die nächste Tabelle den Git-Commit des MicroPython-Haupt-Repositorys, bei dem die .mpy-Version geändert wurde.
.mpy-Versionsänderung |
Git-Commit |
|---|---|
6.2 zu 6.3 |
bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b |
6.1 zu 6.2 |
6967ff3c581a66f73e9f3d78975f47528db39980 |
6 zu 6.1 |
d94141e1473aebae0d3c63aeaa8397651ad6fa01 |
5 zu 6 |
f2040bfc7ee033e48acef9f289790f3b4e6b74e5 |
4 zu 5 |
5716c5cf65e9b2cb46c2906f40302401bdd27517 |
3 zu 4 |
9a5f92ea72754c01cc03e5efcdfe94021120531e |
2 zu 3 |
ff93fd4f50321c6190e1659b19e64fef3045a484 |
1 zu 2 |
dd11af209d226b7d18d5148b239662e30ed60bad |
0 zu 1 |
6a11048af1d01c78bdacddadd1b72dc7ba7c6478 |
ursprüngliche Version 0 |
d8c834c95d506db979ec871417de90b7951edc30 |
Binäre Kodierung von .mpy-Dateien¶
MicroPython .mpy-Dateien sind ein binäres Containerformat, in dem Code-Objekte (Bytecode und nativer Maschinencode) intern in einer verschachtelten Hierarchie gespeichert werden. Der Code für das äußere Modul wird zuerst gespeichert, gefolgt von dessen Kindern. Jedes Kind kann weitere Kinder haben, beispielsweise im Fall einer Klasse mit Methoden oder einer Funktion, die ein Lambda oder eine Comprehension definiert. Um Dateien klein zu halten und gleichzeitig einen großen Wertebereich abzudecken, verwendet es an vielen Stellen das Konzept einer variabel kodierten vorzeichenlosen Ganzzahl (vuint). Ähnlich wie bei der UTF-8-Kodierung speichert diese Kodierung 7 Bit pro Byte, wobei das 8. Bit (MSB) gesetzt ist, wenn ein oder mehrere Bytes folgen. Die Bits der vorzeichenlosen Ganzzahl werden in der vuint in LSB-Form gespeichert.
Die oberste Ebene einer .mpy-Datei besteht aus drei Teilen:
Dem Header.
Den globalen qstr- und Konstantentabellen.
Dem Raw-Code für den äußeren Geltungsbereich des Moduls. Dieser äußere Geltungsbereich wird ausgeführt, wenn die .mpy-Datei importiert wird.
Sie können den Inhalt einer .mpy-Datei mit mpy-tool.py inspizieren, zum Beispiel (ausgeführt aus dem Wurzelverzeichnis des MicroPython-Haupt-Repositorys):
$ ./tools/mpy-tool.py -xd myfile.mpy
Der Header¶
Der .mpy-Header lautet:
Größe |
Feld |
|---|---|
Byte |
Wert 0x4d (ASCII ‚M‘) |
Byte |
.mpy-Hauptversionsnummer |
Byte |
Feature-Flags, native Architektur, Nebenversionsnummer (in älteren Versionen waren dies Feature-Flags) |
Byte |
Anzahl der Bits in einem kleinen Integer |
Das dritte Byte ist wie folgt aufgeteilt (MSB zuerst):
Bit |
Bedeutung |
|---|---|
7 |
reserviert, muss 0 sein |
6 |
auf den Header folgt eine architekturspezifische Flag-vuint |
5..2 |
Nummer der nativen Architektur |
1..0 |
Nebenversionsnummer |
Architekturspezifische Flags¶
Wenn Bit #6 des Feature-Flags-Bytes des Headers gesetzt ist, folgt auf den Header eine vuint, die optionale architekturspezifische Informationen enthält. Der Inhalt dieser Ganzzahl hängt davon ab, für welche native Architektur die Datei bestimmt ist.
Dies wird derzeit verwendet, um zu speichern, welche RISC-V-Prozessorerweiterungen die MPY-Datei zur korrekten Funktion benötigt, abgesehen von I, M, C und Zicsr. Verschiedene Varianten von ArmV7 werden durch ihre Nummer der nativen Architektur identifiziert, aber die Wiederverwendung dieses Mechanismus würde die Dinge für RV32 und RV64 verkomplizieren.
MPY-Dateien, die auf RV32 oder RV64 abzielen und keine bestimmten Prozessorerweiterungen benötigen, müssen keine Flags-Ganzzahl bereitstellen (zusammen mit dem Setzen des entsprechenden Bits im Header). Das Fehlen eines Flags-Werts für RV32- und RV64-MPY-Dateien wird verwendet, um anzuzeigen, dass keine spezifischen Erweiterungen benötigt werden, und spart ein Byte in der endgültigen Ausgabe-Binärdatei.
Siehe auch die Befehlszeilenoption -march-flags sowohl in mpy-tool.py als auch in mpy-cross sowie die Befehlszeilenoption --arch-flags in mpy_ld.py, um diesen Wert beim Erstellen von MPY-Dateien zu setzen.
Die globalen qstr- und Konstantentabellen¶
Eine .mpy-Datei enthält eine einzige qstr-Tabelle und eine einzige Konstantenobjekttabelle. Diese sind global für die .mpy-Datei und werden von allen verschachtelten Raw-Code-Objekten referenziert. Die qstr-Tabelle ordnet die interne qstr-Nummer (intern zur .mpy-Datei) der aufgelösten qstr-Nummer der Laufzeit zu, in die die .mpy-Datei importiert wird. Dies verknüpft die .mpy-Datei mit dem übrigen System, in dem sie ausgeführt wird. Die Konstantenobjekttabelle wird mit Referenzen auf alle Konstantenobjekte gefüllt, die die .mpy-Datei benötigt.
Größe |
Feld |
|---|---|
vuint |
Anzahl der qstrs |
vuint |
Anzahl der Konstantenobjekte |
… |
qstr-Daten |
… |
kodierte Konstantenobjekte |
Raw-Code-Elemente¶
Ein Raw-Code-Element enthält Code, entweder Bytecode oder nativen Maschinencode. Sein Inhalt ist:
Größe |
Feld |
|---|---|
vuint |
Typ, Größe und ob es Sub-Raw-Code-Elemente gibt |
… |
Code (Bytecode oder Maschinencode) |
vuint |
Anzahl der Sub-Raw-Code-Elemente (nur wenn ungleich null) |
… |
Sub-Raw-Code-Elemente |
Die erste vuint in einem Raw-Code-Element kodiert den Typ des in diesem Element gespeicherten Codes (die beiden niederwertigsten Bits), ob dieser Raw-Code Kinder hat (das drittniederwertigste Bit) und die Länge des folgenden Codes (die Menge an RAM, die dafür zu allozieren ist).
Auf die vuint folgt der Code selbst. Sofern der Codetyp nicht Viper-Code mit Relocations ist, sind diese Code-Daten konstant und müssen nicht modifiziert werden.
Wenn dieser Raw-Code Kinder hat (wie durch ein Bit in der ersten vuint angezeigt), folgt nach dem Code eine vuint, die die Anzahl der Sub-Raw-Code-Elemente zählt.
Schließlich werden alle Sub-Raw-Code-Elemente rekursiv gespeichert.