MicroPython .mpy файли

MicroPython визначає концепцію .mpy файлу — двійкового контейнерного формату, що містить попередньо скомпільований код і може імпортуватись як звичайний модуль .py. Файл foo.mpy можна імпортувати за допомогою import foo, якщо foo.mpy можна знайти звичайним способом через механізм імпорту. Зазвичай кожна директорія зі списку sys.path перевіряється по черзі. При пошуку в конкретній директорії спочатку шукається foo.py, якщо не знайдено — шукається foo.mpy, а потім пошук продовжується в наступній директорії, якщо жодного не знайдено. Таким чином, foo.py матиме пріоритет над foo.mpy.

Ці .mpy файли можуть містити bytecode (байт-код), який зазвичай генерується з Python-файлів (.py) за допомогою програми mpy-cross. Для деяких архітектур .mpy файл також може містити машинний код для конкретної архітектури, який можна генерувати різними способами, зокрема з вихідного коду C.

Компілятор mpy-cross

mpy-cross — це крос-компілятор, що перетворює вихідний файл .py на двійковий контейнер .mpy, готовий до імпорту на камері. Він є частиною дерева вихідних кодів MicroPython (того самого, що використовується для збірки мікропрограми камери) та також публікується як пакет pip для використання на стороні хоста без повного checkout мікропрограми:

$ pip install --user mpy-cross

Або через pipx:

$ pipx install mpy-cross

Після встановлення викличте його для окремого вихідного файлу:

$ mpy-cross foo.py

Це створює foo.mpy у поточній директорії, готовий до копіювання у файлову систему камери поруч з іншими модулями або для використання в образі ROMFS.

Найкорисніші параметри командного рядка:

  • -o <path> – вихідний шлях для згенерованого .mpy (за замовчуванням — ім’я вхідного файлу зі зміненим розширенням; -o - виводить у stdout).

  • -O<n> – рівень оптимізації від 0 до 3. Рівень за замовчуванням 0 зберігає перевірки (assertions) та повні дані про розташування в джерелі; 3 прибирає перевірки та рядки документації і переписує блоки if __debug__. Цей рівень відповідає тому самому параметру micropython.opt_level, що його відображає середовище виконання.

  • -march=<arch> – цільова архітектура машинного коду для функцій, декорованих @native та @viper. Обов’язковий, якщо вихідний код використовує ці декоратори. Значення повинне відповідати класу MCU камери: виберіть зі списку, що виводить mpy-cross --help, або зчитайте під час роботи камери за допомогою sys.implementation._mpy.

  • -s <path> – рядок вихідного шляху, що вбудовується у налагоджувальну інформацію .mpy. Корисний, коли шлях на диску відрізняється від шляху імпорту, під яким файл повинен відображатися у трасуваннях стека.

  • -X emit=bytecode|native|viper – вибір генератора за замовчуванням для всього модуля (альтернатива декораторам @native / @viper на рівні окремої функції).

  • --version – виводить версію формату .mpy, яку генерує цей двійковий файл. Це число повинне збігатися з версією, яку підтримує середовище виконання камери (дивіться таблицю версій нижче), інакше імпорт спричинить ValueError('incompatible .mpy file').

Виконайте mpy-cross --help, щоб отримати повний список прапорців.

Пакет pip також надає невеликий API Python-модуля, щоб скрипти збірки могли керувати компілятором у процесі замість того, щоб вручну запускати підпроцес:

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 та mpy_cross.mpy_version — три точки входу; mpy_cross.CrossCompileError містить stderr компілятора, коли щось іде не так. Константи архітектури (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP тощо) відповідають рядкам, які приймає прапорець -march.

Версіонування та сумісність .mpy файлів

