Inbyggd maskinkod i .mpy-filer

Det här avsnittet beskriver hur man bygger och arbetar med .mpy-filer som innehåller inbyggd maskinkod från ett annat språk än Python. Detta gör att du kan skriva kod i ett språk som C, kompilera och länka den till en .mpy-fil och sedan importera filen som en vanlig Python-modul. Detta kan användas för att implementera funktionalitet som är prestandakritisk, eller för att inkludera ett befintligt bibliotek skrivet i ett annat språk.

En av de främsta fördelarna med att använda inbyggda .mpy-filer är att inbyggd maskinkod kan importeras dynamiskt av ett skript, utan att behöva bygga om den huvudsakliga MicroPython-firmwaren. Detta står i kontrast till Externa C-moduler i MicroPython som också tillåter att man definierar egna moduler i C, men dessa måste kompileras in i den huvudsakliga firmware-avbildningen.

Fokus här ligger på att använda C för att bygga inbyggda moduler, men i princip kan vilket språk som helst som kan kompileras till fristående maskinkod placeras i en .mpy-fil.

En inbyggd .mpy-modul byggs med verktyget mpy_ld.py, som finns i katalogen tools/ i projektet. Detta verktyg tar en uppsättning objektfiler (.o-filer) och länkar samman dem för att skapa en inbyggd .mpy-fil. Det kräver CPython 3 och biblioteket pyelftools v0.25 eller senare.

Funktioner som stöds och begränsningar

En .mpy-fil kan innehålla MicroPython-bytekod och/eller inbyggd maskinkod. Om den innehåller inbyggd maskinkod är .mpy-filen kopplad till en specifik arkitektur. Arkitekturer som för närvarande stöds är (dessa är de giltiga alternativen för variabeln ARCH, se nedan):

  • x86 (32-bitars)

  • x64 (64-bitars x86)

  • armv6m (ARM Thumb, t.ex. Cortex-M0)

  • armv7m (ARM Thumb 2, t.ex. Cortex-M3)

  • armv7emsp (ARM Thumb 2, enkel precision flyttal, t.ex. Cortex-M4F, Cortex-M7)

  • armv7emdp (ARM Thumb 2, dubbel precision flyttal, t.ex. Cortex-M7)

  • xtensa (icke-fönstrad, t.ex. ESP8266)

  • xtensawin (fönstrad med fönsterstorlek 8, t.ex. ESP32, ESP32S3)

  • rv32imc (RISC-V 32 bitar med komprimerade instruktioner, t.ex. ESP32C3, ESP32C6)

  • rv64imc (RISC-V 64 bitar med komprimerade instruktioner)

Om den valda plattformen stöder explicita arkitekturflaggor och du vill att den utgående .mpy-filen ska bära värdet av dessa flaggor, måste du skicka dem till flaggvariabeln ARCH_FLAGS när du bygger .mpy-filen.

Vid kompilering och länkning av den inbyggda .mpy-filen måste arkitekturen väljas, och motsvarande fil kan endast importeras på den arkitekturen (och om arkitekturflaggor finns, endast om de matchar målets förmågor). För mer information om .mpy-filer, se MicroPython .mpy-filer.

Inbyggd kod måste kompileras som positionsoberoende kod (PIC) och använda en global offset-tabell (GOT), även om detaljerna kring detta varierar från arkitektur till arkitektur. Vid import av .mpy-filer med inbyggd kod kan importmekanismen utföra viss grundläggande omlokalisering av den inbyggda koden. Detta inkluderar omlokalisering av text-, rodata- och BSS-sektioner.

Funktioner som stöds av länkaren och den dynamiska laddaren är:

  • exekverbar kod (text)

  • skrivskyddad data (rodata), inklusive strängar och konstant data (arrayer, structar osv)

  • nollställd data (BSS)

  • pekare i text till text, rodata och BSS

  • pekare i rodata till text, rodata och BSS

De kända begränsningarna är:

  • datasektioner stöds inte; lösning: använd BSS-data och initiera datavärdena explicit

  • statiska BSS-variabler stöds inte; lösning: använd globala BSS-variabler

  • trådlokala lagringsvariabler stöds inte på rv32imc; lösning: använd globala BSS-variabler eller allokera lite utrymme på heapen för att lagra dem

