MicroPython na mikrokontrolerima

MicroPython je osmišljen tako da može raditi na mikrokontrolerima. Oni imaju hardverska ograničenja koja možda nisu poznata programerima naviknutima na konvencionalna računala. Konkretno, količina RAM-a i trajne „diskovne” pohrane (flash memorije) je ograničena. Ovaj vodič nudi načine da se maksimalno iskoriste ograničeni resursi. Budući da MicroPython radi na kontrolerima temeljenima na raznim arhitekturama, predstavljene metode su generičke: u nekim slučajevima bit će potrebno pribaviti detaljne informacije iz dokumentacije specifične za platformu.

Flash memorija

Na OpenMV kamerama jednostavan način rješavanja ograničenog kapaciteta jest umetanje micro SD kartice. U nekim slučajevima to je nepraktično, bilo zato što uređaj nema utor za SD karticu bilo zbog razloga cijene ili potrošnje energije; stoga se mora koristiti flash memorija na čipu. Ugrađeni program (firmware), uključujući MicroPython podsustav, pohranjen je u ugrađenoj flash memoriji. Preostali kapacitet dostupan je za korištenje. Iz razloga povezanih s fizičkom arhitekturom flash memorije, dio tog kapaciteta može biti nedostupan kao datotečni sustav. U takvim slučajevima taj prostor može se iskoristiti ugrađivanjem korisničkih modula u izgradnju ugrađenog programa (firmware) koja se zatim flešira na uređaj.

Postoje dva načina da se to postigne: zamrznuti moduli i zamrznuti bajtkod. Zamrznuti moduli pohranjuju Python izvorni kod zajedno s ugrađenim programom (firmware). Zamrznuti bajtkod koristi kompajler za unakrsno prevođenje kako bi pretvorio izvorni kod u bajtkod koji se zatim pohranjuje zajedno s ugrađenim programom (firmware). U oba slučaja modulu se može pristupiti naredbom import:

import mymodule

Postupak za izradu zamrznutih modula i bajtkoda ovisi o platformi; upute za izgradnju ugrađenog programa (firmware) mogu se pronaći u README datotekama u odgovarajućem dijelu stabla izvornog koda.

Općenito govoreći, koraci su sljedeći:

  • Klonirajte MicroPython repozitorij.

  • Pribavite (platformi specifičan) lanac alata za izgradnju ugrađenog programa (firmware).

  • Izgradite kompajler za unakrsno prevođenje.

  • Smjestite module koji se trebaju zamrznuti u određeni direktorij (ovisno o tome treba li modul zamrznuti kao izvorni kod ili kao bajtkod).

  • Izgradite ugrađeni program (firmware). Možda će biti potrebna posebna naredba za izgradnju zamrznutog koda bilo koje vrste - pogledajte dokumentaciju platforme.

  • Fleširajte ugrađeni program (firmware) na uređaj.

RAM

Pri smanjenju potrošnje RAM-a treba uzeti u obzir dvije faze: prevođenje i izvođenje. Osim potrošnje memorije, postoji i problem poznat kao fragmentacija gomile (heap). Općenito govoreći, najbolje je svesti na najmanju mjeru ponovljeno stvaranje i uništavanje objekata. Razlog tome obrađen je u odjeljku koji pokriva heap.

Faza prevođenja

Kad se modul uveze, MicroPython prevodi kod u bajtkod koji zatim izvodi MicroPython virtualni stroj (VM). Bajtkod se pohranjuje u RAM. Sam kompajler zahtijeva RAM, ali on postaje dostupan za korištenje kad prevođenje završi.

Ako je već uvezen niz modula, može nastati situacija u kojoj nema dovoljno RAM-a za pokretanje kompajlera. U tom slučaju naredba import proizvest će iznimku zbog nedostatka memorije.

Ako modul instancira globalne objekte prilikom uvoza, on će trošiti RAM u trenutku uvoza, koji onda nije dostupan kompajleru za korištenje pri kasnijim uvozima. Općenito je najbolje izbjegavati kod koji se izvodi pri uvozu; bolji pristup je imati inicijalizacijski kod koji aplikacija pokreće nakon što su svi moduli uvezeni. Time se maksimizira RAM dostupan kompajleru.