Конкретний .mpy файл може бути або не бути сумісним з конкретною системою MicroPython. Сумісність визначається наступним:

  • Версія .mpy файлу: версія файлу повинна збігатися з версією, яку підтримує система, що його завантажує.

  • Підверсія .mpy файлу: якщо .mpy файл містить машинний код для конкретної архітектури, підверсія файлу повинна збігатися з версією, яку підтримує система, що його завантажує. В іншому разі, якщо в .mpy файлі немає машинного коду, підверсія ігнорується під час завантаження.

  • Кількість бітів цілих чисел: .mpy файл вимагатиме мінімальну кількість бітів у small integer (малому цілому числі), і система, що його завантажує, повинна підтримувати принаймні стільки бітів.

  • Архітектура машинного коду: якщо .mpy файл містить машинний код для конкретної архітектури, він вказуватиме архітектуру цього коду, і система, що його завантажує, повинна підтримувати виконання коду для цієї архітектури.

Якщо система MicroPython підтримує імпорт .mpy файлів, поле sys.implementation._mpy існуватиме і повертатиме ціле число, яке кодує версію (молодші 8 бітів), функціональні можливості та архітектуру машинного коду.

Спроба імпортувати .mpy файл, що не проходить один із перших чотирьох тестів, спричинить ValueError('incompatible .mpy file'). Спроба імпортувати .mpy файл, що не проходить тест архітектури машинного коду (якщо він містить такий код), спричинить ValueError('incompatible .mpy arch').

Якщо імпорт .mpy файлу зазнає невдачі, спробуйте наступне:

  • Визначте версію .mpy та прапорці, які підтримує ваша система MicroPython, виконавши:

    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()
    
  • Перевірте дійсність .mpy файлу, оглянувши перші два байти файлу. Перший байт повинен бути великою літерою „M“ у кодуванні ASCII, а другий байт — номером версії, який повинен збігатися з версією системи, отриманою вище. Якщо вони не збігаються — перезберіть .mpy файл.

  • Перевірте, чи версія .mpy системи збігається з версією, яку генерує mpy-cross, що використовувався для збірки .mpy файлу, визначеною за допомогою mpy-cross --version. Якщо вони не збігаються — перекомпілюйте mpy-cross зі сховища Git, отриманого для мітки (або хешу), яку повертає mpy-cross --version.

  • Переконайтеся, що використовуєте правильні прапорці mpy-cross, отримані з наведеного вище коду або з перевірки змінної Makefile MPY_CROSS_FLAGS для порту, який ви використовуєте.

  • Якщо в третьому байті .mpy файлу встановлений біт #6, перевірте, чи сумісний закодований vuint бітів прапорців архітектури з цільовою системою, на яку ви імпортуєте файл.

Наступна таблиця показує відповідність між версіями MicroPython та версіями .mpy.

Версія MicroPython

Версія .mpy

v1.23.0 і вище

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

Для повноти картини наступна таблиця показує коміт Git основного сховища MicroPython, в якому була змінена версія .mpy.

Зміна версії .mpy

Коміт Git

6.2 to 6.3

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 to 6.2

6967ff3c581a66f73e9f3d78975f47528db39980

6 to 6.1

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 to 6

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 to 5

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 to 4

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 to 3

ff93fd4f50321c6190e1659b19e64fef3045a484

1 to 2

dd11af209d226b7d18d5148b239662e30ed60bad

0 to 1

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

початкова версія 0

d8c834c95d506db979ec871417de90b7951edc30

Двійкове кодування .mpy файлів

MicroPython .mpy файли — це двійковий контейнерний формат з об’єктами коду (байт-кодом та машинним кодом для конкретної архітектури), що зберігаються всередині у вкладеній ієрархії. Код зовнішнього модуля зберігається першим, а потім йдуть його дочірні елементи. Кожен дочірній елемент може мати власні дочірні елементи, наприклад, у випадку класу з методами або функції, що визначає лямбда-вираз чи вираз-генератор. Щоб зберігати файли невеликими, при цьому забезпечуючи широкий діапазон можливих значень, у багатьох місцях використовується концепція цілого числа зі змінним кодуванням (vuint). Подібно до кодування UTF-8, це кодування зберігає 7 бітів на байт, де 8-й біт (MSB) встановлюється, якщо за ним іде один або більше байтів. Біти беззнакового цілого числа зберігаються у vuint у вигляді LSB.

