Externa C-moduler i MicroPython

När du utvecklar moduler för användning med MicroPython kan du upptäcka att du stöter på begränsningar i Python-miljön, ofta på grund av att det inte går att komma åt vissa hårdvaruresurser eller på grund av Pythons hastighetsbegränsningar.

Om dina begränsningar inte kan lösas med förslagen i Maximera hastigheten i MicroPython är det ett gångbart alternativ att skriva delar av eller hela modulen i C (och/eller C++ om det är implementerat för din port).

Om din modul är utformad för att komma åt eller arbeta med vanligt förekommande hårdvara eller bibliotek bör du överväga att implementera den i MicroPythons källträd tillsammans med liknande moduler och skicka in den som en pull request. Om du däremot riktar in dig på obskyra eller proprietära system kan det vara mer logiskt att hålla detta utanför huvudrepositoriet för MicroPython.

Det här kapitlet beskriver hur du kompilerar sådana externa moduler in i MicroPythons körbara fil eller fast programvara. Både byggverktygen Make och CMake stöds, och när du skriver en extern modul är det en bra idé att lägga till byggfilerna för båda dessa verktyg så att modulen kan användas på alla portar. Men när du kompilerar en viss port behöver du bara använda en byggmetod, antingen Make eller CMake.

Ett alternativt tillvägagångssätt är att använda Inbyggd maskinkod i .mpy-filer, vilket gör det möjligt att skriva anpassad C-kod som placeras i en .mpy-fil, som kan importeras dynamiskt in i ett körande MicroPython-system utan att den huvudsakliga fasta programvaran behöver kompileras om.

Strukturen hos en extern C-modul

En användardefinierad C-modul i MicroPython är en katalog med följande filer:

  • *.c / *.cpp / *.h källkodsfiler för din modul.

    Dessa innehåller vanligtvis den lågnivåfunktionalitet som implementeras samt MicroPythons bindningsfunktioner som exponerar funktionerna och modulen/modulerna.

    För närvarande är den bästa referensen för att skriva dessa funktioner/moduler att hitta liknande moduler i MicroPython-trädet och använda dem som exempel.

  • micropython.mk innehåller Makefile-fragmentet för den här modulen.

    $(USERMOD_DIR) är tillgängligt i micropython.mk som sökvägen till din modulkatalog. Eftersom det omdefinieras för varje C-modul bör det expanderas i din micropython.mk till en lokal make-variabel, t.ex. EXAMPLE_MOD_DIR := $(USERMOD_DIR)

    Din micropython.mk måste lägga till modulens källkodsfiler till variablerna SRC_USERMOD_C eller SRC_USERMOD_LIB_C. Den förstnämnda bearbetas för definitioner av MP_QSTR_ och MP_REGISTER_MODULE, den senare inte (t.ex. hjälpfunktioner och bibliotekskod som inte är specifik för MicroPython). Dessa sökvägar bör inkludera din expanderade kopia av $(USERMOD_DIR), t.ex.:

    SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c
    SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
    

    Använd på liknande sätt SRC_USERMOD_CXX och SRC_USERMOD_LIB_CXX för C++-källkodsfiler. Om du vill inkludera assemblerfiler använder du SRC_USERMOD_LIB_ASM.

    Om du har anpassade kompilatoralternativ (som -I för att lägga till kataloger att söka i efter headerfiler) bör dessa läggas till i CFLAGS_USERMOD för C-kod och i CXXFLAGS_USERMOD för C++-kod.

  • micropython.cmake innehåller CMake-konfigurationen för den här modulen.

    I micropython.cmake kan du använda ${CMAKE_CURRENT_LIST_DIR} som sökväg till den aktuella modulen.

    Din micropython.cmake bör definiera ett INTERFACE-bibliotek och koppla dina källkodsfiler, kompileringsdefinitioner och inkluderingskataloger till det. Biblioteket bör sedan länkas till målet usermod.

    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)
    

    Se nedan för ett fullständigt användningsexempel.

