
Dalam artikel sebelumnya,
Di mana konstanta Anda disimpan pada mikrokontroler CortexM (menggunakan kompiler C ++ IAR sebagai contoh) , pertanyaan tentang bagaimana menempatkan objek konstan dalam ROM telah dibahas. Sekarang, saya ingin memberi tahu Anda bagaimana Anda dapat menggunakan pola lone generator untuk membuat objek dalam ROM.
Pendahuluan
Banyak yang telah ditulis tentang Singleton (selanjutnya disebut Singleton) sisi positif dan negatifnya. Namun terlepas dari kekurangannya, ia memiliki banyak properti yang berguna, terutama dalam konteks firmware untuk mikrokontroler.
Untuk memulainya, untuk perangkat lunak mikrokontroler yang andal, objek tidak direkomendasikan untuk dibuat secara dinamis, dan oleh karena itu tidak perlu untuk menghapusnya. Seringkali objek dibuat sekali dan hidup dari saat perangkat dimulai, sampai dimatikan. Objek semacam itu bahkan bisa menjadi port leg, di mana LED terhubung, itu dibuat sekali, dan tentu saja tidak akan pergi ke mana pun saat aplikasi sedang berjalan, dan jelas itu bisa Singleton. Seseorang harus membuat objek seperti itu dan itu bisa saja Singleton.
Singleton juga akan memberi Anda jaminan bahwa objek yang sama yang menggambarkan kaki pelabuhan tidak akan dibuat dua kali jika tiba-tiba digunakan di beberapa tempat.
Lain, menurut pendapat saya, properti luar biasa Singleton adalah kemudahan penggunaannya. Misalnya, seperti dalam kasus interrupt handler, sebuah contoh yang ada di akhir artikel. Tapi untuk sekarang, kita akan berurusan dengan Singleton sendiri.
Singleton membuat objek dalam RAM
Secara umum, cukup banyak artikel yang telah ditulis tentang mereka,
Singleton (Loner) atau kelas statis? , atau
Three Age of Singleton Pattern . Oleh karena itu, saya tidak akan fokus pada apa itu Singleton dan menjelaskan semua banyak opsi untuk penerapannya. Sebaliknya, saya akan fokus pada dua opsi yang dapat digunakan dalam firmware.
Untuk mulai dengan, saya akan mengklarifikasi apa perbedaan antara firmware untuk mikrokontroler dari yang biasa dan mengapa beberapa implementasi tunggal untuk perangkat lunak ini "lebih baik" daripada yang lain. Beberapa kriteria berasal dari persyaratan untuk firmware, dan beberapa hanya dari pengalaman saya:
- Dalam firmware tidak disarankan untuk membuat objek secara dinamis
- Seringkali dalam firmware, suatu objek dibuat secara statis dan tidak pernah dihancurkan.
- Nah, jika lokasi objek diketahui pada tahap kompilasi
Berdasarkan asumsi-asumsi ini, kami mempertimbangkan dua varian Singleton dengan objek yang dibuat secara statis, dan mungkin yang paling terkenal dan umum adalah Meyers Singleton, omong-omong, meskipun harus seaman dengan standar C ++, kompiler untuk firmware membuatnya seperti ini (misalnya, IAR), hanya ketika opsi khusus diaktifkan:
template <typename T> class Singleton { public: static T & GetInstance() { static T instance ; return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; } ;
Ini menggunakan inisialisasi tertunda, mis. Inisialisasi objek terjadi hanya saat pertama kali
GetInstance()
dipanggil, anggap ini sebagai inisialisasi dinamis.
int main() {
Dan Singleton tanpa penundaan inisialisasi:
template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private: inline static T instance ;
Kedua Singleton membuat objek dalam RAM, perbedaannya adalah untuk yang kedua, inisialisasi terjadi segera setelah program dimulai, dan yang pertama diinisialisasi pada panggilan pertama.
Bagaimana mereka bisa digunakan dalam kehidupan nyata. Menurut tradisi lama, saya akan mencoba menunjukkan ini menggunakan contoh LED. Jadi, anggaplah kita perlu membuat objek kelas
Led1
, yang sebenarnya hanyalah alias dari kelas
Pin<PortA, 5>
:
using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; Led1 myLed ;
Untuk jaga-jaga, kelas Port dan Pin terlihat seperti ini constexpr std::uint32_t OdrAddrShift = 20U; template <std::uint32_t addr> struct Port { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum> class Pin {
Dalam contoh ini, saya membuat sebanyak 4 objek berbeda dari jenis yang sama di RAM dan ROM, yang sebenarnya bekerja dengan output yang sama dari port A. Yang tidak terlalu baik di sini:
Nah, hal pertama yang saya tampaknya lupa bahwa
GreenLed
dan
Led1
adalah tipe yang sama dan membuat beberapa objek identik mengambil ruang di alamat yang berbeda. Bahkan, saya bahkan lupa bahwa saya telah membuat objek global kelas
Led1
dan
GreenLed
, dan juga menciptakannya secara lokal.
Kedua, umumnya menyatakan objek global tidak diterima,
Pedoman Pemrograman untuk Optimalisasi Kompiler Lebih BaikVariabel modul-lokal - variabel yang dinyatakan statis - lebih disukai daripada
variabel global (non-statis). Juga hindari mengambil alamat variabel statis yang sering diakses.
dan objek lokal hanya tersedia dalam lingkup fungsi main ().
Oleh karena itu, kami menulis ulang contoh ini menggunakan Singleton:
using PortA = Port<GpioaBaseAddr> ; using Led1 = Pin<PortA, 5> ; using GreenLed = Pin<PortA, 5> ; int main() {
Dalam hal ini, apa pun yang saya lupakan, tautan saya akan selalu mengarah ke objek yang sama. Dan saya bisa mendapatkan tautan ini di mana saja dalam program ini, dalam metode apa pun, termasuk, misalnya, dalam metode statis interrupt handler, tetapi lebih lanjut tentang itu nanti. Dalam keadilan, saya harus mengatakan bahwa kode tidak melakukan apa-apa, dan kesalahan dalam logika program belum hilang. Baiklah, mari kita cari tahu di mana dan bagaimana secara umum objek statis yang dibuat oleh Singleton ini ditemukan dan bagaimana ia diinisialisasi?
Objek statis
Sebelum mencari tahu, akan lebih baik untuk memahami apa objek statis itu.
Jika Anda mendeklarasikan anggota kelas dengan kata kunci statis, ini berarti bahwa anggota kelas tidak terikat dengan instance kelas, mereka adalah variabel independen dan Anda dapat mengakses bidang tersebut tanpa membuat objek kelas. Tidak ada yang mengancam kehidupan mereka sejak saat kelahiran sampai program dirilis.
Saat digunakan dalam deklarasi objek, specifier statis hanya menentukan masa pakai objek. Secara kasar, memori untuk objek semacam itu dialokasikan ketika program dimulai dan dibebaskan ketika program berakhir, ketika dimulai, ia juga menginisialisasi. Pengecualian hanya objek statis lokal, yang, meskipun "mati" hanya di akhir program, pada dasarnya "lahir", atau lebih tepatnya, diinisialisasi saat pertama kali mereka melewati deklarasi.
Inisialisasi dinamis dari variabel lokal dengan penyimpanan statis dilakukan untuk pertama kalinya pada saat bagian pertama melalui deklarasi; variabel semacam itu dianggap diinisialisasi setelah selesainya inisialisasi. Jika satu utas melewati deklarasi variabel pada saat inisialisasi oleh utas lain, maka ia harus menunggu inisialisasi selesai.
Dalam panggilan berikut, inisialisasi tidak terjadi. Semua hal di atas dapat direduksi menjadi frasa,
hanya satu contoh objek statis yang bisa ada.Kesulitan seperti itu mengarah pada fakta bahwa penggunaan variabel statis lokal dan objek dalam firmware akan mengarah ke overhead tambahan. Anda dapat memverifikasi ini dengan contoh sederhana:
struct Test1{ Test1(int value): j(value) {} int j; } ; Test1 &foo() { static Test1 test(10) ; return test; } int main() { for (int i = 0; i < 10; ++i) { foo().j ++; } return 0; }
Di sini, pertama kali fungsi
foo()
dipanggil, kompiler harus memeriksa bahwa objek statis lokal
test1
belum diinisialisasi dan memanggil konstruktor objek
Test1(10)
, dan pada lintasan kedua dan selanjutnya, ia harus memastikan bahwa objek sudah diinisialisasi dan melewati langkah ini. akan langsung
return test
.
Untuk melakukan ini, kompiler cukup menambahkan flag
foo()::static guard for test 0x00100004 0x1 Data Lc main.o
pelindung tambahan
foo()::static guard for test 0x00100004 0x1 Data Lc main.o
dan menyisipkan kode verifikasi. Pada deklarasi pertama dari variabel statis, flag pelindung ini tidak disetel dan oleh karena itu objek harus diinisialisasi dengan memanggil konstruktor; selama pass berikutnya, flag ini sudah ditetapkan, sehingga tidak ada lagi kebutuhan untuk inisialisasi dan panggilan konstruktor dilewati. Selain itu, pemeriksaan ini akan dilakukan terus menerus dalam for loop.

Dan jika Anda mengaktifkan opsi yang akan menjamin Anda inisialisasi dalam aplikasi multi-ulir, maka akan ada lebih banyak kode ... (lihat panggilan untuk mengambil dan melepaskan sumber daya selama inisialisasi digarisbawahi dalam oranye)

Dengan demikian, harga menggunakan variabel statis atau objek dalam firmware meningkat baik dalam ukuran RAM dan ukuran kode. Dan fakta ini akan baik untuk diingat dan dipertimbangkan ketika berkembang.
Kerugian lain adalah kenyataan bahwa bendera pelindung dilahirkan bersama dengan variabel statis, masa pakainya sama dengan umur objek statis, itu dibuat oleh kompiler itu sendiri dan Anda tidak memiliki akses ke sana selama pengembangan. Yaitu jika tiba-tiba karena suatu alasan
lihat kecelakaan acakPenyebab kesalahan acak adalah: (1) partikel alfa yang dihasilkan dari proses peluruhan, (2) neutron, (3) sumber eksternal radiasi elektromagnetik, dan (4) crosstalk internal.
Jika flag dari 1 pergi ke 0, maka inisialisasi dengan nilai awal dipanggil lagi. Ini tidak baik, dan orang juga harus ingat. Untuk meringkas variabel statis:
Untuk objek statis apa pun (baik itu variabel lokal atau atribut kelas), memori dialokasikan satu kali dan tidak akan berubah sepanjang aplikasi.
Variabel statis lokal diinisialisasi selama melewati pertama melalui deklarasi variabel.
Atribut kelas statis, serta variabel global statis, diinisialisasi segera setelah aplikasi dimulai. Selain itu, pesanan ini tidak ditentukan
Sekarang kembali ke Singleton.
Singleton menempatkan objek dalam ROM
Dari semua hal di atas, kita dapat menyimpulkan bahwa bagi kita, Singleton Mayers mungkin memiliki kelemahan berikut: RAM tambahan dan biaya ROM, bendera keamanan yang tidak terkendali dan ketidakmampuan untuk menempatkan objek dalam ROM karena inisialisasi dinamis.
Tetapi ia memiliki satu kelebihan yang indah: Anda mengontrol waktu inisialisasi objek. Hanya pengembang yang memanggil
GetInstance()
pertama kalinya saat ia membutuhkannya.
Untuk menghilangkan tiga kekurangan pertama, itu sudah cukup untuk digunakan
Singleton tanpa inisialisasi tertunda template<typename T, class Enable = void> class Singleton { public: Singleton(const Singleton&) = delete ; Singleton& operator = (const Singleton&) = delete ; Singleton() = delete ; static T& GetInstance() { return instance; } private: static T instance ; } ; template<typename T, class Enable> T Singleton<T,Enable>::instance ;
Di sini, tentu saja, ada masalah lain, kita tidak dapat mengontrol waktu inisialisasi objek
instance
, dan kita harus entah bagaimana memberikan inisialisasi yang sangat transparan. Tapi ini masalah terpisah, kita tidak akan memikirkannya sekarang.
Singleton ini dapat diulang sehingga inisialisasi objek benar-benar statis pada waktu kompilasi dan instance dari objek
T
dibuat dalam ROM menggunakan
static constexpr T instance
alih-alih
static T instance
:
template <typename T> class Singleton { public: static constexpr T & GetInstance() { return instance ; } Singleton() = delete ; Singleton(const Singleton<T> &) = delete ; const Singleton<T> & operator=(const Singleton<T> &) = delete ; private:
Di sini, pembuatan dan inisialisasi objek akan dilakukan oleh kompiler pada tahap kompilasi dan objek akan jatuh ke dalam segmen .readonly saja. Benar, kelas itu sendiri harus memenuhi aturan berikut:
- Inisialisasi objek kelas ini harus statis. (Konstruktor harus berupa constexpr)
- Kelas harus memiliki konstruktor salinan constexpr
- Metode kelas objek kelas tidak boleh mengubah data objek kelas (semua metode const)
Sebagai contoh, opsi ini sangat mungkin:
class A { friend class Singleton<A>; public: const A & operator=(const A &) = delete ; int Get() const { return test2.Get(); } void Set(int v) const { test.SetB(v); } private: B& test;
Hebat, Anda bisa menggunakan Singleton untuk membuat objek dalam ROM, tetapi bagaimana jika beberapa objek harus dalam RAM? Jelas, Anda perlu entah bagaimana menyimpan dua spesialisasi untuk Singleton, satu untuk objek RAM, yang lainnya untuk objek dalam ROM. Anda dapat melakukan ini dengan memasukkan, misalnya, untuk semua objek yang harus ditempatkan di kelas dasar ROM:
Spesialisasi untuk Singleton membuat objek dalam ROM dan RAM Dalam hal ini, Anda dapat menggunakannya seperti ini:
Bagaimana Anda bisa menggunakan Singleton dalam kehidupan nyata?
Contoh singleton
Saya akan mencoba menunjukkan ini pada contoh timer dan LED. Tugasnya sederhana, kedipkan LED pada timer. Timer dapat diatur.
Prinsip operasi adalah sebagai berikut, ketika interupsi dipanggil, metode
OnInterrupt()
dari timer akan dipanggil, yang pada gilirannya akan memanggil metode switching LED melalui antarmuka pelanggan.
Jelas, objek LED harus dalam ROM, karena tidak ada gunanya membuatnya dalam RAM, bahkan tidak ada data di dalamnya. Pada prinsipnya, saya sudah menjelaskannya di atas, jadi tambahkan saja pewarisan dari
RomObject
, buat konstruktor constexpr dan juga pewarisi antarmuka untuk memproses peristiwa dari timer.
Tetapi saya akan membuat Timer secara khusus dalam RAM dengan sedikit waybill, saya akan menyimpan tautan ke struktur
TIM_TypeDef
, periode dan tautan pelanggan, dan saya akan mengonfigurasi timer di konstruktor (Meskipun mungkin juga Timer akan masuk ke ROM):
Timer kelas class Timer { public: const Timer & operator=(const Timer &) = delete ; void SetPeriod(const std::uint16_t value) { period = value ; timer.PSC = TimerClockSpeed / 1000U - 1U ; timer.ARR = value ; }
Dalam contoh ini, objek kelas
BlinkTimer
terletak di RAM, dan objek kelas
Led1
terletak di ROM. Tidak ada objek global tambahan dalam kode. Di tempat instance kelas diperlukan, kami cukup memanggil
GetInstance()
untuk kelas ini
Tetap menambahkan pengendali interupsi ke tabel vektor interupsi. Dan di sini, sangat nyaman menggunakan Singleton. Dalam metode statis kelas yang bertanggung jawab untuk menangani interupsi, Anda dapat memanggil metode objek yang dibungkus Singleton.
extern "C" void __iar_program_start(void) ; class InterruptHandler { public: static void DummyHandler() { for(;;) {} } static void Timer2Handler() {
Sedikit tentang tabel itu sendiri, cara kerjanya:Segera setelah power-up atau setelah reset, reset diinterupsi dengan angka -8 , dalam tabel itu adalah elemen nol, menurut sinyal reset, program beralih ke vektor elemen nol, di mana penunjuk ke atas tumpukan diinisialisasi terlebih dahulu. Alamat ini diambil dari lokasi segmen STACK yang Anda konfigurasikan dalam pengaturan tautan. Segera setelah pointer diinisialisasi, pergi ke titik entri program, dalam hal ini, di alamat fungsi __iar_program_start
. Selanjutnya, kode diinisialisasi menginisialisasi variabel global dan statis Anda, menginisialisasi coprocessor dengan floating point, jika dimasukkan dalam pengaturan, dan sebagainya. Jika terjadi interupsi, pengendali interupsi dengan nomor interupsi dalam tabel menuju ke alamat interrupt handler. Dalam kasus kami, ini adalah InterruptHandler::Timer2Handler
, yang, melalui Singleton, memanggil metode OnInterrupt()
dari blink timer kami, yang, pada gilirannya, OnTimeOut()
metode OnTimeOut()
kaki port.
Sebenarnya itu saja, Anda bisa menjalankan program. Contoh kerja untuk IAR 8.40
terletak di sini .
Contoh yang lebih rinci tentang penggunaan Singleton untuk objek dalam ROM dan RAM dapat
ditemukan di sini .
Tautan dokumentasi:
PS Dalam gambar di awal artikel, semua sama, Singleton bukan ROM, tapi BERBUNYI.