Mikrodenetleyicilerde MicroPython¶
MicroPython, mikrodenetleyicilerde çalışabilecek şekilde tasarlanmıştır. Bu cihazlar, geleneksel bilgisayarlara daha aşina olan programcılar için bilinmedik olabilecek donanım kısıtlamalarına sahiptir. Özellikle RAM ve geçici olmayan “disk” (flash bellek) depolama miktarı sınırlıdır. Bu eğitim, sınırlı kaynaklardan en iyi şekilde yararlanmanın yollarını sunar. MicroPython çeşitli mimarilere dayalı denetleyicilerde çalıştığından, sunulan yöntemler geneldir: bazı durumlarda platforma özgü belgelerden ayrıntılı bilgi edinmek gerekecektir.
Flash bellek¶
OpenMV Cam’lerde sınırlı kapasiteyi ele almanın basit yolu bir micro SD kart takmaktır. Bazı durumlarda bu pratik değildir; ya cihazın bir SD kart yuvası yoktur ya da maliyet veya güç tüketimi nedenleriyle; dolayısıyla yonga üzerindeki flash belleğin kullanılması gerekir. MicroPython alt sistemini de içeren aygıt yazılımı (firmware) yerleşik flash bellekte saklanır. Kalan kapasite kullanım için mevcuttur. Flash belleğin fiziksel mimarisiyle bağlantılı nedenlerle bu kapasitenin bir kısmı bir dosya sistemi olarak erişilemez olabilir. Bu gibi durumlarda bu alan, kullanıcı modüllerinin daha sonra cihaza flash’lanacak bir aygıt yazılımı yapısına dahil edilmesiyle kullanılabilir.
Bunu başarmanın iki yolu vardır: dondurulmuş modüller ve dondurulmuş bayt kodu. Dondurulmuş modüller Python kaynağını aygıt yazılımıyla birlikte saklar. Dondurulmuş bayt kodu, kaynağı daha sonra aygıt yazılımıyla birlikte saklanan bayt koduna dönüştürmek için çapraz derleyiciyi kullanır. Her iki durumda da modüle bir import deyimiyle erişilebilir:
import mymodule
Dondurulmuş modüller ve bayt kodu üretme prosedürü platforma bağlıdır; aygıt yazılımını derlemeye yönelik talimatlar, kaynak ağacının ilgili bölümündeki README dosyalarında bulunabilir.
Genel anlamda adımlar şöyledir:
MicroPython deposunu klonlayın.
Aygıt yazılımını derlemek için (platforma özgü) araç zincirini edinin.
Çapraz derleyiciyi derleyin.
Dondurulacak modülleri belirtilen bir dizine yerleştirin (modülün kaynak olarak mı yoksa bayt kodu olarak mı dondurulacağına bağlıdır).
Aygıt yazılımını derleyin. Her iki türden dondurulmuş kodu derlemek için belirli bir komut gerekebilir - platform belgelerine bakın.
Aygıt yazılımını cihaza flash’layın.
RAM¶
RAM kullanımını azaltırken göz önünde bulundurulması gereken iki aşama vardır: derleme ve yürütme. Bellek tüketimine ek olarak, yığın parçalanması (heap fragmentation) olarak bilinen bir sorun da vardır. Genel anlamda, nesnelerin tekrar tekrar oluşturulmasını ve yok edilmesini en aza indirmek en iyisidir. Bunun nedeni heap bölümünde ele alınmaktadır.
Derleme aşaması¶
Bir modül içe aktarıldığında, MicroPython kodu daha sonra MicroPython sanal makinesi (VM) tarafından yürütülen bayt koduna derler. Bayt kodu RAM’de saklanır. Derleyicinin kendisi RAM gerektirir, ancak derleme tamamlandığında bu kullanılabilir hale gelir.
Bir dizi modül zaten içe aktarılmışsa, derleyiciyi çalıştırmak için yeterli RAM bulunmadığı bir durum ortaya çıkabilir. Bu durumda import deyimi bir bellek istisnası üretir.
Bir modül içe aktarma sırasında global nesneler örneklerse, içe aktarma anında RAM tüketir ve bu da sonraki içe aktarmalarda derleyicinin kullanması için kullanılamaz hale gelir. Genel olarak içe aktarma sırasında çalışan koddan kaçınmak en iyisidir; daha iyi bir yaklaşım, tüm modüller içe aktarıldıktan sonra uygulama tarafından çalıştırılan bir başlatma koduna sahip olmaktır. Bu, derleyici için mevcut RAM’i en üst düzeye çıkarır.
RAM tüm modülleri derlemek için hâlâ yetersizse, bir çözüm modülleri önceden derlemektir. MicroPython, Python modüllerini bayt koduna derleyebilen bir çapraz derleyiciye sahiptir (mpy-cross dizinindeki README’ye bakın). Ortaya çıkan bayt kodu dosyasının .mpy uzantısı vardır; dosya sistemine kopyalanabilir ve her zamanki şekilde içe aktarılabilir. Alternatif olarak modüllerin bir kısmı veya tümü dondurulmuş bayt kodu olarak uygulanabilir: çoğu platformda bu, bayt kodu RAM’de saklanmak yerine doğrudan flash bellekten çalıştırıldığı için daha da fazla RAM tasarrufu sağlar.
Yürütme aşaması¶
RAM kullanımını azaltmak için bir dizi kodlama tekniği vardır.
Sabitler
MicroPython, aşağıdaki gibi kullanılabilen bir const anahtar sözcüğü sağlar:
from micropython import const
ROWS = const(33)
_COLS = const(0x10)
a = ROWS
b = _COLS
Sabitin bir değişkene atandığı her iki örnekte de derleyici, sabitin değişmez değerini yerine koyarak sabitin adına bir arama kodlamaktan kaçınır. Bu, bayt kodundan ve dolayısıyla RAM’den tasarruf sağlar. Ancak ROWS değeri en az iki makine sözcüğü işgal eder; biri global sözlükteki anahtar, diğeri değer için. Sözlükte bulunması gereklidir çünkü başka bir modül onu içe aktarabilir veya kullanabilir. Bu RAM, _COLS örneğinde olduğu gibi adın başına bir alt çizgi eklenerek tasarruf edilebilir: bu sembol modül dışında görünmez, dolayısıyla RAM işgal etmez.
const() argümanı, derleme zamanında bir sabite değerlendirilen herhangi bir şey olabilir, örneğin 0x100, 1 << 8 veya (True, "string", b"bytes") (ayrıntılar için aşağıdaki bölüme bakın). Hatta daha önce tanımlanmış başka const sembolleri bile içerebilir, örneğin 1 << BIT.
Sabit veri yapıları
Önemli miktarda sabit veri olduğunda ve platform Flash’tan yürütmeyi desteklediğinde, aşağıdaki şekilde RAM tasarrufu yapılabilir. Veriler Python modüllerinde bulunmalı ve bayt kodu olarak dondurulmalıdır. Veriler bytes nesneleri olarak tanımlanmalıdır. Derleyici, bytes nesnelerinin değişmez olduğunu ‘bilir’ ve nesnelerin RAM’e kopyalanmak yerine flash bellekte kalmasını sağlar. struct modülü, bytes türleri ile diğer Python yerleşik türleri arasında dönüşüm yapmaya yardımcı olabilir.
Dondurulmuş bayt kodunun sonuçlarını değerlendirirken, Python’da dizgilerin, kayan noktalı sayıların, baytların, tam sayıların, karmaşık sayıların ve demetlerin (tuple) değişmez olduğunu unutmayın. Buna göre bunlar flash belleğe dondurulur (demetler için, yalnızca tüm öğeleri değişmezse). Böylece, şu satırda
mystring = "The quick brown fox"
gerçek “The quick brown fox” dizgisi flash bellekte bulunur. Çalışma zamanında dizgiye bir başvuru mystring değişkenine atanır. Başvuru tek bir makine sözcüğü işgal eder. Prensipte sabit verileri saklamak için uzun bir tam sayı kullanılabilir:
bar = 0xDEADBEEF0000DEADBEEF
Dizgi örneğinde olduğu gibi, çalışma zamanında keyfi olarak büyük tam sayıya bir başvuru bar değişkenine atanır. Bu başvuru tek bir makine sözcüğü işgal eder.
Sabit nesnelerden oluşan demetler (tuple) kendileri de sabittir. Bu tür sabit demetler derleyici tarafından optimize edilir, böylece her kullanıldıklarında çalışma zamanında oluşturulmaları gerekmez. Örneğin:
foo = (1, 2, 3, 4, 5, 6, 100000, ("string", b"bytes", False, True))
Bu demetin tamamı tek bir nesne olarak var olur (kod dondurulmuşsa potansiyel olarak flash bellekte) ve gerektiğinde her seferinde başvurulur.
Gereksiz nesne oluşturma
Nesnelerin farkında olmadan oluşturulup yok edilebileceği bir dizi durum vardır. Bu, parçalanma yoluyla RAM’in kullanılabilirliğini azaltabilir. Aşağıdaki bölümlerde bunun örnekleri tartışılmaktadır.
Dizgi birleştirme
Sabit dizgiler üretmeyi amaçlayan aşağıdaki kod parçacıklarını düşünün:
var = "foo" + "bar"
var1 = "foo" "bar"
var2 = """\
foo\
bar"""
Her biri aynı sonucu üretir, ancak ilki çalışma zamanında gereksiz yere iki dizgi nesnesi oluşturur ve üçüncüsünü üretmeden önce birleştirme için daha fazla RAM ayırır. Diğerleri birleştirmeyi derleme zamanında gerçekleştirir, bu da daha verimlidir ve parçalanmayı azaltır.
Dizgilerin bir dosya gibi bir akışa beslenmeden önce dinamik olarak oluşturulması gerektiğinde, bu parça parça yapılırsa RAM tasarrufu sağlar. Büyük bir dizgi nesnesi oluşturmak yerine, bir alt dizgi oluşturun ve bir sonrakini ele almadan önce akışa besleyin.
Dinamik dizgiler oluşturmanın en iyi yolu dizgi format() yöntemidir:
var = "Temperature {:5.2f} Pressure {:06d}\n".format(temp, press)
Arabellekler
UART, I2C ve SPI arabirimlerinin örnekleri gibi cihazlara erişirken, önceden ayrılmış arabellekler kullanmak gereksiz nesne oluşturulmasını önler. Şu iki döngüyü düşünün:
while True:
var = spi.read(100)
# process data
buf = bytearray(100)
while True:
spi.readinto(buf)
# process data in buf
İlki her geçişte bir arabellek oluştururken, ikincisi önceden ayrılmış bir arabelleği yeniden kullanır; bu hem daha hızlıdır hem de bellek parçalanması açısından daha verimlidir.
Baytlar int’lerden daha küçüktür
Çoğu platformda bir tam sayı dört bayt tüketir. foo() fonksiyonuna yapılan üç çağrıyı düşünün:
def foo(bar):
for x in bar:
print(x)
foo([1, 2, 0xff])
foo((1, 2, 0xff))
foo(b'\1\2\xff')
İlk çağrıda, kod her yürütüldüğünde RAM’de bir tam sayı list oluşturulur. İkinci çağrı, derleme aşamasının bir parçası olarak sabit bir tuple nesnesi (yalnızca sabit nesneler içeren bir tuple) oluşturur, dolayısıyla yalnızca bir kez oluşturulur ve list‘ten daha verimlidir. Üçüncü çağrı, minimum miktarda RAM tüketen bir bytes nesnesini verimli bir şekilde oluşturur. Modül bayt kodu olarak dondurulmuş olsaydı, hem tuple hem de bytes nesnesi flash bellekte bulunurdu.
Dizgiler ve Baytlar Karşılaştırması
Python3, Unicode desteğini tanıttı. Bu, bir dizgi ile bir bayt dizisi arasında bir ayrım getirdi. MicroPython, dizgideki tüm karakterler ASCII olduğu sürece (yani değeri < 128 olduğu sürece) Unicode dizgilerinin ek alan kaplamamasını sağlar. Tam 8 bit aralığındaki değerler gerekiyorsa, ek alan gerekmeyeceğinden emin olmak için bytes ve bytearray nesneleri kullanılabilir. Çoğu dizgi yönteminin (örneğin str.strip()) bytes örneklerine de uygulandığını unutmayın, böylece Unicode’u ortadan kaldırma süreci sorunsuz olabilir.
s = 'the quick brown fox' # A string instance
b = b'the quick brown fox' # A bytes instance
Dizgiler ile baytlar arasında dönüşüm yapmak gerektiğinde str.encode() ve bytes.decode() yöntemleri kullanılabilir. Hem dizgilerin hem de baytların değişmez olduğunu unutmayın. Girdi olarak böyle bir nesne alan ve başka bir nesne üreten herhangi bir işlem, sonucu üretmek için en az bir RAM tahsisi gerektirir. Aşağıdaki ikinci satırda yeni bir bytes nesnesi tahsis edilir. Bu, foo bir dizgi olsaydı da gerçekleşirdi.
foo = b' empty whitespace'
foo = foo.lstrip()
Çalışma zamanında derleyici yürütme
eval ve exec Python fonksiyonları, çalışma zamanında önemli miktarda RAM gerektiren derleyiciyi çağırır. micropython-lib‘deki pickle kütüphanesinin exec kullandığını unutmayın. Nesne serileştirmesi için json kütüphanesini kullanmak RAM açısından daha verimli olabilir.
Dizgileri flash bellekte saklama
Python dizgileri değişmezdir, dolayısıyla salt okunur bellekte saklanma potansiyeline sahiptir. Derleyici, Python kodunda tanımlanan dizgileri flash belleğe yerleştirebilir. Dondurulmuş modüllerde olduğu gibi, PC’de kaynak ağacının bir kopyasına ve aygıt yazılımını derlemek için araç zincirine sahip olmak gereklidir. Prosedür, modüller tam olarak hata ayıklanmamış olsa bile, içe aktarılıp çalıştırılabildikleri sürece çalışır.
Modülleri içe aktardıktan sonra şunu yürütün:
micropython.qstr_info(1)
Ardından tüm Q(xxx) satırlarını bir metin düzenleyiciye kopyalayıp yapıştırın. Açıkça geçersiz olan satırları kontrol edin ve kaldırın. ports/stm32 (veya kullanılan mimari için eşdeğer dizin) içinde bulunacak olan qstrdefsport.h dosyasını açın. Düzeltilmiş satırları dosyanın sonuna kopyalayıp yapıştırın. Dosyayı kaydedin, aygıt yazılımını yeniden derleyin ve flash’layın. Sonuç, modülleri içe aktararak ve tekrar şunu çalıştırarak kontrol edilebilir:
micropython.qstr_info(1)
Q(xxx) satırları gitmiş olmalıdır.
Yığın (heap)¶
Çalışan bir program bir nesne örneklediğinde, gerekli RAM yığın (heap) olarak bilinen sabit boyutlu bir havuzdan tahsis edilir. Nesne kapsam dışına çıktığında (başka bir deyişle kod tarafından erişilemez hale geldiğinde) gereksiz nesne “çöp” olarak bilinir. “Çöp toplama” (GC) olarak bilinen bir süreç, bu belleği geri kazanır ve serbest yığına geri döndürür. Bu süreç otomatik olarak çalışır, ancak gc.collect() çalıştırılarak doğrudan da çağrılabilir.
Bu konudaki anlatım biraz karmaşıktır. ‘Hızlı bir çözüm’ için aşağıdakini periyodik olarak çalıştırın:
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
Daha fazla bilgi için, aşağıya ve yerleşik gc modülünün belgelerine bakın.
MicroPython iç yapısı/geliştirici perspektifinden ayrıntılar için, ayrıca Bellek Yönetimi bölümüne bakın.
Parçalanma¶
Diyelim ki bir program bir foo nesnesi, ardından bir bar nesnesi oluşturuyor. Daha sonra foo kapsam dışına çıkıyor ama bar kalıyor. foo tarafından kullanılan RAM, GC tarafından geri kazanılacaktır. Ancak bar daha yüksek bir adrese tahsis edilmişse, foo‘dan geri kazanılan RAM yalnızca foo‘dan daha büyük olmayan nesneler için kullanışlı olacaktır. Karmaşık veya uzun süre çalışan bir programda yığın parçalanabilir: önemli miktarda RAM mevcut olmasına rağmen, belirli bir nesneyi tahsis etmek için yeterli bitişik alan yoktur ve program bir bellek hatasıyla başarısız olur.
Yukarıda özetlenen teknikler bunu en aza indirmeyi amaçlar. Büyük kalıcı arabellekler veya diğer nesneler gerektiğinde, bunları parçalanma oluşmadan önce program yürütme sürecinin erken aşamalarında örneklemek en iyisidir. Yığının durumunu izleyerek ve GC’yi kontrol ederek başka iyileştirmeler yapılabilir; bunlar aşağıda özetlenmiştir.
Raporlama¶
Bellek tahsisini raporlamak ve GC’yi kontrol etmek için bir dizi kütüphane fonksiyonu mevcuttur. Bunlar gc ve micropython modüllerinde bulunur. Aşağıdaki örnek REPL’e yapıştırılabilir (yapıştırma moduna girmek için Ctrl-E, çalıştırmak için Ctrl-D).
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)
Yukarıda kullanılan yöntemler:
gc.collect()Bir çöp toplama işlemini zorla. Dipnota bakın.micropython.mem_info()RAM kullanımının bir özetini yazdır.gc.mem_free()Serbest yığın boyutunu bayt cinsinden döndür.gc.mem_alloc()Şu anda tahsis edilen bayt sayısını döndür.micropython.mem_info(1)Yığın kullanımının bir tablosunu yazdır (aşağıda ayrıntılı olarak açıklanmıştır).
Üretilen sayılar platforma bağlıdır, ancak fonksiyonu bildirmenin derleyici tarafından üretilen bayt kodu biçiminde az miktarda RAM kullandığı görülebilir (derleyici tarafından kullanılan RAM geri kazanılmıştır). Fonksiyonu çalıştırmak 10KiB’den fazla kullanır, ancak dönüşte a çöptür çünkü kapsam dışındadır ve başvurulamaz. Son gc.collect() o belleği kurtarır.
micropython.mem_info(1) tarafından üretilen son çıktı ayrıntı olarak değişiklik gösterir ancak şu şekilde yorumlanabilir:
Sembol |
Anlam |
|---|---|
. |
boş blok |
h |
baş blok |
= |
kuyruk blok |
m |
işaretli baş blok |
T |
demet (tuple) |
L |
liste |
D |
sözlük (dict) |
F |
kayan noktalı sayı (float) |
B |
bayt kodu |
M |
modül |
S |
dizgi veya bayt |
A |
bytearray |
Her harf, 16 bayt olan tek bir bellek bloğunu temsil eder. Yani yığın dökümünün her satırı 0x400 bayt veya 1KiB RAM’i temsil eder.
Çöp toplama kontrolü¶
gc.collect() çalıştırılarak herhangi bir zamanda bir GC talep edilebilir. Bunu aralıklarla yapmak avantajlıdır; ilk olarak parçalanmayı önlemek için, ikinci olarak performans için. Bir GC birkaç milisaniye sürebilir ancak yapılacak az iş olduğunda daha hızlıdır (bir OpenMV Cam’de yaklaşık 1ms). Açık bir çağrı, bu gecikmeyi en aza indirirken programda kabul edilebilir olduğu noktalarda gerçekleşmesini sağlayabilir.
Otomatik GC aşağıdaki durumlarda tetiklenir. Bir tahsis girişimi başarısız olduğunda, bir GC gerçekleştirilir ve tahsis yeniden denenir. Yalnızca bu da başarısız olursa bir istisna üretilir. İkinci olarak, serbest RAM miktarı bir eşiğin altına düşerse otomatik bir GC tetiklenir. Bu eşik, yürütme ilerledikçe uyarlanabilir:
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
Bu, şu anda boş olan yığının %25’inden fazlası dolduğunda bir GC tetikler.
Genel olarak modüller, çalışma zamanında kurucular veya diğer başlatma fonksiyonlarını kullanarak veri nesneleri örneklemelidir. Bunun nedeni, bu işlem başlatma sırasında gerçekleşirse, sonraki modüller içe aktarıldığında derleyicinin RAM açısından aç kalabilmesidir. Modüller içe aktarma sırasında veri örneklerse, içe aktarmadan sonra çalıştırılan gc.collect() sorunu hafifletir.
Dizgi işlemleri¶
MicroPython dizgileri verimli bir şekilde işler ve bunu anlamak, mikrodenetleyicilerde çalışacak uygulamalar tasarlamada yardımcı olabilir. Bir modül derlendiğinde, birden çok kez ortaya çıkan dizgiler yalnızca bir kez saklanır; bu, dizgi içselleştirme (string interning) olarak bilinen bir süreçtir. MicroPython’da içselleştirilmiş bir dizgi qstr olarak bilinir. Normal olarak içe aktarılan bir modülde bu tek örnek RAM’de bulunur, ancak yukarıda açıklandığı gibi, bayt kodu olarak dondurulan modüllerde flash bellekte bulunur.
Dizgi karşılaştırmaları da karakter karakter yerine karma (hashing) kullanılarak verimli bir şekilde gerçekleştirilir. Bu nedenle tam sayılar yerine dizgi kullanmanın bedeli hem performans hem de RAM kullanımı açısından küçük olabilir - bu, C programcıları için şaşırtıcı olabilecek bir gerçektir.
Sonsöz¶
MicroPython nesneleri başvuruyla geçirir, döndürür ve (varsayılan olarak) kopyalar. Bir başvuru tek bir makine sözcüğü işgal eder, dolayısıyla bu işlemler RAM kullanımı ve hız açısından verimlidir.
Boyutu ne bir bayt ne de bir makine sözcüğü olan değişkenlerin gerektiği durumlarda, bunları verimli bir şekilde saklamaya ve dönüşümleri gerçekleştirmeye yardımcı olabilecek standart kütüphaneler vardır. array, struct ve uctypes modüllerine bakın.
Dipnot: gc.collect() dönüş değeri¶
Unix ve Windows platformlarında gc.collect() yöntemi, toplama işleminde geri kazanılan farklı bellek bölgelerinin sayısını (daha kesin olarak, serbest hale getirilen baş sayısını) belirten bir tam sayı döndürür. Verimlilik nedenleriyle çıplak metal (bare metal) bağlantı noktaları bu değeri döndürmez.