Module C externe pentru MicroPython

Atunci când dezvoltați module pentru a fi utilizate cu MicroPython, este posibil să întâmpinați limitări ale mediului Python, deseori din cauza imposibilității de a accesa anumite resurse hardware sau a limitărilor de viteză ale Python.

Dacă limitările dumneavoastră nu pot fi rezolvate cu sugestiile din Maximizarea vitezei MicroPython, scrierea unei părți sau a întregului modul în C (și/sau C++ dacă este implementat pentru portul dumneavoastră) este o opțiune viabilă.

Dacă modulul dumneavoastră este conceput pentru a accesa sau a lucra cu hardware sau biblioteci disponibile pe scară largă, vă rugăm să luați în considerare implementarea acestuia în arborele sursă MicroPython, alături de module similare, și trimiterea lui ca pull request. Dacă însă vizați sisteme obscure sau proprietare, ar putea avea mai mult sens să îl păstrați extern față de depozitul principal MicroPython.

Acest capitol descrie modul de compilare a unor astfel de module externe în executabilul sau imaginea de firmware MicroPython. Sunt acceptate atât instrumentele de compilare Make, cât și CMake, iar la scrierea unui modul extern este o idee bună să adăugați fișierele de compilare pentru ambele instrumente, astfel încât modulul să poată fi utilizat pe toate porturile. Dar la compilarea unui anumit port veți avea nevoie să folosiți o singură metodă de compilare, fie Make, fie CMake.

O abordare alternativă este utilizarea Cod mașină nativ în fișiere .mpy, care permite scrierea de cod C personalizat plasat într-un fișier .mpy, ce poate fi importat dinamic într-un sistem MicroPython în execuție, fără a fi necesară recompilarea firmware-ului principal.

Structura unui modul C extern

Un modul C de utilizator MicroPython este un director care conține următoarele fișiere:

  • Fișiere cu cod sursă *.c / *.cpp / *.h pentru modulul dumneavoastră.

    Acestea vor include de obicei funcționalitatea de nivel scăzut implementată și funcțiile de legătură (binding) MicroPython care expun funcțiile și modulul (modulele).

    În prezent, cea mai bună referință pentru scrierea acestor funcții/module este să găsiți module similare în arborele MicroPython și să le folosiți ca exemple.

  • micropython.mk conține fragmentul de Makefile pentru acest modul.

    $(USERMOD_DIR) este disponibil în micropython.mk ca cale către directorul modulului dumneavoastră. Deoarece este redefinit pentru fiecare modul C, ar trebui să fie expandat în micropython.mk într-o variabilă make locală, de exemplu EXAMPLE_MOD_DIR := $(USERMOD_DIR)

    Fișierul micropython.mk trebuie să adauge fișierele sursă ale modulelor dumneavoastră în variabilele SRC_USERMOD_C sau SRC_USERMOD_LIB_C. Prima va fi procesată pentru definițiile MP_QSTR_ și MP_REGISTER_MODULE, cea de-a doua nu (de exemplu, funcții ajutătoare și cod de bibliotecă care nu este specific MicroPython). Aceste căi ar trebui să includă copia expandată a $(USERMOD_DIR), de exemplu:

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

    În mod similar, utilizați SRC_USERMOD_CXX și SRC_USERMOD_LIB_CXX pentru fișierele sursă C++. Dacă doriți să includeți fișiere de asamblare, utilizați SRC_USERMOD_LIB_ASM.

    Dacă aveți opțiuni de compilator personalizate (cum ar fi -I pentru a adăuga directoare în care se caută fișiere antet), acestea ar trebui adăugate la CFLAGS_USERMOD pentru codul C și la CXXFLAGS_USERMOD pentru codul C++.

  • micropython.cmake conține configurația CMake pentru acest modul.

    În micropython.cmake, puteți utiliza ${CMAKE_CURRENT_LIST_DIR} ca fiind calea către modulul curent.

    Fișierul micropython.cmake ar trebui să definească o bibliotecă INTERFACE și să asocieze cu aceasta fișierele sursă, definițiile de compilare și directoarele de includere. Biblioteca ar trebui apoi legată de ținta 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)
    

    Consultați mai jos un exemplu complet de utilizare.