Ako RAM i dalje nije dovoljan za prevođenje svih modula, jedno rješenje je unaprijed prevođenje modula. MicroPython ima kompajler za unakrsno prevođenje sposoban prevesti Python module u bajtkod (pogledajte README u direktoriju mpy-cross). Rezultirajuća bajtkod datoteka ima ekstenziju .mpy; može se kopirati u datotečni sustav i uvesti na uobičajen način. Alternativno, neki ili svi moduli mogu se implementirati kao zamrznuti bajtkod: na većini platformi to štedi još više RAM-a jer se bajtkod izvodi izravno iz flash memorije umjesto da se pohranjuje u RAM.

Faza izvođenja

Postoji niz tehnika kodiranja za smanjenje potrošnje RAM-a.

Konstante

MicroPython pruža ključnu riječ const koja se može koristiti na sljedeći način:

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

U oba slučaja u kojima je konstanta pridružena varijabli, kompajler će izbjeći kodiranje pretraživanja imena konstante zamjenjujući ga njezinom doslovnom vrijednošću. Time se štedi bajtkod, a time i RAM. Međutim, vrijednost ROWS zauzet će najmanje dvije strojne riječi, po jednu za ključ i vrijednost u rječniku globalnih varijabli. Prisutnost u rječniku je nužna jer bi je drugi modul mogao uvesti ili koristiti. Taj RAM može se uštedjeti dodavanjem podvlake ispred imena kao u _COLS: ovaj simbol nije vidljiv izvan modula pa neće zauzimati RAM.

Argument funkcije const() može biti bilo što što se, u vrijeme prevođenja, izračunava u konstantu, npr. 0x100, 1 << 8 ili (True, "string", b"bytes") (pojedinosti pogledajte u odjeljku ispod). Može čak uključivati i druge const simbole koji su već definirani, npr. 1 << BIT.

Konstantne strukture podataka

Tamo gdje postoji znatna količina konstantnih podataka i platforma podržava izvođenje iz flash memorije, RAM se može uštedjeti na sljedeći način. Podaci bi trebali biti smješteni u Python module i zamrznuti kao bajtkod. Podaci moraju biti definirani kao bytes objekti. Kompajler ‘zna’ da su bytes objekti nepromjenjivi i osigurava da objekti ostanu u flash memoriji umjesto da se kopiraju u RAM. Modul struct može pomoći pri pretvorbi između bytes tipova i drugih ugrađenih Python tipova.

Pri razmatranju implikacija zamrznutog bajtkoda, imajte na umu da su u Pythonu nizovi znakova, decimalni brojevi, bajtovi, cijeli brojevi, kompleksni brojevi i n-torke nepromjenjivi. Sukladno tome, oni će biti zamrznuti u flash memoriju (za n-torke, samo ako su svi njihovi elementi nepromjenjivi). Tako će, u liniji

mystring = "The quick brown fox"

stvarni niz znakova „The quick brown fox” prebivati u flash memoriji. U vrijeme izvođenja referenca na niz znakova pridružuje se varijabli mystring. Referenca zauzima jednu strojnu riječ. U načelu bi se dugi cijeli broj mogao koristiti za pohranu konstantnih podataka:

bar = 0xDEADBEEF0000DEADBEEF

Kao u primjeru s nizom znakova, u vrijeme izvođenja referenca na proizvoljno velik cijeli broj pridružuje se varijabli bar. Ta referenca zauzima jednu strojnu riječ.

N-torke konstantnih objekata same su konstantne. Takve konstantne n-torke kompajler optimizira tako da ih nije potrebno stvarati u vrijeme izvođenja svaki put kad se koriste. Na primjer:

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

Cijela ova n-torka postojat će kao jedan objekt (potencijalno u flash memoriji ako je kod zamrznut) i referencirat će se svaki put kad zatreba.

