Modul C eksternal MicroPython

Saat mengembangkan modul untuk digunakan dengan MicroPython, Anda mungkin menemui keterbatasan pada lingkungan Python, seringkali karena ketidakmampuan mengakses sumber daya perangkat keras tertentu atau keterbatasan kecepatan Python.

Jika keterbatasan Anda tidak dapat diselesaikan dengan saran di Memaksimalkan Kecepatan MicroPython, menulis sebagian atau seluruh modul Anda dalam C (dan/atau C++ jika diimplementasikan untuk port Anda) adalah pilihan yang layak.

Jika modul Anda dirancang untuk mengakses atau bekerja dengan perangkat keras atau pustaka yang umum tersedia, pertimbangkan untuk mengimplementasikannya di dalam pohon sumber MicroPython bersama modul-modul serupa dan mengirimkannya sebagai pull request. Namun jika Anda menargetkan sistem yang tidak umum atau proprietary, mungkin lebih masuk akal untuk menyimpannya di luar repositori MicroPython utama.

Bab ini menjelaskan cara mengompilasi modul eksternal tersebut ke dalam executable atau citra firmware MicroPython. Alat build Make dan CMake didukung, dan saat menulis modul eksternal, sebaiknya tambahkan file build untuk kedua alat tersebut agar modul dapat digunakan di semua port. Namun saat mengompilasi port tertentu, Anda hanya perlu menggunakan satu metode build, yaitu Make atau CMake.

Pendekatan alternatif adalah menggunakan Kode mesin native dalam file .mpy yang memungkinkan penulisan kode C kustom yang ditempatkan dalam file .mpy, yang dapat diimpor secara dinamis ke dalam sistem MicroPython yang sedang berjalan tanpa perlu mengompilasi ulang firmware utama.

Struktur modul C eksternal

Modul C pengguna MicroPython adalah direktori dengan file-file berikut:

  • File kode sumber *.c / *.cpp / *.h untuk modul Anda.

    File-file ini biasanya mencakup fungsionalitas tingkat rendah yang diimplementasikan serta fungsi binding MicroPython untuk mengekspos fungsi dan modul.

    Saat ini, referensi terbaik untuk menulis fungsi/modul ini adalah dengan menemukan modul serupa di dalam pohon MicroPython dan menggunakannya sebagai contoh.

  • micropython.mk berisi fragmen Makefile untuk modul ini.

    $(USERMOD_DIR) tersedia di micropython.mk sebagai jalur ke direktori modul Anda. Karena didefinisikan ulang untuk setiap modul C, sebaiknya diperluas dalam micropython.mk Anda ke variabel make lokal, mis. EXAMPLE_MOD_DIR := $(USERMOD_DIR)

    micropython.mk Anda harus menambahkan file sumber modul Anda ke variabel SRC_USERMOD_C atau SRC_USERMOD_LIB_C. Yang pertama akan diproses untuk definisi MP_QSTR_ dan MP_REGISTER_MODULE, yang kedua tidak (mis. kode helper dan pustaka yang tidak spesifik untuk MicroPython). Jalur-jalur ini harus menyertakan salinan yang diperluas dari $(USERMOD_DIR), mis.:

    SRC_USERMOD_C += $(EXAMPLE_MOD_DIR)/modexample.c
    SRC_USERMOD_LIB_C += $(EXAMPLE_MOD_DIR)/utils/algorithm.c
    

    Demikian pula, gunakan SRC_USERMOD_CXX dan SRC_USERMOD_LIB_CXX untuk file sumber C++. Jika ingin menyertakan file assembly, gunakan SRC_USERMOD_LIB_ASM.

    Jika Anda memiliki opsi compiler kustom (seperti -I untuk menambahkan direktori pencarian file header), opsi tersebut harus ditambahkan ke CFLAGS_USERMOD untuk kode C dan ke CXXFLAGS_USERMOD untuk kode C++.

  • micropython.cmake berisi konfigurasi CMake untuk modul ini.

    Dalam micropython.cmake, Anda dapat menggunakan ${CMAKE_CURRENT_LIST_DIR} sebagai jalur ke modul saat ini.

    micropython.cmake Anda harus mendefinisikan pustaka INTERFACE dan mengaitkan file sumber, definisi kompilasi, serta direktori include dengannya. Pustaka tersebut kemudian harus ditautkan ke target usermod.

    add_library(usermod_cexample INTERFACE)
    
    target_sources(usermod_cexample INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}/examplemodule.c
    )
    
    target_include_directories(usermod_cexample INTERFACE
        ${CMAKE_CURRENT_LIST_DIR}
    )
    
    target_link_libraries(usermod INTERFACE usermod_cexample)
    

    Lihat di bawah untuk contoh penggunaan lengkap.

Contoh dasar

