Moduli C esterni di MicroPython¶
Quando sviluppi moduli da usare con MicroPython potresti scoprire di scontrarti con i limiti dell’ambiente Python, spesso a causa dell’impossibilità di accedere a determinate risorse hardware o per via dei limiti di velocità di Python.
Se i tuoi limiti non possono essere risolti con i suggerimenti in Massimizzare la velocità di MicroPython, scrivere parte o tutto il tuo modulo in C (e/o C++ se implementato per la tua port) è un’opzione praticabile.
Se il tuo modulo è progettato per accedere o lavorare con hardware o librerie comunemente disponibili, valuta la possibilità di implementarlo all’interno dell’albero dei sorgenti di MicroPython accanto a moduli simili e di inviarlo come pull request. Se invece il tuo obiettivo sono sistemi poco diffusi o proprietari, può avere più senso mantenerlo esterno al repository principale di MicroPython.
Questo capitolo descrive come compilare tali moduli esterni nell’eseguibile o nell’immagine firmware di MicroPython. Sono supportati sia gli strumenti di build Make sia CMake, e quando scrivi un modulo esterno è una buona idea aggiungere i file di build per entrambi questi strumenti, in modo che il modulo possa essere usato su tutte le port. Tuttavia, quando compili una particolare port dovrai usare solo un metodo di build, Make oppure CMake.
Un approccio alternativo è usare Codice macchina nativo nei file .mpy, che consente di scrivere codice C personalizzato collocato in un file .mpy, importabile dinamicamente in un sistema MicroPython in esecuzione senza la necessità di ricompilare il firmware principale.
Struttura di un modulo C esterno¶
Un modulo C utente di MicroPython è una directory con i seguenti file:
File di codice sorgente
*.c/*.cpp/*.hper il tuo modulo.Questi includeranno tipicamente la funzionalità di basso livello implementata e le funzioni di binding di MicroPython che espongono le funzioni e il modulo o i moduli.
Attualmente il miglior riferimento per scrivere queste funzioni/moduli è trovare moduli simili nell’albero di MicroPython e usarli come esempi.
micropython.mkcontiene il frammento di Makefile per questo modulo.$(USERMOD_DIR)è disponibile inmicropython.mkcome percorso della directory del tuo modulo. Poiché viene ridefinito per ciascun modulo C, dovrebbe essere espanso nel tuomicropython.mkin una variabile make locale, ad esempioEXAMPLE_MOD_DIR := $(USERMOD_DIR)Il tuo
micropython.mkdeve aggiungere i file sorgente dei tuoi moduli alle variabiliSRC_USERMOD_CoSRC_USERMOD_LIB_C. La prima verrà elaborata per le definizioniMP_QSTR_eMP_REGISTER_MODULE, la seconda no (ad esempio per helper e codice di libreria non specifico di MicroPython). Questi percorsi dovrebbero includere la tua copia espansa di$(USERMOD_DIR), ad esempio:SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
Allo stesso modo, usa
SRC_USERMOD_CXXeSRC_USERMOD_LIB_CXXper i file sorgente C++. Se vuoi includere file assembly usaSRC_USERMOD_LIB_ASM.Se hai opzioni del compilatore personalizzate (come
-Iper aggiungere directory in cui cercare i file header), queste dovrebbero essere aggiunte aCFLAGS_USERMODper il codice C e aCXXFLAGS_USERMODper il codice C++.micropython.cmakecontiene la configurazione CMake per questo modulo.In
micropython.cmakepuoi usare${CMAKE_CURRENT_LIST_DIR}come percorso del modulo corrente.Il tuo
micropython.cmakedovrebbe definire una libreriaINTERFACEe associarvi i tuoi file sorgente, le definizioni di compilazione e le directory di include. La libreria dovrebbe poi essere collegata al targetusermod.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)
Vedi sotto per un esempio d’uso completo.
Esempio di base¶
Il modulo cexample fornisce esempi per una funzione e una classe. La funzione cexample.add_ints(a, b) somma due argomenti interi e restituisce il risultato. Il tipo cexample.Timer() crea timer che possono essere usati per misurare il tempo trascorso da quando l’oggetto è stato istanziato.
Il modulo si trova nell’albero dei sorgenti di MicroPython nella directory examples e ha un file sorgente e un frammento di Makefile con il contenuto descritto sopra:
micropython/
└──examples/
└──usercmodule/
└──cexample/
├── examplemodule.c
├── micropython.mk
└── micropython.cmake
Fai riferimento ai commenti in questi file per ulteriori spiegazioni. Accanto al modulo cexample c’è anche cppexample che funziona allo stesso modo ma mostra un modo per mescolare codice C e C++ in MicroPython.
Compilare il cmodule in MicroPython¶
Per compilare un modulo del genere, compila MicroPython (vedi getting started), applicando 2 modifiche:
Imposta il flag di build-time
USER_C_MODULESin modo che punti ai moduli che vuoi includere. Per le port che usano Make questa variabile dovrebbe essere una directory in cui i moduli vengono cercati automaticamente. Per le port che usano CMake questa variabile dovrebbe essere un file che include i moduli da compilare. Vedi sotto per i dettagli.Abilita i moduli impostando a 1 la corrispondente macro del preprocessore C. Questo è necessario solo se i moduli che stai compilando non sono abilitati automaticamente.
Per compilare i moduli di esempio forniti con MicroPython, imposta USER_C_MODULES alla directory examples/usercmodule per Make, oppure a examples/usercmodule/micropython.cmake per CMake.
Ad esempio, ecco come compilare la port unix con i moduli di esempio:
cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule
Potrebbe essere necessario eseguire make clean una volta all’inizio quando includi nuovi moduli utente nella build. L’output della build mostrerà i moduli trovati:
...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...
Per una port basata su CMake come rp2, l’aspetto sarà un po” diverso (nota che CMake viene effettivamente invocato da make):
cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake
Anche in questo caso potrebbe essere necessario eseguire prima make clean affinché CMake rilevi i moduli utente. L’output della build di CMake elenca i moduli per nome:
...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...
Il contenuto del micropython.cmake di livello superiore può essere usato per controllare quali moduli sono abilitati.
Per i tuoi progetti è più comodo tenere il codice personalizzato fuori dall’albero principale dei sorgenti di MicroPython, quindi una tipica struttura di directory di progetto avrà questo aspetto:
my_project/
├── modules/
│ ├── example1/
│ │ ├── example1.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ ├── example2/
│ │ ├── example2.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ └── micropython.cmake
└── micropython/
├──ports/
... ├──stm32/
...
Quando compili con Make imposta USER_C_MODULES alla directory my_project/modules. Ad esempio, per compilare la port stm32:
cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules
Quando compili con CMake il micropython.cmake di livello superiore – che si trova direttamente nella directory my_project/modules – dovrebbe fare include di tutti i moduli che vuoi rendere disponibili:
include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake) include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)
Poi compila con:
cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake
Puoi anche specificare percorsi assoluti per USER_C_MODULES.
Tutti i moduli specificati dalla variabile USER_C_MODULES (sia quelli trovati in questa directory quando si usa Make, sia quelli aggiunti tramite include quando si usa CMake) verranno compilati, ma solo quelli abilitati saranno disponibili per l’importazione. I moduli utente sono solitamente abilitati per impostazione predefinita (questo lo decide lo sviluppatore del modulo), nel qual caso non c’è altro da fare se non impostare USER_C_MODULES come descritto sopra.
Se un modulo non è abilitato per impostazione predefinita, allora la corrispondente macro del preprocessore C deve essere abilitata. Il nome di questa macro può essere trovato cercando la riga MP_REGISTER_MODULE nel codice sorgente del modulo (di solito appare alla fine del file sorgente principale). Questa macro dovrebbe essere racchiusa in una coppia #if X / #endif, e l’opzione di configurazione X deve essere impostata a 1 usando CFLAGS_EXTRA per rendere disponibile il modulo. Se non c’è alcuna coppia #if X / #endif allora il modulo è abilitato per impostazione predefinita.
Ad esempio, il modulo examples/usercmodule/cexample è abilitato per impostazione predefinita e ha quindi la seguente riga nel suo codice sorgente:
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
In alternativa, per rendere questo modulo disabilitato per impostazione predefinita ma selezionabile tramite un’opzione di configurazione del preprocessore, sarebbe:
#if MODULE_CEXAMPLE_ENABLED MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule); #endif
In questo caso il modulo viene abilitato aggiungendo CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 al comando make, oppure modificando mpconfigport.h o mpconfigboard.h per aggiungere
#define MODULE_CEXAMPLE_ENABLED (1)
Nota che il metodo esatto dipende dalla port, poiché hanno strutture diverse. Se non fatto correttamente, la compilazione andrà a buon fine ma l’importazione non riuscirà a trovare il modulo.
Uso del modulo in MicroPython¶
Una volta compilato nella tua copia di MicroPython, il modulo può ora essere acceduto in Python proprio come qualsiasi altro modulo integrato, ad esempio
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
Allocazione dinamica della memoria C¶
MicroPython usa il proprio «heap Python» per Gestione della memoria, che non è lo stesso «heap C» usato dalle funzioni della libreria C malloc(), free(), ecc. Non tutte le port di MicroPython sono dotate di un «heap C».
Le port Tier 1 e 2 hanno un supporto variabile per l’allocazione dinamica della memoria C tramite un «heap C»:
Le port unix, windows, esp32 e webassembly supportano l’allocazione dinamica della memoria C.
La port rp2 non riuscirà ad allocare alcuna memoria a runtime a meno che il firmware non venga compilato con
MICROPY_C_HEAP_SIZE=nper riservarenbyte di memoria per un heap C. Questa memoria non sarà disponibile per l’uso da parte del codice Python.Le build delle port alif, mimxrt, nrf, renesas-ra, samd e stm32 che includono l’allocazione C dinamica falliranno in fase di link con errori come
undefined reference to `malloc'. MicroPython non ha supporto integrato per l’allocazione C dinamica su queste port. Qualsiasi soluzione richiede di aggiungere manualmente un’implementazione di heap C alla build personalizzata.La port zephyr attualmente non supporta la compilazione con moduli utente.
L’heap Python come heap C¶
Può essere pratico per il codice C chiamare invece le funzioni di allocazione dinamica dell“«heap Python» come m_malloc(), m_malloc0() e m_free().
Vedi La memoria di MicroPython dal codice C per maggiori informazioni su questo approccio.
Moduli C++¶
La maggior parte delle port MicroPython Tier 1 e 2 (e alcune Tier 3) supportano la compilazione di moduli utente C++, usando le variabili d’ambiente specifiche per C++ descritte sopra.
Integrare con successo C++ e MicroPython comporta alcune considerazioni aggiuntive:
Allocazione dinamica della memoria C++¶
I programmi C++ (così come le funzionalità della libreria standard C++) usano tipicamente l’allocazione dinamica della memoria. L’allocatore di memoria predefinito del C++ (cioè gli operatori new e delete) è tipicamente implementato come uno strato sopra Allocazione dinamica della memoria C.
Per le port MicroPython che non includono il supporto all’allocazione dinamica della memoria C, l’allocazione dinamica della memoria C++ può essere supportata in uno di due modi:
Implementare l’allocazione dinamica della memoria C nella tua build personalizzata.
Implementare un allocatore C++ personalizzato nella tua build personalizzata.
Considerazioni sul linking¶
Poiché MicroPython è un progetto basato su C, qualsiasi simbolo che si collega da o verso MicroPython deve essere qualificato come extern "C" nel codice C++.
Si consiglia vivamente di seguire il pattern dimostrato in examples/usercmodule/cppexample, dove il modulo Python è implementato in un file C minimale che fa da wrapper attorno al codice C++.