Nepotrebno stvaranje objekata

Postoji niz situacija u kojima se objekti mogu nehotice stvarati i uništavati. To može smanjiti iskoristivost RAM-a zbog fragmentacije. Sljedeći odjeljci raspravljaju o primjerima toga.

Spajanje nizova znakova

Razmotrite sljedeće isječke koda koji imaju za cilj proizvesti konstantne nizove znakova:

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

Svaki proizvodi isti ishod, međutim prvi nepotrebno stvara dva objekta niza znakova u vrijeme izvođenja, dodjeljuje više RAM-a za spajanje prije nego što proizvede treći. Ostali izvode spajanje u vrijeme prevođenja što je učinkovitije, smanjujući fragmentaciju.

Tamo gdje se nizovi znakova moraju dinamički stvarati prije nego što se predaju toku poput datoteke, ušteda RAM-a postiže se ako se to čini postupno. Umjesto stvaranja velikog objekta niza znakova, stvorite podniz i predajte ga toku prije obrade sljedećeg.

Najbolji način za stvaranje dinamičkih nizova znakova jest pomoću metode format() niza znakova:

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

Međuspremnici

Pri pristupu uređajima poput instanci UART, I2C i SPI sučelja, korištenje unaprijed dodijeljenih međuspremnika izbjegava stvaranje nepotrebnih objekata. Razmotrite ove dvije petlje:

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

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

Prva stvara međuspremnik pri svakom prolazu, dok druga ponovno koristi unaprijed dodijeljeni međuspremnik; ovo je i brže i učinkovitije u pogledu fragmentacije memorije.

Bajtovi su manji od cijelih brojeva

Na većini platformi cijeli broj troši četiri bajta. Razmotrite tri poziva funkcije foo():

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

U prvom pozivu list cijelih brojeva stvara se u RAM-u svaki put kad se kod izvodi. Drugi poziv stvara konstantni tuple objekt (tuple koji sadrži samo konstantne objekte) kao dio faze prevođenja, pa se stvara samo jednom i učinkovitiji je od list. Treći poziv učinkovito stvara bytes objekt koji troši najmanju količinu RAM-a. Ako bi modul bio zamrznut kao bajtkod, i tuple i bytes objekt prebivali bi u flash memoriji.

Nizovi znakova naspram bajtova

Python3 je uveo podršku za Unicode. To je uvelo razliku između niza znakova i polja bajtova. MicroPython osigurava da Unicode nizovi znakova ne zauzimaju dodatni prostor sve dok su svi znakovi u nizu ASCII (tj. imaju vrijednost < 128). Ako su potrebne vrijednosti u punom 8-bitnom rasponu, mogu se koristiti bytes i bytearray objekti kako bi se osiguralo da neće biti potreban dodatni prostor. Imajte na umu da se većina metoda nizova znakova (npr. str.strip()) primjenjuje i na bytes instance pa proces uklanjanja Unicodea može biti bezbolan.

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

Tamo gdje je potrebno pretvarati između nizova znakova i bajtova, mogu se koristiti metode str.encode() i bytes.decode(). Imajte na umu da su i nizovi znakova i bajtovi nepromjenjivi. Svaka operacija koja kao ulaz uzima takav objekt i proizvodi drugi podrazumijeva najmanje jednu dodjelu RAM-a za proizvodnju rezultata. U drugoj liniji ispod dodjeljuje se novi bytes objekt. To bi se dogodilo i da je foo niz znakova.

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

Izvođenje kompajlera u vrijeme izvođenja

Python funkcije eval i exec pozivaju kompajler u vrijeme izvođenja, što zahtijeva značajne količine RAM-a. Imajte na umu da biblioteka pickle iz micropython-lib koristi exec. Za serijalizaciju objekata moglo bi biti učinkovitije u pogledu RAM-a koristiti biblioteku json.

Pohranjivanje nizova znakova u flash memoriju