Modul cexample menyediakan contoh untuk sebuah fungsi dan sebuah kelas. Fungsi cexample.add_ints(a, b) menambahkan dua argumen integer dan mengembalikan hasilnya. Tipe cexample.Timer() membuat timer yang dapat digunakan untuk mengukur waktu yang telah berlalu sejak objek diinisialisasi.

Modul ini dapat ditemukan di pohon sumber MicroPython di direktori examples dan memiliki file sumber serta fragmen Makefile dengan konten seperti yang dijelaskan di atas:

micropython/
└──examples/
   └──usercmodule/
      └──cexample/
         ├── examplemodule.c
         ├── micropython.mk
         └── micropython.cmake

Lihat komentar dalam file-file tersebut untuk penjelasan tambahan. Di samping modul cexample, terdapat juga cppexample yang bekerja dengan cara yang sama namun menunjukkan salah satu cara mencampur kode C dan C++ di MicroPython.

Mengompilasi cmodule ke dalam MicroPython

Untuk membangun modul seperti itu, kompilasi MicroPython (lihat getting started), dengan menerapkan 2 modifikasi:

  1. Tetapkan flag waktu build USER_C_MODULES untuk menunjuk ke modul yang ingin Anda sertakan. Untuk port yang menggunakan Make, variabel ini harus berupa direktori yang secara otomatis dicari untuk modul. Untuk port yang menggunakan CMake, variabel ini harus berupa file yang menyertakan modul yang akan dibangun. Lihat di bawah untuk detailnya.

  2. Aktifkan modul dengan menetapkan makro preprocessor C yang sesuai ke 1. Ini hanya diperlukan jika modul yang Anda bangun tidak diaktifkan secara otomatis.

Untuk membangun modul contoh yang disertakan dengan MicroPython, tetapkan USER_C_MODULES ke direktori examples/usercmodule untuk Make, atau ke examples/usercmodule/micropython.cmake untuk CMake.

Sebagai contoh, berikut cara membangun port unix dengan modul contoh:

cd micropython/ports/unix
make USER_C_MODULES=../../examples/usercmodule

Anda mungkin perlu menjalankan make clean sekali di awal saat menyertakan modul pengguna baru dalam build. Output build akan menampilkan modul yang ditemukan:

...
Including User C Module from ../../examples/usercmodule/cexample
Including User C Module from ../../examples/usercmodule/cppexample
...

Untuk port berbasis CMake seperti rp2, ini akan terlihat sedikit berbeda (perlu diketahui bahwa CMake sebenarnya dipanggil oleh make):

cd micropython/ports/rp2
make USER_C_MODULES=../../examples/usercmodule/micropython.cmake

Sekali lagi, Anda mungkin perlu menjalankan make clean terlebih dahulu agar CMake dapat menemukan modul pengguna. Output build CMake mencantumkan modul berdasarkan nama:

...
Including User C Module(s) from ../../examples/usercmodule/micropython.cmake
Found User C Module(s): usermod_cexample, usermod_cppexample
...

Konten micropython.cmake tingkat atas dapat digunakan untuk mengontrol modul mana yang diaktifkan.

Untuk proyek Anda sendiri, lebih mudah menyimpan kode kustom di luar pohon sumber MicroPython utama, sehingga struktur direktori proyek yang umum akan terlihat seperti ini:

my_project/
├── modules/
│   ├── example1/
│   │   ├── example1.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   ├── example2/
│   │   ├── example2.c
│   │   ├── micropython.mk
│   │   └── micropython.cmake
│   └── micropython.cmake
└── micropython/
    ├──ports/
   ... ├──stm32/
      ...

Saat membangun dengan Make, tetapkan USER_C_MODULES ke direktori my_project/modules. Misalnya, membangun port stm32:

cd my_project/micropython/ports/stm32
make USER_C_MODULES=../../../modules

Saat membangun dengan CMake, micropython.cmake tingkat atas -- yang ditemukan langsung di direktori my_project/modules -- harus include semua modul yang ingin Anda sediakan:

include(${CMAKE_CURRENT_LIST_DIR}/example1/micropython.cmake)
include(${CMAKE_CURRENT_LIST_DIR}/example2/micropython.cmake)

Kemudian build dengan:

cd my_project/micropython/ports/rp2
make USER_C_MODULES=../../../modules/micropython.cmake

Anda juga dapat menentukan jalur absolut ke USER_C_MODULES.

Semua modul yang ditentukan oleh variabel USER_C_MODULES (baik yang ditemukan di direktori ini saat menggunakan Make, maupun yang ditambahkan melalui include saat menggunakan CMake) akan dikompilasi, tetapi hanya yang diaktifkan yang akan tersedia untuk diimpor. Modul pengguna biasanya diaktifkan secara default (ini diputuskan oleh pengembang modul), dalam hal ini tidak ada hal lain yang perlu dilakukan selain menetapkan USER_C_MODULES seperti yang dijelaskan di atas.

