Külső C modulok a MicroPython számára

Amikor a MicroPython rendszerhez modulokat fejlesztesz, előfordulhat, hogy a Python környezet korlátaiba ütközöl, gyakran amiatt, hogy bizonyos hardvererőforrásokhoz nem lehet hozzáférni, vagy a Python sebességi korlátai miatt.

Ha a korlátaid nem oldhatók fel a A MicroPython sebességének maximalizálása szakaszban található javaslatokkal, akkor életképes megoldás lehet a modulod egy részének vagy egészének C-ben (és/vagy C++ nyelven, ha az a portodhoz implementálva van) történő megírása.

Ha a modulod gyakran elérhető hardver vagy könyvtárak elérésére, illetve azokkal való együttműködésre készült, fontold meg, hogy a MicroPython forrásfájában, hasonló modulok mellé implementálod, és pull requestként beküldöd. Ha azonban kevéssé ismert vagy zárt rendszereket célzol meg, érdemesebb lehet ezt a fő MicroPython repository-n kívül tartani.

Ez a fejezet leírja, hogyan fordíthatók le az ilyen külső modulok a MicroPython futtatható állományába vagy firmware-képébe. Mind a Make, mind a CMake build eszköz támogatott, és külső modul írásakor jó ötlet mindkét eszköz build fájljait hozzáadni, hogy a modul minden porton használható legyen. Egy adott port fordításakor azonban csak az egyik build módszert, vagy a Make-et, vagy a CMake-et kell használnod.

Egy másik megközelítés a Natív gépi kód .mpy fájlokban használata, amely lehetővé teszi egyedi C kód írását, amely egy .mpy fájlba kerül, és amely dinamikusan importálható egy futó MicroPython rendszerbe anélkül, hogy a fő firmware-t újra kellene fordítani.

Egy külső C modul felépítése

Egy MicroPython felhasználói C modul egy könyvtár, amely a következő fájlokat tartalmazza:

  • *.c / *.cpp / *.h forráskódfájlok a modulodhoz.

    Ezek jellemzően az implementált alacsony szintű funkcionalitást, valamint a MicroPython kötőfüggvényeket tartalmazzák, amelyek elérhetővé teszik a függvényeket és a modul(oka)t.

    Jelenleg a legjobb referencia ezeknek a függvényeknek/moduloknak a megírásához az, hogy hasonló modulokat keresel a MicroPython fájban, és azokat használod példaként.

  • A micropython.mk tartalmazza az ehhez a modulhoz tartozó Makefile-részletet.

    A $(USERMOD_DIR) elérhető a micropython.mk fájlban a modulkönyvtárad útvonalaként. Mivel ez minden C modulhoz újra van definiálva, a micropython.mk fájlban egy helyi make változóba kell kibontanod, pl. EXAMPLE_MOD_DIR := $(USERMOD_DIR)

    A micropython.mk fájlnak hozzá kell adnia a moduljaid forrásfájljait a SRC_USERMOD_C vagy a SRC_USERMOD_LIB_C változóhoz. Az előbbit MP_QSTR_ és MP_REGISTER_MODULE definíciókra dolgozzák fel, az utóbbit nem (pl. segédfüggvények és olyan könyvtári kód, amely nem MicroPython-specifikus). Ezeknek az útvonalaknak tartalmazniuk kell a $(USERMOD_DIR) kibontott másolatát, pl.:

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

    Hasonlóképpen, C++ forrásfájlokhoz használd a SRC_USERMOD_CXX és SRC_USERMOD_LIB_CXX változókat. Ha assembly fájlokat szeretnél belefoglalni, használd a SRC_USERMOD_LIB_ASM változót.

    Ha egyedi fordítói opcióid vannak (mint a -I, amellyel könyvtárakat adhatsz hozzá a header fájlok kereséséhez), ezeket C kódhoz a CFLAGS_USERMOD, C++ kódhoz pedig a CXXFLAGS_USERMOD változóhoz kell hozzáadni.

  • A micropython.cmake tartalmazza az ehhez a modulhoz tartozó CMake konfigurációt.

    A micropython.cmake fájlban az aktuális modul útvonalaként a ${CMAKE_CURRENT_LIST_DIR} használható.

    A micropython.cmake fájlodnak definiálnia kell egy INTERFACE könyvtárat, és hozzá kell rendelnie a forrásfájljaidat, a fordítási definíciókat és az include könyvtárakat. A könyvtárat ezután a usermod célhoz kell linkelni.

    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)
    

    A teljes használati példát lásd alább.

