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 od0do3. Domyślny0zachowuje asercje i pełne informacje o położeniu w kodzie źródłowym;3usuwa asercje oraz docstringi i przepisuje blokiif __debug__. Poziom kontroluje tę samą powierzchnięmicropython.opt_level, którą udostępnia środowisko uruchomieniowe.-march=<arch>– docelowa architektura natywna dla funkcji opatrzonych dekoratorami@nativei@viper. Wymagane, gdy źródło używa tych dekoratorów. Wartość musi odpowiadać klasie MCU kamery: wybierz ją z listy, którą wypisujempy-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/@viperna 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łaValueError('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-crossużyty do zbudowania pliku .mpy, którą można ustalić za pomocąmpy-cross --version. Jeśli się nie zgadza, ponownie skompilujmpy-crossz repozytorium Git wypisanego (checkout) przy tagu (lub haszu) raportowanym przezmpy-cross --version.Upewnij się, że używasz właściwych flag
mpy-cross, ustalonych za pomocą powyższego kodu lub przez sprawdzenie zmiennej MakefileMPY_CROSS_FLAGSdla 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).