קובצי .mpy של MicroPython

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 מסיר assertions ו-docstrings ומשכתב בלוקים של if __debug__. הרמה שולטת באותו ממשק micropython.opt_level שזמן הריצה חושף.

  • -march=<arch> – ארכיטקטורת היעד הנייטיב עבור פונקציות מעוטרות ב-@native וב-@viper. נדרש כאשר המקור משתמש בעטרים אלה. הערך חייב להתאים למחלקת ה-MCU של המצלמה: בחרו אותו מהרשימה ש-mpy-cross --help מדפיס, או קראו אותו מהמצלמה בזמן הריצה באמצעות sys.implementation._mpy.

  • -s <path> – מחרוזת נתיב המקור המוטמעת במידע ניפוי הבאגים של ה-.mpy. שימושי כאשר הנתיב בדיסק שונה מנתיב הייבוא שתחתיו הקובץ אמור להופיע ב-tracebacks.

  • -X emit=bytecode|native|viper – בחרו את הפולט (emitter) שברירת המחדל עבור המודול כולו (חלופה ברמת הפונקציה לעטרים @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« באות גדולה, והבית השני יהיה מספר הגרסה, שאמור להתאים לגרסת המערכת שלמעלה. אם הוא אינו תואם אזי בנו מחדש את קובץ ה-.mpy.

  • בדקו אם גרסת ה-.mpy של המערכת תואמת לגרסה שנפלטה על ידי mpy-cross ששימש לבניית קובץ ה-.mpy, הניתנת על ידי mpy-cross --version. אם היא אינה תואמת אזי הדרו מחדש את mpy-cross ממאגר ה-Git ש-checkout שלו בוצע ב-tag (או ה-hash) המדווח על ידי mpy-cross --version.

  • ודאו שאתם משתמשים בדגלי mpy-cross הנכונים, הניתנים על ידי הקוד שלמעלה, או על ידי בחינת משתנה ה-Makefile MPY_CROSS_FLAGS עבור ה-port שבו אתם משתמשים.

  • אם הבית השלישי של קובץ ה-.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

לשם השלמות, הטבלה הבאה מציגה את commit ה-Git של מאגר MicroPython הראשי שבו שונתה גרסת ה-.mpy.

שינוי גרסת .mpy

commit של Git

6.2 ל-6.3

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 ל-6.2

6967ff3c581a66f73e9f3d78975f47528db39980

6 ל-6.1

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 ל-6

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 ל-5

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 ל-4

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 ל-3

ff93fd4f50321c6190e1659b19e64fef3045a484

1 ל-2

dd11af209d226b7d18d5148b239662e30ed60bad

0 ל-1

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

גרסה ראשונית 0

d8c834c95d506db979ec871417de90b7951edc30

קידוד בינארי של קובצי .mpy

קובצי .mpy של MicroPython הם פורמט מכל בינארי עם אובייקטי קוד (bytecode וקוד מכונה נייטיב) המאוחסנים פנימית בהיררכיה מקוננת. הקוד עבור המודול החיצוני מאוחסן ראשון, ולאחר מכן באים צאצאיו. לכל צאצא עשויים להיות צאצאים נוספים, למשל במקרה של מחלקה בעלת מתודות, או פונקציה המגדירה lambda או comprehension. כדי לשמור על קבצים קטנים תוך מתן טווח גדול של ערכים אפשריים, הוא משתמש במושג של מספר שלם לא-מסומן בקידוד משתנה (vuint) במקומות רבים. בדומה לקידוד UTF-8, קידוד זה מאחסן 7 ביטים לכל בית כאשר הביט ה-8 (MSB) מוגדר אם בית אחד או יותר באים אחריו. הביטים של המספר השלם הלא-מסומן מאוחסנים ב-vuint בצורת LSB.

הרמה העליונה של קובץ .mpy מורכבת משלושה חלקים:

  • הכותרת.

  • טבלאות ה-qstr הגלובליות וטבלאות הקבועים.

  • ה-raw-code עבור ה-scope החיצוני של המודול. scope חיצוני זה מורץ כאשר קובץ ה-.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, ומופנים אליהן על ידי כל אובייקטי ה-raw-code המקוננים. טבלת ה-qstr ממפה מספר qstr פנימי (פנימי לקובץ ה-.mpy) למספר ה-qstr המומר של זמן הריצה שאליו קובץ ה-.mpy מיובא. זה מקשר את קובץ ה-.mpy עם שאר המערכת שבתוכה הוא מורץ. טבלת אובייקטי הקבועים מאוכלסת בהפניות לכל אובייקטי הקבועים שקובץ ה-.mpy זקוק להם.

גודל

שדה

vuint

מספר ה-qstrs

vuint

מספר אובייקטי הקבועים

נתוני qstr

אובייקטי קבועים מקודדים

רכיבי raw code

רכיב raw-code מכיל קוד, בין אם bytecode ובין אם קוד מכונה נייטיב. תוכנו הוא:

גודל

שדה

vuint

סוג, גודל והאם יש רכיבי sub-raw-code

קוד (bytecode או קוד מכונה)

vuint

מספר רכיבי ה-sub-raw-code (רק אם שונה מאפס)

רכיבי sub-raw-code

ה-vuint הראשון ברכיב raw-code מקודד את סוג הקוד המאוחסן ברכיב זה (שני הביטים הפחות משמעותיים), האם ל-raw-code זה יש צאצאים כלשהם (הביט השלישי הפחות משמעותי), ואת אורך הקוד שבא אחריו (כמות ה-RAM שיש להקצות עבורו).

לאחר ה-vuint בא הקוד עצמו. אלא אם סוג הקוד הוא קוד viper עם relocations, קוד זה הוא נתוני קבוע ואינו צריך שינוי.

אם ל-raw-code זה יש צאצאים כלשהם (כפי שמצוין על ידי ביט ב-vuint הראשון), לאחר הקוד בא vuint המונה את מספר רכיבי ה-sub-raw-code.

לבסוף, כל רכיבי ה-sub-raw-code מאוחסנים, באופן רקורסיבי.