Python nizovi znakova su nepromjenjivi te stoga imaju potencijal da budu pohranjeni u memoriji samo za čitanje. Kompajler može u flash memoriju smjestiti nizove znakova definirane u Python kodu. Kao i kod zamrznutih modula, potrebno je imati kopiju stabla izvornog koda na računalu i lanac alata za izgradnju ugrađenog programa (firmware). Postupak će raditi čak i ako moduli nisu potpuno otklonjeni od pogrešaka, sve dok se mogu uvesti i pokrenuti.

Nakon uvoza modula, izvedite:

micropython.qstr_info(1)

Zatim kopirajte i zalijepite sve Q(xxx) linije u uređivač teksta. Provjerite i uklonite linije koje su očito nevaljane. Otvorite datoteku qstrdefsport.h koja se nalazi u ports/stm32 (ili odgovarajućem direktoriju za korištenu arhitekturu). Kopirajte i zalijepite ispravljene linije na kraj datoteke. Spremite datoteku, ponovno izgradite i fleširajte ugrađeni program (firmware). Ishod se može provjeriti uvozom modula i ponovnim izvođenjem:

micropython.qstr_info(1)

Q(xxx) linije trebale bi nestati.

Gomila (heap)

Kad program koji se izvodi instancira objekt, potreban RAM dodjeljuje se iz skupa fiksne veličine poznatog kao gomila (heap). Kad objekt izađe iz dosega (drugim riječima postane nedostupan kodu), suvišni objekt poznat je kao „smeće”. Proces poznat kao „sakupljanje smeća” (GC) povraća tu memoriju, vraćajući je u slobodnu gomilu. Ovaj proces se izvodi automatski, no može se pozvati i izravno izdavanjem gc.collect().

Rasprava o ovome donekle je složena. Za ‘brzo rješenje’ povremeno izdajte sljedeće:

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

Za više informacija pogledajte ispod i dokumentaciju za ugrađeni modul gc.

Za detalje iz perspektive unutarnjeg ustroja MicroPythona / razvojnog programera pogledajte i Upravljanje memorijom.

Fragmentacija

Recimo da program stvara objekt foo, zatim objekt bar. Naknadno foo izlazi iz dosega, ali bar ostaje. RAM koji koristi foo povratit će GC. Međutim, ako je bar dodijeljen na višu adresu, RAM povraćen od foo bit će koristan samo za objekte ne veće od foo. U složenom programu ili programu koji se dugo izvodi gomila može postati fragmentirana: unatoč tome što postoji znatna količina dostupnog RAM-a, nema dovoljno susjednog prostora za dodjelu određenog objekta, pa program ne uspijeva uz pogrešku memorije.

Gore navedene tehnike imaju za cilj to svesti na najmanju mjeru. Tamo gdje su potrebni veliki trajni međuspremnici ili drugi objekti, najbolje je instancirati ih rano u procesu izvođenja programa prije nego što može nastupiti fragmentacija. Daljnja poboljšanja mogu se postići praćenjem stanja gomile i upravljanjem GC-om; ona su navedena u nastavku.

Izvještavanje

Dostupan je niz funkcija biblioteke za izvještavanje o dodjeli memorije i upravljanje GC-om. One se nalaze u modulima gc i micropython. Sljedeći primjer može se zalijepiti u REPL (Ctrl-E za ulazak u način lijepljenja, Ctrl-D za pokretanje).

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)

Gore korištene metode:

  • gc.collect() Prisilno sakupljanje smeća. Pogledajte fusnotu.

  • micropython.mem_info() Ispisuje sažetak iskorištenosti RAM-a.

  • gc.mem_free() Vraća veličinu slobodne gomile u bajtovima.

  • gc.mem_alloc() Vraća broj trenutno dodijeljenih bajtova.

  • micropython.mem_info(1) Ispisuje tablicu iskorištenosti gomile (detaljno opisanu u nastavku).

Proizvedeni brojevi ovise o platformi, ali može se vidjeti da deklariranje funkcije koristi malu količinu RAM-a u obliku bajtkoda koji emitira kompajler (RAM koji koristi kompajler je povraćen). Pokretanje funkcije koristi preko 10KiB, ali pri povratku a je smeće jer je izvan dosega i ne može se referencirati. Završni gc.collect() povraća tu memoriju.