Alapvető példa

A cexample modul egy függvényre és egy osztályra mutat példát. A cexample.add_ints(a, b) függvény összead két egész argumentumot, és visszaadja az eredményt. A cexample.Timer() típus olyan időzítőket hoz létre, amelyekkel mérhető az objektum példányosítása óta eltelt idő.

A modul megtalálható a MicroPython forrásfájban a példák könyvtárában, és van egy forrásfájlja, valamint egy Makefile-részlete, a fentebb leírt tartalommal:

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

A további magyarázatért lásd az ezekben a fájlokban lévő megjegyzéseket. A cexample modul mellett található egy cppexample is, amely ugyanígy működik, de a C és C++ kód MicroPython-ban való vegyítésének egy módját mutatja be.

A cmodule lefordítása a MicroPython-ba

Egy ilyen modul felépítéséhez fordítsd le a MicroPython-t (lásd a kezdeti lépéseket), 2 módosítást alkalmazva:

  1. Állítsd be a USER_C_MODULES build-időbeli kapcsolót úgy, hogy a belefoglalni kívánt modulokra mutasson. A Make-et használó portoknál ennek a változónak egy könyvtárnak kell lennie, amelyben automatikusan keresik a modulokat. A CMake-et használó portoknál ennek a változónak egy fájlnak kell lennie, amely tartalmazza a felépítendő modulokat. A részletekért lásd alább.

  2. Engedélyezd a modulokat a megfelelő C előfeldolgozó makró 1-re állításával. Erre csak akkor van szükség, ha a felépítendő modulok nem engedélyezettek automatikusan.

A MicroPython-nal érkező példamodulok felépítéséhez állítsd a USER_C_MODULES változót az examples/usercmodule könyvtárra Make esetén, vagy az examples/usercmodule/micropython.cmake fájlra CMake esetén.

Például így építhető fel a unix port a példamodulokkal:

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

Előfordulhat, hogy egyszer le kell futtatnod a make clean parancsot az elején, amikor új felhasználói modulokat foglalsz a build-be. A build kimenete megmutatja a megtalált modulokat:

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

Egy CMake-alapú portnál, mint az rp2, ez kissé másképp néz ki (vedd figyelembe, hogy a CMake-et valójában a make hívja meg):

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

Ismét, lehet, hogy először le kell futtatnod a make clean parancsot, hogy a CMake felvegye a felhasználói modulokat. A CMake build kimenete név szerint felsorolja a modulokat:

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

A legfelső szintű micropython.cmake tartalma használható annak szabályozására, hogy mely modulok legyenek engedélyezve.

A saját projektjeidnél kényelmesebb az egyedi kódot a fő MicroPython forrásfán kívül tartani, így egy tipikus projektkönyvtár-struktúra így néz ki:

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

Make-kel való építéskor állítsd a USER_C_MODULES változót a my_project/modules könyvtárra. Például az stm32 port felépítése:

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

CMake-kel való építéskor a legfelső szintű micropython.cmake fájlnak – amely közvetlenül a my_project/modules könyvtárban található – include paranccsal kell betöltenie az összes elérhetővé tenni kívánt modult:

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

Majd építsd fel a következővel:

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

A USER_C_MODULES változóhoz abszolút útvonalakat is megadhatsz.

A USER_C_MODULES változó által megadott összes modul (akár ebben a könyvtárban található Make használatakor, akár include paranccsal hozzáadva CMake használatakor) lefordításra kerül, de csak azok lesznek importálhatók, amelyek engedélyezve vannak. A felhasználói modulok általában alapértelmezetten engedélyezettek (ezt a modul fejlesztője dönti el), ebben az esetben nincs más teendő, mint beállítani a USER_C_MODULES változót a fentebb leírtak szerint.

Ha egy modul nincs alapértelmezetten engedélyezve, akkor a megfelelő C előfeldolgozó makrót kell engedélyezni. Ez a makrónév a modul forráskódjában a MP_REGISTER_MODULE sor keresésével található meg (általában a fő forrásfájl végén szerepel). Ezt a makrót egy #if X / #endif párnak kell körülvennie, és az X konfigurációs opciót a CFLAGS_EXTRA segítségével 1-re kell állítani, hogy a modul elérhetővé váljon. Ha nincs #if X / #endif pár, akkor a modul alapértelmezetten engedélyezett.

