MicroPython mikrovezérlőkön

A MicroPython úgy lett megtervezve, hogy képes legyen mikrovezérlőkön futni. Ezeknek olyan hardveres korlátai vannak, amelyek a hagyományos számítógépekkel jobban ismerős programozók számára szokatlanok lehetnek. Különösen a RAM és a nem felejtő „lemez” (flash memória) tárhely mennyisége korlátozott. Ez az útmutató módokat kínál a korlátozott erőforrások legjobb kihasználására. Mivel a MicroPython sokféle architektúrán alapuló vezérlőn fut, a bemutatott módszerek általánosak: bizonyos esetekben szükség lesz részletes információk megszerzésére a platformspecifikus dokumentációból.

Flash memória

Az OpenMV Cam eszközökön a korlátozott kapacitás kezelésének egyszerű módja egy micro SD-kártya behelyezése. Bizonyos esetekben ez nem kivitelezhető, akár azért, mert az eszköznek nincs SD-kártya foglalata, akár költség- vagy fogyasztási okokból; ezért a chipen lévő flash memóriát kell használni. A MicroPython alrendszert is tartalmazó firmware a beépített flash memóriában tárolódik. A fennmaradó kapacitás felhasználható. A flash memória fizikai architektúrájával összefüggő okokból e kapacitás egy része fájlrendszerként nem érhető el. Ilyen esetekben ez a terület úgy hasznosítható, hogy a felhasználói modulokat beépítjük egy firmware-buildbe, amelyet azután az eszközre flashelünk.

Ennek két módja van: a befagyasztott modulok (frozen modules) és a befagyasztott bájtkód (frozen bytecode). A befagyasztott modulok a Python forráskódot a firmware-rel együtt tárolják. A befagyasztott bájtkód a keresztfordítóval alakítja át a forrást bájtkóddá, amelyet azután a firmware-rel együtt tárol. Mindkét esetben a modul egy import utasítással érhető el:

import mymodule

A befagyasztott modulok és bájtkód előállításának eljárása platformfüggő; a firmware buildelésének utasításai a forrásfa megfelelő részében található README fájlokban találhatók.

Általánosságban a lépések a következők:

  • Klónozza a MicroPython tárolóját.

  • Szerezze be a (platformspecifikus) eszközláncot a firmware buildeléséhez.

  • Buildelje a keresztfordítót.

  • Helyezze a befagyasztandó modulokat egy megadott könyvtárba (attól függően, hogy a modult forrásként vagy bájtkódként kívánja befagyasztani).

  • Buildelje a firmware-t. Bármelyik típusú befagyasztott kód buildeléséhez egy adott parancsra lehet szükség - lásd a platform dokumentációját.

  • Flashelje a firmware-t az eszközre.

RAM

A RAM-használat csökkentésekor két fázist kell figyelembe venni: a fordítást és a végrehajtást. A memóriafogyasztás mellett van egy heap-töredezettség néven ismert probléma is. Általánosságban a legjobb minimalizálni az objektumok ismételt létrehozását és megsemmisítését. Ennek okát a heap témakört tárgyaló szakasz ismerteti.

Fordítási fázis

Amikor egy modul importálódik, a MicroPython bájtkóddá fordítja a kódot, amelyet azután a MicroPython virtuális gép (VM) hajt végre. A bájtkód a RAM-ban tárolódik. Maga a fordító is igényel RAM-ot, de ez a fordítás befejeztével felszabadul.

Ha már számos modult importáltak, előállhat az a helyzet, hogy nincs elegendő RAM a fordító futtatásához. Ebben az esetben az import utasítás memóriakivételt vált ki.

Ha egy modul importáláskor globális objektumokat példányosít, akkor importáláskor RAM-ot fogyaszt, ami azután a fordító számára nem áll rendelkezésre a későbbi importálásoknál. Általában a legjobb elkerülni az importáláskor futó kódot; jobb megközelítés egy olyan inicializáló kód, amelyet az alkalmazás futtat le az összes modul importálása után. Ez maximalizálja a fordító számára rendelkezésre álló RAM-ot.

Ha a RAM még mindig nem elegendő az összes modul fordításához, egy megoldás a modulok előfordítása. A MicroPython rendelkezik egy keresztfordítóval, amely képes Python modulokat bájtkóddá fordítani (lásd a README-t az mpy-cross könyvtárban). Az eredményül kapott bájtkód-fájl .mpy kiterjesztésű; átmásolható a fájlrendszerbe, és a szokásos módon importálható. Alternatívaként egyes vagy az összes modul befagyasztott bájtkódként is megvalósítható: a legtöbb platformon ez még több RAM-ot takarít meg, mivel a bájtkód közvetlenül a flash memóriából fut, ahelyett, hogy a RAM-ban tárolódna.