Jika modul tidak diaktifkan secara default, maka makro preprocessor C yang sesuai harus diaktifkan. Nama makro ini dapat ditemukan dengan mencari baris MP_REGISTER_MODULE dalam kode sumber modul (biasanya muncul di akhir file sumber utama). Makro ini harus dikelilingi oleh pasangan #if X / #endif, dan opsi konfigurasi X harus diset ke 1 menggunakan CFLAGS_EXTRA agar modul tersedia. Jika tidak ada pasangan #if X / #endif maka modul diaktifkan secara default.

Misalnya, modul examples/usercmodule/cexample diaktifkan secara default sehingga memiliki baris berikut dalam kode sumbernya:

MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);

Sebagai alternatif, untuk membuat modul ini dinonaktifkan secara default namun dapat dipilih melalui opsi konfigurasi preprocessor, maka akan menjadi:

#if MODULE_CEXAMPLE_ENABLED
MP_REGISTER_MODULE(MP_QSTR_cexample, example_user_cmodule);
#endif

Dalam hal ini modul diaktifkan dengan menambahkan CFLAGS_EXTRA=-DMODULE_CEXAMPLE_ENABLED=1 ke perintah make, atau mengedit mpconfigport.h atau mpconfigboard.h untuk menambahkan

#define MODULE_CEXAMPLE_ENABLED (1)

Perlu diketahui bahwa metode yang tepat bergantung pada port karena masing-masing memiliki struktur yang berbeda. Jika tidak dilakukan dengan benar, kompilasi akan berhasil tetapi import akan gagal menemukan modul.

Penggunaan modul di MicroPython

Setelah dibangun ke dalam salinan MicroPython Anda, modul tersebut kini dapat diakses di Python seperti modul builtin lainnya, mis.

import cexample
print(cexample.add_ints(1, 3))
# should display 4
from cexample import Timer
from time import sleep_ms

watch = Timer()
sleep_ms(1000)
print(watch.time())
# should display approximately 1000

Alokasi Memori Dinamis C

MicroPython menggunakan "Python heap" sendiri untuk Manajemen Memori, yang tidak sama dengan "C heap" yang digunakan oleh fungsi pustaka C seperti malloc(), free(), dll. Tidak semua port MicroPython dilengkapi dengan "C heap" sama sekali.

Port Tingkat 1 & 2 memiliki dukungan yang bervariasi untuk alokasi memori dinamis C melalui "C heap":

  • Port unix, windows, esp32, dan webassembly mendukung alokasi memori dinamis C.

  • Port rp2 akan gagal mengalokasikan memori apa pun saat runtime kecuali firmware dibangun dengan MICROPY_C_HEAP_SIZE=n untuk mereservasi n byte memori untuk C heap. Memori ini tidak akan tersedia untuk digunakan oleh kode Python.

  • Build port alif, mimxrt, nrf, renesas-ra, samd, dan stm32 yang menyertakan alokasi C dinamis akan gagal saat link-time dengan error seperti undefined reference to `malloc'. MicroPython tidak memiliki dukungan bawaan untuk alokasi C dinamis pada port-port ini. Solusi apa pun memerlukan penambahan implementasi C heap secara manual ke build kustom.

  • Port zephyr saat ini tidak mendukung pembangunan dengan modul pengguna.

Python heap sebagai C heap

Mungkin praktis bagi kode C untuk memanggil fungsi alokasi dinamis "Python heap" seperti m_malloc(), m_malloc0(), dan m_free().

Lihat Memori MicroPython dari Kode C untuk informasi lebih lanjut tentang pendekatan ini.

Modul C++

Sebagian besar port MicroPython Tingkat 1 & 2 (dan beberapa Tingkat 3) mendukung pembangunan modul pengguna C++, menggunakan variabel lingkungan spesifik C++ yang dijelaskan di atas.

Mengintegrasikan C++ dan MicroPython dengan sukses melibatkan beberapa pertimbangan tambahan:

Alokasi Memori Dinamis C++

Program C++ (serta fitur Pustaka Standar C++) biasanya menggunakan alokasi memori dinamis. Pengalokasi memori default C++ (yaitu operator new dan delete) biasanya diimplementasikan sebagai lapisan di atas Alokasi Memori Dinamis C.

Untuk port MicroPython yang tidak menyertakan dukungan alokasi memori dinamis C, alokasi memori dinamis C++ dapat didukung dengan salah satu dari dua cara:

  • Implementasikan alokasi memori dinamis C dalam build kustom Anda.

  • Implementasikan pengalokasi C++ kustom dalam build kustom Anda.

Pertimbangan Linkage

Karena MicroPython adalah proyek berbasis C, setiap simbol yang terhubung ke atau dari MicroPython harus dikualifikasi dengan extern "C" dalam kode C++.

Sangat disarankan untuk mengikuti pola yang didemonstrasikan di examples/usercmodule/cppexample, di mana modul Python diimplementasikan dalam file wrapper C minimal yang membungkus kode C++.