Završni izlaz koji proizvodi micropython.mem_info(1) razlikovat će se u detaljima, ali može se tumačiti na sljedeći način:

Simbol

Značenje

.

slobodan blok

h

glavni blok

=

repni blok

m

označeni glavni blok

T

n-torka

L

lista

D

rječnik

F

decimalni broj

B

bajtkod

M

modul

S

niz znakova ili bajtovi

A

bytearray

Svako slovo predstavlja jedan blok memorije, pri čemu je blok 16 bajtova. Dakle, svaka linija ispisa gomile predstavlja 0x400 bajtova ili 1KiB RAM-a.

Upravljanje sakupljanjem smeća

GC se može zatražiti u bilo kojem trenutku izdavanjem gc.collect(). Korisno je to činiti u intervalima, prvo da bi se preduhitrila fragmentacija i drugo radi performansi. GC može trajati nekoliko milisekundi, ali je brži kad ima malo posla (oko 1ms na OpenMV kameri). Eksplicitni poziv može svesti to kašnjenje na najmanju mjeru istovremeno osiguravajući da se dogodi u točkama programa u kojima je to prihvatljivo.

Automatski GC izazvan je pod sljedećim okolnostima. Kad pokušaj dodjele ne uspije, izvodi se GC i dodjela se ponovno pokušava. Samo ako ovo ne uspije, podiže se iznimka. Drugo, automatski GC bit će pokrenut ako količina slobodnog RAM-a padne ispod praga. Ovaj prag može se prilagoditi kako izvođenje napreduje:

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

Ovo će izazvati GC kad više od 25% trenutno slobodne gomile postane zauzeto.

Općenito bi moduli trebali instancirati podatkovne objekte u vrijeme izvođenja koristeći konstruktore ili druge inicijalizacijske funkcije. Razlog je taj što, ako se to dogodi pri inicijalizaciji, kompajler može ostati zakinut za RAM kad se uvoze kasniji moduli. Ako moduli ipak instanciraju podatke pri uvozu, tada će gc.collect() izdan nakon uvoza ublažiti problem.

Operacije s nizovima znakova

MicroPython rukuje nizovima znakova na učinkovit način i razumijevanje toga može pomoći pri osmišljavanju aplikacija za rad na mikrokontrolerima. Kad se modul prevede, nizovi znakova koji se pojavljuju više puta pohranjuju se samo jednom, što je proces poznat kao interniranje nizova znakova. U MicroPythonu interniran niz znakova poznat je kao qstr. U normalno uvezenom modulu ta jedna instanca bit će smještena u RAM-u, ali kao što je gore opisano, u modulima zamrznutima kao bajtkod bit će smještena u flash memoriji.

Usporedbe nizova znakova također se izvode učinkovito koristeći raspršivanje (hashing) umjesto znak po znak. Kazna za korištenje nizova znakova umjesto cijelih brojeva stoga može biti mala i u pogledu performansi i u pogledu potrošnje RAM-a - činjenica koja bi mogla iznenaditi C programere.

Postskriptum

MicroPython prosljeđuje, vraća i (zadano) kopira objekte po referenci. Referenca zauzima jednu strojnu riječ pa su ovi procesi učinkoviti u pogledu potrošnje RAM-a i brzine.

Tamo gdje su potrebne varijable čija veličina nije ni bajt ni strojna riječ, postoje standardne biblioteke koje mogu pomoći u učinkovitoj pohrani te u izvođenju pretvorbi. Pogledajte module array, struct i uctypes.

Fusnota: povratna vrijednost gc.collect()

Na Unix i Windows platformama metoda gc.collect() vraća cijeli broj koji označava broj odvojenih memorijskih regija koje su povraćene u sakupljanju (preciznije, broj glava koje su pretvorene u slobodne blokove). Iz razloga učinkovitosti, portovi za rad na golom metalu (bare metal) ne vraćaju tu vrijednost.