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.hför att komma åt MicroPythons dynamiska API, och måste åtminstone definiera en funktion som kallasmpy_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.Makefileinnehåller byggkonfigurationen för modulen och listar de källfiler som används för att bygga .mpy-modulen. Den bör definieraMPY_DIRsom platsen för MicroPython-arkivet (för att hitta header-filer, det relevanta Makefile-fragmentet och verktygetmpy_ld.py),MODsom namnet på modulen,SRCsom listan över källfiler, eventuellt ange maskinarkitekturen viaARCH, tillsammans med valfria maskinarkitekturflaggor som anges viaARCH_FLAGS, och därefter inkluderapy/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/ochtools/).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