Végrehajtási fázis

Számos kódolási technika létezik a RAM-használat csökkentésére.

Konstansok

A MicroPython biztosít egy const kulcsszót, amely a következőképpen használható:

from micropython import const
ROWS = const(33)
_COLS = const(0x10)
a = ROWS
b = _COLS

Mindkét esetben, ahol a konstans egy változóhoz van rendelve, a fordító elkerüli a konstans nevének keresését kódoló utasítást azáltal, hogy a literális értékével helyettesíti azt. Ez bájtkódot és így RAM-ot takarít meg. Azonban a ROWS érték legalább két gépi szót foglal el, egyet-egyet a kulcs és az érték számára a globális szótárban. A szótárban való jelenlét azért szükséges, mert egy másik modul importálhatja vagy használhatja. Ez a RAM megtakarítható, ha a név elé aláhúzásjelet teszünk, mint a _COLS esetében: ez a szimbólum a modulon kívül nem látható, így nem foglal el RAM-ot.

A const() argumentuma bármi lehet, ami fordítási időben konstans értékre értékelődik ki, pl. 0x100, 1 << 8 vagy (True, "string", b"bytes") (a részleteket lásd az alábbi szakaszban). Akár olyan más const szimbólumokat is tartalmazhat, amelyek már definiálva lettek, pl. 1 << BIT.

Konstans adatstruktúrák

Ahol jelentős mennyiségű konstans adat van, és a platform támogatja a flash memóriából való végrehajtást, ott a RAM a következőképpen takarítható meg. Az adatokat Python modulokban kell elhelyezni, és bájtkódként befagyasztani. Az adatokat bytes objektumokként kell definiálni. A fordító «tudja», hogy a bytes objektumok megváltoztathatatlanok, és gondoskodik arról, hogy az objektumok a flash memóriában maradjanak, ahelyett, hogy a RAM-ba másolnák őket. A struct modul segíthet a bytes típusok és más Python beépített típusok közötti átalakításban.

A befagyasztott bájtkód következményeinek mérlegelésekor vegye figyelembe, hogy a Pythonban a karakterláncok, lebegőpontos számok, bájtok, egész számok, komplex számok és tuple-ök megváltoztathatatlanok. Ennek megfelelően ezek befagyasztásra kerülnek a flash memóriába (a tuple-ök esetében csak akkor, ha minden elemük megváltoztathatatlan). Így a következő sorban

mystring = "The quick brown fox"

a tényleges „The quick brown fox” karakterlánc a flash memóriában fog elhelyezkedni. Futásidőben a karakterláncra mutató hivatkozás a mystring változóhoz lesz rendelve. A hivatkozás egyetlen gépi szót foglal el. Elvileg egy hosszú egész szám is használható lenne konstans adatok tárolására:

bar = 0xDEADBEEF0000DEADBEEF

Akárcsak a karakterlánc-példában, futásidőben a tetszőlegesen nagy egész számra mutató hivatkozás a bar változóhoz lesz rendelve. Ez a hivatkozás egyetlen gépi szót foglal el.

A konstans objektumokból álló tuple-ök maguk is konstansak. Az ilyen konstans tuple-öket a fordító optimalizálja, így nem kell őket futásidőben minden használatkor létrehozni. Például:

foo = (1, 2, 3, 4, 5, 6, 100000, ("string", b"bytes", False, True))

Ez a teljes tuple egyetlen objektumként fog létezni (potenciálisan a flash memóriában, ha a kód be van fagyasztva), és minden szükséges alkalommal hivatkozik rá.

Szükségtelen objektumlétrehozás

Számos olyan helyzet van, amikor objektumok akaratlanul létrejöhetnek és megsemmisülhetnek. Ez töredezettség révén csökkentheti a RAM használhatóságát. A következő szakaszok ennek eseteit tárgyalják.

Karakterlánc-összefűzés

Vegye fontolóra a következő kódrészleteket, amelyek konstans karakterláncok előállítását célozzák:

var = "foo" + "bar"
var1 = "foo" "bar"
var2 = """\
foo\
bar"""

