Berlangganan statis menggunakan template Pengamat menggunakan C ++ dan mikrokontroler Cortex M4


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 { //          HandleEvent() for(auto it: pSubscribers) { if (it != nullptr) { it->HandleEvent() ; } } } } ; 

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 { //  ,    ,  Toggle() ; } }; 

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) //   { while ((Port::IDR::Read() & (1 << pinNum)) == 0) //     { }; result = true; } return result; } } ; //     GPIOC.13 using UserButton = Button<GPIOC, 13> ; struct ISubscriber { virtual void HandleEvent() = 0; } ; struct IPublisher { virtual void Notify() const = 0; virtual void Subscribe(ISubscriber* subscriber) = 0; } ; template <typename Port, std::uint32_t pinNum> struct Led: ISubscriber { static void Toggle() { Port::ODR::Toggle(1 << pinNum); } void HandleEvent() override { Toggle() ; } }; struct ButtonController : IPublisher { void Run() { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override { for(auto it: pSubscribers) { if (it != nullptr) { it->HandleEvent() ; } } } void Subscribe(ISubscriber* subscriber) override { if (index < pSubscribers.size()) { pSubscribers[index] = subscriber ; index ++ ; } //   3   ...   } private: std::array<ISubscriber*, 3> pSubscribers ; std::size_t index = 0U ; } ; 

Bagaimana cara berlangganan melihat kode? Jadi:


 int main() { //  Led1    5  GPIOC static Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static Led<GPIOC,9> Led3 ; ButtonController buttonController ; //  3  buttonController.Subscribe(&Led1) ; buttonController.Subscribe(&Led2) ; buttonController.Subscribe(&Led3) ; //       buttonController.Run() ; } 

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:


Modulkode rodata rodata baru
main.o4886421

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) //   { while ((Port::IDR::Read() & (1 << pinNum)) == 0) //     { }; result = true; } return result; } } ; template <typename Port, std::uint32_t pinNum> struct Led: ISubscriber { constexpr Led() { } static void Toggle() { Port::ODR::Toggle(1<<pinNum); } void HandleEvent() const override { Toggle() ; } }; //     GPIOC.13 using UserButton = Button<GPIOC, 13> ; 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 ++ ; } } void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override { for(auto it: pSubscribers) { if (it != nullptr) { it->HandleEvent() ; } } } private: static constexpr std::size_t size = 3U; ISubscriber* pSubscribers[size] ; } ; 

Sekarang berlangganan dapat dilakukan pada waktu kompilasi:


 int main() { //  Led1    5  GPIOC static constexpr Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static constexpr Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static constexpr Led<GPIOC,9> Led3 ; static constexpr ButtonController buttonController(&Led1, &Led2, &Led3) ; buttonController.Run() ; return 0 ; } ; 

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:


Modulkode rodata rodata baru
main.o172760

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() { //  Led1    5  GPIOC static Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static Led<GPIOC,9> Led3 ; //   ButtonController<Led1, Led2, Led3> buttonController ; buttonController.Run() ; return 0 ; } 

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 (..) berfungsi

Metode 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) //   { while ((Port::IDR::Read() & (1 << pinNum)) == 0) //     { }; result = true; } return result; } } ; //     GPIOC.13 using UserButton = Button<GPIOC, 13> ; template <typename Port, std::uint32_t pinNum> struct Led { static void Toggle() { Port::ODR::Toggle(1<<pinNum); } void HandleEvent() const { Toggle() ; } }; template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { ((subscribers.HandleEvent()), ...) ; } } ; int main() { //  Led1    5  GPIOC static constexpr Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static constexpr Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static constexpr Led<GPIOC,9> Led3 ; static constexpr ButtonController<Led1, Led2, Led3> buttonController ; buttonController.Run() ; return 0 ; } 

Panggilan Notify() pada metode Run() berubah menjadi panggilan sekuensial sederhana


 Led1.HandleEvent() ; Led2.HandleEvent() ; Led3.HandleEvent() ; 

Bagaimana dengan memori di sini?


Modulkode rodata rodata baru
main.o18640

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!

Source: https://habr.com/ru/post/id481606/


All Articles