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 / *.h Quellcode-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.mk enthält das Makefile-Fragment für dieses Modul.

    $(USERMOD_DIR) ist in micropython.mk als Pfad zu Ihrem Modulverzeichnis verfügbar. Da es für jedes C-Modul neu definiert wird, sollte es in Ihrer micropython.mk zu einer lokalen Make-Variablen expandiert werden, z. B. EXAMPLE_MOD_DIR := $(USERMOD_DIR)

    Ihre micropython.mk muss die Quelldateien Ihrer Module zu den Variablen SRC_USERMOD_C oder SRC_USERMOD_LIB_C hinzufügen. Erstere werden auf MP_QSTR_- und MP_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_CXX und SRC_USERMOD_LIB_CXX. Wenn Sie Assembler-Dateien einbinden möchten, verwenden Sie SRC_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 zu CFLAGS_USERMOD und für C++-Code zu CXXFLAGS_USERMOD hinzugefügt werden.

  • micropython.cmake enthält die CMake-Konfiguration für dieses Modul.

    In micropython.cmake können Sie ${CMAKE_CURRENT_LIST_DIR} als Pfad zum aktuellen Modul verwenden.

    Ihre micropython.cmake sollte eine INTERFACE-Bibliothek definieren und ihr Ihre Quelldateien, Compile-Definitionen und Include-Verzeichnisse zuordnen. Die Bibliothek sollte dann mit dem usermod-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:

  1. 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.

  2. 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=n erstellt, um n Bytes 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.