מודולי C חיצוניים של MicroPython

בעת פיתוח מודולים לשימוש עם MicroPython ייתכן שתיתקלו במגבלות של סביבת Python, לעיתים קרובות בשל חוסר היכולת לגשת למשאבי חומרה מסוימים או בשל מגבלות מהירות של Python.

אם לא ניתן לפתור את המגבלות שלכם באמצעות ההצעות ב-מיצוי המהירות המרבית של MicroPython, כתיבת חלק מהמודול או כולו ב-C (ו/או C++ אם הוא ממומש עבור הפורט שלכם) היא אפשרות ברת-קיימא.

אם המודול שלכם נועד לגשת לחומרה או לספריות נפוצות או לעבוד איתן, שקלו לממש אותו בתוך עץ המקור של MicroPython לצד מודולים דומים ולהגיש אותו כ-pull request. אולם אם אתם מכוונים למערכות נדירות או קנייניות, ייתכן שהגיוני יותר לשמור אותו חיצונית למאגר הראשי של MicroPython.

פרק זה מתאר כיצד לקמפל מודולים חיצוניים כאלה לתוך קובץ ההרצה של MicroPython או לתוך תמונת הקושחה. גם כלי הבנייה Make וגם CMake נתמכים, ובעת כתיבת מודול חיצוני מומלץ להוסיף את קבצי הבנייה עבור שני הכלים הללו כך שניתן יהיה להשתמש במודול בכל הפורטים. אך בעת קימפול פורט מסוים תצטרכו להשתמש רק בשיטת בנייה אחת, או Make או CMake.

גישה חלופית היא להשתמש ב-קוד מכונה מקורי (native) בקבצי .mpy המאפשר כתיבת קוד C מותאם אישית שממוקם בקובץ .mpy, אשר ניתן לייבא אותו באופן דינמי לתוך מערכת MicroPython הפועלת ללא צורך לקמפל מחדש את הקושחה הראשית.

מבנה של מודול C חיצוני

מודול C של משתמש ב-MicroPython הוא תיקייה עם הקבצים הבאים:

  • קובצי קוד מקור *.c / *.cpp / *.h עבור המודול שלכם.

    אלה יכללו בדרך כלל את הפונקציונליות ברמה הנמוכה המיושמת ואת פונקציות הקישור של MicroPython שחושפות את הפונקציות והמודול(ים).

    כרגע המקור הטוב ביותר לכתיבת פונקציות/מודולים אלה הוא למצוא מודולים דומים בתוך עץ MicroPython ולהשתמש בהם כדוגמאות.

  • micropython.mk מכיל את קטע ה-Makefile עבור מודול זה.

    $(USERMOD_DIR) זמין ב-micropython.mk כנתיב לתיקיית המודול שלכם. מכיוון שהוא מוגדר מחדש עבור כל מודול C, יש להרחיב אותו ב-micropython.mk שלכם למשתנה make מקומי, למשל EXAMPLE_MOD_DIR := $(USERMOD_DIR)

    ה-micropython.mk שלכם חייב להוסיף את קובצי המקור של המודולים שלכם למשתנים SRC_USERMOD_C או SRC_USERMOD_LIB_C. הראשון יעובד עבור הגדרות MP_QSTR_ ו-MP_REGISTER_MODULE, השני לא (למשל פונקציות עזר וקוד ספרייה שאינם ספציפיים ל-MicroPython). נתיבים אלה צריכים לכלול את העותק המורחב שלכם של $(USERMOD_DIR), למשל:

    SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c
    SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
    

    באופן דומה, השתמשו ב-SRC_USERMOD_CXX וב-SRC_USERMOD_LIB_CXX עבור קובצי מקור של C++. אם ברצונכם לכלול קובצי assembly השתמשו ב-SRC_USERMOD_LIB_ASM.

    אם יש לכם אפשרויות מהדר מותאמות אישית (כמו -I להוספת תיקיות לחיפוש קובצי header), יש להוסיף אותן ל-CFLAGS_USERMOD עבור קוד C ול-CXXFLAGS_USERMOD עבור קוד C++.

  • micropython.cmake מכיל את תצורת ה-CMake עבור מודול זה.

    ב-micropython.cmake, תוכלו להשתמש ב-${CMAKE_CURRENT_LIST_DIR} כנתיב למודול הנוכחי.

    ה-micropython.cmake שלכם צריך להגדיר ספריית INTERFACE ולשייך אליה את קובצי המקור, הגדרות הקימפול ותיקיות ה-include שלכם. לאחר מכן יש לקשר את הספרייה ליעד usermod.

    add_library(usermod_cexample INTERFACE)
    
    target_sources(usermod_cexample INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}/examplemodule.c
    )
    
    target_include_directories(usermod_cexample INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}
    )
    
    target_link_libraries(usermod INTERFACE usermod_cexample)
    

    ראו להלן דוגמת שימוש מלאה.

