Externe C-Module für MicroPython¶
Bei der Entwicklung von Modulen für MicroPython stoßen Sie möglicherweise auf Einschränkungen der Python-Umgebung, oft aufgrund eines fehlenden Zugriffs auf bestimmte Hardware-Ressourcen oder aufgrund von Geschwindigkeitsbeschränkungen von Python.
Wenn sich Ihre Einschränkungen nicht mit den Vorschlägen in Maximierung der MicroPython-Geschwindigkeit lösen lassen, ist es eine praktikable Option, Ihr Modul ganz oder teilweise in C (und/oder C++, sofern für Ihren Port implementiert) zu schreiben.
Wenn Ihr Modul für den Zugriff auf gängige Hardware oder Bibliotheken bzw. die Arbeit damit ausgelegt ist, sollten Sie in Betracht ziehen, es innerhalb des MicroPython-Quellbaums neben ähnlichen Modulen zu implementieren und als Pull-Request einzureichen. Wenn Sie hingegen exotische oder proprietäre Systeme anvisieren, kann es sinnvoller sein, dieses außerhalb des Haupt-Repositorys von MicroPython zu halten.
Dieses Kapitel beschreibt, wie solche externen Module in die ausführbare MicroPython-Datei oder das Firmware-Image kompiliert werden. Sowohl die Build-Tools Make als auch CMake werden unterstützt, und beim Schreiben eines externen Moduls ist es ratsam, die Build-Dateien für beide Tools hinzuzufügen, damit das Modul auf allen Ports verwendet werden kann. Beim Kompilieren eines bestimmten Ports müssen Sie jedoch nur eine Build-Methode verwenden, entweder Make oder CMake.
Ein alternativer Ansatz ist die Verwendung von Nativer Maschinencode in .mpy-Dateien, womit sich benutzerdefinierter C-Code schreiben lässt, der in einer .mpy-Datei abgelegt wird. Diese kann dynamisch in ein laufendes MicroPython-System importiert werden, ohne dass die Haupt-Firmware neu kompiliert werden muss.
Struktur eines externen C-Moduls¶
Ein MicroPython-Benutzer-C-Modul ist ein Verzeichnis mit den folgenden Dateien:
*.c/*.cpp/*.hQuellcode-Dateien für Ihr Modul.Diese umfassen typischerweise die implementierte Low-Level-Funktionalität und die MicroPython-Bindungsfunktionen, um die Funktionen und Module verfügbar zu machen.
Derzeit ist die beste Referenz zum Schreiben dieser Funktionen/Module, ähnliche Module innerhalb des MicroPython-Baums zu finden und sie als Beispiele zu verwenden.
micropython.mkenthält das Makefile-Fragment für dieses Modul.$(USERMOD_DIR)ist inmicropython.mkals Pfad zu Ihrem Modulverzeichnis verfügbar. Da es für jedes C-Modul neu definiert wird, sollte es in Ihrermicropython.mkzu einer lokalen Make-Variablen expandiert werden, z. B.EXAMPLE_MOD_DIR := $(USERMOD_DIR)Ihre
micropython.mkmuss die Quelldateien Ihrer Module zu den VariablenSRC_USERMOD_CoderSRC_USERMOD_LIB_Chinzufügen. Erstere werden aufMP_QSTR_- undMP_REGISTER_MODULE-Definitionen hin verarbeitet, letztere nicht (z. B. Hilfsfunktionen und Bibliothekscode, der nicht MicroPython-spezifisch ist). Diese Pfade sollten Ihre expandierte Kopie von$(USERMOD_DIR)enthalten, z. B.:SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
Verwenden Sie für C++-Quelldateien entsprechend
SRC_USERMOD_CXXundSRC_USERMOD_LIB_CXX. Wenn Sie Assembler-Dateien einbinden möchten, verwenden SieSRC_USERMOD_LIB_ASM.Wenn Sie benutzerdefinierte Compiler-Optionen haben (wie
-I, um Verzeichnisse zur Suche nach Header-Dateien hinzuzufügen), sollten diese für C-Code zuCFLAGS_USERMODund für C++-Code zuCXXFLAGS_USERMODhinzugefügt werden.micropython.cmakeenthält die CMake-Konfiguration für dieses Modul.In
micropython.cmakekönnen Sie${CMAKE_CURRENT_LIST_DIR}als Pfad zum aktuellen Modul verwenden.Ihre
micropython.cmakesollte eineINTERFACE-Bibliothek definieren und ihr Ihre Quelldateien, Compile-Definitionen und Include-Verzeichnisse zuordnen. Die Bibliothek sollte dann mit demusermod-Target verlinkt werden.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)
Ein vollständiges Anwendungsbeispiel finden Sie weiter unten.
Grundlegendes Beispiel¶
Das Modul cexample enthält Beispiele für eine Funktion und eine Klasse. Die Funktion cexample.add_ints(a, b) addiert zwei ganzzahlige Argumente und gibt das Ergebnis zurück. Der Typ cexample.Timer() erstellt Timer, mit denen sich die seit der Instanziierung des Objekts verstrichene Zeit messen lässt.
Das Modul befindet sich im MicroPython-Quellbaum im Verzeichnis examples und besteht aus einer Quelldatei und einem Makefile-Fragment mit dem oben beschriebenen Inhalt:
micropython/
└──examples/
└──usercmodule/
└──cexample/
├── examplemodule.c
├── micropython.mk
└── micropython.cmake
Weitere Erläuterungen finden Sie in den Kommentaren in diesen Dateien. Neben dem Modul cexample gibt es auch cppexample, das auf dieselbe Weise funktioniert, aber eine Möglichkeit zeigt, C- und C++-Code in MicroPython zu mischen.
Das cmodule in MicroPython kompilieren¶
Um ein solches Modul zu erstellen, kompilieren Sie MicroPython (siehe Erste Schritte) und nehmen Sie 2 Änderungen vor:
Setzen Sie das Build-Flag
USER_C_MODULES, damit es auf die Module zeigt, die Sie einbinden möchten. Bei Ports, die Make verwenden, sollte diese Variable ein Verzeichnis sein, das automatisch nach Modulen durchsucht wird. Bei Ports, die CMake verwenden, sollte diese Variable eine Datei sein, die die zu erstellenden Module einbindet. Einzelheiten finden Sie weiter unten.Aktivieren Sie die Module, indem Sie das entsprechende C-Präprozessor-Makro auf 1 setzen. Dies ist nur erforderlich, wenn die zu erstellenden Module nicht automatisch aktiviert werden.
Um die mit MicroPython gelieferten Beispielmodule zu erstellen, setzen Sie USER_C_MODULES für Make auf das Verzeichnis examples/usercmodule oder für CMake auf examples/usercmodule/micropython.cmake.
Hier ist beispielsweise, wie der Unix-Port mit den Beispielmodulen erstellt wird:
cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule
Möglicherweise müssen Sie zu Beginn einmal make clean ausführen, wenn Sie neue Benutzermodule in den Build aufnehmen. Die Build-Ausgabe zeigt die gefundenen Module an:
...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...
Bei einem CMake-basierten Port wie rp2 sieht dies etwas anders aus (beachten Sie, dass CMake tatsächlich von make aufgerufen wird):
cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake
Auch hier müssen Sie möglicherweise zuerst make clean ausführen, damit CMake die Benutzermodule erkennt. Die CMake-Build-Ausgabe listet die Module namentlich auf:
...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...
Über den Inhalt der micropython.cmake auf oberster Ebene lässt sich steuern, welche Module aktiviert werden.
Für Ihre eigenen Projekte ist es praktischer, benutzerdefinierten Code außerhalb des Haupt-Quellbaums von MicroPython zu halten, sodass eine typische Projektverzeichnisstruktur wie folgt aussieht:
my_project/
├── modules/
│ ├── example1/
│ │ ├── example1.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ ├── example2/
│ │ ├── example2.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ └── micropython.cmake
└── micropython/
├──ports/
... ├──stm32/
...
Setzen Sie beim Erstellen mit Make USER_C_MODULES auf das Verzeichnis my_project/modules. Zum Beispiel beim Erstellen des stm32-Ports:
cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules
Beim Erstellen mit CMake sollte die micropython.cmake auf oberster Ebene – die sich direkt im Verzeichnis my_project/modules befindet – alle Module per include einbinden, die Sie verfügbar haben möchten:
include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake) include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)
Erstellen Sie dann mit:
cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake
Sie können auch absolute Pfade für USER_C_MODULES angeben.
Alle durch die Variable USER_C_MODULES angegebenen Module (entweder bei Verwendung von Make in diesem Verzeichnis gefunden oder bei Verwendung von CMake per include hinzugefügt) werden kompiliert, aber nur die aktivierten stehen zum Importieren zur Verfügung. Benutzermodule sind in der Regel standardmäßig aktiviert (dies entscheidet der Entwickler des Moduls); in diesem Fall ist nichts weiter zu tun, als USER_C_MODULES wie oben beschrieben zu setzen.
Wenn ein Modul nicht standardmäßig aktiviert ist, muss das entsprechende C-Präprozessor-Makro aktiviert werden. Dieser Makroname lässt sich finden, indem man im Quellcode des Moduls nach der Zeile MP_REGISTER_MODULE sucht (sie erscheint normalerweise am Ende der Haupt-Quelldatei). Dieses Makro sollte von einem #if X / #endif-Paar umschlossen sein, und die Konfigurationsoption X muss mit CFLAGS_EXTRA auf 1 gesetzt werden, um das Modul verfügbar zu machen. Wenn es kein #if X / #endif-Paar gibt, ist das Modul standardmäßig aktiviert.
Das Modul examples/usercmodule/cexample ist beispielsweise standardmäßig aktiviert und hat daher die folgende Zeile in seinem Quellcode:
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
Um dieses Modul stattdessen standardmäßig zu deaktivieren, aber über eine Präprozessor-Konfigurationsoption auswählbar zu machen, würde es so lauten:
#if MODULE_CEXAMPLE_ENABLED MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule); #endif
In diesem Fall wird das Modul aktiviert, indem CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 zum make-Befehl hinzugefügt wird oder mpconfigport.h bzw. mpconfigboard.h bearbeitet wird, um Folgendes hinzuzufügen
#define MODULE_CEXAMPLE_ENABLED (1)
Beachten Sie, dass die genaue Methode vom Port abhängt, da diese unterschiedliche Strukturen haben. Wenn sie nicht korrekt ausgeführt wird, kompiliert das Modul zwar, aber beim Import wird es nicht gefunden.
Modulverwendung in MicroPython¶
Sobald das Modul in Ihre MicroPython-Kopie eingebaut ist, kann nun in Python wie auf jedes andere eingebaute Modul darauf zugegriffen werden, z. B.
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
Dynamische C-Speicherzuweisung¶
MicroPython verwendet seinen eigenen „Python-Heap“ für die Speicherverwaltung, der nicht mit dem von den C-Bibliotheksfunktionen malloc(), free() usw. verwendeten „C-Heap“ identisch ist. Nicht jeder MicroPython-Port verfügt überhaupt über einen „C-Heap“.
Tier-1- und Tier-2-Ports unterstützen die dynamische C-Speicherzuweisung über einen „C-Heap“ in unterschiedlichem Maße:
Die Ports unix, windows, esp32 und webassembly unterstützen die dynamische C-Speicherzuweisung.
Der rp2-Port wird zur Laufzeit keinen Speicher zuweisen können, es sei denn, die Firmware wird mit
MICROPY_C_HEAP_SIZE=nerstellt, umnBytes Speicher für einen C-Heap zu reservieren. Dieser Speicher steht Python-Code nicht zur Verfügung.Builds der Ports alif, mimxrt, nrf, renesas-ra, samd und stm32, die dynamische C-Zuweisung enthalten, schlagen beim Linken mit Fehlern wie
undefined reference to `malloc'fehl. MicroPython hat auf diesen Ports keine eingebaute Unterstützung für dynamische C-Zuweisung. Jede Lösung erfordert das manuelle Hinzufügen einer C-Heap-Implementierung zum benutzerdefinierten Build.Der zephyr-Port unterstützt derzeit kein Erstellen mit Benutzermodulen.
Python-Heap als C-Heap¶
Es kann praktischer sein, dass C-Code stattdessen die dynamischen Zuweisungsfunktionen des „Python-Heaps“ wie m_malloc(), m_malloc0() und m_free() aufruft.
Weitere Informationen zu diesem Ansatz finden Sie unter MicroPython-Speicher aus C-Code.
C++-Module¶
Die meisten Tier-1- und Tier-2-MicroPython-Ports (und einige Tier-3-Ports) unterstützen das Erstellen von C++-Benutzermodulen unter Verwendung der oben beschriebenen C++-spezifischen Umgebungsvariablen.
Die erfolgreiche Integration von C++ und MicroPython bringt einige zusätzliche Überlegungen mit sich:
Dynamische C++-Speicherzuweisung¶
C++-Programme (sowie Funktionen der C++-Standardbibliothek) verwenden typischerweise dynamische Speicherzuweisung. Der Standard-Speicherallokator von C++ (d. h. die Operatoren new und delete) ist typischerweise als Schicht über dem Dynamische C-Speicherzuweisung implementiert.
Bei MicroPython-Ports, die keine Unterstützung für dynamische C-Speicherzuweisung enthalten, kann die dynamische C++-Speicherzuweisung auf eine von zwei Arten unterstützt werden:
Implementieren Sie die dynamische C-Speicherzuweisung in Ihrem benutzerdefinierten Build.
Implementieren Sie einen benutzerdefinierten C++-Allokator in Ihrem benutzerdefinierten Build.
Überlegungen zum Linken¶
Da MicroPython ein C-basiertes Projekt ist, müssen alle Symbole, die zu oder von MicroPython verlinken, in C++-Code als extern "C" qualifiziert werden.
Es wird dringend empfohlen, dem in examples/usercmodule/cppexample gezeigten Muster zu folgen, bei dem das Python-Modul in einem minimalen C-Datei-Wrapper um den C++-Code herum implementiert ist.