Pliki .mpy w MicroPython

MicroPython definiuje koncepcję pliku .mpy, który jest binarnym formatem kontenera przechowującym prekompilowany kod i który można importować jak zwykły moduł .py. Plik foo.mpy można zaimportować za pomocą import foo, o ile foo.mpy da się znaleźć w zwykły sposób przez mechanizm importu. Zazwyczaj każdy katalog wymieniony w sys.path jest przeszukiwany po kolei. Podczas przeszukiwania konkretnego katalogu najpierw poszukiwany jest foo.py, a jeśli go nie znaleziono, to poszukiwany jest foo.mpy; następnie, jeśli żaden z nich nie zostanie znaleziony, przeszukiwanie kontynuowane jest w kolejnym katalogu. W związku z tym foo.py ma pierwszeństwo przed foo.mpy.

Pliki .mpy mogą zawierać bytecode, który zazwyczaj generowany jest z plików źródłowych Pythona (plików .py) za pomocą programu mpy-cross. W przypadku niektórych architektur plik .mpy może również zawierać natywny kod maszynowy, który można wygenerować na różne sposoby, w szczególności z kodu źródłowego w C.

Kompilator mpy-cross

mpy-cross to kompilator skrośny, który zamienia plik źródłowy .py na binarny kontener .mpy gotowy do zaimportowania na kamerze. Jest częścią drzewa źródeł MicroPython (tego samego, którego używa się do budowy oprogramowania układowego kamery) i jest też publikowany jako pakiet pip do użycia po stronie hosta bez pełnego pobrania źródeł oprogramowania układowego:

$ pip install --user mpy-cross

Lub za pomocą pipx:

$ pipx install mpy-cross

Po zainstalowaniu wywołaj go na pojedynczym pliku źródłowym:

$ mpy-cross foo.py

Tworzy to foo.mpy w bieżącym katalogu, gotowy do skopiowania na system plików kamery obok innych modułów lub do wykorzystania w obrazie ROMFS.

Najbardziej przydatne opcje wiersza poleceń:

  • -o <path> – ścieżka wyjściowa dla wygenerowanego pliku .mpy (domyślnie nazwa pliku wejściowego z zamienionym rozszerzeniem; -o - zapisuje na stdout).

  • -O<n> – poziom optymalizacji od 0 do 3. Domyślny 0 zachowuje asercje i pełne informacje o położeniu w kodzie źródłowym; 3 usuwa asercje oraz docstringi i przepisuje bloki if __debug__. Poziom kontroluje tę samą powierzchnię micropython.opt_level, którą udostępnia środowisko uruchomieniowe.

  • -march=<arch> – docelowa architektura natywna dla funkcji opatrzonych dekoratorami @native i @viper. Wymagane, gdy źródło używa tych dekoratorów. Wartość musi odpowiadać klasie MCU kamery: wybierz ją z listy, którą wypisuje mpy-cross --help, lub odczytaj ją z kamery w czasie wykonania za pomocą sys.implementation._mpy.

  • -s <path> – łańcuch ze ścieżką źródłową osadzony w informacjach debugowania pliku .mpy. Przydatne, gdy ścieżka na dysku różni się od ścieżki importu, pod którą plik powinien być widoczny w śladach wywołań (tracebackach).

  • -X emit=bytecode|native|viper – wybiera domyślny emiter dla całego modułu (alternatywa na poziomie całego modułu wobec dekoratorów @native / @viper na poziomie poszczególnych funkcji).

  • --version – wypisuje wersję formatu .mpy, którą emituje ten plik binarny. Liczba ta musi odpowiadać wersji obsługiwanej przez środowisko uruchomieniowe kamery (zobacz tabelę wydań poniżej), w przeciwnym razie import wywoła ValueError('incompatible .mpy file').

Uruchom mpy-cross --help, aby uzyskać pełną listę flag.

Pakiet pip udostępnia również niewielkie API modułu Pythona, dzięki czemu skrypty budujące mogą sterować kompilatorem wewnątrz procesu zamiast ręcznie rozwidlać podproces:

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 i mpy_cross.mpy_version to trzy punkty wejścia; mpy_cross.CrossCompileError przenosi treść stderr kompilatora, gdy coś pójdzie nie tak. Stałe architektury (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP itd.) odpowiadają łańcuchom akceptowanym przez flagę -march.

Wersjonowanie i zgodność plików .mpy

