MicroPython pada mikrokontroler¶
MicroPython dirancang untuk dapat berjalan pada mikrokontroler. Perangkat ini memiliki keterbatasan perangkat keras yang mungkin tidak familiar bagi programmer yang lebih terbiasa dengan komputer konvensional. Secara khusus, jumlah RAM dan penyimpanan nonvolatile "disk" (flash memory) sangat terbatas. Tutorial ini menawarkan cara-cara untuk memaksimalkan penggunaan sumber daya yang terbatas. Karena MicroPython berjalan pada kontroler yang didasarkan pada berbagai arsitektur, metode yang disajikan bersifat umum: dalam beberapa kasus akan diperlukan untuk mendapatkan informasi terperinci dari dokumentasi khusus platform.
Flash memory¶
Pada OpenMV Cams, cara sederhana untuk mengatasi kapasitas yang terbatas adalah dengan memasang kartu micro SD. Dalam beberapa kasus ini tidak praktis, baik karena perangkat tidak memiliki slot kartu SD atau karena alasan biaya atau konsumsi daya; oleh karena itu flash on-chip harus digunakan. Firmware termasuk subsistem MicroPython disimpan di flash onboard. Kapasitas yang tersisa tersedia untuk digunakan. Karena alasan yang terkait dengan arsitektur fisik flash memory, sebagian kapasitas ini mungkin tidak dapat diakses sebagai filesystem. Dalam kasus seperti itu, ruang ini dapat digunakan dengan memasukkan modul pengguna ke dalam build firmware yang kemudian di-flash ke perangkat.
Ada dua cara untuk mencapai ini: frozen modules dan frozen bytecode. Frozen modules menyimpan kode sumber Python bersama firmware. Frozen bytecode menggunakan cross compiler untuk mengonversi kode sumber menjadi bytecode yang kemudian disimpan bersama firmware. Dalam kedua kasus, modul dapat diakses dengan pernyataan import:
import mymodule
Prosedur untuk menghasilkan frozen modules dan bytecode bergantung pada platform; instruksi untuk membangun firmware dapat ditemukan di file README di bagian yang relevan dari pohon kode sumber.
Secara umum langkah-langkahnya adalah sebagai berikut:
Clone repository MicroPython.
Dapatkan toolchain (khusus platform) untuk membangun firmware.
Bangun cross compiler.
Tempatkan modul yang akan di-freeze dalam direktori yang ditentukan (bergantung pada apakah modul akan di-freeze sebagai kode sumber atau sebagai bytecode).
Bangun firmware. Perintah khusus mungkin diperlukan untuk membangun frozen code dari salah satu tipe - lihat dokumentasi platform.
Flash firmware ke perangkat.
RAM¶
Ketika mengurangi penggunaan RAM, ada dua fase yang perlu dipertimbangkan: kompilasi dan eksekusi. Selain konsumsi memori, ada juga masalah yang dikenal sebagai fragmentasi heap. Secara umum, yang terbaik adalah meminimalkan pembuatan dan penghancuran objek yang berulang. Alasan untuk ini dibahas di bagian yang mencakup heap.
Fase kompilasi¶
Ketika sebuah modul diimpor, MicroPython mengompilasi kode menjadi bytecode yang kemudian dieksekusi oleh mesin virtual (VM) MicroPython. Bytecode disimpan di RAM. Kompiler sendiri membutuhkan RAM, tetapi ini tersedia untuk digunakan ketika kompilasi selesai.
Jika sejumlah modul telah diimpor, situasi dapat terjadi di mana RAM tidak cukup untuk menjalankan kompiler. Dalam hal ini, pernyataan import akan menghasilkan pengecualian memori.
Jika sebuah modul membuat objek global saat diimpor, modul tersebut akan mengonsumsi RAM pada saat impor, yang kemudian tidak tersedia untuk kompiler digunakan pada impor berikutnya. Secara umum, yang terbaik adalah menghindari kode yang berjalan saat impor; pendekatan yang lebih baik adalah memiliki kode inisialisasi yang dijalankan oleh aplikasi setelah semua modul telah diimpor. Ini memaksimalkan RAM yang tersedia untuk kompiler.
Jika RAM masih tidak cukup untuk mengompilasi semua modul, salah satu solusinya adalah dengan pra-kompilasi modul. MicroPython memiliki cross compiler yang mampu mengompilasi modul Python menjadi bytecode (lihat README di direktori mpy-cross). File bytecode yang dihasilkan memiliki ekstensi .mpy; file ini dapat disalin ke filesystem dan diimpor dengan cara biasa. Sebagai alternatif, beberapa atau semua modul dapat diimplementasikan sebagai frozen bytecode: pada sebagian besar platform ini menghemat lebih banyak RAM karena bytecode dijalankan langsung dari flash daripada disimpan di RAM.
Fase eksekusi¶
Ada sejumlah teknik pengkodean untuk mengurangi penggunaan RAM.
Konstanta
MicroPython menyediakan kata kunci const yang dapat digunakan sebagai berikut:
from micropython import const
ROWS = const(33)
_COLS = const(0x10)
a = ROWS
b = _COLS
Dalam kedua kasus di mana konstanta ditetapkan ke variabel, kompiler akan menghindari pengkodean pencarian ke nama konstanta dengan mengganti nilai literalnya. Ini menghemat bytecode dan karenanya RAM. Namun nilai ROWS akan menempati setidaknya dua kata mesin, masing-masing satu untuk kunci dan nilai dalam kamus global. Kehadirannya dalam kamus diperlukan karena modul lain mungkin mengimport atau menggunakannya. RAM ini dapat dihemat dengan mengawali nama dengan garis bawah seperti pada _COLS: simbol ini tidak terlihat di luar modul sehingga tidak akan menempati RAM.
Argumen untuk const() bisa berupa apa saja yang, pada saat kompilasi, dievaluasi menjadi konstanta misalnya 0x100, 1 << 8 atau (True, "string", b"bytes") (lihat bagian di bawah untuk detail). Bahkan dapat menyertakan simbol const lain yang sudah didefinisikan, misalnya 1 << BIT.
Struktur data konstan
Di mana ada volume substansial data konstan dan platform mendukung eksekusi dari Flash, RAM dapat dihemat sebagai berikut. Data harus ditempatkan dalam modul Python dan di-freeze sebagai bytecode. Data harus didefinisikan sebagai objek bytes. Kompiler 'mengetahui' bahwa objek bytes bersifat immutable dan memastikan bahwa objek tetap berada di flash memory daripada disalin ke RAM. Modul struct dapat membantu dalam konversi antara tipe bytes dan tipe bawaan Python lainnya.
Ketika mempertimbangkan implikasi dari frozen bytecode, perhatikan bahwa dalam Python, string, float, bytes, integer, bilangan kompleks, dan tuple bersifat immutable. Dengan demikian ini akan di-freeze ke dalam flash (untuk tuple, hanya jika semua elemennya bersifat immutable). Jadi, pada baris
mystring = "The quick brown fox"
string aktual "The quick brown fox" akan berada di flash. Pada saat runtime, referensi ke string ditetapkan ke variabel mystring. Referensi tersebut menempati satu kata mesin. Pada prinsipnya, bilangan bulat panjang dapat digunakan untuk menyimpan data konstan:
bar = 0xDEADBEEF0000DEADBEEF
Seperti dalam contoh string, pada saat runtime referensi ke bilangan bulat yang berukuran besar secara arbitrer ditetapkan ke variabel bar. Referensi tersebut menempati satu kata mesin.
Tuple dari objek konstan itu sendiri bersifat konstan. Tuple konstan semacam itu dioptimalkan oleh kompiler sehingga tidak perlu dibuat pada saat runtime setiap kali digunakan. Contohnya:
foo = (1, 2, 3, 4, 5, 6, 100000, ("string", b"bytes", False, True))
Seluruh tuple ini akan ada sebagai objek tunggal (berpotensi di flash jika kode di-freeze) dan direferensikan setiap kali diperlukan.
Pembuatan objek yang tidak perlu
Ada sejumlah situasi di mana objek mungkin secara tidak sengaja dibuat dan dihancurkan. Ini dapat mengurangi kegunaan RAM melalui fragmentasi. Bagian-bagian berikut membahas contoh-contoh ini.
Penggabungan string
Pertimbangkan fragmen kode berikut yang bertujuan menghasilkan string konstan:
var = "foo" + "bar"
var1 = "foo" "bar"
var2 = """\
foo\
bar"""
Masing-masing menghasilkan keluaran yang sama, namun yang pertama secara tidak perlu membuat dua objek string pada saat runtime, mengalokasikan lebih banyak RAM untuk penggabungan sebelum menghasilkan yang ketiga. Yang lain melakukan penggabungan pada saat kompilasi yang lebih efisien, mengurangi fragmentasi.
Di mana string harus dibuat secara dinamis sebelum dimasukkan ke stream seperti file, akan menghemat RAM jika ini dilakukan secara bertahap. Daripada membuat objek string besar, buat substring dan masukkan ke stream sebelum menangani yang berikutnya.
Cara terbaik untuk membuat string dinamis adalah dengan menggunakan metode format() dari string:
var = "Temperature {:5.2f} Pressure {:06d}\n".format(temp, press)
Buffer
Saat mengakses perangkat seperti instansi antarmuka UART, I2C, dan SPI, menggunakan buffer yang telah dialokasikan sebelumnya menghindari pembuatan objek yang tidak perlu. Pertimbangkan dua loop berikut:
while True:
var = spi.read(100)
# process data
buf = bytearray(100)
while True:
spi.readinto(buf)
# process data in buf
Yang pertama membuat buffer pada setiap pass sedangkan yang kedua menggunakan kembali buffer yang telah dialokasikan sebelumnya; ini lebih cepat dan lebih efisien dalam hal fragmentasi memori.
Bytes lebih kecil dari int
Pada sebagian besar platform, sebuah integer mengonsumsi empat byte. Pertimbangkan tiga panggilan ke fungsi foo():
def foo(bar):
for x in bar:
print(x)
foo([1, 2, 0xff])
foo((1, 2, 0xff))
foo(b'\1\2\xff')
Pada panggilan pertama, sebuah list dari integer dibuat di RAM setiap kali kode dieksekusi. Panggilan kedua membuat objek tuple konstan (sebuah tuple yang hanya berisi objek konstan) sebagai bagian dari fase kompilasi, sehingga hanya dibuat sekali dan lebih efisien daripada list. Panggilan ketiga secara efisien membuat objek bytes yang mengonsumsi jumlah RAM minimum. Jika modul di-freeze sebagai bytecode, baik tuple maupun objek bytes akan berada di flash.
String Versus Bytes
Python3 memperkenalkan dukungan Unicode. Ini memperkenalkan perbedaan antara string dan array of bytes. MicroPython memastikan bahwa string Unicode tidak memerlukan ruang tambahan selama semua karakter dalam string adalah ASCII (yaitu memiliki nilai < 128). Jika nilai dalam rentang 8-bit penuh diperlukan, objek bytes dan bytearray dapat digunakan untuk memastikan bahwa tidak ada ruang tambahan yang diperlukan. Perhatikan bahwa sebagian besar metode string (misalnya str.strip()) juga berlaku untuk instansi bytes sehingga proses mengeliminasi Unicode dapat dilakukan tanpa kesulitan.
s = 'the quick brown fox' # A string instance
b = b'the quick brown fox' # A bytes instance
Di mana perlu untuk mengonversi antara string dan bytes, metode str.encode() dan bytes.decode() dapat digunakan. Perhatikan bahwa baik string maupun bytes bersifat immutable. Setiap operasi yang mengambil objek semacam itu sebagai input dan menghasilkan objek lain mengimplikasikan setidaknya satu alokasi RAM untuk menghasilkan hasilnya. Pada baris kedua di bawah, objek bytes baru dialokasikan. Ini juga akan terjadi jika foo adalah sebuah string.
foo = b' empty whitespace'
foo = foo.lstrip()
Eksekusi compiler saat runtime
Fungsi Python eval dan exec menjalankan kompiler pada saat runtime, yang membutuhkan jumlah RAM yang signifikan. Perhatikan bahwa library pickle dari micropython-lib menggunakan exec. Mungkin lebih efisien RAM untuk menggunakan library json untuk serialisasi objek.
Menyimpan string di flash
String Python bersifat immutable sehingga berpotensi disimpan di memori read-only. Kompiler dapat menempatkan string yang didefinisikan dalam kode Python di flash. Seperti dengan frozen modules, perlu memiliki salinan pohon kode sumber di PC dan toolchain untuk membangun firmware. Prosedur ini akan bekerja bahkan jika modul belum sepenuhnya di-debug, selama modul dapat diimpor dan dijalankan.
Setelah mengimpor modul, jalankan:
micropython.qstr_info(1)
Kemudian salin dan tempel semua baris Q(xxx) ke editor teks. Periksa dan hapus baris yang jelas tidak valid. Buka file qstrdefsport.h yang akan ditemukan di ports/stm32 (atau direktori yang setara untuk arsitektur yang digunakan). Salin dan tempel baris yang telah diperbaiki di akhir file. Simpan file, bangun ulang dan flash firmware. Hasilnya dapat diperiksa dengan mengimpor modul dan kembali mengeluarkan:
micropython.qstr_info(1)
Baris Q(xxx) seharusnya sudah hilang.
Heap¶
Ketika sebuah program yang berjalan membuat instansi objek, RAM yang diperlukan dialokasikan dari pool berukuran tetap yang dikenal sebagai heap. Ketika objek keluar dari scope (dengan kata lain menjadi tidak dapat diakses oleh kode), objek yang berlebihan dikenal sebagai "garbage". Proses yang dikenal sebagai "garbage collection" (GC) mengambil kembali memori tersebut, mengembalikannya ke heap bebas. Proses ini berjalan secara otomatis, namun dapat dipanggil secara langsung dengan mengeluarkan gc.collect().
Pembahasan tentang ini agak rumit. Untuk 'perbaikan cepat' keluarkan perintah berikut secara berkala:
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
Untuk informasi lebih lanjut, lihat di bawah dan dokumentasi untuk modul bawaan gc.
Untuk detail dari perspektif internal/developer MicroPython, lihat juga Manajemen Memori.
Fragmentasi¶
Misalkan sebuah program membuat objek foo, kemudian objek bar. Selanjutnya foo keluar dari scope tetapi bar tetap ada. RAM yang digunakan oleh foo akan diambil kembali oleh GC. Namun jika bar dialokasikan ke alamat yang lebih tinggi, RAM yang diambil kembali dari foo hanya akan berguna untuk objek yang tidak lebih besar dari foo. Dalam program yang kompleks atau berjalan lama, heap dapat menjadi terfragmentasi: meskipun ada sejumlah besar RAM yang tersedia, tidak ada ruang yang cukup berurutan untuk mengalokasikan objek tertentu, dan program gagal dengan kesalahan memori.
Teknik-teknik yang diuraikan di atas bertujuan untuk meminimalkan ini. Di mana buffer besar permanen atau objek lain diperlukan, yang terbaik adalah membuat instansi ini lebih awal dalam proses eksekusi program sebelum fragmentasi dapat terjadi. Peningkatan lebih lanjut dapat dilakukan dengan memantau status heap dan dengan mengontrol GC; ini diuraikan di bawah.
Pelaporan¶
Sejumlah fungsi library tersedia untuk melaporkan alokasi memori dan untuk mengontrol GC. Ini dapat ditemukan di modul gc dan micropython. Contoh berikut dapat ditempel di REPL (Ctrl-E untuk masuk mode paste, Ctrl-D untuk menjalankannya).
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)
Metode yang digunakan di atas:
gc.collect()Paksa garbage collection. Lihat catatan kaki.micropython.mem_info()Cetak ringkasan utilisasi RAM.gc.mem_free()Kembalikan ukuran heap bebas dalam byte.gc.mem_alloc()Kembalikan jumlah byte yang saat ini dialokasikan.micropython.mem_info(1)Cetak tabel utilisasi heap (dijelaskan di bawah).
Angka yang dihasilkan bergantung pada platform, tetapi dapat dilihat bahwa mendeklarasikan fungsi menggunakan sejumlah kecil RAM dalam bentuk bytecode yang dipancarkan oleh kompiler (RAM yang digunakan oleh kompiler telah diambil kembali). Menjalankan fungsi menggunakan lebih dari 10KiB, tetapi saat kembali a adalah garbage karena keluar dari scope dan tidak dapat direferensikan. gc.collect() terakhir memulihkan memori tersebut.
Output akhir yang dihasilkan oleh micropython.mem_info(1) akan bervariasi dalam detailnya tetapi dapat diinterpretasikan sebagai berikut:
Simbol |
Arti |
|---|---|
. |
blok bebas |
h |
blok head |
= |
blok tail |
m |
blok head yang ditandai |
T |
tuple |
L |
list |
D |
dict |
F |
float |
B |
byte code |
M |
module |
S |
string atau bytes |
A |
bytearray |
Setiap huruf mewakili satu blok memori, satu blok berukuran 16 byte. Jadi setiap baris dump heap mewakili 0x400 byte atau 1KiB RAM.
Kontrol garbage collection¶
GC dapat diminta kapan saja dengan mengeluarkan gc.collect(). Sangat menguntungkan untuk melakukan ini secara berkala, pertama untuk mencegah fragmentasi dan kedua untuk kinerja. GC dapat memakan beberapa milidetik tetapi lebih cepat ketika ada sedikit pekerjaan yang harus dilakukan (sekitar 1ms pada OpenMV Cam). Panggilan eksplisit dapat meminimalkan penundaan tersebut sekaligus memastikan itu terjadi pada titik-titik dalam program saat hal itu dapat diterima.
GC otomatis diprovokasi dalam keadaan berikut. Ketika upaya alokasi gagal, GC dilakukan dan alokasi dicoba kembali. Hanya jika ini gagal, pengecualian akan dimunculkan. Kedua, GC otomatis akan dipicu jika jumlah RAM bebas jatuh di bawah ambang batas. Ambang batas ini dapat disesuaikan seiring berjalannya eksekusi:
gc.collect()
gc.threshold(gc.mem_free() // 4 + gc.mem_alloc())
Ini akan memicu GC ketika lebih dari 25% dari heap bebas yang saat ini tersedia menjadi terisi.
Secara umum modul harus membuat instansi objek data pada saat runtime menggunakan konstruktor atau fungsi inisialisasi lainnya. Alasannya adalah bahwa jika ini terjadi pada inisialisasi, kompiler mungkin kekurangan RAM saat modul berikutnya diimpor. Jika modul membuat instansi data saat impor, maka gc.collect() yang dikeluarkan setelah impor akan meringankan masalah tersebut.
Operasi string¶
MicroPython menangani string dengan cara yang efisien dan memahami ini dapat membantu dalam merancang aplikasi untuk berjalan pada mikrokontroler. Ketika sebuah modul dikompilasi, string yang muncul beberapa kali disimpan hanya sekali, sebuah proses yang dikenal sebagai string interning. Dalam MicroPython, string yang di-intern dikenal sebagai qstr. Dalam modul yang diimpor secara normal, instansi tunggal tersebut akan berada di RAM, tetapi seperti yang dijelaskan di atas, dalam modul yang di-freeze sebagai bytecode akan berada di flash.
Perbandingan string juga dilakukan secara efisien menggunakan hashing daripada karakter per karakter. Penalti untuk menggunakan string daripada integer mungkin kecil baik dalam hal kinerja maupun penggunaan RAM - fakta yang mungkin mengejutkan programmer C.
Penutup¶
MicroPython melewati, mengembalikan, dan (secara default) menyalin objek dengan referensi. Sebuah referensi menempati satu kata mesin sehingga proses-proses ini efisien dalam penggunaan RAM dan kecepatan.
Di mana variabel diperlukan yang ukurannya bukan byte maupun kata mesin, ada library standar yang dapat membantu dalam menyimpan ini secara efisien dan dalam melakukan konversi. Lihat modul array, struct, dan uctypes.
Catatan kaki: nilai kembalian gc.collect()¶
Pada platform Unix dan Windows, metode gc.collect() mengembalikan integer yang menandakan jumlah wilayah memori berbeda yang diambil kembali dalam koleksi (lebih tepatnya, jumlah head yang diubah menjadi free). Untuk alasan efisiensi, port bare metal tidak mengembalikan nilai ini.