MicroPython על מיקרו-בקרים¶
MicroPython תוכננה כך שתוכל לרוץ על מיקרו-בקרים. למיקרו-בקרים אלה יש מגבלות חומרה שעשויות להיות לא מוכרות למתכנתים הרגילים יותר למחשבים קונבנציונליים. בפרט, כמות ה-RAM ושטח האחסון הלא-נדיף (”דיסק“ בזיכרון פלאש (flash)) מוגבלת. מדריך זה מציע דרכים להפיק את המרב מהמשאבים המוגבלים. מכיוון ש-MicroPython רצה על בקרים המבוססים על מגוון ארכיטקטורות, השיטות המוצגות הן כלליות: במקרים מסוימים יהיה צורך להשיג מידע מפורט מתיעוד ספציפי לפלטפורמה.
זיכרון פלאש (flash)¶
במצלמות OpenMV Cam הדרך הפשוטה להתמודד עם הקיבולת המוגבלת היא להתקין כרטיס micro SD. במקרים מסוימים זה אינו מעשי, בין אם משום שלהתקן אין חריץ לכרטיס SD ובין אם מסיבות של עלות או צריכת חשמל; ולכן יש להשתמש בזיכרון הפלאש (flash) שעל השבב. הקושחה, כולל תת-המערכת של MicroPython, מאוחסנת בזיכרון הפלאש (flash) המובנה. הקיבולת הנותרת זמינה לשימוש. מסיבות הקשורות בארכיטקטורה הפיזית של זיכרון הפלאש (flash), חלק מהקיבולת הזו עשוי להיות בלתי נגיש כמערכת קבצים. במקרים כאלה ניתן לנצל שטח זה על ידי שילוב מודולים של המשתמש בבנייה של הקושחה, אשר נצרבת לאחר מכן אל ההתקן.
ישנן שתי דרכים להשיג זאת: מודולים קפואים (frozen modules) ובייטקוד קפוא (frozen bytecode). מודולים קפואים מאחסנים את קוד המקור של Python יחד עם הקושחה. בייטקוד קפוא משתמש במהדר הצולב (cross compiler) כדי להמיר את קוד המקור לבייטקוד, אשר מאוחסן לאחר מכן יחד עם הקושחה. בשני המקרים ניתן לגשת למודול באמצעות פקודת import:
import mymodule
ההליך לייצור מודולים קפואים ובייטקוד תלוי בפלטפורמה; הוראות לבניית הקושחה ניתן למצוא בקובצי README בחלק הרלוונטי של עץ קוד המקור.
באופן כללי השלבים הם כדלקמן:
שיבוט (clone) של מאגר MicroPython.
השגת ערכת הכלים (toolchain) (הספציפית לפלטפורמה) לבניית הקושחה.
בניית המהדר הצולב (cross compiler).
הצבת המודולים שיש להקפיא בתיקייה מוגדרת (בהתאם לכך אם המודול עומד להיות מוקפא כקוד מקור או כבייטקוד).
בניית הקושחה. ייתכן שתידרש פקודה ספציפית לבניית קוד קפוא מכל אחד מהסוגים - ראו את תיעוד הפלטפורמה.
צריבת הקושחה אל ההתקן.
RAM¶
בעת הפחתת השימוש ב-RAM יש שני שלבים שיש לקחת בחשבון: הידור (compilation) וביצוע (execution). בנוסף לצריכת הזיכרון, קיימת גם בעיה המכונה פיצול הערימה (heap fragmentation). באופן כללי עדיף למזער את היצירה וההריסה החוזרות של אובייקטים. הסיבה לכך מכוסה בסעיף העוסק ב-heap.
שלב ההידור¶
כאשר מודול מיובא, MicroPython מהדרת את הקוד לבייטקוד, אשר מבוצע לאחר מכן על ידי המכונה הווירטואלית של MicroPython (VM). הבייטקוד מאוחסן ב-RAM. המהדר עצמו דורש RAM, אך זה הופך זמין לשימוש כאשר ההידור הושלם.
אם כבר יובאו מספר מודולים, עלול להיווצר מצב שבו אין מספיק RAM כדי להריץ את המהדר. במקרה זה פקודת ה-import תפיק חריגת זיכרון (memory exception).
אם מודול יוצר אובייקטים גלובליים בעת הייבוא, הוא יצרוך RAM בזמן הייבוא, ו-RAM זה לא יהיה זמין למהדר לשימוש בייבואים הבאים. באופן כללי עדיף להימנע מקוד שרץ בעת הייבוא; גישה טובה יותר היא להחזיק קוד אתחול אשר מורץ על ידי היישום לאחר שכל המודולים יובאו. זה ממקסם את ה-RAM הזמין למהדר.
אם ה-RAM עדיין אינו מספיק כדי להדר את כל המודולים, פתרון אחד הוא להדר מראש את המודולים. ל-MicroPython יש מהדר צולב (cross compiler) המסוגל להדר מודולים של Python לבייטקוד (ראו את ה-README בתיקייה mpy-cross). לקובץ הבייטקוד שמתקבל יש סיומת .mpy; ניתן להעתיק אותו אל מערכת הקבצים ולייבא אותו בדרך הרגילה. לחלופין ניתן לממש חלק מהמודולים או את כולם כבייטקוד קפוא: ברוב הפלטפורמות זה חוסך אף יותר RAM, מכיוון שהבייטקוד רץ ישירות מהפלאש (flash) במקום שיאוחסן ב-RAM.
שלב הביצוע¶
ישנן מספר טכניקות קידוד להפחתת השימוש ב-RAM.
קבועים
MicroPython מספקת מילת מפתח const אשר ניתן להשתמש בה כדלקמן:
from micropython import const
ROWS = const(33)
_COLS = const(0x10)
a = ROWS
b = _COLS
בשני המקרים שבהם הקבוע מוצב אל משתנה, המהדר ימנע מקידוד של חיפוש (lookup) לשם הקבוע על ידי החלפתו בערכו המילולי. זה חוסך בייטקוד ולכן RAM. עם זאת, הערך ROWS יתפוס לפחות שתי מילות מכונה (machine words), אחת עבור המפתח ואחת עבור הערך במילון הגלובלי. הנוכחות במילון הכרחית משום שמודול אחר עשוי לייבא או להשתמש בו. ניתן לחסוך RAM זה על ידי הקדמת קו תחתון לשם, כמו ב-_COLS: סמל זה אינו גלוי מחוץ למודול ולכן לא יתפוס RAM.
הארגומנט ל-const() יכול להיות כל דבר אשר, בזמן ההידור, מתערך לקבוע, למשל 0x100, 1 << 8 או (True, "string", b"bytes") (ראו את הסעיף שלהלן לפרטים). הוא יכול אפילו לכלול סמלים קבועים אחרים שכבר הוגדרו, למשל 1 << BIT.
מבני נתונים קבועים
כאשר ישנו נפח משמעותי של נתונים קבועים והפלטפורמה תומכת בביצוע מתוך הפלאש (flash), ניתן לחסוך RAM כדלקמן. הנתונים צריכים להיות ממוקמים במודולים של Python ומוקפאים כבייטקוד. הנתונים חייבים להיות מוגדרים כאובייקטי bytes. המהדר »יודע« שאובייקטי bytes הם בלתי-ניתנים-לשינוי (immutable) ומבטיח שהאובייקטים יישארו בזיכרון הפלאש (flash) במקום שיועתקו אל RAM. המודול struct יכול לסייע בהמרה בין טיפוסי bytes לבין טיפוסים מובנים אחרים של Python.
בעת בחינת ההשלכות של בייטקוד קפוא, שימו לב שב-Python מחרוזות, מספרים מסוג float, bytes, מספרים שלמים, מספרים מרוכבים ו-tuples הם בלתי-ניתנים-לשינוי (immutable). בהתאם לכך, אלה יוקפאו אל הפלאש (flash) (עבור tuples, רק אם כל הרכיבים שלהם בלתי-ניתנים-לשינוי). כך, בשורה
mystring = "The quick brown fox"
המחרוזת בפועל ”The quick brown fox“ תשכון בפלאש (flash). בזמן הריצה הפניה אל המחרוזת מוצבת אל המשתנה mystring. ההפניה תופסת מילת מכונה (machine word) אחת. עקרונית ניתן להשתמש במספר שלם ארוך כדי לאחסן נתונים קבועים:
bar = 0xDEADBEEF0000DEADBEEF
כמו בדוגמת המחרוזת, בזמן הריצה הפניה אל המספר השלם הגדול-באופן-שרירותי מוצבת אל המשתנה bar. הפניה זו תופסת מילת מכונה (machine word) אחת.
tuples של אובייקטים קבועים הם עצמם קבועים. tuples קבועים כאלה עוברים מיטוב (optimization) על ידי המהדר כך שאין צורך ליצור אותם בזמן הריצה בכל פעם שמשתמשים בהם. לדוגמה:
foo = (1, 2, 3, 4, 5, 6, 100000, ("string", b"bytes", False, True))
ה-tuple כולו יתקיים כאובייקט יחיד (פוטנציאלית בפלאש (flash) אם הקוד קפוא) ויקבל הפניה בכל פעם שיש בו צורך.
יצירת אובייקטים מיותרת
ישנם מספר מצבים שבהם אובייקטים עשויים להיווצר ולהיהרס מבלי משים. זה יכול להפחית את השימושיות של ה-RAM באמצעות פיצול (fragmentation). הסעיפים הבאים דנים במקרים של תופעה זו.
שרשור מחרוזות
שקלו את קטעי הקוד הבאים שמטרתם להפיק מחרוזות קבועות:
var = "foo" + "bar"
var1 = "foo" "bar"
var2 = """\
foo\
bar"""
כל אחד מפיק את אותה תוצאה, אך הראשון יוצר באופן מיותר שני אובייקטי מחרוזת בזמן הריצה, מקצה עוד RAM עבור השרשור לפני הפקת השלישי. האחרים מבצעים את השרשור בזמן ההידור, מה שיעיל יותר ומפחית פיצול (fragmentation).
כאשר יש ליצור מחרוזות באופן דינמי לפני הזנתן אל זרם (stream) כגון קובץ, ניתן לחסוך RAM אם זה נעשה באופן הדרגתי. במקום ליצור אובייקט מחרוזת גדול, צרו תת-מחרוזת והזינו אותה אל הזרם לפני הטיפול בבאה אחריה.
הדרך הטובה ביותר ליצירת מחרוזות דינמיות היא באמצעות מתודת המחרוזת format():
var = "Temperature {:5.2f} Pressure {:06d}\n".format(temp, press)
חוצצים (buffers)
בעת גישה להתקנים כגון מופעים (instances) של ממשקי UART, I2C ו-SPI, השימוש בחוצצים (buffers) המוקצים מראש מונע את יצירתם של אובייקטים מיותרים. שקלו את שתי הלולאות הבאות:
while True:
var = spi.read(100)
# process data
buf = bytearray(100)
while True:
spi.readinto(buf)
# process data in buf
הראשונה יוצרת חוצץ (buffer) בכל מעבר, ואילו השנייה עושה שימוש חוזר בחוצץ (buffer) שהוקצה מראש; זה גם מהיר יותר וגם יעיל יותר מבחינת פיצול הזיכרון.
Bytes קטנים ממספרים שלמים
ברוב הפלטפורמות מספר שלם צורך ארבעה bytes. שקלו את שלוש הקריאות לפונקציה foo():
def foo(bar):
for x in bar:
print(x)
foo([1, 2, 0xff])
foo((1, 2, 0xff))
foo(b'\1\2\xff')
בקריאה הראשונה list של מספרים שלמים נוצר ב-RAM בכל פעם שהקוד מבוצע. הקריאה השנייה יוצרת אובייקט tuple קבוע (tuple המכיל רק אובייקטים קבועים) כחלק משלב ההידור, כך שהוא נוצר רק פעם אחת והוא יעיל יותר מה-list. הקריאה השלישית יוצרת ביעילות אובייקט bytes הצורך את הכמות המינימלית של RAM. אם המודול היה מוקפא כבייטקוד, גם ה-tuple וגם אובייקט ה-bytes היו שוכנים בפלאש (flash).
מחרוזות לעומת Bytes
Python3 הציגה תמיכה ב-Unicode. זה הציג הבחנה בין מחרוזת לבין מערך של bytes. MicroPython מבטיחה שמחרוזות Unicode לא תופסות שטח נוסף כל עוד כל התווים במחרוזת הם ASCII (כלומר בעלי ערך < 128). אם נדרשים ערכים בטווח ה-8-ביט המלא, ניתן להשתמש באובייקטי bytes ו-bytearray כדי להבטיח שלא יידרש שטח נוסף. שימו לב שרוב מתודות המחרוזת (למשל str.strip()) חלות גם על מופעי bytes, כך שתהליך החיסול של Unicode יכול להיות נטול-כאב.
s = 'the quick brown fox' # A string instance
b = b'the quick brown fox' # A bytes instance
כאשר יש צורך להמיר בין מחרוזות ל-bytes, ניתן להשתמש במתודות str.encode() ו-bytes.decode(). שימו לב שגם מחרוזות וגם bytes הם בלתי-ניתנים-לשינוי (immutable). כל פעולה שמקבלת כקלט אובייקט כזה ומפיקה אחר משתמעת ממנה לפחות הקצאת RAM אחת לצורך הפקת התוצאה. בשורה השנייה שלהלן מוקצה אובייקט bytes חדש. זה היה קורה גם אילו foo היה מחרוזת.
foo = b' empty whitespace'
foo = foo.lstrip()
ביצוע המהדר בזמן הריצה
פונקציות ה-Python eval ו-exec מפעילות את המהדר בזמן הריצה, מה שדורש כמויות משמעותיות של RAM. שימו לב שספריית ה-pickle מתוך micropython-lib עושה שימוש ב-exec. ייתכן שיעיל יותר מבחינת RAM להשתמש בספריית ה-json עבור סריאליזציה (serialisation) של אובייקטים.
אחסון מחרוזות בפלאש (flash)
מחרוזות Python הן בלתי-ניתנות-לשינוי (immutable) ולכן יש להן את הפוטנציאל להיות מאוחסנות בזיכרון לקריאה בלבד (read only). המהדר יכול להציב בפלאש (flash) מחרוזות המוגדרות בקוד Python. כמו במודולים קפואים, יש צורך להחזיק עותק של עץ קוד המקור במחשב ואת ערכת הכלים (toolchain) לבניית הקושחה. ההליך יעבוד אפילו אם המודולים לא עברו ניפוי באגים מלא, כל עוד ניתן לייבא ולהריץ אותם.
לאחר ייבוא המודולים, הריצו:
micropython.qstr_info(1)
לאחר מכן העתיקו והדביקו את כל שורות ה-Q(xxx) אל עורך טקסט. בדקו והסירו שורות שהן בבירור לא תקפות. פתחו את הקובץ qstrdefsport.h שיימצא ב-ports/stm32 (או בתיקייה המקבילה עבור הארכיטקטורה שבשימוש). העתיקו והדביקו את השורות המתוקנות בסוף הקובץ. שמרו את הקובץ, בנו מחדש וצרבו את הקושחה. ניתן לבדוק את התוצאה על ידי ייבוא המודולים והרצה שוב של:
micropython.qstr_info(1)
שורות ה-Q(xxx) אמורות להיעלם.
הערימה (heap)¶
כאשר תוכנית רצה יוצרת אובייקט, ה-RAM הנחוץ מוקצה ממאגר בגודל קבוע המכונה הערימה (heap). כאשר האובייקט יוצא מהתחום (scope) (במילים אחרות הופך בלתי נגיש לקוד), האובייקט המיותר מכונה ”זבל“ (garbage). תהליך המכונה ”איסוף זבל“ (garbage collection, GC) משחזר זיכרון זה ומחזיר אותו אל הערימה הפנויה. תהליך זה רץ אוטומטית, אך ניתן להפעילו ישירות על ידי הרצת gc.collect().
השיח בנושא זה מורכב במידת מה. לקבלת »תיקון מהיר« הריצו את הדבר הבא מעת לעת:
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
למידע נוסף, ראו להלן ואת התיעוד עבור המודול המובנה gc.
לפרטים מנקודת המבט של המנגנונים הפנימיים/המפתחים של MicroPython, ראו גם ניהול זיכרון.
פיצול (fragmentation)¶
נניח שתוכנית יוצרת אובייקט foo, ולאחר מכן אובייקט bar. בהמשך foo יוצא מהתחום (scope) אך bar נשאר. ה-RAM שבשימוש foo ישוחזר על ידי ה-GC. עם זאת, אם bar הוקצה לכתובת גבוהה יותר, ה-RAM ששוחזר מ-foo יהיה שמיש רק עבור אובייקטים שאינם גדולים מ-foo. בתוכנית מורכבת או ארוכת-ריצה הערימה (heap) עלולה להתפצל: למרות שקיימת כמות משמעותית של RAM זמין, אין מספיק שטח רציף כדי להקצות אובייקט מסוים, והתוכנית נכשלת עם שגיאת זיכרון.
הטכניקות המתוארות לעיל מכוונות למזער זאת. כאשר נדרשים חוצצים (buffers) קבועים גדולים או אובייקטים אחרים, עדיף ליצור אותם מוקדם בתהליך ביצוע התוכנית, לפני שהפיצול (fragmentation) יכול להתרחש. ניתן לבצע שיפורים נוספים על ידי ניטור מצב הערימה (heap) ועל ידי שליטה ב-GC; אלה מתוארים להלן.
דיווח¶
מספר פונקציות ספרייה זמינות לדיווח על הקצאת זיכרון ולשליטה ב-GC. אלה נמצאות במודולים gc ו-micropython. ניתן להדביק את הדוגמה הבאה ב-REPL (Ctrl-E כדי להיכנס למצב הדבקה, Ctrl-D כדי להריץ אותה).
import gc
import micropython
gc.collect()
micropython.mem_info()
print('-----------------------------')
print('Initial free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
def func():
a = bytearray(10000)
gc.collect()
print('Func definition: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
func()
print('Func run free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
gc.collect()
print('Garbage collect free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
print('-----------------------------')
micropython.mem_info(1)
מתודות שבהן נעשה שימוש לעיל:
gc.collect()כפיית איסוף זבל. ראו את הערת השוליים.micropython.mem_info()הדפסת סיכום של ניצול ה-RAM.gc.mem_free()החזרת גודל הערימה הפנויה ב-bytes.gc.mem_alloc()החזרת מספר ה-bytes המוקצים כעת.micropython.mem_info(1)הדפסת טבלה של ניצול הערימה (heap) (מפורט להלן).
המספרים המופקים תלויים בפלטפורמה, אך ניתן לראות שהכרזת הפונקציה משתמשת בכמות קטנה של RAM בצורת בייטקוד הנפלט על ידי המהדר (ה-RAM שבו השתמש המהדר שוחזר). הרצת הפונקציה משתמשת ביותר מ-10KiB, אך עם החזרה a הוא זבל מכיוון שהוא מחוץ לתחום (scope) ולא ניתן להפנות אליו. ה-gc.collect() הסופי משחזר זיכרון זה.
הפלט הסופי המופק על ידי micropython.mem_info(1) ישתנה בפרטים אך ניתן לפרשו כדלקמן:
סמל |
משמעות |
|---|---|
. |
בלוק פנוי |
h |
בלוק ראש |
= |
בלוק זנב |
m |
בלוק ראש מסומן |
T |
tuple |
L |
list |
D |
dict |
F |
float |
B |
בייטקוד |
M |
מודול |
S |
מחרוזת או bytes |
A |
bytearray |
כל אות מייצגת בלוק זיכרון יחיד, כאשר בלוק הוא 16 bytes. כך שכל שורה של תדפיס הערימה (heap) מייצגת 0x400 bytes או 1KiB של RAM.
שליטה באיסוף זבל¶
ניתן לדרוש GC בכל עת על ידי הרצת gc.collect(). כדאי לעשות זאת במרווחים, ראשית כדי להקדים את הפיצול (fragmentation) ושנית למען הביצועים. GC יכול לקחת מספר מילישניות אך מהיר יותר כשיש מעט עבודה לבצע (כ-1ms על OpenMV Cam). קריאה מפורשת יכולה למזער השהיה זו תוך הבטחה שהיא מתרחשת בנקודות בתוכנית שבהן זה מקובל.
GC אוטומטי מעורר בנסיבות הבאות. כאשר ניסיון הקצאה נכשל, מבוצע GC וההקצאה מנוסה מחדש. רק אם זה נכשל מועלית חריגה. שנית, GC אוטומטי יופעל אם כמות ה-RAM הפנוי יורדת מתחת לסף. ניתן להתאים סף זה ככל שהביצוע מתקדם:
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
זה יעורר GC כאשר יותר מ-25% מהערימה הפנויה כעת הופך תפוס.
באופן כללי מודולים צריכים ליצור אובייקטי נתונים בזמן הריצה באמצעות בנאים (constructors) או פונקציות אתחול אחרות. הסיבה היא שאם זה מתרחש בעת האתחול, המהדר עלול להיות מורעב מ-RAM כאשר מודולים הבאים מיובאים. אם מודולים אכן יוצרים נתונים בעת הייבוא, אזי gc.collect() שמורץ לאחר הייבוא יקל על הבעיה.
פעולות על מחרוזות¶
MicroPython מטפלת במחרוזות באופן יעיל, והבנת זאת יכולה לסייע בתכנון יישומים שירוצו על מיקרו-בקרים. כאשר מודול מהודר, מחרוזות המופיעות מספר פעמים מאוחסנות פעם אחת בלבד, תהליך המכונה ביון מחרוזות (string interning). ב-MicroPython מחרוזת מבויינת (interned) מכונה qstr. במודול שמיובא באופן רגיל מופע יחיד זה ימוקם ב-RAM, אך כפי שתואר לעיל, במודולים המוקפאים כבייטקוד הוא ימוקם בפלאש (flash).
השוואות מחרוזות מבוצעות אף הן ביעילות באמצעות גיבוב (hashing) ולא תו אחר תו. הקנס על השימוש במחרוזות במקום במספרים שלמים עשוי לכן להיות קטן, הן מבחינת הביצועים והן מבחינת השימוש ב-RAM - עובדה שעשויה להפתיע מתכנתי C.
אחרית דבר¶
MicroPython מעבירה, מחזירה ו(כברירת מחדל) מעתיקה אובייקטים על ידי הפניה. הפניה תופסת מילת מכונה (machine word) אחת ולכן תהליכים אלה יעילים מבחינת השימוש ב-RAM והמהירות.
כאשר נדרשים משתנים שגודלם אינו byte ולא מילת מכונה (machine word), קיימות ספריות סטנדרטיות שיכולות לסייע באחסונם ביעילות ובביצוע המרות. ראו את המודולים array, struct ו-uctypes.
הערת שוליים: ערך ההחזרה של gc.collect()¶
בפלטפורמות Unix ו-Windows מתודת ה-gc.collect() מחזירה מספר שלם המציין את מספר אזורי הזיכרון הנפרדים ששוחזרו באיסוף (ליתר דיוק, מספר הראשים שהומרו לפנויים). מסיבות של יעילות, פורטים של חומרה חשופה (bare metal) אינם מחזירים ערך זה.