Externe C-modules voor MicroPython¶
Bij het ontwikkelen van modules voor gebruik met MicroPython kun je tegen beperkingen van de Python-omgeving aanlopen, vaak doordat bepaalde hardwarebronnen niet toegankelijk zijn of vanwege de snelheidsbeperkingen van Python.
Als je beperkingen niet kunnen worden opgelost met de suggesties in Maximaliseren van de snelheid van MicroPython, is het schrijven van een deel of de gehele module in C (en/of C++ als dit voor jouw port is geïmplementeerd) een haalbare optie.
Als je module is ontworpen om toegang te krijgen tot of te werken met breed beschikbare hardware of libraries, overweeg dan om deze binnen de MicroPython-broncode te implementeren naast vergelijkbare modules en deze in te dienen als pull request. Als je echter obscure of propriëtaire systemen aanstuurt, kan het zinvoller zijn om deze buiten de hoofdrepository van MicroPython te houden.
Dit hoofdstuk beschrijft hoe je dergelijke externe modules in het uitvoerbare bestand of de firmware-image van MicroPython compileert. Zowel Make- als CMake-buildtools worden ondersteund, en bij het schrijven van een externe module is het verstandig om voor beide tools de buildbestanden toe te voegen, zodat de module op alle ports kan worden gebruikt. Maar bij het compileren van een specifieke port hoef je slechts één buildmethode te gebruiken, ofwel Make ofwel CMake.
Een alternatieve aanpak is het gebruik van Native machinecode in .mpy-bestanden, waarmee je aangepaste C-code kunt schrijven die in een .mpy-bestand wordt geplaatst en die dynamisch in een draaiend MicroPython-systeem kan worden geïmporteerd zonder dat de hoofdfirmware opnieuw hoeft te worden gecompileerd.
Structuur van een externe C-module¶
Een MicroPython-C-module van de gebruiker is een directory met de volgende bestanden:
*.c/*.cpp/*.hbroncodebestanden voor je module.Deze bevatten doorgaans de laaggelaagde functionaliteit die wordt geïmplementeerd en de MicroPython-bindingfuncties om de functies en module(s) beschikbaar te maken.
Momenteel is de beste referentie voor het schrijven van deze functies/modules om vergelijkbare modules binnen de MicroPython-broncode te vinden en deze als voorbeeld te gebruiken.
micropython.mkbevat het Makefile-fragment voor deze module.$(USERMOD_DIR)is inmicropython.mkbeschikbaar als het pad naar je moduledirectory. Aangezien deze voor elke C-module opnieuw wordt gedefinieerd, moet deze in jemicropython.mkworden uitgebreid naar een lokale make-variabele, bijvoorbeeldEXAMPLE_MOD_DIR := $(USERMOD_DIR)Je
micropython.mkmoet de broncodebestanden van je modules toevoegen aan de variabelenSRC_USERMOD_CofSRC_USERMOD_LIB_C. De eerste wordt verwerkt opMP_QSTR_- enMP_REGISTER_MODULE-definities, de laatste niet (bijvoorbeeld helpers en librarycode die niet MicroPython-specifiek is). Deze paden moeten je uitgebreide kopie van$(USERMOD_DIR)bevatten, bijvoorbeeld:SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
Gebruik op dezelfde manier
SRC_USERMOD_CXXenSRC_USERMOD_LIB_CXXvoor C++-broncodebestanden. Als je assemblybestanden wilt opnemen, gebruik danSRC_USERMOD_LIB_ASM.Als je aangepaste compileropties hebt (zoals
-Iom directories toe te voegen waarin naar headerbestanden wordt gezocht), moeten deze worden toegevoegd aanCFLAGS_USERMODvoor C-code en aanCXXFLAGS_USERMODvoor C++-code.micropython.cmakebevat de CMake-configuratie voor deze module.In
micropython.cmakekun je${CMAKE_CURRENT_LIST_DIR}gebruiken als het pad naar de huidige module.Je
micropython.cmakemoet eenINTERFACE-library definiëren en je broncodebestanden, compileerdefinities en include-directories hieraan koppelen. De library moet vervolgens worden gelinkt aan deusermod-target.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)
Zie hieronder voor een volledig gebruiksvoorbeeld.
Basisvoorbeeld¶
De module cexample biedt voorbeelden voor een functie en een class. De functie cexample.add_ints(a, b) telt twee integer-argumenten bij elkaar op en retourneert het resultaat. Het type cexample.Timer() maakt timers die kunnen worden gebruikt om de verstreken tijd te meten sinds het object is geïnstantieerd.
De module is te vinden in de MicroPython-broncode in de examples-directory en heeft een broncodebestand en een Makefile-fragment met inhoud zoals hierboven beschreven:
micropython/
└──examples/
└──usercmodule/
└──cexample/
├── examplemodule.c
├── micropython.mk
└── micropython.cmake
Raadpleeg de opmerkingen in deze bestanden voor aanvullende uitleg. Naast de module cexample is er ook cppexample, die op dezelfde manier werkt maar één manier laat zien om C- en C++-code in MicroPython te mengen.
De cmodule in MicroPython compileren¶
Om zo’n module te bouwen, compileer je MicroPython (zie aan de slag) en pas je 2 aanpassingen toe:
Stel de buildtime-flag
USER_C_MODULESin om naar de modules te wijzen die je wilt opnemen. Voor ports die Make gebruiken moet deze variabele een directory zijn die automatisch op modules wordt doorzocht. Voor ports die CMake gebruiken moet deze variabele een bestand zijn dat de te bouwen modules opneemt. Zie hieronder voor details.Schakel de modules in door de bijbehorende C-preprocessormacro op 1 te zetten. Dit is alleen nodig als de modules die je bouwt niet automatisch zijn ingeschakeld.
Om de voorbeeldmodules te bouwen die bij MicroPython worden geleverd, stel je USER_C_MODULES in op de directory examples/usercmodule voor Make, of op examples/usercmodule/micropython.cmake voor CMake.
Hier is bijvoorbeeld hoe je de unix-port bouwt met de voorbeeldmodules:
cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule
Mogelijk moet je eenmalig make clean uitvoeren aan het begin wanneer je nieuwe gebruikersmodules in de build opneemt. De build-uitvoer toont de gevonden modules:
...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...
Voor een CMake-gebaseerde port zoals rp2 ziet dit er iets anders uit (merk op dat CMake feitelijk wordt aangeroepen door make):
cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake
Ook hier moet je mogelijk eerst make clean uitvoeren zodat CMake de gebruikersmodules oppikt. De CMake-build-uitvoer toont de modules op naam:
...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...
De inhoud van de top-level micropython.cmake kan worden gebruikt om te bepalen welke modules zijn ingeschakeld.
Voor je eigen projecten is het handiger om aangepaste code buiten de hoofdbroncode van MicroPython te houden, dus een typische projectdirectorystructuur ziet er als volgt uit:
my_project/
├── modules/
│ ├── example1/
│ │ ├── example1.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ ├── example2/
│ │ ├── example2.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ └── micropython.cmake
└── micropython/
├──ports/
... ├──stm32/
...
Stel bij het bouwen met Make USER_C_MODULES in op de directory my_project/modules. Bijvoorbeeld, het bouwen van de stm32-port:
cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules
Bij het bouwen met CMake moet de top-level micropython.cmake – die zich direct in de directory my_project/modules bevindt – alle modules die je beschikbaar wilt hebben include-en:
include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake) include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)
Bouw vervolgens met:
cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake
Je kunt ook absolute paden opgeven voor USER_C_MODULES.
Alle modules die door de variabele USER_C_MODULES worden opgegeven (ofwel gevonden in deze directory bij gebruik van Make, ofwel toegevoegd via include bij gebruik van CMake) worden gecompileerd, maar alleen de modules die zijn ingeschakeld zijn beschikbaar voor importeren. Gebruikersmodules zijn meestal standaard ingeschakeld (dit wordt bepaald door de ontwikkelaar van de module), in welk geval er niets meer hoeft te gebeuren dan USER_C_MODULES instellen zoals hierboven beschreven.
Als een module niet standaard is ingeschakeld, moet de bijbehorende C-preprocessormacro worden ingeschakeld. Deze macronaam kun je vinden door te zoeken naar de regel MP_REGISTER_MODULE in de broncode van de module (deze staat meestal aan het einde van het hoofdbroncodebestand). Deze macro moet worden omgeven door een #if X / #endif-paar, en de configuratieoptie X moet met CFLAGS_EXTRA op 1 worden gezet om de module beschikbaar te maken. Als er geen #if X / #endif-paar is, dan is de module standaard ingeschakeld.
De module examples/usercmodule/cexample is bijvoorbeeld standaard ingeschakeld en heeft daarom de volgende regel in zijn broncode:
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
Om deze module daarentegen standaard uitgeschakeld maar selecteerbaar via een preprocessor-configuratieoptie te maken, zou het zijn:
#if MODULE_CEXAMPLE_ENABLED MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule); #endif
In dit geval wordt de module ingeschakeld door CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 toe te voegen aan het make-commando, of door mpconfigport.h of mpconfigboard.h te bewerken om toe te voegen
#define MODULE_CEXAMPLE_ENABLED (1)
Merk op dat de exacte methode afhangt van de port, aangezien deze verschillende structuren hebben. Als dit niet correct wordt gedaan, zal het wel compileren maar zal het importeren de module niet kunnen vinden.
Modulegebruik in MicroPython¶
Eenmaal ingebouwd in je kopie van MicroPython, is de module nu in Python toegankelijk net als elke andere ingebouwde module, bijvoorbeeld
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 geheugenallocatie in C¶
MicroPython gebruikt zijn eigen “Python heap” voor Geheugenbeheer, die niet hetzelfde is als de “C heap” die wordt gebruikt door C-libraryfuncties zoals malloc(), free(), enz. Niet elke MicroPython-port wordt überhaupt met een “C heap” geleverd.
Tier 1- en 2-ports hebben uiteenlopende ondersteuning voor dynamische C-geheugenallocatie via een “C heap”:
De unix-, windows-, esp32- en webassembly-ports ondersteunen dynamische C-geheugenallocatie.
De rp2-port zal er niet in slagen om tijdens runtime geheugen te alloceren, tenzij de firmware is gebouwd met
MICROPY_C_HEAP_SIZE=nomnbytes geheugen te reserveren voor een C heap. Dit geheugen is niet beschikbaar voor gebruik door Python-code.Builds van de alif-, mimxrt-, nrf-, renesas-ra-, samd- en stm32-port die dynamische C-allocatie bevatten zullen tijdens het linken mislukken met fouten zoals
undefined reference to `malloc'. MicroPython heeft op deze ports geen ingebouwde ondersteuning voor dynamische C-allocatie. Elke oplossing vereist het handmatig toevoegen van een C-heap-implementatie aan de aangepaste build.De zephyr-port ondersteunt momenteel het bouwen met gebruikersmodules niet.
Python heap als C heap¶
Het kan praktisch zijn voor C-code om in plaats daarvan dynamische allocatiefuncties van de “Python heap” aan te roepen, zoals m_malloc(), m_malloc0() en m_free().
Zie MicroPython-geheugen vanuit C-code voor meer informatie over deze aanpak.
C++-modules¶
De meeste Tier 1- en 2-MicroPython-ports (en sommige Tier 3) ondersteunen het bouwen van C++-gebruikersmodules, met behulp van de hierboven beschreven C++-specifieke omgevingsvariabelen.
Het succesvol integreren van C++ en MicroPython brengt enkele aanvullende overwegingen met zich mee:
Dynamische geheugenallocatie in C++¶
C++-programma’s (evenals features van de C++ Standard Library) gebruiken doorgaans dynamische geheugenallocatie. De standaard C++-geheugenallocator (d.w.z. de operators new en delete) is doorgaans geïmplementeerd als een laag boven op Dynamische geheugenallocatie in C.
Voor MicroPython-ports die geen ondersteuning voor dynamische C-geheugenallocatie bevatten, kan dynamische C++-geheugenallocatie op een van twee manieren worden ondersteund:
Implementeer dynamische C-geheugenallocatie in je aangepaste build.
Implementeer een aangepaste C++-allocator in je aangepaste build.
Overwegingen bij het linken¶
Omdat MicroPython een op C gebaseerd project is, moeten alle symbolen die naar of vanuit MicroPython linken in C++-code worden gekwalificeerd met extern "C".
Het wordt sterk aanbevolen om het patroon te volgen dat wordt gedemonstreerd in examples/usercmodule/cppexample, waar de Python-module is geïmplementeerd in een minimale C-bestandswrapper rondom de C++-code.