Grundläggande exempel

Modulen cexample tillhandahåller exempel på en funktion och en klass. Funktionen cexample.add_ints(a, b) adderar två heltalsargument och returnerar resultatet. Typen cexample.Timer() skapar timers som kan användas för att mäta den tid som förflutit sedan objektet instansierades.

Modulen finns i MicroPythons källträd i exempelkatalogen och har en källkodsfil och ett Makefile-fragment med innehåll enligt beskrivningen ovan:

micropython/
└──examples/
   └──usercmodule/
      └──cexample/
         ├── examplemodule.c
         ├── micropython.mk
         └── micropython.cmake

Se kommentarerna i dessa filer för ytterligare förklaring. Bredvid modulen cexample finns även cppexample som fungerar på samma sätt men visar ett sätt att blanda C- och C++-kod i MicroPython.

Kompilera cmodule in i MicroPython

För att bygga en sådan modul kompilerar du MicroPython (se komma igång) och tillämpar två ändringar:

  1. Sätt byggtidsflaggan USER_C_MODULES så att den pekar på de moduler du vill inkludera. För portar som använder Make ska denna variabel vara en katalog som automatiskt genomsöks efter moduler. För portar som använder CMake ska denna variabel vara en fil som inkluderar de moduler som ska byggas. Se nedan för detaljer.

  2. Aktivera modulerna genom att sätta motsvarande C-preprocessormakro till 1. Detta behövs bara om de moduler du bygger inte aktiveras automatiskt.

För att bygga de exempelmoduler som följer med MicroPython sätter du USER_C_MODULES till katalogen examples/usercmodule för Make, eller till examples/usercmodule/micropython.cmake för CMake.

Här är till exempel hur du bygger unix-porten med exempelmodulerna:

cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule

Du kan behöva köra make clean en gång i början när du inkluderar nya användarmoduler i bygget. Byggutdata visar de moduler som hittats:

...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...

För en CMake-baserad port som rp2 ser detta lite annorlunda ut (observera att CMake faktiskt anropas av make):

cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake

Återigen kan du behöva köra make clean först för att CMake ska upptäcka användarmodulerna. CMakes byggutdata listar modulerna med namn:

...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...

Innehållet i micropython.cmake på toppnivå kan användas för att styra vilka moduler som aktiveras.

För dina egna projekt är det bekvämare att hålla anpassad kod utanför MicroPythons huvudsakliga källträd, så en typisk projektkatalogstruktur ser ut så här:

my_project/
├── modules/
│   ├── example1/
│   │   ├── example1.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   ├── example2/
│   │   ├── example2.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   └── micropython.cmake
└── micropython/
    ├──ports/
   ... ├──stm32/
      ...

När du bygger med Make sätter du USER_C_MODULES till katalogen my_project/modules. Till exempel, för att bygga stm32-porten:

cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules

När du bygger med CMake bör micropython.cmake på toppnivå – som finns direkt i katalogen my_project/modulesinclude alla de moduler du vill ha tillgängliga:

include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)

Bygg sedan med:

cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake

Du kan även ange absoluta sökvägar till USER_C_MODULES.

Alla moduler som anges av variabeln USER_C_MODULES (antingen de som hittas i denna katalog när Make används, eller de som läggs till via include när CMake används) kommer att kompileras, men endast de som är aktiverade kommer att vara tillgängliga för import. Användarmoduler är vanligtvis aktiverade som standard (detta avgörs av modulens utvecklare), och i så fall behöver du inte göra något mer än att sätta USER_C_MODULES enligt beskrivningen ovan.

Om en modul inte är aktiverad som standard måste motsvarande C-preprocessormakro aktiveras. Detta makronamn kan hittas genom att söka efter raden MP_REGISTER_MODULE i modulens källkod (den förekommer vanligtvis i slutet av den huvudsakliga källkodsfilen). Detta makro bör omges av ett par #if X / #endif, och konfigurationsalternativet X måste sättas till 1 med hjälp av CFLAGS_EXTRA för att göra modulen tillgänglig. Om det inte finns något par #if X / #endif är modulen aktiverad som standard.

