
Kesehatan yang baik untuk semua!
Pada malam Tahun Baru, saya ingin terus berbicara tentang penggunaan C ++ pada mikrokontroler, kali ini saya akan mencoba untuk berbicara tentang menggunakan templat Observer (tapi selanjutnya saya akan menyebutnya Publisher-Subscriber atau hanya Subscriber, seperti kata-kata), serta penerapan langganan statis ke C ++ 17 dan keuntungan dari pendekatan ini di beberapa aplikasi.
Pendahuluan
Pelanggan Template adalah salah satu template paling umum yang digunakan dalam pengembangan perangkat lunak. Dengan itu, misalnya, mereka melakukan pemrosesan klik tombol di Windows Form. Dan memang, di tempat mana pun Anda perlu merespons perubahan dalam parameter sistem, apakah itu perubahan dalam file atau memperbarui nilai yang diukur dari sensor, inilah saatnya tanpa berpikir gunakan templat Pelanggan.
Keuntungan dari templat ini adalah kami melepaskan pengetahuan tentang Penerbit dan Pelanggan tanpa terikat dengan objek tertentu. Kami dapat menandatangani siapa saja kepada siapa pun, tanpa mempengaruhi implementasi objek Penerbit dan Pelanggan.
Kondisi awal
Sebelum kita berkenalan dengan templat, mari kita sepakat dulu bahwa kami ingin mengembangkan perangkat lunak yang andal di mana:
- jangan gunakan alokasi memori dinamis
- meminimalkan kerja dengan pointer
- kami menggunakan konstanta sebanyak mungkin sehingga tidak ada yang bisa mengubah siapa pun sebanyak mungkin
- tetapi pada saat yang sama kami menggunakan konstanta sesedikit mungkin yang terletak di RAM
Sekarang mari kita lihat implementasi standar dari template Pelanggan.
Implementasi standar
Misalkan kita memiliki tombol, dan ketika Anda mengklik tombol, kita perlu mengedipkan LED, tetapi berapa banyak dari mereka yang tidak diketahui sejauh ini, dan memang, Anda mungkin perlu berkedip bukan dengan LED, tetapi dengan sorotan pada kapal untuk mengirimkan pesan dalam kode Morse. Penting bahwa kita tidak tahu siapa yang akan berlangganan. Sayangnya, saya tidak memiliki sorotan, jadi semua contoh dalam artikel ini demi kesederhanaan dan pemahaman yang lebih baik dibuat dengan LED.
Jadi, ketika Anda menekan tombol, Anda perlu memberi tahu LED tentang pers ini. Pada gilirannya, setelah belajar tentang menekan LED harus beralih ke keadaan sebaliknya.
Implementasi standar di UML adalah sebagai berikut ...