Så om din C-kod har skrivbar data, se till att datan definieras globalt, utan en initierare, och endast skrivs till inuti funktioner.

Den inbyggda modulen länkas inte automatiskt mot standardbiblioteken med statisk länkning som libm.a och libgcc.a, vilket kan leda till undefined symbol-fel. Du kan länka körningsbiblioteken genom att sätta LINK_RUNTIME = 1 i din Makefile. Egna statiskt länkade bibliotek kan också länkas genom att lägga till MPY_LD_FLAGS += -l path/to/library.a. Observera att dessa länkas in i den inbyggda modulen och inte delas med andra moduler eller systemet.

Länkarbegränsning: den inbyggda modulen länkas inte mot symboltabellen för den fullständiga MicroPython-firmwaren. Istället länkas den mot en explicit tabell av exporterade symboler som finns i mp_fun_table (i py/nativeglue.h), vilken är fastställd vid byggtid för firmwaren. Det är därför inte möjligt att helt enkelt anropa någon godtycklig HAL/OS/RTOS/system-funktion, till exempel, såvida inte den ligger på en fast adress. I så fall kan sökvägen till ett länkarskript som innehåller en serie symbolnamn och deras fasta adresser skickas till mpy_ld.py via kommandoradsargumentet --externs. På så sätt får symboler som förekommer i länkarskriptet företräde framför vad som tillhandahålls av objektfilerna, men för närvarande kommer objektfilernas implementering fortfarande att ligga kvar i den slutliga MPY-filen. Länkarskriptets parser har begränsade förmågor och används för närvarande endast för att tolka listan med ROM-symboler för ESP8266-porten (se ports/esp8266/boards/eagle.rom.addr.v6.ld).

Nya symboler kan läggas till i slutet av tabellen och firmwaren byggas om. Symbolerna måste också läggas till i fun_table-dictionaryt i tools/mpy_ld.py på samma plats. Detta gör att mpy_ld.py kan plocka upp de nya symbolerna och tillhandahålla omlokaliseringar för dem när mpy:n importeras. Slutligen, om symbolen är en funktion, bör ett makro eller en stub läggas till i py/dynruntime.h för att göra det enkelt att anropa funktionen.

Definiera en inbyggd modul

En inbyggd .mpy-modul definieras av en uppsättning filer som används för att bygga .mpy:n. Filsystemslayouten består av två huvuddelar, källfilerna och Makefilen:

  • I det enklaste fallet krävs endast en enda C-källfil, som innehåller all kod som kommer att kompileras in i .mpy-modulen. Denna C-källkod måste inkludera filen py/dynruntime.h för att komma åt MicroPythons dynamiska API, och måste åtminstone definiera en funktion som kallas mpy_init. Denna funktion blir modulens ingångspunkt, som anropas när modulen importeras.

    Modulen kan delas upp i flera C-källfiler om så önskas. Delar av modulen kan också implementeras i Python. Alla källfiler bör listas i Makefilen, genom att lägga till dem i variabeln SRC (se nedan). Detta inkluderar både C-källfiler samt eventuella Python-filer som kommer att inkluderas i den resulterande .mpy-filen.

  • Makefile innehåller byggkonfigurationen för modulen och listar de källfiler som används för att bygga .mpy-modulen. Den bör definiera MPY_DIR som platsen för MicroPython-arkivet (för att hitta header-filer, det relevanta Makefile-fragmentet och verktyget mpy_ld.py), MOD som namnet på modulen, SRC som listan över källfiler, eventuellt ange maskinarkitekturen via ARCH, tillsammans med valfria maskinarkitekturflaggor som anges via ARCH_FLAGS, och därefter inkludera py/dynruntime.mk.

Minimalt exempel

Detta avsnitt ger ett fullt fungerande exempel på en enkel modul som heter factorial. Denna modul tillhandahåller en enda funktion factorial.factorial(x) som beräknar fakulteten av indata och returnerar resultatet.

Kataloglayout:

factorial/
├── factorial.c
└── Makefile

Filen factorial.c innehåller:

// Include the header file to get access to the MicroPython API
#include "py/dynruntime.h"

// Helper function to compute factorial
static mp_int_t factorial_helper(mp_int_t x) {
    if (x == 0) {
        return 1;
    }
    return x * factorial_helper(x - 1);
}