Верхній рівень .mpy файлу складається з трьох частин:

  • Заголовок.

  • Глобальні таблиці qstr та констант.

  • Сирий код зовнішньої області видимості модуля. Ця зовнішня область видимості виконується під час імпорту .mpy файлу.

Ви можете переглянути вміст .mpy файлу за допомогою mpy-tool.py, наприклад (запустіть з кореня основного сховища MicroPython):

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

Заголовок

Заголовок .mpy має такий вигляд:

розмір

поле

байт

значення 0x4d (ASCII „M“)

байт

основний номер версії .mpy

байт

бітові прапорці функцій, архітектура машинного коду, номер підверсії (у старіших версіях — тільки бітові прапорці функцій)

байт

кількість бітів у малому цілому числі

Третій байт розділено таким чином (MSB першим):

біт

значення

7

зарезервований, повинен бути 0

6

за заголовком іде vuint з прапорцями, специфічними для архітектури

5..2

номер архітектури машинного коду

1..0

номер підверсії

Прапорці, специфічні для архітектури

Якщо біт #6 байту бітових прапорців функцій у заголовку встановлений, після заголовку слідуватиме vuint, що містить необов’язкову інформацію, специфічну для архітектури. Вміст цього цілого числа залежить від того, для якої архітектури машинного коду призначений файл.

Наразі це використовується для збереження інформації про те, які розширення процесора RISC-V потребує MPY файл для коректної роботи, крім I, M, C та Zicsr. Різні варіанти ArmV7 ідентифікуються за номером архітектури машинного коду, але повторне використання цього механізму ускладнило б ситуацію для RV32 та RV64.

MPY файли, призначені для RV32 або RV64, які не потребують жодних конкретних розширень процесора, не зобов’язані надавати ціле число з прапорцями (разом із встановленням відповідного біта в заголовку). Відсутність значення прапорців для MPY файлів RV32 та RV64 означає, що жодних конкретних розширень не потрібно, та економить один байт у кінцевому вихідному двійковому файлі.

Дивіться також параметр командного рядка -march-flags як в mpy-tool.py, так і в mpy-cross, а також параметр --arch-flags в mpy_ld.py для встановлення цього значення під час створення MPY файлів.

Глобальні таблиці qstr та констант

Файл .mpy містить єдину таблицю qstr та єдину таблицю константних об’єктів. Вони є глобальними для .mpy файлу і на них посилаються всі вкладені об’єкти сирого коду. Таблиця qstr відображає внутрішній номер qstr (внутрішній для .mpy файлу) на розв’язаний номер qstr середовища виконання, в яке імпортується .mpy файл. Це зв’язує .mpy файл з рештою системи, в якій він виконується. Таблиця константних об’єктів наповнюється посиланнями на всі константні об’єкти, які потребує .mpy файл.

розмір

поле

vuint

кількість qstr

vuint

кількість константних об’єктів

дані qstr

закодовані константні об’єкти

Елементи сирого коду

Елемент сирого коду містить код — або байт-код, або машинний код для конкретної архітектури. Його вміст:

розмір

поле

vuint

тип, розмір та наявність дочірніх елементів сирого коду

код (байт-код або машинний код)

vuint

кількість дочірніх елементів сирого коду (тільки якщо не нуль)

дочірні елементи сирого коду

Перший vuint в елементі сирого коду кодує тип коду, що зберігається в цьому елементі (два молодших біти), наявність дочірніх елементів у цього сирого коду (третій молодший біт), та довжину коду, що йде далі (обсяг RAM для виділення).

Після vuint йде безпосередньо сам код. Якщо тип коду не є viper-кодом з переміщеннями, цей код є константними даними і не потребує змін.

Якщо цей сирий код має дочірні елементи (що позначається бітом у першому vuint), після коду йде vuint, що підраховує кількість дочірніх елементів сирого коду.

Нарешті, всі дочірні елементи сирого коду зберігаються рекурсивно.