Dany plik .mpy może, ale nie musi być zgodny z danym systemem MicroPython. Zgodność opiera się na następujących elementach:

  • Wersja pliku .mpy: wersja pliku musi odpowiadać wersji obsługiwanej przez system, który go ładuje.

  • Podwersja pliku .mpy: jeśli plik .mpy zawiera natywny kod maszynowy, to podwersja pliku musi odpowiadać wersji obsługiwanej przez system, który go ładuje. W przeciwnym razie, jeśli w pliku .mpy nie ma natywnego kodu maszynowego, podwersja jest ignorowana podczas ładowania.

  • Bity małej liczby całkowitej: plik .mpy będzie wymagał minimalnej liczby bitów w small integer, a system, który go ładuje, musi obsługiwać co najmniej tyle bitów.

  • Architektura natywna: jeśli plik .mpy zawiera natywny kod maszynowy, to określi architekturę tego kodu maszynowego, a system, który go ładuje, musi obsługiwać wykonywanie kodu tej architektury.

Jeśli system MicroPython obsługuje importowanie plików .mpy, to pole sys.implementation._mpy będzie istnieć i zwróci liczbę całkowitą, która koduje wersję (dolne 8 bitów), cechy oraz architekturę natywną.

Próba zaimportowania pliku .mpy, który nie przejdzie jednego z pierwszych czterech testów, wywoła ValueError('incompatible .mpy file'). Próba zaimportowania pliku .mpy, który nie przejdzie testu architektury natywnej (jeśli zawiera natywny kod maszynowy), wywoła ValueError('incompatible .mpy arch').

Jeśli importowanie pliku .mpy się nie powiedzie, spróbuj wykonać następujące czynności:

  • Określ wersję .mpy i flagi obsługiwane przez Twój system MicroPython, wykonując:

    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()
    
  • Sprawdź poprawność pliku .mpy, badając pierwsze dwa bajty pliku. Pierwszy bajt powinien być wielką literą „M”, a drugi bajt będzie numerem wersji, który powinien odpowiadać wersji systemu ustalonej powyżej. Jeśli się nie zgadza, przebuduj plik .mpy.

  • Sprawdź, czy wersja .mpy systemu odpowiada wersji emitowanej przez mpy-cross użyty do zbudowania pliku .mpy, którą można ustalić za pomocą mpy-cross --version. Jeśli się nie zgadza, ponownie skompiluj mpy-cross z repozytorium Git wypisanego (checkout) przy tagu (lub haszu) raportowanym przez mpy-cross --version.

  • Upewnij się, że używasz właściwych flag mpy-cross, ustalonych za pomocą powyższego kodu lub przez sprawdzenie zmiennej Makefile MPY_CROSS_FLAGS dla używanego portu.

  • Jeśli trzeci bajt pliku .mpy ma ustawiony bit nr 6, sprawdź, czy zakodowana liczba vuint z flagami specyficznymi dla architektury jest zgodna z celem, na którym importujesz plik.

Poniższa tabela pokazuje odpowiedniość między wydaniem MicroPython a wersją .mpy.

Wydanie MicroPython

wersja .mpy

v1.23.0 i nowsze

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

Dla kompletności kolejna tabela pokazuje commit Git głównego repozytorium MicroPython, przy którym zmieniono wersję .mpy.

zmiana wersji .mpy

commit Git

6.2 do 6.3

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 do 6.2

6967ff3c581a66f73e9f3d78975f47528db39980

6 do 6.1

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 do 6

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 do 5

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 do 4

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 do 3

ff93fd4f50321c6190e1659b19e64fef3045a484

1 do 2

dd11af209d226b7d18d5148b239662e30ed60bad

0 do 1

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

wersja początkowa 0

d8c834c95d506db979ec871417de90b7951edc30

Kodowanie binarne plików .mpy

Pliki .mpy w MicroPython są binarnym formatem kontenera z obiektami kodu (bytecode i natywny kod maszynowy) przechowywanymi wewnętrznie w zagnieżdżonej hierarchii. Najpierw przechowywany jest kod modułu zewnętrznego, a następnie jego elementy potomne. Każdy element potomny może mieć kolejne elementy potomne, na przykład w przypadku klasy posiadającej metody lub funkcji definiującej wyrażenie lambda albo wyrażenie listowe. Aby pliki pozostawały małe, a jednocześnie zapewniały szeroki zakres możliwych wartości, w wielu miejscach używa się koncepcji liczby całkowitej bez znaku o zmiennym kodowaniu (vuint). Podobnie jak w kodowaniu UTF-8, kodowanie to przechowuje 7 bitów na bajt, przy czym 8. bit (MSB) jest ustawiony, jeśli następuje jeden lub więcej bajtów. Bity liczby całkowitej bez znaku są przechowywane w vuint w postaci LSB.