Mindegyik ugyanazt az eredményt adja, azonban az első szükségtelenül két karakterlánc-objektumot hoz létre futásidőben, és további RAM-ot foglal le az összefűzéshez, mielőtt a harmadikat előállítaná. A többiek fordítási időben végzik az összefűzést, ami hatékonyabb, és csökkenti a töredezettséget.

Ahol a karakterláncokat dinamikusan kell létrehozni, mielőtt egy adatfolyamba, például egy fájlba táplálnák őket, RAM-ot takarít meg, ha ezt darabokban végezzük. Ahelyett, hogy egy nagy karakterlánc-objektumot hoznánk létre, hozzunk létre egy részkarakterláncot, és tápláljuk az adatfolyamba, mielőtt a következővel foglalkoznánk.

A dinamikus karakterláncok létrehozásának legjobb módja a karakterlánc format() metódusa:

var = "Temperature {:5.2f} Pressure {:06d}\n".format(temp, press)

Pufferek

Az olyan eszközök elérésekor, mint az UART, I2C és SPI interfészek példányai, az előre lefoglalt pufferek használata elkerüli a szükségtelen objektumok létrehozását. Vegye fontolóra ezt a két ciklust:

while True:
    var = spi.read(100)
    # process data

buf = bytearray(100)
while True:
    spi.readinto(buf)
    # process data in buf

Az első minden áthaladáskor létrehoz egy puffert, míg a második egy előre lefoglalt puffert használ újra; ez egyszerre gyorsabb és hatékonyabb a memóriatöredezettség szempontjából.

A bájtok kisebbek, mint az egész számok

A legtöbb platformon egy egész szám négy bájtot fogyaszt. Vegye fontolóra a foo() függvény három hívását:

def foo(bar):
    for x in bar:
        print(x)
foo([1, 2, 0xff])
foo((1, 2, 0xff))
foo(b'\1\2\xff')

Az első hívásban egy egész számokból álló list jön létre a RAM-ban minden alkalommal, amikor a kód végrehajtódik. A második hívás egy konstans tuple objektumot (egy kizárólag konstans objektumokat tartalmazó tuple-t) hoz létre a fordítási fázis részeként, így csak egyszer jön létre, és hatékonyabb, mint a list. A harmadik hívás hatékonyan hoz létre egy bytes objektumot, amely a minimális mennyiségű RAM-ot fogyasztja. Ha a modul bájtkódként lenne befagyasztva, mind a tuple, mind a bytes objektum a flash memóriában helyezkedne el.

Karakterláncok kontra bájtok

A Python3 bevezette a Unicode támogatást. Ez különbséget tett egy karakterlánc és egy bájttömb között. A MicroPython gondoskodik arról, hogy a Unicode karakterláncok ne foglaljanak el további helyet, amíg a karakterlánc minden karaktere ASCII (azaz értéke < 128). Ha a teljes 8 bites tartományba eső értékekre van szükség, akkor a bytes és bytearray objektumok használhatók annak biztosítására, hogy ne legyen szükség további helyre. Vegye figyelembe, hogy a legtöbb karakterlánc-metódus (pl. str.strip()) a bytes példányokra is alkalmazható, így a Unicode kiküszöbölésének folyamata fájdalommentes lehet.

s = 'the quick brown fox'   # A string instance
b = b'the quick brown fox'  # A bytes instance

Ahol szükséges a karakterláncok és bájtok közötti átalakítás, ott a str.encode() és a bytes.decode() metódusok használhatók. Vegye figyelembe, hogy mind a karakterláncok, mind a bájtok megváltoztathatatlanok. Bármely művelet, amely bemenetként egy ilyen objektumot vesz és egy másikat állít elő, legalább egy RAM-lefoglalást von maga után az eredmény előállításához. Az alábbi második sorban egy új bytes objektum kerül lefoglalásra. Ez akkor is megtörténne, ha a foo egy karakterlánc lenne.

foo = b'   empty whitespace'
foo = foo.lstrip()

Fordító futtatása futásidőben

A Python eval és exec függvényei futásidőben hívják meg a fordítót, ami jelentős mennyiségű RAM-ot igényel. Vegye figyelembe, hogy a micropython-lib-ből származó pickle könyvtár exec-et használ. RAM-hatékonyabb lehet a json könyvtár használata az objektumok szerializálásához.

Karakterláncok tárolása a flash memóriában

