Fișiere .mpy MicroPython

MicroPython definește conceptul de fișier .mpy, care este un format de fișier container binar ce conține cod precompilat și care poate fi importat ca un modul .py normal. Fișierul foo.mpy poate fi importat prin import foo, atâta timp cât foo.mpy poate fi găsit în mod obișnuit de mecanismul de import. De regulă, fiecare director listat în sys.path este căutat în ordine. La căutarea într-un anumit director, mai întâi se caută foo.py și, dacă acesta nu este găsit, se caută apoi foo.mpy, după care căutarea continuă în următorul director dacă niciunul nu a fost găsit. Astfel, foo.py va avea prioritate față de foo.mpy.

Aceste fișiere .mpy pot conține bytecode, care este de obicei generat din fișiere sursă Python (fișiere .py) prin programul mpy-cross. Pentru unele arhitecturi, un fișier .mpy poate conține de asemenea cod mașină nativ, care poate fi generat în diverse moduri, cel mai notabil din cod sursă C.

Compilatorul mpy-cross

mpy-cross este compilatorul încrucișat care transformă un fișier sursă .py într-un container binar .mpy gata de importat pe cameră. Face parte din arborele de surse MicroPython (același folosit pentru a construi firmware-ul camerei) și este de asemenea publicat ca pachet pip pentru utilizare pe gazdă, fără a necesita o copie completă a firmware-ului:

$ pip install --user mpy-cross

Sau prin pipx:

$ pipx install mpy-cross

După instalare, invocă-l pe un singur fișier sursă:

$ mpy-cross foo.py

Aceasta produce foo.mpy în directorul curent, gata de copiat pe sistemul de fișiere al camerei alături de alte module sau de introdus într-o imagine ROMFS.

Cele mai utile opțiuni de linie de comandă:

  • -o <path> – calea de ieșire pentru fișierul .mpy generat (implicit este numele fișierului de intrare cu extensia înlocuită; -o - scrie la stdout).

  • -O<n> – nivelul de optimizare de la 0 la 3. Valoarea implicită 0 păstrează aserțiunile și localizările complete din sursă; 3 elimină aserțiunile și docstring-urile și rescrie blocurile if __debug__. Nivelul controlează aceeași suprafață micropython.opt_level pe care o expune mediul de execuție.

  • -march=<arch> – arhitectura nativă țintă pentru funcțiile decorate cu @native și @viper. Necesară atunci când sursa folosește acești decoratori. Valoarea trebuie să corespundă clasei MCU a camerei: alege-o din lista pe care o afișează mpy-cross --help sau citește-o de pe cameră în timpul execuției cu sys.implementation._mpy.

  • -s <path> – șirul cu calea sursă încorporat în informațiile de depanare ale fișierului .mpy. Util când calea de pe disc diferă de calea de import sub care fișierul ar trebui să apară în trasee.

  • -X emit=bytecode|native|viper – alege emițătorul implicit pentru întregul modul (o alternativă per funcție la decoratorii @native / @viper).

  • --version – afișează versiunea formatului .mpy pe care o emite acest binar. Acel număr trebuie să corespundă versiunii pe care o suportă mediul de execuție al camerei (vezi tabelul de versiuni de mai jos), altfel importul va genera ValueError('incompatible .mpy file').

Rulează mpy-cross --help pentru lista completă de indicatori.

Pachetul pip expune de asemenea o mică API de modul Python, astfel încât scripturile de build pot acționa compilatorul în proces, în loc să bifurce manual un subproces:

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 sunt cele trei puncte de intrare; mpy_cross.CrossCompileError transportă stderr-ul compilatorului atunci când ceva nu merge bine. Constantele de arhitectură (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP etc.) corespund șirurilor pe care le acceptă indicatorul -march.

Versionarea și compatibilitatea fișierelor .mpy