Najwyższy poziom pliku .mpy składa się z trzech części:

  • Nagłówek.

  • Globalne tabele qstr i stałych.

  • Surowy kod (raw-code) dla zewnętrznego zakresu modułu. Ten zewnętrzny zakres jest wykonywany podczas importowania pliku .mpy.

Zawartość pliku .mpy można zbadać za pomocą mpy-tool.py, na przykład (uruchomione z katalogu głównego głównego repozytorium MicroPython):

$ ./tools/mpy-tool.py -xd myfile.mpy

Nagłówek

Nagłówek .mpy to:

rozmiar

pole

bajt

wartość 0x4d (ASCII „M”)

bajt

główny numer wersji .mpy

bajt

flagi cech, architektura natywna, poboczny numer wersji (we wcześniejszych wersjach były to flagi cech)

bajt

liczba bitów w małej liczbie całkowitej

Trzeci bajt jest podzielony w następujący sposób (najpierw MSB):

bit

znaczenie

7

zarezerwowane, musi być 0

6

po nagłówku następuje vuint z flagami specyficznymi dla architektury

5..2

numer architektury natywnej

1..0

poboczny numer wersji

Flagi specyficzne dla architektury

Jeśli bit nr 6 bajtu flag cech w nagłówku jest ustawiony, to po nagłówku nastąpi vuint zawierający opcjonalne informacje specyficzne dla architektury. Zawartość tej liczby całkowitej zależy od tego, dla której architektury natywnej przeznaczony jest plik.

Jest to obecnie używane do przechowywania informacji o tym, które rozszerzenia procesora RISC-V są potrzebne plikowi MPY do poprawnego działania, poza I, M, C oraz Zicsr. Różne odmiany ArmV7 są identyfikowane przez ich numer architektury natywnej, ale ponowne wykorzystanie tego mechanizmu skomplikowałoby sprawy dla RV32 i RV64.

Pliki MPY przeznaczone dla RV32 lub RV64, które nie potrzebują żadnych szczególnych rozszerzeń procesora, nie muszą dostarczać liczby całkowitej z flagami (przy odpowiednim ustawieniu bitu w nagłówku). Brak wartości flag dla plików MPY RV32 i RV64 oznacza, że nie są potrzebne żadne specyficzne rozszerzenia, i oszczędza jeden bajt w końcowym pliku wynikowym.

Zobacz także opcję wiersza poleceń -march-flags zarówno w mpy-tool.py, jak i w mpy-cross, oraz opcję wiersza poleceń --arch-flags w mpy_ld.py, aby ustawić tę wartość podczas tworzenia plików MPY.

Globalne tabele qstr i stałych

Plik .mpy zawiera jedną tabelę qstr i jedną tabelę obiektów stałych. Są one globalne dla pliku .mpy i odwołują się do nich wszystkie zagnieżdżone obiekty surowego kodu (raw-code). Tabela qstr mapuje wewnętrzny numer qstr (wewnętrzny dla pliku .mpy) na rozwiązany numer qstr środowiska uruchomieniowego, do którego plik .mpy jest importowany. Łączy to plik .mpy z resztą systemu, w którym jest on wykonywany. Tabela obiektów stałych jest wypełniana odwołaniami do wszystkich obiektów stałych, których potrzebuje plik .mpy.

rozmiar

pole

vuint

liczba qstr

vuint

liczba obiektów stałych

dane qstr

zakodowane obiekty stałych

Elementy surowego kodu (raw code)

Element surowego kodu (raw-code) zawiera kod, czyli bytecode albo natywny kod maszynowy. Jego zawartość to:

rozmiar

pole

vuint

typ, rozmiar oraz informacja o tym, czy istnieją podelementy surowego kodu (sub-raw-code)

kod (bytecode lub kod maszynowy)

vuint

liczba podelementów surowego kodu (sub-raw-code) (tylko jeśli niezerowa)

podelementy surowego kodu (sub-raw-code)

Pierwszy vuint w elemencie surowego kodu (raw-code) koduje typ kodu przechowywanego w tym elemencie (dwa najmniej znaczące bity), informację o tym, czy ten surowy kod ma elementy potomne (trzeci najmniej znaczący bit), oraz długość kodu, który następuje (ilość pamięci RAM do przydzielenia na niego).

Po vuint następuje sam kod. Jeśli typ kodu nie jest kodem viper z relokacjami, kod ten jest danymi stałymi i nie wymaga modyfikacji.

Jeśli ten surowy kod ma jakieś elementy potomne (na co wskazuje bit w pierwszym vuint), po kodzie następuje vuint zliczający liczbę podelementów surowego kodu (sub-raw-code).

Na końcu, rekurencyjnie, przechowywane są ewentualne podelementy surowego kodu (sub-raw-code).