A Python karakterláncok megváltoztathatatlanok, ezért lehetőség van a csak olvasható memóriában való tárolásukra. A fordító a Python kódban definiált karakterláncokat elhelyezheti a flash memóriában. A befagyasztott modulokhoz hasonlóan szükséges, hogy a PC-n legyen a forrásfa egy másolata és az eszközlánc a firmware buildeléséhez. Az eljárás akkor is működik, ha a modulok nincsenek teljesen hibakeresve, amennyiben importálhatók és futtathatók.

A modulok importálása után hajtsa végre:

micropython.qstr_info(1)

Ezután másolja és illessze be az összes Q(xxx) sort egy szövegszerkesztőbe. Ellenőrizze és távolítsa el a nyilvánvalóan érvénytelen sorokat. Nyissa meg a qstrdefsport.h fájlt, amely a ports/stm32 könyvtárban található (vagy a használt architektúrának megfelelő könyvtárban). Másolja és illessze be a kijavított sorokat a fájl végére. Mentse el a fájlt, buildelje újra és flashelje a firmware-t. Az eredmény ellenőrizhető a modulok importálásával és az alábbi ismételt kiadásával:

micropython.qstr_info(1)

A Q(xxx) soroknak el kell tűnniük.

A heap

Amikor egy futó program egy objektumot példányosít, a szükséges RAM egy fix méretű, heap néven ismert készletből kerül lefoglalásra. Amikor az objektum kikerül a hatókörből (más szóval elérhetetlenné válik a kód számára), a feleslegessé vált objektumot „szemétnek” nevezzük. Egy „szemétgyűjtés” (garbage collection, GC) néven ismert folyamat visszanyeri ezt a memóriát, visszaadva azt a szabad heap-nek. Ez a folyamat automatikusan fut, azonban közvetlenül is meghívható a gc.collect() kiadásával.

Az erről szóló fejtegetés meglehetősen összetett. Egy «gyors megoldásként» adja ki rendszeresen a következőt:

gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())

További információért lásd alább és a beépített gc modul dokumentációját.

A MicroPython belső működése/fejlesztői szemszögéből származó részletekért lásd még: Memóriakezelés.

Töredezettség

Tegyük fel, hogy egy program létrehoz egy foo objektumot, majd egy bar objektumot. Ezt követően a foo kikerül a hatókörből, de a bar megmarad. A foo által használt RAM-ot a GC visszanyeri. Ha azonban a bar egy magasabb címre lett lefoglalva, a foo-ból visszanyert RAM csak a foo-nál nem nagyobb objektumok számára lesz hasznos. Egy összetett vagy hosszan futó programban a heap töredezetté válhat: annak ellenére, hogy jelentős mennyiségű RAM áll rendelkezésre, nincs elegendő összefüggő hely egy adott objektum lefoglalásához, és a program memóriahibával leáll.

A fent vázolt technikák ennek minimalizálását célozzák. Ahol nagy, állandó pufferekre vagy más objektumokra van szükség, a legjobb ezeket korán példányosítani a program végrehajtási folyamatában, mielőtt a töredezettség bekövetkezhetne. További javulások érhetők el a heap állapotának figyelésével és a GC vezérlésével; ezeket alább vázoljuk.

Jelentéskészítés

Számos könyvtárfüggvény áll rendelkezésre a memóriafoglalás jelentésére és a GC vezérlésére. Ezek a gc és micropython modulokban találhatók. A következő példa beilleszthető a REPL-be (Ctrl-E a beillesztési mód belépéséhez, Ctrl-D a futtatáshoz).

