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> – Optimierungsstufe 0 bis 3. Der Standardwert 0 behält Assertions und vollständige Quellpositionen bei; 3 entfernt Assertions und Docstrings und schreibt if __debug__-Blöcke um. Die Stufe steuert dieselbe micropython.opt_level-Schnittstelle, die die Laufzeit bereitstellt.

  • -march=<arch> – native Zielarchitektur für mit @native und @viper dekorierte Funktionen. Erforderlich, wenn der Quellcode diese Dekoratoren verwendet. Der Wert muss zur MCU-Klasse der Cam passen: Wählen Sie ihn aus der Liste, die mpy-cross --help ausgibt, oder lesen Sie ihn zur Laufzeit von der Cam mit sys.implementation._mpy aus.

  • -s <path> – Quellpfad-Zeichenkette, die in die Debug-Informationen der .mpy eingebettet 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 Import ValueError('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-cross erzeugten Version übereinstimmt, das zum Erstellen der .mpy-Datei verwendet wurde, ermittelt über mpy-cross --version. Falls sie nicht übereinstimmt, kompilieren Sie mpy-cross neu aus dem Git-Repository, das auf dem von mpy-cross --version gemeldeten 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-Variablen MPY_CROSS_FLAGS fü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.