Zewnętrzne moduły C dla MicroPython¶
Podczas tworzenia modułów do użytku z MicroPython możesz napotkać ograniczenia środowiska Python, często wynikające z braku dostępu do określonych zasobów sprzętowych lub z ograniczeń szybkości Pythona.
Jeśli Twoich ograniczeń nie da się rozwiązać za pomocą wskazówek z Maksymalizacja szybkości MicroPython, dobrą opcją jest napisanie części lub całości modułu w C (i/lub C++, jeśli zostało zaimplementowane dla Twojego portu).
Jeśli Twój moduł ma na celu dostęp do powszechnie dostępnego sprzętu lub bibliotek albo współpracę z nimi, rozważ zaimplementowanie go wewnątrz drzewa źródłowego MicroPython, obok podobnych modułów, i zgłoszenie go jako pull request. Jeśli jednak celujesz w nietypowe lub zastrzeżone systemy, bardziej sensowne może być utrzymywanie go poza głównym repozytorium MicroPython.
Ten rozdział opisuje, jak skompilować takie zewnętrzne moduły do pliku wykonywalnego lub obrazu oprogramowania układowego MicroPython. Obsługiwane są zarówno narzędzia Make, jak i CMake. Pisząc zewnętrzny moduł, warto dodać pliki kompilacji dla obu tych narzędzi, aby moduł mógł być używany na wszystkich portach. Jednak podczas kompilowania konkretnego portu będziesz musiał użyć tylko jednej metody kompilacji - albo Make, albo CMake.
Alternatywnym podejściem jest użycie Natywny kod maszynowy w plikach .mpy, które pozwala napisać niestandardowy kod C umieszczany w pliku .mpy. Można go dynamicznie importować do działającego systemu MicroPython bez potrzeby ponownej kompilacji głównego oprogramowania układowego.
Struktura zewnętrznego modułu C¶
Moduł C użytkownika MicroPython to katalog zawierający następujące pliki:
Pliki kodu źródłowego
*.c/*.cpp/*.hTwojego modułu.Zwykle zawierają one implementowaną funkcjonalność niskiego poziomu oraz funkcje wiążące MicroPython, które udostępniają funkcje i moduł(y).
Obecnie najlepszym źródłem informacji o pisaniu tych funkcji/modułów jest znalezienie podobnych modułów w drzewie MicroPython i wykorzystanie ich jako przykładów.
micropython.mkzawiera fragment pliku Makefile dla tego modułu.$(USERMOD_DIR)jest dostępna wmicropython.mkjako ścieżka do katalogu Twojego modułu. Ponieważ jest ona ponownie definiowana dla każdego modułu C, powinna zostać rozwinięta w Twoimmicropython.mkdo lokalnej zmiennej make, np.EXAMPLE_MOD_DIR := $(USERMOD_DIR)Twój
micropython.mkmusi dodać pliki źródłowe modułów do zmiennychSRC_USERMOD_ClubSRC_USERMOD_LIB_C. Pierwsza będzie przetwarzana pod kątem definicjiMP_QSTR_iMP_REGISTER_MODULE, druga nie (np. funkcje pomocnicze i kod biblioteczny, który nie jest specyficzny dla MicroPython). Ścieżki te powinny zawierać Twoją rozwiniętą kopię$(USERMOD_DIR), np.:SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
Analogicznie, dla plików źródłowych C++ użyj
SRC_USERMOD_CXXiSRC_USERMOD_LIB_CXX. Jeśli chcesz dołączyć pliki asemblera, użyjSRC_USERMOD_LIB_ASM.Jeśli masz niestandardowe opcje kompilatora (takie jak
-I, aby dodać katalogi do przeszukiwania w celu znalezienia plików nagłówkowych), powinny one zostać dodane doCFLAGS_USERMODdla kodu C oraz doCXXFLAGS_USERMODdla kodu C++.micropython.cmakezawiera konfigurację CMake dla tego modułu.W
micropython.cmakemożesz użyć${CMAKE_CURRENT_LIST_DIR}jako ścieżki do bieżącego modułu.Twój
micropython.cmakepowinien zdefiniować bibliotekęINTERFACEi powiązać z nią pliki źródłowe, definicje kompilacji oraz katalogi nagłówków. Następnie bibliotekę należy zlinkować z celemusermod.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)
Pełny przykład użycia znajdziesz poniżej.
Podstawowy przykład¶
Moduł cexample udostępnia przykłady funkcji i klasy. Funkcja cexample.add_ints(a, b) dodaje do siebie dwa argumenty całkowite i zwraca wynik. Typ cexample.Timer() tworzy liczniki czasu (timery), które można wykorzystać do pomiaru czasu, jaki upłynął od utworzenia instancji obiektu.
Moduł można znaleźć w drzewie źródłowym MicroPython w katalogu examples i zawiera plik źródłowy oraz fragment pliku Makefile o treści opisanej powyżej:
micropython/
└──examples/
└──usercmodule/
└──cexample/
├── examplemodule.c
├── micropython.mk
└── micropython.cmake
Dodatkowe wyjaśnienia znajdziesz w komentarzach w tych plikach. Obok modułu cexample znajduje się również cppexample, który działa w ten sam sposób, ale pokazuje jeden ze sposobów łączenia kodu C i C++ w MicroPython.
Kompilowanie cmodule do MicroPython¶
Aby zbudować taki moduł, skompiluj MicroPython (zobacz getting started), stosując 2 modyfikacje:
Ustaw flagę kompilacji
USER_C_MODULEStak, aby wskazywała na moduły, które chcesz dołączyć. W przypadku portów używających Make zmienna ta powinna być katalogiem, który jest automatycznie przeszukiwany w poszukiwaniu modułów. W przypadku portów używających CMake zmienna ta powinna być plikiem, który dołącza moduły do zbudowania. Szczegóły znajdziesz poniżej.Włącz moduły, ustawiając odpowiednie makro preprocesora C na 1. Jest to potrzebne tylko wtedy, gdy budowane moduły nie są włączane automatycznie.
Aby zbudować przykładowe moduły dostarczane z MicroPython, ustaw USER_C_MODULES na katalog examples/usercmodule dla Make lub na examples/usercmodule/micropython.cmake dla CMake.
Na przykład tak buduje się port unix z przykładowymi modułami:
cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule
Możliwe, że będziesz musiał raz uruchomić make clean na początku, gdy dołączasz nowe moduły użytkownika do kompilacji. Wynik kompilacji pokaże znalezione moduły:
...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...
W przypadku portu opartego na CMake, takiego jak rp2, będzie to wyglądać nieco inaczej (zwróć uwagę, że CMake jest faktycznie wywoływany przez make):
cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake
Ponownie, możliwe, że najpierw będziesz musiał uruchomić make clean, aby CMake wykrył moduły użytkownika. Wynik kompilacji CMake wymienia moduły z nazwy:
...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...
Zawartość najwyższego poziomu micropython.cmake może być użyta do kontrolowania, które moduły są włączone.
W przypadku własnych projektów wygodniej jest trzymać niestandardowy kod poza głównym drzewem źródłowym MicroPython, więc typowa struktura katalogu projektu będzie wyglądać tak:
my_project/
├── modules/
│ ├── example1/
│ │ ├── example1.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ ├── example2/
│ │ ├── example2.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ └── micropython.cmake
└── micropython/
├──ports/
... ├──stm32/
...
Podczas budowania z użyciem Make ustaw USER_C_MODULES na katalog my_project/modules. Na przykład budowanie portu stm32:
cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules
Podczas budowania z użyciem CMake plik najwyższego poziomu micropython.cmake – znajdujący się bezpośrednio w katalogu my_project/modules – powinien dołączać (include) wszystkie moduły, które chcesz mieć dostępne:
include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake) include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)
Następnie zbuduj za pomocą:
cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake
Możesz również podać ścieżki bezwzględne do USER_C_MODULES.
Wszystkie moduły określone przez zmienną USER_C_MODULES (znalezione w tym katalogu w przypadku używania Make lub dodane poprzez include w przypadku używania CMake) zostaną skompilowane, ale tylko te, które są włączone, będą dostępne do importowania. Moduły użytkownika są zwykle włączone domyślnie (decyduje o tym deweloper modułu), w którym to przypadku nie trzeba robić nic więcej poza ustawieniem USER_C_MODULES w sposób opisany powyżej.
Jeśli moduł nie jest włączony domyślnie, należy włączyć odpowiednie makro preprocesora C. Nazwę tego makra można znaleźć, wyszukując wiersz MP_REGISTER_MODULE w kodzie źródłowym modułu (zwykle pojawia się on na końcu głównego pliku źródłowego). Makro to powinno być otoczone parą #if X / #endif, a opcja konfiguracyjna X musi zostać ustawiona na 1 za pomocą CFLAGS_EXTRA, aby moduł był dostępny. Jeśli nie ma pary #if X / #endif, to moduł jest włączony domyślnie.
Na przykład moduł examples/usercmodule/cexample jest włączony domyślnie, więc ma w swoim kodzie źródłowym następujący wiersz:
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
Alternatywnie, aby ten moduł był domyślnie wyłączony, ale wybierany za pomocą opcji konfiguracyjnej preprocesora, byłoby to:
#if MODULE_CEXAMPLE_ENABLED MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule); #endif
W tym przypadku moduł jest włączany przez dodanie CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 do polecenia make albo przez edycję mpconfigport.h lub mpconfigboard.h i dodanie
#define MODULE_CEXAMPLE_ENABLED (1)
Zwróć uwagę, że dokładna metoda zależy od portu, ponieważ porty mają różne struktury. Jeśli nie zostanie to wykonane poprawnie, kompilacja się powiedzie, ale importowanie nie znajdzie modułu.
Użycie modułu w MicroPython¶
Po wbudowaniu do Twojej kopii MicroPython moduł można teraz wykorzystywać w Pythonie tak jak każdy inny wbudowany moduł, np.
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
Dynamiczna alokacja pamięci w C¶
MicroPython wykorzystuje własną „stertę Pythona” do Zarządzanie pamięcią, która nie jest tym samym co „sterta C” używana przez funkcje biblioteki C malloc(), free() itd. Nie każdy port MicroPython w ogóle ma „stertę C”.
Porty Tier 1 i 2 mają zróżnicowane wsparcie dla dynamicznej alokacji pamięci w C za pośrednictwem „sterty C”:
Porty unix, windows, esp32 i webassembly obsługują dynamiczną alokację pamięci w C.
Port rp2 nie zaalokuje żadnej pamięci w czasie wykonywania, chyba że oprogramowanie układowe zostanie zbudowane z
MICROPY_C_HEAP_SIZE=n, aby zarezerwowaćnbajtów pamięci dla sterty C. Ta pamięć nie będzie dostępna do użytku przez kod Pythona.Kompilacje portów alif, mimxrt, nrf, renesas-ra, samd i stm32 zawierające dynamiczną alokację C zakończą się niepowodzeniem na etapie linkowania z błędami takimi jak
undefined reference to `malloc'. MicroPython nie ma wbudowanego wsparcia dla dynamicznej alokacji C na tych portach. Każde rozwiązanie wymaga ręcznego dodania implementacji sterty C do niestandardowej kompilacji.Port zephyr obecnie nie obsługuje budowania z modułami użytkownika.
Sterta Pythona jako sterta C¶
Dla kodu C może być praktyczniejsze wywoływanie zamiast tego funkcji dynamicznej alokacji „sterty Pythona”, takich jak m_malloc(), m_malloc0() i m_free().
Więcej informacji o tym podejściu znajdziesz w Pamięć MicroPython z poziomu kodu C.
Moduły C++¶
Większość portów MicroPython Tier 1 i 2 (oraz niektóre Tier 3) obsługuje budowanie modułów użytkownika w C++ z wykorzystaniem opisanych powyżej zmiennych środowiskowych specyficznych dla C++.
Pomyślne zintegrowanie C++ i MicroPython wiąże się z kilkoma dodatkowymi kwestiami:
Dynamiczna alokacja pamięci w C++¶
Programy C++ (a także funkcje biblioteki standardowej C++) zwykle korzystają z dynamicznej alokacji pamięci. Domyślny alokator pamięci C++ (tj. operatory new i delete) jest zwykle implementowany jako warstwa nad Dynamiczna alokacja pamięci w C.
W przypadku portów MicroPython, które nie zawierają wsparcia dla dynamicznej alokacji pamięci w C, dynamiczną alokację pamięci w C++ można obsłużyć na jeden z dwóch sposobów:
Zaimplementuj dynamiczną alokację pamięci w C w swojej niestandardowej kompilacji.
Zaimplementuj niestandardowy alokator C++ w swojej niestandardowej kompilacji.
Kwestie linkowania¶
Ponieważ MicroPython jest projektem opartym na C, wszelkie symbole, które linkują się do MicroPython lub z niego, muszą być kwalifikowane jako extern "C" w kodzie C++.
Zdecydowanie zaleca się stosowanie wzorca przedstawionego w examples/usercmodule/cppexample, gdzie moduł Pythona jest zaimplementowany w minimalnym opakowaniu w pliku C wokół kodu C++.