דוגמה בסיסית

המודול cexample מספק דוגמאות לפונקציה ולמחלקה. הפונקציה cexample.add_ints(a, b) מחברת שני ארגומנטים שלמים ומחזירה את התוצאה. הטיפוס cexample.Timer() יוצר טיימרים שניתן להשתמש בהם למדידת הזמן שחלף מאז יצירת המופע של האובייקט.

ניתן למצוא את המודול בעץ המקור של MicroPython בתיקיית הדוגמאות והוא כולל קובץ מקור וקטע Makefile עם תוכן כפי שתואר לעיל:

micropython/
└──examples/
   └──usercmodule/
      └──cexample/
         ├── examplemodule.c
         ├── micropython.mk
         └── micropython.cmake

עיינו בהערות בקבצים אלה לקבלת הסבר נוסף. לצד המודול cexample קיים גם cppexample שעובד באותו אופן אך מדגים דרך אחת לשילוב קוד C ו-C++ ב-MicroPython.

קימפול ה-cmodule לתוך MicroPython

כדי לבנות מודול כזה, קמפלו את MicroPython (ראו getting started), תוך החלת 2 שינויים:

  1. הגדירו את דגל זמן-הבנייה USER_C_MODULES כך שיצביע על המודולים שברצונכם לכלול. עבור פורטים המשתמשים ב-Make משתנה זה צריך להיות תיקייה שתיסרק אוטומטית לאיתור מודולים. עבור פורטים המשתמשים ב-CMake משתנה זה צריך להיות קובץ הכולל את המודולים לבנייה. ראו פרטים להלן.

  2. אפשרו את המודולים על ידי הגדרת מאקרו הקדם-מעבד של C המתאים ל-1. זה נחוץ רק אם המודולים שאתם בונים אינם מאופשרים אוטומטית.

לבניית מודולי הדוגמה המגיעים עם MicroPython, הגדירו את USER_C_MODULES לתיקייה examples/usercmodule עבור Make, או ל-examples/usercmodule/micropython.cmake עבור CMake.

לדוגמה, כך בונים את פורט ה-unix עם מודולי הדוגמה:

cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule

ייתכן שתצטרכו להריץ make clean פעם אחת בתחילה בעת הכללת מודולי משתמש חדשים בבנייה. פלט הבנייה יציג את המודולים שנמצאו:

...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...

עבור פורט מבוסס-CMake כמו rp2, זה ייראה מעט שונה (שימו לב ש-CMake למעשה מופעל על ידי make):

cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake

שוב, ייתכן שתצטרכו להריץ make clean תחילה כדי ש-CMake יזהה את מודולי המשתמש. פלט הבנייה של CMake מציג את המודולים לפי שם:

...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...

ניתן להשתמש בתוכן של micropython.cmake ברמה העליונה כדי לשלוט אילו מודולים מאופשרים.

עבור הפרויקטים שלכם נוח יותר לשמור קוד מותאם אישית מחוץ לעץ המקור הראשי של MicroPython, כך שמבנה תיקיית פרויקט טיפוסי ייראה כך:

my_project/
├── modules/
│   ├── example1/
│   │   ├── example1.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   ├── example2/
│   │   ├── example2.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   └── micropython.cmake
└── micropython/
    ├──ports/
   ... ├──stm32/
      ...

בעת בנייה עם Make הגדירו את USER_C_MODULES לתיקייה my_project/modules. לדוגמה, בניית פורט ה-stm32:

cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules

בעת בנייה עם CMake, ה-micropython.cmake ברמה העליונה – הנמצא ישירות בתיקייה my_project/modules – צריך לבצע include לכל המודולים שברצונכם להפוך לזמינים:

include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)

לאחר מכן בנו עם:

cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake

תוכלו גם לציין נתיבים מוחלטים ל-USER_C_MODULES.

כל המודולים שצוינו על ידי המשתנה USER_C_MODULES (בין אם נמצאו בתיקייה זו בעת שימוש ב-Make, או נוספו באמצעות include בעת שימוש ב-CMake) יקומפלו, אך רק אלה שמאופשרים יהיו זמינים לייבוא. מודולי משתמש מאופשרים בדרך כלל כברירת מחדל (זה נקבע על ידי מפתח המודול), ובמקרה זה אין צורך בדבר מעבר להגדרת USER_C_MODULES כמתואר לעיל.

אם מודול אינו מאופשר כברירת מחדל אז יש לאפשר את מאקרו הקדם-מעבד של C המתאים. ניתן למצוא את שם המאקרו הזה על ידי חיפוש השורה MP_REGISTER_MODULE בקוד המקור של המודול (היא מופיעה בדרך כלל בסוף קובץ המקור הראשי). מאקרו זה צריך להיות מוקף בצמד #if X / #endif, ויש להגדיר את אפשרות התצורה X ל-1 באמצעות CFLAGS_EXTRA כדי להפוך את המודול לזמין. אם אין צמד #if X / #endif אז המודול מאופשר כברירת מחדל.