Exemplu de bază

Modulul cexample oferă exemple pentru o funcție și o clasă. Funcția cexample.add_ints(a, b) adună doi argumenți întregi și returnează rezultatul. Tipul cexample.Timer() creează temporizatoare care pot fi utilizate pentru a măsura timpul scurs de la instanțierea obiectului.

Modulul poate fi găsit în arborele sursă MicroPython în directorul de exemple și are un fișier sursă și un fragment de Makefile cu conținutul descris mai sus:

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

Consultați comentariile din aceste fișiere pentru explicații suplimentare. Pe lângă modulul cexample există și cppexample, care funcționează în același mod, dar arată o modalitate de a combina cod C și C++ în MicroPython.

Compilarea modulului cmodule în MicroPython

Pentru a construi un astfel de modul, compilați MicroPython (consultați getting started), aplicând 2 modificări:

  1. Setați indicatorul de compilare USER_C_MODULES pentru a indica modulele pe care doriți să le includeți. Pentru porturile care folosesc Make, această variabilă ar trebui să fie un director care este căutat automat pentru module. Pentru porturile care folosesc CMake, această variabilă ar trebui să fie un fișier care include modulele de compilat. Consultați mai jos pentru detalii.

  2. Activați modulele setând macroul corespunzător al preprocesorului C la 1. Acest lucru este necesar doar dacă modulele pe care le construiți nu sunt activate automat.

Pentru a construi modulele de exemplu care vin împreună cu MicroPython, setați USER_C_MODULES la directorul examples/usercmodule pentru Make, sau la examples/usercmodule/micropython.cmake pentru CMake.

De exemplu, iată cum se construiește portul unix cu modulele de exemplu:

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

Este posibil să fie nevoie să rulați make clean o dată la început, atunci când includeți module de utilizator noi în compilare. Rezultatul compilării va afișa modulele găsite:

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

Pentru un port bazat pe CMake, cum ar fi rp2, acest lucru va arăta puțin diferit (rețineți că CMake este de fapt invocat de make):

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

Din nou, este posibil să fie nevoie să rulați mai întâi make clean pentru ca CMake să preia modulele de utilizator. Rezultatul compilării CMake listează modulele după nume:

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

Conținutul fișierului micropython.cmake de nivel superior poate fi utilizat pentru a controla ce module sunt activate.

Pentru propriile proiecte este mai convenabil să păstrați codul personalizat în afara arborelui sursă principal MicroPython, astfel încât o structură tipică de director de proiect va arăta astfel:

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

Când construiți cu Make, setați USER_C_MODULES la directorul my_project/modules. De exemplu, construirea portului stm32:

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

Când construiți cu CMake, fișierul micropython.cmake de nivel superior – aflat direct în directorul my_project/modules – ar trebui să facă include la toate modulele pe care doriți să le aveți disponibile:

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

Apoi construiți cu:

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

Puteți specifica de asemenea căi absolute către USER_C_MODULES.

Toate modulele specificate de variabila USER_C_MODULES (fie găsite în acest director când se folosește Make, fie adăugate prin include când se folosește CMake) vor fi compilate, dar numai cele care sunt activate vor fi disponibile pentru import. Modulele de utilizator sunt de obicei activate în mod implicit (acest lucru este decis de dezvoltatorul modulului), caz în care nu mai este nimic de făcut decât să setați USER_C_MODULES așa cum este descris mai sus.

Dacă un modul nu este activat în mod implicit, atunci macroul corespunzător al preprocesorului C trebuie activat. Numele acestui macro poate fi găsit căutând linia MP_REGISTER_MODULE în codul sursă al modulului (de obicei apare la sfârșitul fișierului sursă principal). Acest macro ar trebui să fie încadrat de o pereche #if X / #endif, iar opțiunea de configurare X trebuie setată la 1 folosind CFLAGS_EXTRA pentru a face modulul disponibil. Dacă nu există o pereche #if X / #endif, atunci modulul este activat în mod implicit.