Például az examples/usercmodule/cexample modul alapértelmezetten engedélyezett, ezért a következő sor szerepel a forráskódjában:

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);

Alternatívaként, ha ezt a modult alapértelmezetten letiltottá, de egy előfeldolgozó konfigurációs opcióval kiválaszthatóvá szeretnéd tenni, ez így nézne ki:

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

Ebben az esetben a modul engedélyezhető a CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 hozzáadásával a make parancshoz, vagy az mpconfigport.h vagy mpconfigboard.h szerkesztésével a következő hozzáadásával

#define MODULE_CEXAMPLE_ENABLED (1)

Vedd figyelembe, hogy a pontos módszer a porttól függ, mivel azok eltérő struktúrájúak. Ha nem helyesen történik, akkor le fog fordulni, de az importálás nem fogja megtalálni a modult.

Modulhasználat a MicroPython-ban

Miután beépítetted a MicroPython másolatodba, a modul mostantól Python-ban éppúgy elérhető, mint bármely más beépített modul, pl.

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 dinamikus memóriafoglalás

A MicroPython a saját „Python heap”-jét használja a Memóriakezelés célokra, amely nem azonos a C könyvtárfüggvények, mint a malloc(), free() stb. által használt „C heap”-pel. Nem minden MicroPython port rendelkezik egyáltalán „C heap”-pel.

Az 1. és 2. szintű portok eltérő mértékben támogatják a C dinamikus memóriafoglalást egy „C heap”-en keresztül:

  • A unix, windows, esp32 és webassembly portok támogatják a C dinamikus memóriafoglalást.

  • Az rp2 port futásidőben nem tud memóriát foglalni, hacsak a firmware nem a MICROPY_C_HEAP_SIZE=n beállítással van felépítve, hogy n bájt memóriát foglaljon le egy C heap számára. Ez a memória nem lesz elérhető a Python kód számára.

  • Az alif, mimxrt, nrf, renesas-ra, samd és stm32 portok dinamikus C foglalást tartalmazó buildjei linkeléskor olyan hibákkal fognak elbukni, mint az undefined reference to `malloc'. A MicroPython ezeken a portokon nem tartalmaz beépített támogatást a dinamikus C foglaláshoz. Bármilyen megoldás megköveteli egy C heap implementáció kézi hozzáadását az egyedi build-hez.

  • A zephyr port jelenleg nem támogatja a felhasználói modulokkal való felépítést.

Python heap mint C heap

A C kód számára gyakorlatias lehet, ha helyette „Python heap” dinamikus foglalási függvényeket hív meg, mint a m_malloc(), m_malloc0() és m_free().

Erről a megközelítésről további információért lásd a MicroPython memória C kódból szakaszt.

C++ modulok

A legtöbb 1. és 2. szintű MicroPython port (és néhány 3. szintű is) támogatja a C++ felhasználói modulok felépítését a fentebb leírt C++-specifikus környezeti változók használatával.

A C++ és a MicroPython sikeres integrálása néhány további szempontot is magában foglal:

C++ dinamikus memóriafoglalás

A C++ programok (valamint a C++ szabványos könyvtár jellemzői) jellemzően dinamikus memóriafoglalást használnak. A C++ alapértelmezett memóriafoglalója (azaz a new és delete operátorok) jellemzően a C dinamikus memóriafoglalás tetejére épülő rétegként van implementálva.

Azoknál a MicroPython portoknál, amelyek nem tartalmaznak C dinamikus memóriafoglalási támogatást, a C++ dinamikus memóriafoglalás kétféleképpen támogatható:

  • Implementálj C dinamikus memóriafoglalást az egyedi build-edben.

  • Implementálj egy egyedi C++ memóriafoglalót az egyedi build-edben.

Linkelési szempontok

Mivel a MicroPython egy C-alapú projekt, minden olyan szimbólumot, amely a MicroPython-hoz vagy onnan linkelődik, extern "C" minősítéssel kell ellátni a C++ kódban.

Erősen ajánlott a examples/usercmodule/cppexample példában bemutatott minta követése, ahol a Python modul egy minimális C fájlban van implementálva, amely a C++ kódot burkolja.