Módulos C externos de MicroPython¶
Al desarrollar módulos para usar con MicroPython, es posible que te encuentres con limitaciones del entorno de Python, a menudo debido a la imposibilidad de acceder a ciertos recursos de hardware o a limitaciones de velocidad de Python.
Si tus limitaciones no se pueden resolver con las sugerencias de Maximizar la velocidad de MicroPython, escribir parte o la totalidad de tu módulo en C (o C++ si está implementado para tu port) es una opción viable.
Si tu módulo está diseñado para acceder o trabajar con hardware o bibliotecas de uso común, considera implementarlo dentro del árbol de código fuente de MicroPython junto a módulos similares y enviarlo como un pull request. Sin embargo, si tu objetivo son sistemas oscuros o propietarios, puede que tenga más sentido mantenerlo fuera del repositorio principal de MicroPython.
Este capítulo describe cómo compilar dichos módulos externos en el ejecutable o la imagen de firmware de MicroPython. Se admiten tanto las herramientas de compilación Make como CMake, y al escribir un módulo externo es buena idea añadir los archivos de compilación para ambas herramientas, de modo que el módulo pueda usarse en todos los ports. Pero al compilar un port concreto solo necesitarás usar un método de compilación, ya sea Make o CMake.
Un enfoque alternativo es usar Código máquina nativo en archivos .mpy, que permite escribir código C personalizado que se coloca en un archivo .mpy y que se puede importar dinámicamente en un sistema MicroPython en ejecución sin necesidad de recompilar el firmware principal.
Estructura de un módulo C externo¶
Un módulo C de usuario de MicroPython es un directorio con los siguientes archivos:
Archivos de código fuente
*.c/*.cpp/*.hde tu módulo.Normalmente incluirán la funcionalidad de bajo nivel que se está implementando y las funciones de enlace de MicroPython para exponer las funciones y el módulo o módulos.
Actualmente, la mejor referencia para escribir estas funciones/módulos es buscar módulos similares dentro del árbol de MicroPython y usarlos como ejemplos.
micropython.mkcontiene el fragmento de Makefile para este módulo.$(USERMOD_DIR)está disponible enmicropython.mkcomo la ruta al directorio de tu módulo. Como se redefine para cada módulo C, debería expandirse en tumicropython.mka una variable local de make, por ejemploEXAMPLE_MOD_DIR := $(USERMOD_DIR)Tu
micropython.mkdebe añadir los archivos de código fuente de tus módulos a las variablesSRC_USERMOD_CoSRC_USERMOD_LIB_C. La primera se procesará en busca de definicionesMP_QSTR_yMP_REGISTER_MODULE, la segunda no (por ejemplo, código auxiliar y de bibliotecas que no es específico de MicroPython). Estas rutas deben incluir tu copia expandida de$(USERMOD_DIR), por ejemplo:SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
De forma similar, usa
SRC_USERMOD_CXXySRC_USERMOD_LIB_CXXpara archivos de código fuente C++. Si quieres incluir archivos de ensamblador usaSRC_USERMOD_LIB_ASM.Si tienes opciones de compilador personalizadas (como
-Ipara añadir directorios donde buscar archivos de cabecera), estas deben añadirse aCFLAGS_USERMODpara código C y aCXXFLAGS_USERMODpara código C++.micropython.cmakecontiene la configuración de CMake para este módulo.En
micropython.cmake, puedes usar${CMAKE_CURRENT_LIST_DIR}como la ruta al módulo actual.Tu
micropython.cmakedebe definir una bibliotecaINTERFACEy asociarle tus archivos de código fuente, definiciones de compilación y directorios de inclusión. La biblioteca debe luego enlazarse con el objetivousermod.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)
Consulta más abajo un ejemplo de uso completo.
Ejemplo básico¶
El módulo cexample proporciona ejemplos de una función y una clase. La función cexample.add_ints(a, b) suma dos argumentos enteros y devuelve el resultado. El tipo cexample.Timer() crea temporizadores que pueden usarse para medir el tiempo transcurrido desde que se instancia el objeto.
El módulo puede encontrarse en el árbol de código fuente de MicroPython en el directorio de ejemplos y tiene un archivo de código fuente y un fragmento de Makefile con el contenido descrito anteriormente:
micropython/
└──examples/
└──usercmodule/
└──cexample/
├── examplemodule.c
├── micropython.mk
└── micropython.cmake
Consulta los comentarios de estos archivos para obtener una explicación adicional. Junto al módulo cexample también está cppexample, que funciona de la misma manera pero muestra una forma de mezclar código C y C++ en MicroPython.
Compilar el cmodule en MicroPython¶
Para compilar dicho módulo, compila MicroPython (consulta getting started), aplicando 2 modificaciones:
Establece el indicador de tiempo de compilación
USER_C_MODULESpara que apunte a los módulos que quieras incluir. Para los ports que usan Make, esta variable debe ser un directorio que se busca automáticamente en busca de módulos. Para los ports que usan CMake, esta variable debe ser un archivo que incluya los módulos que se van a compilar. Consulta los detalles más abajo.Habilita los módulos estableciendo a 1 la macro de preprocesador de C correspondiente. Esto solo es necesario si los módulos que estás compilando no se habilitan automáticamente.
Para compilar los módulos de ejemplo que vienen con MicroPython, establece USER_C_MODULES en el directorio examples/usercmodule para Make, o en examples/usercmodule/micropython.cmake para CMake.
Por ejemplo, así es como se compila el port de unix con los módulos de ejemplo:
cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule
Puede que necesites ejecutar make clean una vez al principio al incluir nuevos módulos de usuario en la compilación. La salida de la compilación mostrará los módulos encontrados:
...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...
Para un port basado en CMake como rp2, esto se verá un poco diferente (ten en cuenta que CMake en realidad lo invoca make):
cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake
De nuevo, puede que necesites ejecutar make clean primero para que CMake detecte los módulos de usuario. La salida de la compilación de CMake lista los módulos por nombre:
...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...
El contenido del micropython.cmake de nivel superior puede usarse para controlar qué módulos se habilitan.
Para tus propios proyectos es más conveniente mantener el código personalizado fuera del árbol de código fuente principal de MicroPython, por lo que la estructura típica de un directorio de proyecto se verá así:
my_project/
├── modules/
│ ├── example1/
│ │ ├── example1.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ ├── example2/
│ │ ├── example2.c
│ │ ├── micropython.mk
│ │ └── micropython.cmake
│ └── micropython.cmake
└── micropython/
├──ports/
... ├──stm32/
...
Al compilar con Make, establece USER_C_MODULES en el directorio my_project/modules. Por ejemplo, al compilar el port stm32:
cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules
Al compilar con CMake, el micropython.cmake de nivel superior – ubicado directamente en el directorio my_project/modules – debe include todos los módulos que quieras tener disponibles:
include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake) include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)
Luego compila con:
cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake
También puedes especificar rutas absolutas en USER_C_MODULES.
Todos los módulos especificados por la variable USER_C_MODULES (ya sean encontrados en este directorio al usar Make, o añadidos mediante include al usar CMake) se compilarán, pero solo aquellos que estén habilitados estarán disponibles para importarse. Los módulos de usuario normalmente están habilitados de forma predeterminada (esto lo decide el desarrollador del módulo), en cuyo caso no hay nada más que hacer que establecer USER_C_MODULES como se describe arriba.
Si un módulo no está habilitado de forma predeterminada, entonces se debe habilitar la macro de preprocesador de C correspondiente. Este nombre de macro puede encontrarse buscando la línea MP_REGISTER_MODULE en el código fuente del módulo (normalmente aparece al final del archivo de código fuente principal). Esta macro debe estar rodeada por un par #if X / #endif, y la opción de configuración X debe establecerse en 1 usando CFLAGS_EXTRA para hacer que el módulo esté disponible. Si no hay un par #if X / #endif, entonces el módulo está habilitado de forma predeterminada.
Por ejemplo, el módulo examples/usercmodule/cexample está habilitado de forma predeterminada, por lo que tiene la siguiente línea en su código fuente:
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
Alternativamente, para que este módulo esté deshabilitado de forma predeterminada pero seleccionable mediante una opción de configuración del preprocesador, sería:
#if MODULE_CEXAMPLE_ENABLED MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule); #endif
En este caso, el módulo se habilita añadiendo CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 al comando make, o editando mpconfigport.h o mpconfigboard.h para añadir
#define MODULE_CEXAMPLE_ENABLED (1)
Ten en cuenta que el método exacto depende del port, ya que tienen estructuras diferentes. Si no se hace correctamente, compilará pero la importación no logrará encontrar el módulo.
Uso del módulo en MicroPython¶
Una vez compilado en tu copia de MicroPython, ahora se puede acceder al módulo en Python igual que a cualquier otro módulo integrado, por ejemplo
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
Asignación dinámica de memoria en C¶
MicroPython usa su propio «montículo de Python» para Gestión de memoria, que no es el mismo que el «montículo de C» usado por las funciones de la biblioteca de C malloc(), free(), etc. No todos los ports de MicroPython incluyen un «montículo de C».
Los ports de Nivel 1 y 2 tienen un soporte variable para la asignación dinámica de memoria en C mediante un «montículo de C»:
Los ports unix, windows, esp32 y webassembly admiten la asignación dinámica de memoria en C.
El port rp2 fallará al asignar cualquier memoria en tiempo de ejecución a menos que el firmware se compile con
MICROPY_C_HEAP_SIZE=npara reservarnbytes de memoria para un montículo de C. Esta memoria no estará disponible para que la use el código Python.Las compilaciones de los ports alif, mimxrt, nrf, renesas-ra, samd y stm32 que incluyen asignación dinámica de C fallarán en tiempo de enlazado con errores como
undefined reference to `malloc'. MicroPython no tiene soporte integrado para la asignación dinámica de C en estos ports. Cualquier solución requiere añadir manualmente una implementación de montículo de C a la compilación personalizada.El port zephyr actualmente no admite la compilación con módulos de usuario.
El montículo de Python como montículo de C¶
Puede ser práctico que el código C llame en su lugar a funciones de asignación dinámica del «montículo de Python» como m_malloc(), m_malloc0() y m_free().
Consulta Memoria de MicroPython desde código C para obtener más información sobre este enfoque.
Módulos C++¶
La mayoría de los ports de MicroPython de Nivel 1 y 2 (y algunos de Nivel 3) admiten la compilación de módulos de usuario en C++, usando las variables de entorno específicas de C++ descritas anteriormente.
Integrar C++ y MicroPython con éxito implica algunas consideraciones adicionales:
Asignación dinámica de memoria en C++¶
Los programas C++ (así como las características de la Biblioteca Estándar de C++) suelen usar asignación dinámica de memoria. El asignador de memoria predeterminado de C++ (es decir, los operadores new y delete) normalmente se implementa como una capa sobre Asignación dinámica de memoria en C.
Para los ports de MicroPython que no incluyen soporte de asignación dinámica de memoria en C, la asignación dinámica de memoria en C++ puede admitirse de una de estas dos formas:
Implementar la asignación dinámica de memoria en C en tu compilación personalizada.
Implementar un asignador de C++ personalizado en tu compilación personalizada.
Consideraciones de enlazado¶
Dado que MicroPython es un proyecto basado en C, cualquier símbolo que enlace hacia o desde MicroPython debe calificarse como extern "C" en el código C++.
Se recomienda encarecidamente seguir el patrón demostrado en examples/usercmodule/cppexample, donde el módulo de Python se implementa en un envoltorio mínimo de archivo C alrededor del código C++.