MicroPython na mikrokontrolérech¶
MicroPython je navržen tak, aby byl schopen běžet na mikrokontrolérech. Ty mají hardwarová omezení, která mohou být neznámá programátorům, kteří jsou zvyklí na běžné počítače. Konkrétně je omezené množství paměti RAM a nevolatilního „diskového“ (flash paměť) úložiště. Tento tutoriál nabízí způsoby, jak co nejlépe využít omezené zdroje. Protože MicroPython běží na řadičích založených na různých architekturách, jsou prezentované metody obecné: v některých případech bude nutné získat podrobné informace z dokumentace specifické pro danou platformu.
Flash paměť¶
Na OpenMV Cam je jednoduchým způsobem, jak řešit omezenou kapacitu, vložení micro SD karty. V některých případech to není praktické, buď proto, že zařízení nemá slot pro SD kartu, nebo z důvodů ceny či spotřeby energie; proto se musí použít flash paměť na čipu. Firmware včetně subsystému MicroPython je uložen v integrované flash paměti. Zbývající kapacita je k dispozici pro použití. Z důvodů souvisejících s fyzickou architekturou flash paměti může být část této kapacity nepřístupná jako souborový systém. V takových případech lze tento prostor využít začleněním uživatelských modulů do sestavení firmwaru, který je poté nahrán do zařízení.
Existují dva způsoby, jak toho dosáhnout: zmrazené moduly (frozen modules) a zmrazený bytecode (frozen bytecode). Zmrazené moduly ukládají zdrojový kód v Pythonu spolu s firmwarem. Zmrazený bytecode používá křížový kompilátor k převedení zdrojového kódu na bytecode, který je poté uložen spolu s firmwarem. V obou případech lze modul zpřístupnit příkazem import:
import mymodule
Postup pro vytváření zmrazených modulů a bytecode závisí na platformě; pokyny pro sestavení firmwaru lze nalézt v souborech README v příslušné části zdrojového stromu.
Obecně řečeno jsou kroky následující:
Naklonujte repozitář MicroPython.
Získejte (pro platformu specifický) toolchain pro sestavení firmwaru.
Sestavte křížový kompilátor.
Umístěte moduly určené ke zmrazení do zadaného adresáře (v závislosti na tom, zda má být modul zmrazen jako zdrojový kód nebo jako bytecode).
Sestavte firmware. Pro sestavení zmrazeného kódu kteréhokoli typu může být nutný specifický příkaz - viz dokumentace platformy.
Nahrajte firmware do zařízení.
RAM¶
Při snižování využití paměti RAM je třeba zvážit dvě fáze: kompilaci a vykonávání. Kromě spotřeby paměti existuje také problém známý jako fragmentace haldy. Obecně řečeno je nejlepší minimalizovat opakované vytváření a rušení objektů. Důvod je popsán v části věnované heap.
Fáze kompilace¶
Když je modul importován, MicroPython zkompiluje kód na bytecode, který je poté vykonán virtuálním strojem (VM) MicroPython. Bytecode je uložen v paměti RAM. Samotný kompilátor vyžaduje paměť RAM, ta se však uvolní pro použití po dokončení kompilace.
Pokud již byla importována řada modulů, může nastat situace, kdy není dostatek paměti RAM ke spuštění kompilátoru. V takovém případě příkaz import vyvolá výjimku paměti.
Pokud modul při importu vytváří instance globálních objektů, spotřebuje paměť RAM v okamžiku importu, která pak není k dispozici pro kompilátor při následných importech. Obecně je nejlepší vyhnout se kódu, který se spouští při importu; lepším přístupem je mít inicializační kód, který je spuštěn aplikací po importu všech modulů. To maximalizuje paměť RAM dostupnou pro kompilátor.
Pokud je paměť RAM stále nedostatečná pro kompilaci všech modulů, jedním řešením je předkompilace modulů. MicroPython má křížový kompilátor schopný kompilovat moduly Pythonu na bytecode (viz README v adresáři mpy-cross). Výsledný soubor bytecode má příponu .mpy; lze jej zkopírovat do souborového systému a importovat obvyklým způsobem. Případně mohou být některé nebo všechny moduly implementovány jako zmrazený bytecode: na většině platforem to šetří ještě více paměti RAM, protože bytecode je spouštěn přímo z flash paměti namísto uložení v paměti RAM.
Fáze vykonávání¶
Existuje řada programovacích technik pro snížení využití paměti RAM.
Konstanty
MicroPython poskytuje klíčové slovo const, které lze použít následovně:
from micropython import const
ROWS = const(33)
_COLS = const(0x10)
a = ROWS
b = _COLS
V obou případech, kdy je konstanta přiřazena proměnné, se kompilátor vyhne kódování vyhledávání názvu konstanty tím, že nahradí její literální hodnotu. To šetří bytecode a tím i paměť RAM. Hodnota ROWS však zabere alespoň dvě strojová slova, jedno pro klíč a jedno pro hodnotu ve slovníku globálních proměnných. Přítomnost ve slovníku je nezbytná, protože jiný modul by ji mohl importovat nebo použít. Tuto paměť RAM lze ušetřit přidáním podtržítka před název, jako v _COLS: tento symbol není viditelný mimo modul, takže nezabere paměť RAM.
Argumentem const() může být cokoli, co se v době kompilace vyhodnotí jako konstanta, např. 0x100, 1 << 8 nebo (True, "string", b"bytes") (podrobnosti viz část níže). Může dokonce zahrnovat další již definované konstantní symboly, např. 1 << BIT.
Konstantní datové struktury
Tam, kde je značný objem konstantních dat a platforma podporuje vykonávání z flash paměti, lze paměť RAM ušetřit následovně. Data by měla být umístěna v modulech Pythonu a zmrazena jako bytecode. Data musí být definována jako objekty bytes. Kompilátor ‚ví‘, že objekty bytes jsou neměnné, a zajišťuje, že objekty zůstanou ve flash paměti namísto kopírování do paměti RAM. Modul struct může pomoci při převodu mezi typy bytes a dalšími vestavěnými typy Pythonu.
Při zvažování důsledků zmrazeného bytecode mějte na paměti, že v Pythonu jsou řetězce, čísla s plovoucí desetinnou čárkou, bajty, celá čísla, komplexní čísla a n-tice neměnné. Proto budou zmrazeny do flash paměti (u n-tic pouze tehdy, jsou-li všechny jejich prvky neměnné). Tedy na řádku
mystring = "The quick brown fox"
bude skutečný řetězec „The quick brown fox“ umístěn ve flash paměti. Za běhu je odkaz na řetězec přiřazen proměnné mystring. Odkaz zabere jediné strojové slovo. V principu by k uložení konstantních dat mohlo být použito dlouhé celé číslo:
bar = 0xDEADBEEF0000DEADBEEF
Stejně jako v příkladu s řetězcem je za běhu odkaz na libovolně velké celé číslo přiřazen proměnné bar. Tento odkaz zabere jediné strojové slovo.
N-tice konstantních objektů jsou samy o sobě konstantní. Takové konstantní n-tice jsou optimalizovány kompilátorem, takže nemusí být vytvářeny za běhu pokaždé, když jsou použity. Například:
foo = (1, 2, 3, 4, 5, 6, 100000, ("string", b"bytes", False, True))
Celá tato n-tice bude existovat jako jediný objekt (potenciálně ve flash paměti, pokud je kód zmrazen) a bude odkazována pokaždé, když je potřeba.
Zbytečné vytváření objektů
Existuje řada situací, kdy mohou být objekty nevědomky vytvářeny a rušeny. To může snížit využitelnost paměti RAM prostřednictvím fragmentace. Následující části pojednávají o případech tohoto jevu.
Zřetězení řetězců
Zvažte následující fragmenty kódu, jejichž cílem je vytvořit konstantní řetězce:
var = "foo" + "bar"
var1 = "foo" "bar"
var2 = """\
foo\
bar"""
Každý produkuje stejný výsledek, avšak první zbytečně vytváří za běhu dva řetězcové objekty a před vytvořením třetího alokuje více paměti RAM pro zřetězení. Ostatní provádějí zřetězení v době kompilace, což je efektivnější a snižuje fragmentaci.
Tam, kde musí být řetězce vytvářeny dynamicky před předáním do streamu, jako je soubor, ušetří se paměť RAM, pokud se to provádí po částech. Namísto vytvoření velkého řetězcového objektu vytvořte podřetězec a předejte jej do streamu, než se začnete zabývat dalším.
Nejlepší způsob vytváření dynamických řetězců je pomocí metody řetězce format():
var = "Temperature {:5.2f} Pressure {:06d}\n".format(temp, press)
Buffery
Při přístupu k zařízením, jako jsou instance rozhraní UART, I2C a SPI, použití předem alokovaných bufferů zabraňuje vytváření zbytečných objektů. Zvažte tyto dvě smyčky:
while True:
var = spi.read(100)
# process data
buf = bytearray(100)
while True:
spi.readinto(buf)
# process data in buf
První vytváří buffer při každém průchodu, zatímco druhá znovu používá předem alokovaný buffer; to je jak rychlejší, tak efektivnější z hlediska fragmentace paměti.
Bajty jsou menší než int
Na většině platforem celé číslo zabírá čtyři bajty. Zvažte tři volání funkce foo():
def foo(bar):
for x in bar:
print(x)
foo([1, 2, 0xff])
foo((1, 2, 0xff))
foo(b'\1\2\xff')
In the first call a list of integers is created in RAM each time the code is
executed. The second call creates a constant tuple object (a tuple containing
only constant objects) as part of the compilation phase, so it is only created
once and is more efficient than the list. The third call efficiently
creates a bytes object consuming the minimum amount of RAM. If the module
were frozen as bytecode, both the tuple and bytes object would reside in flash.
Řetězce versus bajty
Python3 zavedl podporu Unicode. To zavedlo rozdíl mezi řetězcem a polem bajtů. MicroPython zajišťuje, že řetězce Unicode nezabírají žádný další prostor, pokud jsou všechny znaky v řetězci ASCII (tj. mají hodnotu < 128). Pokud jsou vyžadovány hodnoty v plném 8bitovém rozsahu, lze použít objekty bytes a bytearray, aby se zajistilo, že nebude vyžadován žádný další prostor. Mějte na paměti, že většina metod řetězců (např. str.strip()) platí také pro instance bytes, takže proces eliminace Unicode může být bezbolestný.
s = 'the quick brown fox' # A string instance
b = b'the quick brown fox' # A bytes instance
Tam, kde je nutné převádět mezi řetězci a bajty, lze použít metody str.encode() a bytes.decode(). Mějte na paměti, že řetězce i bajty jsou neměnné. Jakákoli operace, která bere takový objekt jako vstup a produkuje jiný, znamená alespoň jednu alokaci paměti RAM pro vytvoření výsledku. Na druhém řádku níže je alokován nový objekt bytes. K tomu by došlo také, pokud by foo byl řetězec.
foo = b' empty whitespace'
foo = foo.lstrip()
Vykonávání kompilátoru za běhu
Funkce Pythonu eval a exec vyvolávají kompilátor za běhu, což vyžaduje značné množství paměti RAM. Mějte na paměti, že knihovna pickle z micropython-lib používá exec. Z hlediska paměti RAM může být efektivnější použít knihovnu json pro serializaci objektů.
Ukládání řetězců ve flash paměti
Řetězce v Pythonu jsou neměnné, a mají tedy potenciál být uloženy v paměti pouze pro čtení. Kompilátor může umístit do flash paměti řetězce definované v kódu Pythonu. Stejně jako u zmrazených modulů je nutné mít kopii zdrojového stromu na PC a toolchain pro sestavení firmwaru. Postup bude fungovat, i když moduly nebyly plně odladěny, pokud je lze importovat a spustit.
Po importu modulů spusťte:
micropython.qstr_info(1)
Poté zkopírujte a vložte všechny řádky Q(xxx) do textového editoru. Zkontrolujte a odstraňte řádky, které jsou zjevně neplatné. Otevřete soubor qstrdefsport.h, který naleznete v ports/stm32 (nebo v ekvivalentním adresáři pro použitou architekturu). Zkopírujte a vložte opravené řádky na konec souboru. Uložte soubor, znovu sestavte a nahrajte firmware. Výsledek lze zkontrolovat importem modulů a opětovným vydáním:
micropython.qstr_info(1)
Řádky Q(xxx) by měly zmizet.
Halda (heap)¶
Když běžící program vytvoří instanci objektu, je potřebná paměť RAM alokována z fondu pevné velikosti známého jako halda (heap). Když objekt přestane být v dosahu (jinými slovy se stane pro kód nepřístupným), nadbytečný objekt je znám jako „odpad“ (garbage). Proces známý jako „sběr odpadu“ (garbage collection, GC) tuto paměť získá zpět a vrátí ji do volné haldy. Tento proces běží automaticky, lze jej však vyvolat přímo vydáním gc.collect().
Pojednání o tomto je poněkud složité. Pro ‚rychlé řešení‘ vydávejte periodicky následující:
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
Více informací viz níže a dokumentace vestavěného modulu gc.
Pro podrobnosti z pohledu vnitřností/vývojáře MicroPython viz také Správa paměti.
Fragmentace¶
Řekněme, že program vytvoří objekt foo, poté objekt bar. Následně foo přestane být v dosahu, ale bar zůstává. Paměť RAM použitá objektem foo bude získána zpět pomocí GC. Pokud však byl bar alokován na vyšší adresu, paměť RAM získaná zpět z foo bude užitečná pouze pro objekty nikoli větší než foo. Ve složitém nebo dlouho běžícím programu se může halda fragmentovat: navzdory tomu, že je k dispozici značné množství paměti RAM, není dostatek souvislého prostoru pro alokaci konkrétního objektu a program selže s chybou paměti.
Výše uvedené techniky mají za cíl toto minimalizovat. Tam, kde jsou vyžadovány velké trvalé buffery nebo jiné objekty, je nejlepší vytvořit jejich instance brzy v procesu vykonávání programu, dříve než může dojít k fragmentaci. Dalších vylepšení lze dosáhnout monitorováním stavu haldy a řízením GC; ta jsou popsána níže.
Reportování¶
K dispozici je řada knihovních funkcí pro reportování o alokaci paměti a pro řízení GC. Tyto se nacházejí v modulech gc a micropython. Následující příklad lze vložit do REPL (Ctrl-E pro vstup do režimu vkládání, Ctrl-D pro jeho spuštění).
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)
Výše použité metody:
gc.collect()Vynutí sběr odpadu. Viz poznámka pod čarou.micropython.mem_info()Vypíše souhrn využití paměti RAM.gc.mem_free()Vrátí velikost volné haldy v bajtech.gc.mem_alloc()Vrátí počet aktuálně alokovaných bajtů.micropython.mem_info(1)Vypíše tabulku využití haldy (podrobně popsáno níže).
Vyprodukovaná čísla závisí na platformě, ale lze vidět, že deklarace funkce využívá malé množství paměti RAM ve formě bytecode emitovaného kompilátorem (paměť RAM použitá kompilátorem byla získána zpět). Spuštění funkce využívá přes 10 KiB, ale po návratu je a odpadem, protože je mimo dosah a nelze na něj odkazovat. Závěrečné gc.collect() tuto paměť obnoví.
Závěrečný výstup vyprodukovaný micropython.mem_info(1) se bude lišit v detailech, ale lze jej interpretovat následovně:
Symbol |
Význam |
|---|---|
. |
volný blok |
h |
hlavní blok (head) |
= |
koncový blok (tail) |
m |
označený hlavní blok |
T |
n-tice (tuple) |
L |
seznam (list) |
D |
slovník (dict) |
F |
float |
B |
bytecode |
M |
modul |
S |
řetězec nebo bajty |
A |
bytearray |
Každé písmeno představuje jeden blok paměti, přičemž blok má 16 bajtů. Každý řádek výpisu haldy tedy představuje 0x400 bajtů neboli 1 KiB paměti RAM.
Řízení sběru odpadu¶
GC lze vyžádat kdykoli vydáním gc.collect(). Je výhodné to dělat v intervalech, zaprvé pro předcházení fragmentaci a zadruhé kvůli výkonu. GC může trvat několik milisekund, ale je rychlejší, když je málo práce (přibližně 1 ms na OpenMV Cam). Explicitní volání může toto zpoždění minimalizovat a zároveň zajistit, že nastane v bodech programu, kdy je to přijatelné.
Automatický GC je vyvolán za následujících okolností. Když pokus o alokaci selže, provede se GC a alokace se zopakuje. Pouze pokud i toto selže, je vyvolána výjimka. Zadruhé bude automatický GC spuštěn, pokud množství volné paměti RAM klesne pod práh. Tento práh lze přizpůsobit v průběhu vykonávání:
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
To vyvolá GC, když se obsadí více než 25 % aktuálně volné haldy.
Obecně by moduly měly vytvářet instance datových objektů za běhu pomocí konstruktorů nebo jiných inicializačních funkcí. Důvodem je, že pokud k tomu dojde při inicializaci, může být kompilátor připraven o paměť RAM při importu následných modulů. Pokud moduly přesto vytvářejí instance dat při importu, pak gc.collect() vydané po importu problém zmírní.
Operace s řetězci¶
MicroPython zpracovává řetězce efektivním způsobem a pochopení tohoto může pomoci při navrhování aplikací, které mají běžet na mikrokontrolérech. Když je modul kompilován, řetězce, které se vyskytují vícekrát, jsou uloženy pouze jednou, což je proces známý jako internování řetězců (string interning). V MicroPython je internovaný řetězec znám jako qstr. V normálně importovaném modulu bude tato jediná instance umístěna v paměti RAM, ale jak je popsáno výše, v modulech zmrazených jako bytecode bude umístěna ve flash paměti.
Porovnání řetězců se rovněž provádí efektivně pomocí hashování namísto znak po znaku. Penalizace za použití řetězců namísto celých čísel tak může být malá jak z hlediska výkonu, tak využití paměti RAM - skutečnost, která může programátory v jazyce C překvapit.
Dodatek¶
MicroPython předává, vrací a (ve výchozím nastavení) kopíruje objekty pomocí odkazu. Odkaz zabírá jediné strojové slovo, takže tyto procesy jsou efektivní z hlediska využití paměti RAM a rychlosti.
Tam, kde jsou vyžadovány proměnné, jejichž velikost není ani bajt, ani strojové slovo, existují standardní knihovny, které mohou pomoci s jejich efektivním ukládáním a s prováděním převodů. Viz moduly array, struct a uctypes.
Poznámka pod čarou: návratová hodnota gc.collect()¶
Na platformách Unix a Windows vrací metoda gc.collect() celé číslo, které značí počet odlišných paměťových oblastí, které byly při sběru získány zpět (přesněji počet hlav, které byly přeměněny na volné bloky). Z důvodů efektivity porty pro holý hardware (bare metal) tuto hodnotu nevracejí.