Un anumit fișier .mpy poate fi sau nu compatibil cu un anumit sistem MicroPython. Compatibilitatea se bazează pe următoarele:

  • Versiunea fișierului .mpy: versiunea fișierului trebuie să corespundă versiunii suportate de sistemul care îl încarcă.

  • Sub-versiunea fișierului .mpy: dacă fișierul .mpy conține cod mașină nativ, atunci sub-versiunea fișierului trebuie să corespundă versiunii suportate de sistemul care îl încarcă. În caz contrar, dacă nu există cod mașină nativ în fișierul .mpy, atunci sub-versiunea este ignorată la încărcare.

  • Biții întregilor mici: fișierul .mpy va necesita un număr minim de biți într-un small integer, iar sistemul care îl încarcă trebuie să suporte cel puțin acest număr de biți.

  • Arhitectura nativă: dacă fișierul .mpy conține cod mașină nativ, atunci va specifica arhitectura acelui cod mașină, iar sistemul care îl încarcă trebuie să suporte execuția codului acelei arhitecturi.

Dacă un sistem MicroPython suportă importul fișierelor .mpy, atunci câmpul sys.implementation._mpy va exista și va returna un întreg care codifică versiunea (cei mai puțin semnificativi 8 biți), caracteristicile și arhitectura nativă.

Încercarea de a importa un fișier .mpy care eșuează unul dintre primele patru teste va genera ValueError('incompatible .mpy file'). Încercarea de a importa un fișier .mpy care eșuează testul de arhitectură nativă (dacă conține cod mașină nativ) va genera ValueError('incompatible .mpy arch').

Dacă importul unui fișier .mpy eșuează, atunci încearcă următoarele:

  • Determină versiunea și indicatorii .mpy suportați de sistemul tău MicroPython executând:

    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()
    
  • Verifică validitatea fișierului .mpy inspectând primii doi octeți ai fișierului. Primul octet ar trebui să fie un «M» majuscul, iar al doilea octet va fi numărul versiunii, care ar trebui să corespundă versiunii sistemului de mai sus. Dacă nu corespunde, reconstruiește fișierul .mpy.

  • Verifică dacă versiunea .mpy a sistemului corespunde versiunii emise de mpy-cross care a fost folosit pentru a construi fișierul .mpy, aflată prin mpy-cross --version. Dacă nu corespunde, recompilează mpy-cross din depozitul Git verificat la eticheta (sau hash-ul) raportat de mpy-cross --version.

  • Asigură-te că folosești indicatorii mpy-cross corecți, aflați prin codul de mai sus sau inspectând variabila Makefile MPY_CROSS_FLAGS pentru portul pe care îl folosești.

  • Dacă al treilea octet al fișierului .mpy are bitul #6 setat, atunci verifică dacă vuint-ul de biți de indicatori specifici arhitecturii codificat este compatibil cu ținta pe care imporți fișierul.

Tabelul următor arată corespondența dintre versiunea de lansare MicroPython și versiunea .mpy.

Versiune de lansare MicroPython

versiune .mpy

v1.23.0 și ulterior

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

Pentru completitudine, tabelul următor arată commit-ul Git din depozitul principal MicroPython la care s-a schimbat versiunea .mpy.

schimbare versiune .mpy

commit Git

6.2 la 6.3

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 la 6.2

6967ff3c581a66f73e9f3d78975f47528db39980

6 la 6.1

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 la 6

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 la 5

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 la 4

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 la 3

ff93fd4f50321c6190e1659b19e64fef3045a484

1 la 2

dd11af209d226b7d18d5148b239662e30ed60bad

0 la 1

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

versiunea inițială 0

d8c834c95d506db979ec871417de90b7951edc30

Codificarea binară a fișierelor .mpy

Fișierele .mpy MicroPython sunt un format de container binar cu obiecte de cod (bytecode și cod mașină nativ) stocate intern într-o ierarhie imbricată. Codul pentru modulul exterior este stocat primul, iar apoi urmează copiii săi. Fiecare copil poate avea la rândul lui copii, de exemplu în cazul unei clase care are metode sau al unei funcții care definește o expresie lambda sau o comprehensiune. Pentru a menține fișierele mici și totodată a oferi un interval larg de valori posibile, se folosește în multe locuri conceptul de întreg fără semn codificat variabil (vuint). Similar cu codificarea UTF-8, această codificare stochează 7 biți pe octet, cu al 8-lea bit (MSB) setat dacă urmează unul sau mai mulți octeți. Biții întregului fără semn sunt stocați în vuint în forma LSB.

