โมดูล C ภายนอกสำหรับ MicroPython

เมื่อพัฒนาโมดูลสำหรับใช้งานกับ MicroPython คุณอาจพบข้อจำกัดของสภาพแวดล้อม Python ซึ่งมักเกิดจากการไม่สามารถเข้าถึงทรัพยากรฮาร์ดแวร์บางอย่าง หรือข้อจำกัดด้านความเร็วของ Python

หากข้อจำกัดของคุณไม่สามารถแก้ไขได้ด้วยคำแนะนำใน การเพิ่มความเร็วสูงสุดใน MicroPython การเขียนโมดูลบางส่วนหรือทั้งหมดใน C (และ/หรือ C++ หากมีการรองรับสำหรับพอร์ตของคุณ) ถือเป็นทางเลือกที่ทำได้

หากโมดูลของคุณออกแบบมาเพื่อเข้าถึงหรือทำงานกับฮาร์ดแวร์หรือไลบรารีทั่วไป โปรดพิจารณานำไปติดตั้งในซอร์สโค้ด MicroPython ควบคู่กับโมดูลที่คล้ายกัน และส่งเป็น pull request หากอย่างไรก็ตามคุณกำหนดเป้าหมายเป็นระบบที่คลุมเครือหรือระบบที่เป็นกรรมสิทธิ์ การเก็บไว้ภายนอก MicroPython repository หลักอาจเหมาะสมกว่า

บทนี้อธิบายวิธีคอมไพล์โมดูลภายนอกดังกล่าวเข้าไปในไฟล์ปฏิบัติการ MicroPython หรือ firmware image รองรับทั้ง Make และ CMake และเมื่อเขียนโมดูลภายนอก ควรเพิ่มไฟล์ build สำหรับเครื่องมือทั้งสองนี้เพื่อให้โมดูลสามารถใช้งานได้กับทุกพอร์ต แต่เมื่อคอมไพล์พอร์ตใดพอร์ตหนึ่ง คุณจะต้องใช้เพียงวิธีเดียว ไม่ว่าจะเป็น Make หรือ CMake

อีกแนวทางหนึ่งคือการใช้ โค้ดเครื่องแบบ native ในไฟล์ .mpy ซึ่งช่วยให้เขียนโค้ด C แบบกำหนดเองที่วางในไฟล์ .mpy และสามารถนำเข้าแบบไดนามิกเข้าสู่ระบบ MicroPython ที่กำลังทำงานอยู่ โดยไม่ต้องคอมไพล์ firmware ใหม่

โครงสร้างของโมดูล C ภายนอก