// This is the function which will be called from Python, as factorial(x)
static mp_obj_t factorial(mp_obj_t x_obj) {
    // Extract the integer from the MicroPython input object
    mp_int_t x = mp_obj_get_int(x_obj);
    // Calculate the factorial
    mp_int_t result = factorial_helper(x);
    // Convert the result to a MicroPython integer object and return it
    return mp_obj_new_int(result);
}
// Define a Python reference to the function above
static MP_DEFINE_CONST_FUN_OBJ_1(factorial_obj, factorial);

// This is the entry point and is called when the module is imported
mp_obj_t mpy_init(mp_obj_fun_bc_t *self, size_t n_args, size_t n_kw, mp_obj_t *args) {
    // This must be first, it sets up the globals dict and other things
    MP_DYNRUNTIME_INIT_ENTRY

    // Make the function available in the module's namespace
    mp_store_global(MP_QSTR_factorial, MP_OBJ_FROM_PTR(&factorial_obj));

    // This must be last, it restores the globals dict
    MP_DYNRUNTIME_INIT_EXIT
}

Filen Makefile innehåller:

# Location of top-level MicroPython directory
MPY_DIR = ../../..

# Name of module
MOD = factorial

# Source files (.c or .py)
SRC = factorial.c

# Architecture to build for (x86, x64, armv6m, armv7m, xtensa, xtensawin, rv32imc, rv64imc)
ARCH = x64

# Include to get the rules for compiling and linking the module
include $(MPY_DIR)/py/dynruntime.mk

Kompilera modulen

De nödvändiga verktygen som behövs för att bygga en inbyggd .mpy-fil är:

  • MicroPython-arkivet (åtminstone katalogerna py/ och tools/).

  • CPython 3, och biblioteket pyelftools (t.ex. pip install 'pyelftools>=0.25').

  • GNU make.

  • En C-kompilator för målarkitekturen (om C-källkod används).

  • Eventuellt mpy-cross, byggd från MicroPython-arkivet (om .py-källkod används).

Var noga med att välja rätt ARCH för det mål du ska köra på. Bygg sedan med:

$ make

Utan att ändra Makefilen kan du ange målarkitekturen via:

$ make ARCH=armv7m

Samma gäller för valfria arkitekturflaggor via:

$ make ARCH=rv32imc ARCH_FLAGS=zba

Användning av modulen i MicroPython

När modulen är byggd bör det finnas en fil som heter factorial.mpy. Kopiera den så att den är åtkomlig på filsystemet för ditt MicroPython-system och kan hittas i importsökvägen. Modulen kan nu nås i Python precis som vilken annan modul som helst, till exempel:

import factorial
print(factorial.factorial(10))
# should display 3628800

Använda Picolibc vid byggande av moduler

Att använda Picolibc som ditt C-standardbibliotek stöds inte bara, utan är faktiskt standard för plattformarna rv32imc och rv64imc. Det finns dock ett par saker värda att nämna för att säkerställa att du inte stöter på problem senare vid byggande av kod.

Vissa förbyggda Picolibc-versioner (till exempel de som tillhandahålls av Ubuntu Linux som paketen picolibc-arm-none-eabi, picolibc-riscv64-unknown-elf och picolibc-xtensa-lx106-elf) förutsätter att trådlokal lagring (TLS) finns tillgänglig vid körning, men tyvärr stöder MicroPython-moduler inte detta på vissa arkitekturer (nämligen rv32imc och rv64imc). Detta innebär att vissa funktioner som tillhandahålls av Picolibc som standard kommer att använda TLS, vilket returnerar ett fel antingen under kompilering eller under länkning.

För ett exempel på hur detta kan påverka dig innehåller exempelmodulen examples/natmod/btree en lösning för att säkerställa att errno fungerar (leta efter __PICOLIBC_ERRNO_FUNCTION i Makefilen och följ spåret därifrån).

Ytterligare exempel

Se examples/natmod/ för ytterligare exempel som visar många av de tillgängliga funktionerna hos inbyggda .mpy-moduler. Sådana funktioner inkluderar:

  • användning av flera C-källfiler

  • inkludering av Python-kod tillsammans med C-kod

  • rodata- och BSS-data

  • minnesallokering

  • användning av flyttal

  • undantagshantering

  • inkludering av externa C-bibliotek