import gc
import micropython
gc.collect()
micropython.mem_info()
print('-----------------------------')
print('Initial free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
def func():
    a = bytearray(10000)
gc.collect()
print('Func definition: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
func()
print('Func run free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
gc.collect()
print('Garbage collect free: {} allocated: {}'.format(gc.mem_free(), gc.mem_alloc()))
print('-----------------------------')
micropython.mem_info(1)

A fent alkalmazott metódusok:

  • gc.collect() Szemétgyűjtés kényszerítése. Lásd a lábjegyzetet.

  • micropython.mem_info() A RAM-kihasználtság összefoglalójának kiírása.

  • gc.mem_free() A szabad heap méretének visszaadása bájtban.

  • gc.mem_alloc() A jelenleg lefoglalt bájtok számának visszaadása.

  • micropython.mem_info(1) A heap-kihasználtság táblázatának kiírása (lentebb részletezve).

Az előállított számok platformfüggőek, de látható, hogy a függvény deklarálása a fordító által kibocsátott bájtkód formájában kis mennyiségű RAM-ot használ (a fordító által használt RAM visszanyerésre került). A függvény futtatása több mint 10KiB-ot használ, de visszatéréskor az a szemét, mert kikerült a hatókörből, és nem lehet hivatkozni rá. A végső gc.collect() visszanyeri azt a memóriát.

A micropython.mem_info(1) által előállított végső kimenet részleteiben változó lesz, de a következőképpen értelmezhető:

Szimbólum

Jelentés

.

szabad blokk

h

fejblokk

=

farokblokk

m

megjelölt fejblokk

T

tuple

L

lista

D

dict

F

float

B

bájtkód

M

modul

S

karakterlánc vagy bájtok

A

bytearray

Minden betű egyetlen memóriablokkot képvisel, ahol egy blokk 16 bájt. Tehát a heap-kiíratás minden sora 0x400 bájtot, azaz 1KiB RAM-ot képvisel.

A szemétgyűjtés vezérlése

A GC bármikor kikényszeríthető a gc.collect() kiadásával. Előnyös ezt időközönként megtenni, egyrészt a töredezettség megelőzésére, másrészt a teljesítmény miatt. Egy GC több ezredmásodpercig is eltarthat, de gyorsabb, ha kevés a tennivaló (egy OpenMV Cam eszközön körülbelül 1ms). Egy explicit hívás minimalizálhatja ezt a késleltetést, miközben biztosítja, hogy az a programnak olyan pontjain következzen be, ahol az elfogadható.

Az automatikus GC a következő körülmények között váltódik ki. Amikor egy lefoglalási kísérlet meghiúsul, GC kerül végrehajtásra, és a lefoglalás újra megpróbálásra kerül. Csak akkor váltódik ki kivétel, ha ez is meghiúsul. Másodszor, automatikus GC váltódik ki, ha a szabad RAM mennyisége egy küszöbérték alá esik. Ez a küszöbérték a végrehajtás előrehaladtával módosítható:

gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())

Ez akkor vált ki GC-t, amikor a jelenleg szabad heap több mint 25%-a foglalttá válik.

Általában a moduloknak az adatobjektumokat futásidőben kellene példányosítaniuk konstruktorok vagy más inicializáló függvények használatával. Ennek oka, hogy ha ez inicializáláskor történik, a fordító RAM-hiányba kerülhet, amikor a későbbi modulok importálódnak. Ha a modulok mégis adatokat példányosítanak importáláskor, akkor az import után kiadott gc.collect() enyhíti a problémát.

Karakterlánc-műveletek

A MicroPython hatékonyan kezeli a karakterláncokat, és ennek megértése segíthet a mikrovezérlőkön futó alkalmazások tervezésében. Amikor egy modul lefordul, a többször előforduló karakterláncok csak egyszer tárolódnak, ezt a folyamatot karakterlánc-interningnek nevezzük. A MicroPythonban egy internelt karakterláncot qstr-nek neveznek. Egy normálisan importált modulban az az egyetlen példány a RAM-ban fog elhelyezkedni, de ahogy fent leírtuk, a bájtkódként befagyasztott modulokban a flash memóriában fog elhelyezkedni.

A karakterlánc-összehasonlítások szintén hatékonyan, hasheléssel történnek, nem pedig karakterről karakterre. A karakterláncok egész számok helyetti használatának hátránya így kicsi lehet mind a teljesítmény, mind a RAM-használat szempontjából - ez a tény meglepetés lehet a C programozók számára.

Utóirat

A MicroPython az objektumokat hivatkozás szerint adja át, adja vissza és (alapértelmezetten) másolja. Egy hivatkozás egyetlen gépi szót foglal el, így ezek a folyamatok hatékonyak a RAM-használat és a sebesség szempontjából.

Ahol olyan változókra van szükség, amelyek mérete sem egy bájt, sem egy gépi szó, ott szabványos könyvtárak állnak rendelkezésre, amelyek segíthetnek ezek hatékony tárolásában és az átalakítások elvégzésében. Lásd az array, struct és uctypes modulokat.

Lábjegyzet: a gc.collect() visszatérési értéke

Unix és Windows platformokon a gc.collect() metódus egy egész számot ad vissza, amely a gyűjtés során visszanyert különálló memóriaterületek számát jelzi (pontosabban a free-vé alakított head-ek számát). Hatékonysági okokból a bare metal portok nem adják vissza ezt az értéket.