לדוגמה, המודול examples/usercmodule/cexample מאופשר כברירת מחדל ולכן יש בקוד המקור שלו את השורה הבאה:

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);

לחלופין, כדי להפוך מודול זה למושבת כברירת מחדל אך ניתן לבחירה באמצעות אפשרות תצורת קדם-מעבד, זה היה:

#if MODULE_CEXAMPLE_ENABLED
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
#endif

במקרה זה המודול מאופשר על ידי הוספת CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 לפקודה make, או עריכת mpconfigport.h או mpconfigboard.h כדי להוסיף

#define MODULE_CEXAMPLE_ENABLED (1)

שימו לב שהשיטה המדויקת תלויה בפורט מכיוון שיש להם מבנים שונים. אם זה לא נעשה כראוי המודול יקומפל אך הייבוא ייכשל במציאת המודול.

שימוש במודול ב-MicroPython

לאחר שהמודול נבנה לתוך העותק שלכם של MicroPython, ניתן כעת לגשת אליו ב-Python בדיוק כמו כל מודול מובנה אחר, למשל

import cexample
print(cexample.add_ints(1, 3))
# should display 4
from cexample import Timer
from time import sleep_ms

watch = Timer()
sleep_ms(1000)
print(watch.time())
# should display approximately 1000

הקצאת זיכרון דינמית ב-C

MicroPython משתמש ב-”Python heap“ משלו עבור ניהול זיכרון, שאינו זהה ל-”C heap“ המשמש את פונקציות ספריית C malloc(), free() וכו«. לא כל פורט של MicroPython מגיע בכלל עם ”C heap“.

פורטים מדרגה 1 ו-2 מספקים תמיכה משתנה בהקצאת זיכרון דינמית ב-C באמצעות ”C heap“:

  • פורטי unix, windows, esp32 ו-webassembly תומכים בהקצאת זיכרון דינמית ב-C.

  • פורט rp2 ייכשל בהקצאת זיכרון כלשהו בזמן ריצה אלא אם הקושחה נבנתה עם MICROPY_C_HEAP_SIZE=n כדי לשמור n בתים של זיכרון עבור C heap. זיכרון זה לא יהיה זמין לשימוש קוד Python.

  • בנייות של פורטי alif, mimxrt, nrf, renesas-ra, samd ו-stm32 הכוללות הקצאת C דינמית ייכשלו בזמן הקישור עם שגיאות כמו undefined reference to `malloc'. ל-MicroPython אין תמיכה מובנית בהקצאת C דינמית בפורטים אלה. כל פתרון מחייב הוספה ידנית של מימוש C heap לבנייה המותאמת אישית.

  • פורט zephyr אינו תומך כרגע בבנייה עם מודולי משתמש.

Python heap כ-C heap

ייתכן שיהיה מעשי עבור קוד C לקרוא במקום זאת לפונקציות הקצאה דינמית של ”Python heap“ כמו m_malloc(), m_malloc0() ו-m_free().

ראו זיכרון MicroPython מקוד C למידע נוסף על גישה זו.

מודולי C++

רוב פורטי MicroPython מדרגה 1 ו-2 (וחלק מדרגה 3) תומכים בבניית מודולי משתמש של C++, באמצעות משתני הסביבה הספציפיים ל-C++ שתוארו לעיל.

שילוב מוצלח של C++ ו-MicroPython כרוך במספר שיקולים נוספים:

הקצאת זיכרון דינמית ב-C++

תוכניות C++ (וכן תכונות של ספריית C++ הסטנדרטית) משתמשות בדרך כלל בהקצאת זיכרון דינמית. מקצה הזיכרון של C++ כברירת מחדל (כלומר האופרטורים new ו-delete) ממומש בדרך כלל כשכבה מעל הקצאת זיכרון דינמית ב-C.

עבור פורטי MicroPython שאינם כוללים תמיכה בהקצאת זיכרון דינמית ב-C, ניתן לתמוך בהקצאת זיכרון דינמית ב-C++ באחת משתי דרכים:

  • ממשו הקצאת זיכרון דינמית ב-C בבנייה המותאמת אישית שלכם.

  • ממשו מקצה C++ מותאם אישית בבנייה המותאמת אישית שלכם.

שיקולי קישור (Linkage)

מכיוון ש-MicroPython הוא פרויקט מבוסס-C, כל סמל המקושר אל או מ-MicroPython צריך להיות מוגדר עם extern "C" בקוד C++.

מומלץ מאוד לעקוב אחר התבנית המודגמת ב-examples/usercmodule/cppexample, שבה מודול ה-Python ממומש בעטיפת קובץ C מינימלית סביב קוד ה-C++.