Di sini kelas ButtonController
bertanggung jawab atas pemungutan suara tombol dan memberi tahu pelanggan tentang klik, dan Led
dalam hal ini adalah pelanggan. Kedua kelas ini dipisahkan melalui ISubsriber
dan ISubsriber
dan tidak ada kelas yang tahu tentang yang lain. Dengan demikian, objek apa pun yang diwarisi dari antarmuka ISubscriber
dapat berlangganan acara dari ButtonController
.
Karena alokasi memori dinamis dilarang, saya mendeklarasikan array 3 elemen untuk berlangganan. Yaitu maksimal bisa 3 pelanggan. Jadi, dalam perkiraan pertama, metode pemberitahuan pelanggan dari kelas ButttonsController
mungkin terlihat
struct ButtonController : IPublisher { void Run() { for(;;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override {
Semua garam dalam metode Notify()
dari kelas Publisher
. Dalam metode ini, kita membahas daftar pelanggan dan memanggil metode HandleEvent()
pada masing-masing pelanggan, dan ini keren, karena setiap pelanggan mengimplementasikan metode ini dengan caranya sendiri dan dapat melakukannya di sana. semua apa pun yang diinginkan hati Anda (pada kenyataannya, Anda harus berhati-hati, jika iblis tidak tahu apa yang dilakukan pelanggan di sana, Anda dapat memanggil metodenya, misalnya, dari gangguan dan Anda harus waspada untuk mencegah pelanggan melakukan hal-hal yang lama dan buruk)
Dalam kasus kami, LED diizinkan untuk melakukan apa saja, sehingga ia melakukan peralihan statusnya:
template <typename Port, std::uint32_t pinNum> struct Led: ISubscriber { static void Toggle() { Port::ODR::Toggle(1 << pinNum); } void HandleEvent() override {
Implementasi penuh semua kelas template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0)
Bagaimana cara berlangganan melihat kode? Jadi:
int main() {
Berita baiknya adalah kita dapat menandatangani objek apa pun dan waktu pembuatannya tidak masalah bagi kita. Ini bisa berupa objek global, statis atau lokal. Di satu sisi, ini bagus, tetapi di sisi lain, mengapa kita perlu berlangganan runtime dalam kode ini. Memang, pada kenyataannya, di sini alamat objek Led1
, Led2
, Led3
dikenal pada tahap kompilasi. Jadi mengapa Anda tidak bisa berlangganan pada tahap kompilasi dan menyimpan array pointer ke pelanggan di ROM?
Selain itu, ada risiko kesalahan potensial, misalnya, berapa banyak yang bertanya-tanya apa yang akan terjadi ketika memanggil metode Subsribe()
jika dipanggil dari beberapa utas? Kami dibatasi hanya untuk 3 pelanggan, dan apa yang terjadi jika kami menandatangani 4 LED?
Dalam kebanyakan kasus, kami membutuhkan langganan ini sekali seumur hidup selama inisialisasi, kami hanya menyimpan pointer ke pelanggan dan hanya itu. Pointer akan menjaga alamat pelanggan ini seumur hidup. Dan hari itu tak terhindarkan ketika bisa dihancurkan karena wabah supernova (tentu saja, jika kita mempertimbangkan periode waktu yang cukup lama). Tetapi bagaimanapun juga, kemungkinan kegagalan RAM jauh lebih tinggi daripada ROM dan tidak disarankan untuk menyimpan data permanen dalam RAM.
Nah, berita buruknya adalah solusi arsitektur seperti itu membutuhkan banyak ruang di ROM dan RAM. Untuk jaga-jaga, kami menulis berapa banyak ROM dan RAM yang dibutuhkan solusi ini:
Yaitu total 552 byte dalam ROM dan 21 byte dalam RAM - katakanlah tidak begitu banyak untuk menekan tombol dan berkedip tiga LED.
Nah, untuk melindungi diri dari masalah seperti itu dan mengurangi konsumsi sumber daya controller, mari kita pertimbangkan opsi dengan berlangganan statis.
Berlangganan Statis
Untuk membuat langganan menjadi statis, Anda dapat menggunakan beberapa pendekatan. Saya akan menamai mereka seperti ini:
- Yang tradisional adalah pendekatan yang sama, tetapi menggunakan konstruktor constexpr dan mengatur daftar pelanggan yang melaluinya.
Tidak konvensional Menggunakan templat - mentransfer daftar pelanggan melalui parameter templat. (di sini, templat adalah definisi dari bidang metaprogramming, bukan pola desain)
Pendekatan tradisional untuk berlangganan statis
Mari kita coba berlangganan pada tahap kompilasi. Untuk melakukan ini, kami sedikit mengubah arsitektur kami:

Gambar tidak jauh berbeda dari aslinya, tetapi ada beberapa perbedaan: metode Subscribe()
telah dihapus dan sekarang berlangganan akan dilakukan langsung di konstruktor. Konstruktor harus menerima sejumlah variabel argumen, dan agar dapat masuk secara statis pada tahap kompilasi, ia akan menjadi constexpr
. Array pelanggan akan diinisialisasi di dalamnya dan inisialisasi ini dapat dilakukan pada waktu kompilasi:
struct ButtonController : IPublisher { template<typename... Args> constexpr ButtonController(Args const*... args): pSubscribers() { std::initializer_list<ISubscriber const*> result = {args...} ; std::size_t index = 0U; for(auto it: result) { if (index < size) { pSubscribers[index] = const_cast<ISubscriber*>(it); } index ++ ; } } private: static constexpr std::size_t size = 3U; ISubscriber* pSubscribers[size] ; } ;
Kode lengkap untuk implementasi seperti itu struct ISubscriber { virtual void HandleEvent() const = 0; } ; struct IPublisher { virtual void Notify() const = 0; } ; template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0)
Sekarang berlangganan dapat dilakukan pada waktu kompilasi:
int main() {
Di sini, objek buttonController
benar buttonController
benar terletak di ROM bersama dengan array pointer ke pelanggan:
main :: buttonController 0x800'1f04 0x10 Data main.o [1]
Semuanya sepertinya tidak ada apa-apanya, kecuali bahwa kami hanya terbatas pada 3 pelanggan. Dan kelas penerbit harus memiliki konstruktor constexpr dan secara umum benar-benar konstan untuk menjamin pointer ke pelanggan di ROM, jika tidak, bahkan dengan alamat pelanggan yang dikenal, objek kita, bersama dengan semua konten, akan kembali ke RAM.
Dari kelemahan lain - karena fungsi virtual masih digunakan, tabel fungsi virtual sedikit demi sedikit ROM kami. Dan sumber dayanya, meskipun terjangkau, tetapi tidak terbatas. Dalam sebagian besar aplikasi, dimungkinkan untuk memalu dan mengambil mikrokontroler yang lebih besar, tetapi sering terjadi bahwa setiap byte penting, terutama ketika menyangkut produk yang diproduksi oleh ratusan ribu, seperti sensor fisik fisik.
Mari kita lihat bagaimana keadaan dengan memori dalam solusi ini:
Dan meskipun hasilnya "menakjubkan": total konsumsi RAM adalah 0 byte, dan ROM adalah 248 byte, yang setengah dari jumlah pada solusi pertama, rasanya masih ada ruang untuk perbaikan. Dari 248 byte ini, sekitar 50 hanya menempati tabel metode virtual.
Penyimpangan kecil:
Langkah dalam ukuran ROM 256 kB untuk mikrokontroler modern adalah norma (misalnya, mikrokontroler TI Cortex M4 memiliki ROM 256 kB, dan versi berikutnya sudah 512 kB). Dan itu tidak akan menjadi sangat baik ketika, karena 50 byte tambahan, kita harus mengambil controller dengan ROM 256 kByte lebih besar dan lebih mahal, oleh karena itu, meninggalkan fungsi virtual dapat menghemat ... sebanyak 50 sen (perbedaan antara mikrokontroler di 256 dan 512 kBytes ROM adalah tentang 50-60 sen).
Ini kedengarannya konyol untuk 1 mikrokontroler, tetapi dalam jumlah 400.000 perangkat per tahun, Anda dapat menghemat $ 200.000. Sudah tidak terlalu lucu, tetapi mempertimbangkan jenis tikus apa. mereka dapat menghargai tawaran dengan ijazah dan kartu hadiah untuk 3.000 rubel, sama sekali tidak ada keraguan tentang kebenaran menolak fungsi virtual dan menghemat 50 byte tambahan dalam ROM.
Pendekatan yang tidak konvensional
Mari kita lihat bagaimana Anda dapat melakukan hal yang sama tanpa fungsi virtual dan menyimpan lebih banyak ROM.
Pertama, mari kita cari tahu bagaimana jadinya:
int main() {
Tugas kami adalah memisahkan dua objek Publisher ( ButtonController
) dan Subscriber ( Led
) dari satu sama lain sehingga mereka tidak saling mengenal, tetapi pada saat yang sama ButtonController
dapat memberi tahu Led
.
Anda dapat mendeklarasikan kelas ButtonController
beberapa cara.
template <Led<GPIOC,5>& subscriber1, Led<GPIOC,8>& subscriber2, Led<GPIOC,9>& subscriber3> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { subscriber1.HandleEvent() ; subscriber2.HandleEvent() ; subscriber3.HandleEvent() ; } ... } ;
Tapi Anda mengerti, di sini kita terikat pada tipe tertentu dan kita harus mengulang definisi kelas BbuttonController
setiap kali dalam proyek baru. Dan saya ingin mengambil dan menggunakan ButtonController
di proyek baru tanpa ButtonController
.
C ++ 17 datang ke penyelamatan, di mana Anda tidak dapat menentukan jenisnya, tetapi minta kompiler untuk menyimpulkan jenisnya untuk Anda - inilah yang Anda butuhkan. Kita bisa, seperti dalam pendekatan tradisional, melepaskan pengetahuan dari Penerbit dan Pelanggan, sementara jumlah pelanggan praktis tidak terbatas.
template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { pass((subscribers.HandleEvent() , true)...) ; } ... } ;
Cara kerja pass (..) berfungsiMetode Notify()
memiliki fungsi panggilan ke pass()
; ia digunakan untuk memperluas parameter template dengan sejumlah variabel argumen
void Notify() const { pass((subscribers.HandleEvent() , true)...) ; }
Implementasi fungsi pass()
tidak dapat dibayangkan, hanya fungsi yang mengambil sejumlah variabel argumen:
template<typename... Args> void pass(Args...) const { } } ;
Bagaimana fungsi HandleEvent()
berkembang menjadi beberapa panggilan untuk masing-masing pelanggan?
Karena fungsi pass()
mengambil beberapa argumen dari tipe apa pun, Anda bisa meneruskan beberapa argumen dari tipe bool
, misalnya, Anda bisa memanggil fungsi pass(true, true, true)
. Dalam hal ini, tentu saja, tidak ada yang akan terjadi, tetapi kita tidak perlu.
Baris (subscribers.HandleEvent() , true)
menggunakan operator "," (koma), yang mengeksekusi kedua operan (dari kiri ke kanan) dan mengembalikan nilai operator kedua, yaitu, di sini subscribers.HandleEvent()
akan dieksekusi terlebih dahulu, kemudian true
dengan fungsinya pass()
akan disetel ke true
.
Ya, "..." adalah entri standar untuk memperluas sejumlah variabel argumen. Untuk kasus kami, tindakan dari kompiler dapat dijelaskan secara sangat skematis sebagai berikut:
pass((subscribers.HandleEvent() , true)...) ; -> pass((Led1.HandleEvent() , true), (Led2.HandleEvent() , true), (Led3.HandleEvent() , true)) ; -> Led1.HandleEvent() ; -> pass(true, (Led2.HandleEvent() , true), (Led3.HandleEvent() , true)) ; -> Led2.HandleEvent() ; -> pass(true, true, (Led3.HandleEvent() , true)) ; -> Led3.HandleEvent() ; -> pass(true, true, true) ;
Alih-alih tautan, Anda dapat menggunakan pointer:
template <auto* ... subscribers> struct ButtonController { ... } ;
Tambahan: Sebenarnya, terima kasih kepada vamireh yang menunjukkan bahwa semua tarian ini bersama rebana fungsi pass
dalam C ++ 17 tidak diperlukan. Karena operator "," koma didukung dalam ekspresi lipatan (yang diperkenalkan dalam standar C ++ 17), kode disederhanakan lebih lanjut:
template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { ((subscribers.HandleEvent()), ...) ; } } ;
Secara arsitektur, ini terlihat sangat sederhana secara umum:

Saya menambahkan kelas LCD lain di sini, tetapi murni sebagai contoh, untuk menunjukkan bahwa sekarang tidak masalah jenis dan jumlah pelanggan, yang utama adalah bahwa ia akan menerapkan metode HandleEvent()
.
Dan semua kode secara umum juga lebih mudah sekarang:
template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0)
Panggilan Notify()
pada metode Run()
berubah menjadi panggilan sekuensial sederhana
Led1.HandleEvent() ; Led2.HandleEvent() ; Led3.HandleEvent() ;
Bagaimana dengan memori di sini?
Total ROM 190 byte dan 0 byte RAM. Sekarang pesanannya, hampir 3 kali lebih kecil dari versi standar, sementara itu melakukan hal yang persis sama.
Dengan demikian, jika Anda memiliki alamat pelanggan yang diketahui sebelumnya dalam aplikasi dan Anda mengikuti ketentuan yang ditentukan pada awal artikel
Ketentuan di awal artikel- jangan gunakan alokasi memori dinamis
- meminimalkan kerja dengan pointer
- kami menggunakan konstanta sebanyak mungkin sehingga tidak ada yang bisa mengubah siapa pun sebanyak mungkin
- tetapi pada saat yang sama kami menggunakan konstanta sesedikit mungkin yang terletak di RAM
Dengan percaya diri, Anda dapat menggunakan implementasi dari template Penerbit-Pelanggan untuk mengurangi garis kode dan menghemat sumber daya, dan di sana Anda melihat dan Anda dapat mengklaim tidak hanya kartu hadiah, tetapi juga bonus berdasarkan hasil tahun ini.
Contoh pengujian di bawah IAR 8.40.2 ada di sini
Semua dengan kedatangan! Dan semoga sukses di tahun baru!