Till exempel är modulen examples/usercmodule/cexample aktiverad som standard och har därför följande rad i sin källkod:

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);

Alternativt, för att göra denna modul inaktiverad som standard men valbar via ett preprocessor-konfigurationsalternativ, skulle det vara:

#if MODULE_CEXAMPLE_ENABLED
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
#endif

I detta fall aktiveras modulen genom att lägga till CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 till make-kommandot, eller genom att redigera mpconfigport.h eller mpconfigboard.h för att lägga till

#define MODULE_CEXAMPLE_ENABLED (1)

Observera att den exakta metoden beror på porten eftersom de har olika strukturer. Om det inte görs korrekt kommer det att kompilera men importen kommer inte att hitta modulen.

Användning av modulen i MicroPython

När modulen väl är inbyggd i din kopia av MicroPython kan den nu nås i Python precis som vilken annan inbyggd modul som helst, t.ex.

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

Dynamisk minnesallokering i C

MicroPython använder sin egen ”Python-heap” för Minneshantering, vilket inte är samma sak som den ”C-heap” som används av C-bibliotekets funktioner malloc(), free() osv. Inte alla MicroPython-portar har överhuvudtaget en ”C-heap”.

Tier 1- och 2-portar har varierande stöd för dynamisk minnesallokering i C via en ”C-heap”:

  • Portarna unix, windows, esp32 och webassembly stöder dynamisk minnesallokering i C.

  • rp2-porten kommer att misslyckas med att allokera något minne vid körning om inte den fasta programvaran byggs med MICROPY_C_HEAP_SIZE=n för att reservera n byte minne för en C-heap. Detta minne kommer inte att vara tillgängligt för Python-kod att använda.

  • Byggen för portarna alif, mimxrt, nrf, renesas-ra, samd och stm32 som inkluderar dynamisk C-allokering kommer att misslyckas vid länkning med fel som undefined reference to `malloc'. MicroPython har inget inbyggt stöd för dynamisk C-allokering på dessa portar. Varje lösning kräver att man manuellt lägger till en C-heap-implementering i det anpassade bygget.

  • zephyr-porten stöder för närvarande inte byggen med användarmoduler.

Python-heapen som C-heap

Det kan vara praktiskt för C-kod att i stället anropa dynamiska allokeringsfunktioner för ”Python-heapen”, såsom m_malloc(), m_malloc0() och m_free().

Se MicroPython-minne från C-kod för mer information om detta tillvägagångssätt.

C++-moduler

De flesta MicroPython-portar i Tier 1 och 2 (och vissa i Tier 3) stöder byggande av användarmoduler i C++, med hjälp av de C++-specifika miljövariabler som beskrivs ovan.

Att framgångsrikt integrera C++ och MicroPython innebär några ytterligare överväganden:

Dynamisk minnesallokering i C++

C++-program (liksom funktioner i C++-standardbiblioteket) använder vanligtvis dynamisk minnesallokering. C++:s standardminnesallokerare (dvs. operatorerna new och delete) implementeras vanligtvis som ett lager ovanpå Dynamisk minnesallokering i C.

För MicroPython-portar som inte inkluderar stöd för dynamisk minnesallokering i C kan dynamisk minnesallokering i C++ stödjas på ett av två sätt:

  • Implementera dynamisk minnesallokering i C i ditt anpassade bygge.

  • Implementera en anpassad C++-allokerare i ditt anpassade bygge.

Länkningsöverväganden

Eftersom MicroPython är ett C-baserat projekt måste alla symboler som länkar till eller från MicroPython kvalificeras med extern "C" i C++-kod.

Det rekommenderas starkt att följa mönstret som demonstreras i examples/usercmodule/cppexample, där Python-modulen implementeras i en minimal C-filomslutning runt C++-koden.