Modules C externes pour MicroPython¶
Lors du développement de modules destinés à MicroPython, il se peut que vous rencontriez des limitations liées à l’environnement Python, souvent dues à l’impossibilité d’accéder à certaines ressources matérielles ou aux limites de vitesse de Python.
Si ces limitations ne peuvent pas être résolues à l’aide des suggestions de Maximiser la vitesse de MicroPython, écrire tout ou partie de votre module en C (et/ou en C++ si cela est implémenté pour votre port) constitue une option envisageable.
Si votre module est conçu pour accéder à du matériel ou à des bibliothèques couramment disponibles, ou pour travailler avec eux, envisagez de l’implémenter au sein de l’arborescence des sources de MicroPython, aux côtés de modules similaires, et de le soumettre sous forme de pull request. Si en revanche vous ciblez des systèmes obscurs ou propriétaires, il peut être plus judicieux de le conserver à l’extérieur du dépôt principal de MicroPython.
Ce chapitre décrit comment compiler de tels modules externes dans l’exécutable ou l’image du micrologiciel MicroPython. Les outils de compilation Make et CMake sont tous deux pris en charge, et lors de l’écriture d’un module externe, il est judicieux d’ajouter les fichiers de compilation pour ces deux outils afin que le module puisse être utilisé sur tous les ports. Cependant, lors de la compilation d’un port particulier, vous n’aurez besoin d’utiliser qu’une seule méthode de compilation, Make ou CMake.
Une autre approche consiste à utiliser Code machine natif dans les fichiers .mpy, qui permet d’écrire du code C personnalisé placé dans un fichier .mpy, lequel peut être importé dynamiquement dans un système MicroPython en cours d’exécution sans qu’il soit nécessaire de recompiler le micrologiciel principal.
Structure d’un module C externe¶
Un module C utilisateur MicroPython est un répertoire contenant les fichiers suivants :
Les fichiers de code source
*.c/*.cpp/*.hde votre module.Ceux-ci comprennent généralement les fonctionnalités de bas niveau implémentées ainsi que les fonctions de liaison MicroPython servant à exposer les fonctions et le ou les modules.
Actuellement, la meilleure référence pour écrire ces fonctions/modules est de rechercher des modules similaires dans l’arborescence MicroPython et de les utiliser comme exemples.
micropython.mkcontient le fragment de Makefile pour ce module.$(USERMOD_DIR)est disponible dansmicropython.mken tant que chemin vers le répertoire de votre module. Comme il est redéfini pour chaque module C, il doit être développé dans votremicropython.mkvers une variable make locale, par exempleEXAMPLE_MOD_DIR := $(USERMOD_DIR)Votre
micropython.mkdoit ajouter les fichiers source de vos modules aux variablesSRC_USERMOD_CouSRC_USERMOD_LIB_C. La première sera traitée pour les définitionsMP_QSTR_etMP_REGISTER_MODULE, la seconde non (par exemple, les fonctions d’aide et le code de bibliothèque qui ne sont pas spécifiques à MicroPython). Ces chemins doivent inclure votre copie développée de$(USERMOD_DIR), par exempleSRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
De même, utilisez
SRC_USERMOD_CXXetSRC_USERMOD_LIB_CXXpour les fichiers source C++. Si vous souhaitez inclure des fichiers assembleur, utilisezSRC_USERMOD_LIB_ASM.Si vous avez des options de compilateur personnalisées (comme
-Ipour ajouter des répertoires de recherche de fichiers d’en-tête), celles-ci doivent être ajoutées àCFLAGS_USERMODpour le code C et àCXXFLAGS_USERMODpour le code C++.micropython.cmakecontient la configuration CMake pour ce module.Dans
micropython.cmake, vous pouvez utiliser${CMAKE_CURRENT_LIST_DIR}comme chemin vers le module courant.Votre
micropython.cmakedoit définir une bibliothèqueINTERFACEet y associer vos fichiers source, vos définitions de compilation et vos répertoires d’inclusion. La bibliothèque doit ensuite être liée à la cibleusermod.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)
Voir ci-dessous pour un exemple d’utilisation complet.
Exemple de base¶
Le module cexample fournit des exemples pour une fonction et une classe. La fonction cexample.add_ints(a, b) additionne deux arguments entiers et renvoie le résultat. Le type cexample.Timer() crée des minuteurs qui peuvent être utilisés pour mesurer le temps écoulé depuis l’instanciation de l’objet.
Le module se trouve dans l’arborescence des sources de MicroPython dans le répertoire des exemples et comporte un fichier source ainsi qu’un fragment de Makefile dont le contenu est décrit ci-dessus
micropython/
└──examples/
└──usercmodule/
└──cexample/
├── examplemodule.c
├── micropython.mk
└── micropython.cmake
Reportez-vous aux commentaires de ces fichiers pour des explications complémentaires. À côté du module cexample se trouve également cppexample, qui fonctionne de la même manière mais illustre une façon de mélanger du code C et C++ dans MicroPython.
Compiler le cmodule dans MicroPython¶
Pour compiler un tel module, compilez MicroPython (voir prise en main) en appliquant 2 modifications :
Définissez l’indicateur de compilation
USER_C_MODULESpour qu’il pointe vers les modules que vous souhaitez inclure. Pour les ports qui utilisent Make, cette variable doit être un répertoire qui est automatiquement parcouru à la recherche de modules. Pour les ports qui utilisent CMake, cette variable doit être un fichier qui inclut les modules à compiler. Voir ci-dessous pour plus de détails.Activez les modules en réglant sur 1 la macro de préprocesseur C correspondante. Cela n’est nécessaire que si les modules que vous compilez ne sont pas activés automatiquement.
Pour compiler les modules d’exemple fournis avec MicroPython, définissez USER_C_MODULES sur le répertoire examples/usercmodule pour Make, ou sur examples/usercmodule/micropython.cmake pour CMake.
Par exemple, voici comment compiler le port unix avec les modules d’exemple :
cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule
Il se peut que vous deviez exécuter make clean une fois au début lorsque vous incluez de nouveaux modules utilisateur dans la compilation. La sortie de compilation affichera les modules trouvés
...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...
Pour un port basé sur CMake tel que rp2, cela se présentera un peu différemment (notez que CMake est en réalité invoqué par make) :
cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake
Là encore, il se peut que vous deviez d’abord exécuter make clean pour que CMake prenne en compte les modules utilisateur. La sortie de compilation de CMake liste les modules par nom
...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...
Le contenu du micropython.cmake de premier niveau peut être utilisé pour contrôler quels modules sont activés.
Pour vos propres projets, il est plus pratique de garder le code personnalisé en dehors de l’arborescence principale des sources de MicroPython ; une structure de répertoire de projet typique ressemblera donc à ceci
my_project/
├── modules/
│ ├── example1/
│ │ ├── example1.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ ├── example2/
│ │ ├── example2.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ └── micropython.cmake
└── micropython/
├──ports/
... ├──stm32/
...
Lors d’une compilation avec Make, définissez USER_C_MODULES sur le répertoire my_project/modules. Par exemple, pour compiler le port stm32 :
cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules
Lors d’une compilation avec CMake, le micropython.cmake de premier niveau – qui se trouve directement dans le répertoire my_project/modules – doit faire un include de tous les modules que vous souhaitez rendre disponibles :
include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake) include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)
Compilez ensuite avec :
cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake
Vous pouvez également spécifier des chemins absolus pour USER_C_MODULES.
Tous les modules spécifiés par la variable USER_C_MODULES (qu’ils soient trouvés dans ce répertoire lors de l’utilisation de Make, ou ajoutés via include lors de l’utilisation de CMake) seront compilés, mais seuls ceux qui sont activés pourront être importés. Les modules utilisateur sont généralement activés par défaut (c’est le développeur du module qui en décide), auquel cas il n’y a rien de plus à faire que de définir USER_C_MODULES comme décrit ci-dessus.
Si un module n’est pas activé par défaut, la macro de préprocesseur C correspondante doit alors être activée. Le nom de cette macro peut être trouvé en recherchant la ligne MP_REGISTER_MODULE dans le code source du module (elle apparaît généralement à la fin du fichier source principal). Cette macro doit être entourée d’une paire #if X / #endif, et l’option de configuration X doit être réglée sur 1 à l’aide de CFLAGS_EXTRA pour rendre le module disponible. S’il n’y a pas de paire #if X / #endif, alors le module est activé par défaut.
Par exemple, le module examples/usercmodule/cexample est activé par défaut et comporte donc la ligne suivante dans son code source :
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
À l’inverse, pour rendre ce module désactivé par défaut mais sélectionnable via une option de configuration du préprocesseur, ce serait :
#if MODULE_CEXAMPLE_ENABLED MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule); #endif
Dans ce cas, le module est activé en ajoutant CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 à la commande make, ou en modifiant mpconfigport.h ou mpconfigboard.h pour y ajouter
#define MODULE_CEXAMPLE_ENABLED (1)
Notez que la méthode exacte dépend du port, car ceux-ci ont des structures différentes. Si ce n’est pas fait correctement, la compilation réussira mais l’importation ne parviendra pas à trouver le module.
Utilisation du module dans MicroPython¶
Une fois intégré à votre copie de MicroPython, le module est désormais accessible en Python comme n’importe quel autre module intégré, par exemple
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
Allocation dynamique de mémoire en C¶
MicroPython utilise son propre « tas Python » pour la Gestion de la mémoire, qui n’est pas le même que le « tas C » utilisé par les fonctions de bibliothèque C malloc(), free(), etc. Tous les ports MicroPython ne sont pas dotés d’un « tas C ».
Les ports de niveau 1 et 2 offrent une prise en charge variable de l’allocation dynamique de mémoire en C via un « tas C » :
Les ports unix, windows, esp32 et webassembly prennent en charge l’allocation dynamique de mémoire en C.
Le port rp2 échouera à allouer la moindre mémoire à l’exécution, à moins que le micrologiciel ne soit compilé avec
MICROPY_C_HEAP_SIZE=npour réservernoctets de mémoire pour un tas C. Cette mémoire ne sera pas disponible pour le code Python.Les compilations des ports alif, mimxrt, nrf, renesas-ra, samd et stm32 qui incluent une allocation dynamique en C échoueront à l’édition de liens avec des erreurs telles que
undefined reference to `malloc'. MicroPython ne prend pas en charge nativement l’allocation dynamique en C sur ces ports. Toute solution nécessite d’ajouter manuellement une implémentation de tas C à la compilation personnalisée.Le port zephyr ne prend actuellement pas en charge la compilation avec des modules utilisateur.
Le tas Python comme tas C¶
Il peut être pratique pour le code C d’appeler plutôt les fonctions d’allocation dynamique du « tas Python » telles que m_malloc(), m_malloc0() et m_free().
Voir La mémoire MicroPython depuis le code C pour plus d’informations sur cette approche.
Modules C++¶
La plupart des ports MicroPython de niveau 1 et 2 (et certains de niveau 3) prennent en charge la compilation de modules utilisateur C++, à l’aide des variables d’environnement spécifiques au C++ décrites ci-dessus.
Intégrer C++ et MicroPython avec succès implique quelques considérations supplémentaires :
Allocation dynamique de mémoire en C++¶
Les programmes C++ (ainsi que les fonctionnalités de la bibliothèque standard C++) utilisent généralement l’allocation dynamique de mémoire. L’allocateur de mémoire C++ par défaut (c’est-à-dire les opérateurs new et delete) est généralement implémenté comme une couche au-dessus de Allocation dynamique de mémoire en C.
Pour les ports MicroPython qui n’incluent pas la prise en charge de l’allocation dynamique de mémoire en C, l’allocation dynamique de mémoire en C++ peut être prise en charge de deux manières :
Implémentez l’allocation dynamique de mémoire en C dans votre compilation personnalisée.
Implémentez un allocateur C++ personnalisé dans votre compilation personnalisée.
Considérations relatives à l’édition de liens¶
Comme MicroPython est un projet basé sur le C, tous les symboles qui établissent un lien vers ou depuis MicroPython doivent être qualifiés par extern "C" dans le code C++.
Il est fortement recommandé de suivre le modèle illustré dans examples/usercmodule/cppexample, où le module Python est implémenté dans un fichier C minimal servant d’enveloppe autour du code C++.