Nivelul superior al unui fișier .mpy constă din trei părți:

  • Antetul.

  • Tabelele globale de qstr și de constante.

  • Codul brut pentru domeniul exterior al modulului. Acest domeniu exterior este executat atunci când fișierul .mpy este importat.

Poți inspecta conținutul unui fișier .mpy folosind mpy-tool.py, de exemplu (rulat din rădăcina depozitului principal MicroPython):

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

Antetul

Antetul .mpy este:

dimensiune

câmp

octet

valoarea 0x4d (ASCII «M»)

octet

numărul versiunii majore .mpy

octet

indicatori de caracteristici, arhitectură nativă, numărul versiunii minore (era indicatori de caracteristici în versiunile mai vechi)

octet

numărul de biți dintr-un întreg mic

Al treilea octet este împărțit după cum urmează (MSB primul):

bit

semnificație

7

rezervat, trebuie să fie 0

6

un vuint cu indicatori specifici arhitecturii urmează antetului

5..2

numărul arhitecturii native

1..0

numărul versiunii minore

Indicatori specifici arhitecturii

Dacă bitul #6 al octetului de indicatori de caracteristici din antet este setat, atunci un vuint care conține informații opționale specifice arhitecturii va urma antetului. Conținutul acestui întreg depinde de arhitectura nativă pentru care este destinat fișierul.

Acest lucru este utilizat în prezent pentru a stoca ce extensii ale procesorului RISC-V are nevoie fișierul MPY pentru a funcționa corect, pe lângă I, M, C și Zicsr. Diferitele variante de ArmV7 sunt identificate prin numărul lor de arhitectură nativă, dar reutilizarea acelui mecanism ar complica lucrurile pentru RV32 și RV64.

Fișierele MPY care vizează RV32 sau RV64 și care nu au nevoie de extensii specifice ale procesorului nu trebuie să furnizeze un întreg de indicatori (împreună cu setarea bitului corespunzător în antet). Absența unei valori de indicatori pentru fișierele MPY RV32 și RV64 este folosită pentru a indica faptul că nu sunt necesare extensii specifice și economisește un octet în binarul de ieșire final.

Vezi de asemenea opțiunea de linie de comandă -march-flags atât în mpy-tool.py, cât și în mpy-cross, precum și opțiunea de linie de comandă --arch-flags din mpy_ld.py pentru a seta această valoare la crearea fișierelor MPY.

Tabelele globale de qstr și de constante

Un fișier .mpy conține un singur tabel de qstr și un singur tabel de obiecte constante. Acestea sunt globale pentru fișierul .mpy, fiind referențiate de toate obiectele de cod brut imbricate. Tabelul de qstr mapează numărul intern de qstr (intern fișierului .mpy) la numărul de qstr rezolvat al mediului de execuție în care este importat fișierul .mpy. Acest lucru leagă fișierul .mpy de restul sistemului în care se execută. Tabelul de obiecte constante este populat cu referințe la toate obiectele constante de care are nevoie fișierul .mpy.

dimensiune

câmp

vuint

numărul de qstr-uri

vuint

numărul de obiecte constante

date qstr

obiecte constante codificate

Elementele de cod brut

Un element de cod brut conține cod, fie bytecode, fie cod mașină nativ. Conținutul său este:

dimensiune

câmp

vuint

tipul, dimensiunea și dacă există sub-elemente de cod brut

cod (bytecode sau cod mașină)

vuint

numărul de sub-elemente de cod brut (doar dacă este diferit de zero)

sub-elemente de cod brut

Primul vuint dintr-un element de cod brut codifică tipul de cod stocat în acest element (cei mai puțin semnificativi doi biți), dacă acest cod brut are copii (al treilea cel mai puțin semnificativ bit) și lungimea codului care urmează (cantitatea de RAM de alocat pentru el).

După vuint urmează codul propriu-zis. Dacă tipul de cod nu este cod viper cu relocări, acest cod este date constante și nu trebuie modificat.

Dacă acest cod brut are copii (așa cum indică un bit din primul vuint), după cod urmează un vuint care numără sub-elementele de cod brut.

În final, orice sub-elemente de cod brut sunt stocate, recursiv.