โมดูล C สำหรับผู้ใช้ MicroPython คือไดเรกทอรีที่ประกอบด้วยไฟล์ดังต่อไปนี้:

  • ไฟล์ซอร์สโค้ด *.c / *.cpp / *.h สำหรับโมดูลของคุณ

    โดยทั่วไปไฟล์เหล่านี้จะรวมถึงฟังก์ชันระดับล่างที่นำมาใช้งานและฟังก์ชัน binding ของ MicroPython เพื่อเปิดเผยฟังก์ชันและโมดูล

    ปัจจุบันแหล่งอ้างอิงที่ดีที่สุดสำหรับการเขียนฟังก์ชัน/โมดูลเหล่านี้คือการค้นหาโมดูลที่คล้ายกันใน MicroPython tree และใช้เป็นตัวอย่าง

  • micropython.mk ประกอบด้วย Makefile fragment สำหรับโมดูลนี้

    $(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 ส่วนหลังจะไม่ถูกประมวลผล (เช่น helpers และโค้ดไลบรารีที่ไม่ได้เฉพาะเจาะจงกับ 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

    หากคุณมีตัวเลือก compiler แบบกำหนดเอง (เช่น -I เพื่อเพิ่มไดเรกทอรีในการค้นหาไฟล์ header) ควรเพิ่มไว้ใน CFLAGS_USERMOD สำหรับโค้ด C และ CXXFLAGS_USERMOD สำหรับโค้ด C++

  • micropython.cmake ประกอบด้วยการกำหนดค่า CMake สำหรับโมดูลนี้

    ใน micropython.cmake คุณสามารถใช้ ${CMAKE_CURRENT_LIST_DIR} เป็นเส้นทางไปยังโมดูลปัจจุบัน

    micropython.cmake ของคุณควรกำหนดไลบรารี INTERFACE และเชื่อมโยงไฟล์ซอร์ส คำจำกัดความการคอมไพล์ และไดเรกทอรี include เข้ากับมัน จากนั้นควรเชื่อมโยงไลบรารีกับ target 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 source tree ในไดเรกทอรี examples และมีไฟล์ซอร์สและ Makefile fragment พร้อมเนื้อหาตามที่อธิบายไว้ข้างต้น:

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

โปรดดูความคิดเห็นในไฟล์เหล่านี้สำหรับคำอธิบายเพิ่มเติม ถัดจากโมดูล cexample ยังมี cppexample ซึ่งทำงานในลักษณะเดียวกัน แต่แสดงวิธีหนึ่งในการผสม C และ C++ ใน MicroPython

การคอมไพล์ cmodule เข้าสู่ MicroPython

ในการ build โมดูลดังกล่าว ให้คอมไพล์ MicroPython (ดู การเริ่มต้นใช้งาน) โดยใช้การแก้ไข 2 อย่าง:

  1. ตั้งค่าแฟล็ก build-time USER_C_MODULES ให้ชี้ไปยังโมดูลที่คุณต้องการรวม สำหรับพอร์ตที่ใช้ Make ตัวแปรนี้ควรเป็นไดเรกทอรีที่ถูกค้นหาโดยอัตโนมัติสำหรับโมดูล สำหรับพอร์ตที่ใช้ CMake ตัวแปรนี้ควรเป็นไฟล์ที่รวมโมดูลที่จะ build ดูรายละเอียดด้านล่าง

  2. เปิดใช้งานโมดูลโดยตั้งค่า C preprocessor macro ที่สอดคล้องกันเป็น 1 ซึ่งจำเป็นเฉพาะเมื่อโมดูลที่คุณ build ไม่ได้เปิดใช้งานโดยอัตโนมัติ

สำหรับการ build โมดูลตัวอย่างที่มาพร้อมกับ MicroPython ให้ตั้งค่า USER_C_MODULES เป็นไดเรกทอรี examples/usercmodule สำหรับ Make หรือ examples/usercmodule/micropython.cmake สำหรับ CMake

ตัวอย่างเช่น นี่คือวิธี build พอร์ต unix พร้อมโมดูลตัวอย่าง:

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

คุณอาจต้องรัน make clean หนึ่งครั้งเมื่อเริ่มต้นเมื่อรวมโมดูลผู้ใช้ใหม่ใน build ผลลัพธ์ของ build จะแสดงโมดูลที่พบ:

...
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 build แสดงโมดูลตามชื่อ:

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

เนื้อหาของ micropython.cmake ระดับบนสุดสามารถใช้เพื่อควบคุมว่าโมดูลใดถูกเปิดใช้งาน

สำหรับโครงการของคุณเอง สะดวกกว่าที่จะเก็บโค้ดแบบกำหนดเองไว้นอก MicroPython source tree หลัก ดังนั้นโครงสร้างไดเรกทอรีโครงการทั่วไปจะมีลักษณะดังนี้:

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

เมื่อ build ด้วย Make ให้ตั้งค่า USER_C_MODULES เป็นไดเรกทอรี my_project/modules ตัวอย่างเช่น การ build พอร์ต stm32:

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

เมื่อ build ด้วย CMake ไฟล์ micropython.cmake ระดับบนสุด ที่อยู่โดยตรงในไดเรกทอรี my_project/modules ควร include โมดูลทั้งหมดที่คุณต้องการให้พร้อมใช้งาน:

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

จากนั้น build ด้วย:

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 preprocessor macro ที่สอดคล้องกัน ชื่อ macro นี้สามารถค้นหาได้โดยการค้นหาบรรทัด MP_REGISTER_MODULE ในซอร์สโค้ดของโมดูล (ปกติจะปรากฏที่ท้ายไฟล์ซอร์สหลัก) macro นี้ควรล้อมรอบด้วยคู่ #if X / #endif และตัวเลือกการกำหนดค่า X ต้องตั้งค่าเป็น 1 โดยใช้ CFLAGS_EXTRA เพื่อให้โมดูลพร้อมใช้งาน หากไม่มีคู่ #if X / #endif แสดงว่าโมดูลเปิดใช้งานโดยค่าเริ่มต้น

ตัวอย่างเช่น โมดูล examples/usercmodule/cexample เปิดใช้งานโดยค่าเริ่มต้น จึงมีบรรทัดต่อไปนี้ในซอร์สโค้ด:

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);

หรือเพื่อทำให้โมดูลนี้ปิดใช้งานโดยค่าเริ่มต้น แต่สามารถเลือกได้ผ่านตัวเลือกการกำหนดค่า preprocessor จะเป็น:

#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

เมื่อ build เข้าไปในสำเนา MicroPython ของคุณแล้ว โมดูลสามารถเข้าถึงได้ใน Python เหมือนกับโมดูล builtin อื่นๆ เช่น

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" เลย

พอร์ต Tier 1 & 2 มีการสนับสนุนการจัดสรรหน่วยความจำแบบไดนามิก C ผ่าน "C heap" ที่แตกต่างกัน:

  • พอร์ต unix, windows, esp32 และ webassembly รองรับการจัดสรรหน่วยความจำแบบไดนามิก C

  • พอร์ต rp2 จะล้มเหลวในการจัดสรรหน่วยความจำใดๆ ในขณะรันไทม์ เว้นแต่ firmware จะถูก build ด้วย MICROPY_C_HEAP_SIZE=n เพื่อสำรอง n ไบต์ของหน่วยความจำสำหรับ C heap หน่วยความจำนี้จะไม่พร้อมใช้งานสำหรับโค้ด Python

  • การ build พอร์ต alif, mimxrt, nrf, renesas-ra, samd และ stm32 ที่รวม dynamic C allocation จะล้มเหลวในขั้นตอน link-time พร้อมข้อผิดพลาด เช่น undefined reference to `malloc' MicroPython ไม่มีการสนับสนุน dynamic C allocation ในตัวสำหรับพอร์ตเหล่านี้ โซลูชันใดๆ ต้องเพิ่ม C heap implementation ด้วยตนเองในการ build แบบกำหนดเอง

  • พอร์ต zephyr ปัจจุบันไม่รองรับการ build ด้วยโมดูลผู้ใช้

Python heap ในฐานะ C heap

สำหรับโค้ด C อาจเป็นประโยชน์ที่จะเรียกฟังก์ชันการจัดสรรหน่วยความจำแบบไดนามิกของ "Python heap" เช่น m_malloc(), m_malloc0() และ m_free() แทน

ดู หน่วยความจำ MicroPython จากโค้ด C สำหรับข้อมูลเพิ่มเติมเกี่ยวกับแนวทางนี้

โมดูล C++

พอร์ต MicroPython Tier 1 & 2 ส่วนใหญ่ (และบาง Tier 3) รองรับการ build โมดูลผู้ใช้ C++ โดยใช้ตัวแปรสภาพแวดล้อมเฉพาะ C++ ที่อธิบายไว้ข้างต้น

การผสาน C++ และ MicroPython อย่างประสบความสำเร็จเกี่ยวข้องกับข้อควรพิจารณาเพิ่มเติมบางประการ:

การจัดสรรหน่วยความจำแบบไดนามิก C++

โปรแกรม C++ (รวมถึงลักษณะเด่นของ C++ Standard Library) มักใช้การจัดสรรหน่วยความจำแบบไดนามิก ตัวจัดสรรหน่วยความจำเริ่มต้นของ C++ (เช่น operators new และ delete) มักถูกนำมาใช้งานเป็นชั้นบน การจัดสรรหน่วยความจำแบบไดนามิก C

สำหรับพอร์ต MicroPython ที่ไม่รวม C dynamic memory allocation คุณสามารถรองรับ C++ dynamic memory allocation ได้ด้วยวิธีใดวิธีหนึ่งในสองวิธี:

  • นำ C dynamic memory allocation ไปติดตั้งใน build แบบกำหนดเองของคุณ

  • นำ C++ allocator แบบกำหนดเองไปติดตั้งใน build แบบกำหนดเองของคุณ

ข้อควรพิจารณาเกี่ยวกับ Linkage

เนื่องจาก MicroPython เป็นโครงการบน C ดังนั้นสัญลักษณ์ใดๆ ที่เชื่อมโยงไปหรือมาจาก MicroPython จำเป็นต้องระบุ extern "C" ในโค้ด C++

แนะนำอย่างยิ่งให้ปฏิบัติตามรูปแบบที่แสดงใน examples/usercmodule/cppexample ซึ่ง Python module ถูกนำมาใช้งานในไฟล์ wrapper C ขนาดเล็กที่ล้อมรอบโค้ด C++