De exemplu, modulul examples/usercmodule/cexample este activat în mod implicit, deci are următoarea linie în codul său sursă:

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);

Alternativ, pentru a face acest modul dezactivat în mod implicit, dar selectabil printr-o opțiune de configurare a preprocesorului, ar fi:

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

În acest caz, modulul este activat prin adăugarea CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 la comanda make, sau prin editarea mpconfigport.h ori mpconfigboard.h pentru a adăuga

#define MODULE_CEXAMPLE_ENABLED (1)

Rețineți că metoda exactă depinde de port, deoarece acestea au structuri diferite. Dacă nu este făcut corect, se va compila, dar importul nu va reuși să găsească modulul.

Utilizarea modulului în MicroPython

Odată inclus în copia dumneavoastră de MicroPython, modulul poate fi acum accesat în Python la fel ca orice alt modul încorporat, de exemplu

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

Alocarea dinamică a memoriei în C

MicroPython utilizează propriul „heap Python” pentru Gestionarea memoriei, care nu este același cu „heap-ul C” folosit de funcțiile bibliotecii C malloc(), free() etc. Nu fiecare port MicroPython vine în general cu un „heap C”.

Porturile de nivel 1 și 2 au suport variabil pentru alocarea dinamică a memoriei C printr-un „heap C”:

  • Porturile unix, windows, esp32 și webassembly acceptă alocarea dinamică a memoriei C.

  • Portul rp2 nu va reuși să aloce nicio memorie în timpul execuției, decât dacă firmware-ul este construit cu MICROPY_C_HEAP_SIZE=n pentru a rezerva n octeți de memorie pentru un heap C. Această memorie nu va fi disponibilă pentru a fi utilizată de codul Python.

  • Compilările porturilor alif, mimxrt, nrf, renesas-ra, samd și stm32 care includ alocare dinamică C vor eșua la momentul legării cu erori precum undefined reference to `malloc'. MicroPython nu are suport încorporat pentru alocarea dinamică C pe aceste porturi. Orice soluție necesită adăugarea manuală a unei implementări de heap C la compilarea personalizată.

  • Portul zephyr nu acceptă în prezent compilarea cu module de utilizator.

Heap-ul Python ca heap C

Ar putea fi practic ca în schimb codul C să apeleze funcții de alocare dinamică din „heap-ul Python”, cum ar fi m_malloc(), m_malloc0() și m_free().

Consultați Memoria MicroPython din codul C pentru mai multe informații despre această abordare.

Module C++

Majoritatea porturilor MicroPython de nivel 1 și 2 (și unele de nivel 3) acceptă construirea de module de utilizator C++, folosind variabilele de mediu specifice C++ descrise mai sus.

Integrarea cu succes a C++ și MicroPython implică unele considerații suplimentare:

Alocarea dinamică a memoriei în C++

Programele C++ (precum și caracteristicile bibliotecii standard C++) utilizează de obicei alocarea dinamică a memoriei. Alocatorul de memorie implicit al C++ (adică operatorii new și delete) este de obicei implementat ca un strat deasupra Alocarea dinamică a memoriei în C.

Pentru porturile MicroPython care nu includ suport pentru alocarea dinamică a memoriei C, alocarea dinamică a memoriei C++ poate fi acceptată în unul din două moduri:

  • Implementați alocarea dinamică a memoriei C în compilarea dumneavoastră personalizată.

  • Implementați un alocator C++ personalizat în compilarea dumneavoastră personalizată.

Considerații privind legarea

Deoarece MicroPython este un proiect bazat pe C, orice simboluri care se leagă la sau de la MicroPython trebuie calificate extern "C" în codul C++.

Se recomandă cu insistență să urmați modelul demonstrat în examples/usercmodule/cppexample, unde modulul Python este implementat într-un înveliș (wrapper) de fișier C minimal în jurul codului C++.