10 ++ cara untuk bekerja dengan register perangkat keras di C ++ (misalnya, IAR dan Cortex M)

Memilih jalur teraman
Fig. I. Kiyko

Kesehatan yang baik untuk semua!

Anda mungkin ingat anekdot berjanggut, dan mungkin kisah nyata tentang bagaimana seorang siswa ditanyai tentang cara mengukur ketinggian bangunan menggunakan barometer. Siswa tersebut mengutip, menurut pendapat saya, sekitar 20 atau 30 cara, tanpa menyebutkan langsung (melalui perbedaan tekanan) yang diharapkan oleh guru.

Dalam nada yang kira-kira sama, saya ingin terus membahas penggunaan C ++ untuk mikrokontroler dan mempertimbangkan cara-cara bekerja dengan register menggunakan C ++. Dan saya ingin mencatat bahwa untuk mencapai akses yang aman ke register tidak akan ada cara mudah. Saya akan mencoba menunjukkan semua pro dan kontra dari metode ini. Jika Anda tahu lebih banyak cara, lemparkan ke dalam komentar. Jadi mari kita mulai:

Metode 1. Jelas dan jelas bukan yang terbaik


Metode yang paling umum, yang juga digunakan dalam C ++, adalah dengan menggunakan deskripsi struktur register dari file header dari pabrikan. Untuk demonstrasi, saya akan mengambil dua register port A (ODR - register data output dan IDR - input data register) dari mikrokontroler STM32F411, sehingga saya dapat melakukan "menyulam" "Hello world" - mengedipkan LED.

int main() { GPIOA->ODR ^= (1 << 5) ; GPIOA->IDR ^= (1 << 5) ; //,      } 

Mari kita lihat apa yang terjadi di sini dan bagaimana desain ini bekerja. Header mikroprosesor berisi struktur GPIO_TypeDef dan definisi pointer ke struktur GPIOA ini. Ini terlihat seperti ini:

 typedef struct { __IO uint32_t MODER; //port mode register, Address offset: 0x00 __IO uint32_t OTYPER; //port output type register, Address offset: 0x04 __IO uint32_t OSPEEDR; //port output speed register, Address offset: 0x08 __IO uint32_t PUPDR; //port pull-up/pull-down register, Address offset: 0x0C __IO uint32_t IDR; //port input data register, Address offset: 0x10 __IO uint32_t ODR; //port output data register, Address offset: 0x14 __IO uint32_t BSRR; //port bit set/reset register, Address offset: 0x18 __IO uint32_t LCKR; //port configuration lock register, Address offset: 0x1C __IO uint32_t AFR[2]; //alternate function registers, Address offset: 0x20-0x24 } GPIO_TypeDef; #define PERIPH_BASE 0x40000000U //Peripheral base address in the alias region #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000U) #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000U) #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 

Untuk membuatnya dalam kata-kata manusia yang sederhana, maka seluruh struktur tipe GPIO_TypeDef "meletakkan" di alamat GPIOA_BASE , dan ketika Anda merujuk ke bidang tertentu dari struktur, Anda pada dasarnya merujuk ke alamat struktur ini + mengimbangi ke elemen struktur ini. Jika Anda menghapus #define GPIOA , maka kode akan terlihat seperti ini:

 ((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ; ((GPIO_TypeDef *) GPIOA_BASE)->IDR ^= (1 << 5) ; // 

Sehubungan dengan bahasa pemrograman C ++, alamat integer dikonversi ke tipe pointer ke struktur GPIO_TypeDef . Tetapi dalam C ++, ketika menggunakan konversi C, kompiler mencoba untuk melakukan konversi dalam urutan berikut:

  • const_cast
  • static_cast
  • static_cast di sebelah const_cast,
  • reinterpret_cast
  • reinterpret_cast di sebelah const_cast

yaitu jika kompiler tidak dapat mengonversi jenis menggunakan const_cast, ia mencoba menerapkan static_cast dan sebagainya. Akibatnya, panggilan:

 ((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ; 

tidak ada yang seperti:

 reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE)->ODR ^= (1 << 5) ; 

Bahkan, untuk aplikasi C ++, akan benar untuk "menarik" struktur ke alamat seperti ini:

 GPIO_TypeDef * GPIOA{reinterpret_cast<GPIO_TypeDef *>(GPIOA_BASE)} ; 

Bagaimanapun, karena konversi tipe, ada minus besar untuk pendekatan ini untuk C ++. Ini terdiri dalam kenyataan bahwa reinterpret_cast tidak dapat digunakan baik dalam konstruktor dan fungsi constexpr , maupun dalam parameter template, dan ini secara signifikan mengurangi penggunaan fitur C ++ untuk mikrokontroler.
Saya akan menjelaskan ini dengan contoh. Adalah mungkin untuk melakukannya:

  struct Test { const int a; const int b; } ; template<Test* mystruct> constexpr const int Geta() { return mystruct->a; } Test test{1,2}; int main() { Geta<&test>() ; } 

Tapi Anda belum bisa melakukan ini:

 template<GPIO_TypeDef * mystruct> constexpr volatile uint32_t GetIdr() { return mystruct->IDR; } int main() { //GPIOA  reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE) //  ,        GetIdr<GPIOA>() ; // } //      : struct Port { constexpr Port(GPIO_TypeDef * ptr): port(*ptr) {} GPIO_TypeDef & port ; } //  GPIOA  reinterpret_cast,   //  constexpr      constexpr Port portA{GPIOA}; //    

Dengan demikian, penggunaan langsung dari pendekatan ini membebankan batasan yang signifikan pada penggunaan C ++. Kami tidak akan dapat menemukan objek yang ingin menggunakan GPIOA di ROM menggunakan alat bahasa, dan kami tidak akan dapat mengambil keuntungan dari pemrograman program untuk objek seperti itu.
Selain itu, secara umum, metode ini tidak aman (seperti yang dikatakan oleh mitra Barat kami). Bagaimanapun, sangat mungkin untuk membuat beberapa NON-FUN
Sehubungan dengan hal di atas, kami merangkum:

Pro


  • Judul dari pabrikan digunakan (diperiksa, tidak ada kesalahan)
  • Tidak ada gerakan dan biaya tambahan, yang Anda ambil dan gunakan
  • Kemudahan penggunaan
  • Semua orang tahu dan mengerti metode ini.
  • Tanpa overhead

Cons


  • Penggunaan metaprogramming terbatas
  • Ketidakmampuan untuk digunakan dalam konstruktor constexpr
  • Saat menggunakan pembungkus di kelas, konsumsi tambahan RAM adalah penunjuk ke objek struktur ini
  • Kamu bisa bikin bodoh
Sekarang mari kita lihat metode nomor 2

Metode 2. Brutal


Jelas bahwa setiap programmer embed mengingat alamat semua register untuk semua mikrokontroler, sehingga Anda dapat selalu menggunakan metode berikut, yang mengikuti dari yang pertama:

 *reinterpret_cast<volatile uint32_t *>(GpioaOdrAddr) ^= (1 <<5) ; *reinterpret_cast<volatile uint32_t *>(GpioaIdrAddr) ^= (1 <<5) ; // 

Di mana saja dalam program ini, Anda selalu dapat memanggil konversi ke alamat register volatile uint32_t dan menginstal setidaknya sesuatu di sana.
Terutama tidak ada nilai tambah di sini, tetapi untuk minus yang ada ditambahkan ketidaknyamanan untuk digunakan dan kebutuhan untuk menulis alamat masing-masing register dalam file terpisah sendiri. Oleh karena itu, kita beralih ke metode nomor 3.

Metode 3. Jelas dan jelas lebih benar


Jika akses ke register terjadi melalui bidang struktur, maka alih-alih pointer ke objek struktur, Anda bisa menggunakan alamat struktur integer. Alamat struktur ada di file header dari pabrikan (misalnya, GPIOA_BASE untuk GPIOA), jadi Anda tidak perlu mengingatnya, tetapi Anda bisa menggunakannya dalam template dan ekspresi constexpr, lalu "overlay" struktur ke alamat ini.

 template<uint32_t addr, uint32_t pinNum> struct Pin { using Registers = GPIO_TypeDef ; __forceinline static void Toggle() { //     addr Registers *GpioPort{reinterpret_cast<Registers*>(addr)}; GpioPort->ODR ^= (1 << pinNum) ; } }; int main() { using Led1 = Pin<GPIOA_BASE, 5> ; Led1::Toggle() ; } 

Tidak ada minus khusus, dari sudut pandang saya. Pada prinsipnya, opsi bekerja. Tapi tetap saja, mari kita lihat cara lain.

Metode 4. Bungkus Eksoteris


Untuk penikmat kode yang dapat dimengerti, Anda dapat membuat pembungkus atas register sehingga nyaman untuk mengaksesnya dan terlihat โ€œcantikโ€, membuat konstruktor, mendefinisikan kembali operator:

 class Register { public: explicit Register(uint32_t addr) : ptr{ reinterpret_cast<volatile uint32_t *>(addr) } { } __forceinline inline Register& operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile uint32_t *ptr; //    }; int main() { Register Odr{GpioaOdrAddr}; Odr ^= (1 << 5); Register Idr{GpioaIdrAddr}; Idr ^= (1 << 5); // } 

Seperti yang Anda lihat, sekali lagi Anda harus mengingat alamat integer dari semua register, atau mengaturnya di suatu tempat, dan Anda juga harus menyimpan pointer ke alamat register. Tetapi apa yang tidak terlalu baik lagi, reinterpret_cast terjadi lagi di konstruktor
Beberapa kelemahan, dan pada kenyataan bahwa dalam versi pertama dan kedua ada ditambahkan kebutuhan untuk setiap register yang digunakan untuk menyimpan pointer ke 4 byte dalam RAM. Secara umum, bukan opsi. Kami melihat yang berikut ini.

Metode 4,5. Bungkus Eksoteris dengan Pola


Kami menambahkan butiran metaprogramming, tetapi tidak ada banyak manfaat dari ini. Metode ini berbeda dari yang sebelumnya hanya dalam hal alamat ditransfer bukan ke konstruktor, tetapi dalam parameter templat, kami menyimpan sedikit pada register ketika meneruskan alamat ke konstruktor, itu sudah baik:

 template<uint32_t addr> class Register { public: Register() : ptr{reinterpret_cast<volatile uint32_t *>(addr)} { } __forceinline inline Register &operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile std::uint32_t *ptr; }; int main() { using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5); // } 

Jadi, penggaruk yang sama, tampilan samping.

Metode 5. Masuk akal


Jelas, Anda harus menyingkirkan pointer, jadi mari kita lakukan hal yang sama, tetapi hapus pointer yang tidak perlu dari kelas.

 template<uint32_t addr> class Register { public: __forceinline Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5); // 

Anda bisa tinggal di sini dan berpikir sedikit. Metode ini segera memecahkan 2 masalah yang sebelumnya diwarisi dari metode pertama. Pertama, sekarang saya bisa menggunakan pointer ke objek Register di template, dan kedua, saya bisa meneruskannya ke konstruktor constexrp .

 template<Register * register> void Xor(uint32_t mask) { *register ^= mask ; } Register<GpioaOdrAddr> GpioaOdr; int main() { Xor<&GpioaOdr>(1 << 5) ; //  } //   struct Port { constexpr Port(Register& ref): register(ref) {} Register & register ; } constexpr Port portA{GpioaOdr}; 

Tentu saja, perlu lagi, baik memiliki memori eidetik untuk alamat register, atau untuk secara manual menentukan semua alamat register di suatu tempat dalam file terpisah ...

Pro


  • Kemudahan penggunaan
  • Kemampuan untuk menggunakan metaprogramming
  • Kemampuan untuk digunakan dalam konstruktor constexpr

Cons


  • File header yang diverifikasi dari produsen tidak digunakan
  • Anda harus mengatur sendiri semua alamat register
  • Anda perlu membuat objek Daftar kelas
  • Kamu bisa bikin bodoh

Bagus, tapi masih banyak kekurangan ...

Metode 6. Lebih cerdas daripada masuk akal


Pada metode sebelumnya, untuk mengakses register itu perlu untuk membuat objek register ini, ini adalah pemborosan RAM dan ROM, jadi kami melakukan pembungkus dengan metode statis.

 template<uint32_t addr> class Register { public: __forceinline inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile uint32_t *>(addr) ^= mask; } }; int main() { using namespace Case6 ; using Odr = Register<GpioaOdrAddr>; Odr::Xor(1 << 5); using Idr = Register<GpioaIdrAddr>; Idr::Xor(1 << 5); // } 

Satu tambah ditambahkan
  • Tanpa overhead. Kode ringkas cepat, sama seperti pada opsi 1 (Saat menggunakan pembungkus di kelas, tidak ada biaya RAM tambahan, karena objek tidak dibuat, tetapi metode statis digunakan tanpa membuat objek)
Silakan ...

Metode 7. Hapus kebodohan


Jelas, saya selalu melakukan NON-LUCU dalam kode dan menulis sesuatu ke dalam register, yang sebenarnya tidak dimaksudkan untuk ditulis. Tidak apa-apa, tentu saja, tetapi KEBODOHAN harus dilarang. Mari kita dilarang melakukan omong kosong. Untuk melakukan ini, kami memperkenalkan struktur tambahan:

  struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; 

Sekarang kita dapat mengatur register untuk ditulis, dan register itu hanya baca:

 template<uint32_t addr, typename RegisterType> class Register { public: //       WriteReg,    // ,  ,       __forceinline template <typename T = RegisterType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T>::value>> Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; 

Sekarang mari kita coba untuk mengkompilasi pengujian kami dan melihat bahwa tes tidak dikompilasi, karena operator ^= untuk register Idr tidak ada:

  int main() { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr Odr ; Odr ^= (1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr Idr ; Idr ^= (1 << 5) ; //,  Idr    } 

Jadi, sekarang ada lebih banyak ...

Pro


  • Kemudahan penggunaan
  • Kemampuan untuk menggunakan metaprogramming
  • Kemampuan untuk digunakan dalam konstruktor constexpr
  • Kode ringkas cepat, sama seperti pada opsi 1
  • Saat menggunakan pembungkus di kelas, tidak ada biaya RAM tambahan, karena objek tidak dibuat, tetapi metode statis digunakan tanpa membuat objek
  • Anda tidak bisa melakukan kebodohan

Cons


  • File header yang diverifikasi dari produsen tidak digunakan
  • Anda harus mengatur sendiri semua alamat register
  • Anda perlu membuat objek Daftar kelas

Jadi mari kita hilangkan kesempatan untuk membuat kelas untuk menabung lebih banyak

Metode 8. Tanpa NONSENSE dan tanpa objek kelas


Kode segera:

  struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; int main { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr::Xor(1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr::Xor(1 << 5) ; //,  Idr    } 

Kami menambahkan satu lagi plus, kami tidak membuat objek. Tapi lanjutkan, kita masih punya kontra

Metode 9. Metode 8 dengan integrasi struktur


Dalam metode sebelumnya, hanya case yang didefinisikan. Tetapi dalam metode 1, semua register digabungkan ke dalam struktur sehingga Anda dapat dengan mudah mengaksesnya dengan modul. Ayo lakukan ...

 namespace Case9 { struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; template<uint32_t addr> struct Gpio { using Moder = Register<addr, ReadWriteReg>; //      using Otyper = Register<addr + OtyperShift, ReadWriteReg> ; using Ospeedr = Register<addr + OspeedrShift,ReadWriteReg> ; using Pupdr = Register<addr + PupdrShift,ReadWriteReg> ; using Idr = Register<addr + IdrShift, ReadReg> ; using Odr = Register<addr + OdrShift, WriteReg> ; }; int main() { using Gpioa = Gpio<GPIOA_BASE> ; Gpioa::Odr::Xor(1 << 5) ; Gpioa::Idr::Xor((1 << 5) ); //,  Idr    } 

Di sini kekurangannya adalah bahwa struktur harus didaftarkan kembali, dan offset semua register harus diingat dan ditentukan di suatu tempat. Akan lebih baik jika offset ditetapkan oleh kompiler, dan bukan oleh orangnya, tetapi ini nanti, tetapi untuk saat ini kami akan mempertimbangkan metode menarik lain yang disarankan oleh rekan saya.

Metode 10. Bungkus register melalui pointer ke anggota struktur


Di sini kita menggunakan konsep seperti itu sebagai penunjuk ke anggota struktur dan akses ke sana .

 template<uint32_t addr, typename T> class RegisterStructWrapper { public: __forceinline template<typename P> inline static void Xor(PT::*member, int mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; //   ,     . } } ; using GpioaWrapper = RegisterStructWrapper<GPIOA_BASE, GPIO_TypeDef> ; int main() { GpioaWrapper::Xor(&GPIO_TypeDef::ODR, (1 << 5)) ; GpioaWrapper::Xor(&GPIO_TypeDef::IDR, (1 << 5)) ; // return 0 ; } 

Pro


  • Kemudahan penggunaan
  • Kemampuan untuk menggunakan metaprogramming
  • Kemampuan untuk digunakan dalam konstruktor constexpr
  • Kode ringkas cepat, sama seperti pada opsi 1
  • Saat menggunakan pembungkus di kelas, tidak ada biaya RAM tambahan, karena objek tidak dibuat, tetapi metode statis digunakan tanpa membuat objek
  • File header yang diverifikasi dari pabrik digunakan.
  • Tidak perlu mengatur sendiri semua alamat register
  • Tidak perlu membuat objek Daftar kelas

Cons


  • Anda dapat membuat Foolishness dan bahkan berspekulasi pada kelengkapan kode

Metode 10.5. Gabungkan Metode 9 dan 10


Untuk mengetahui pergeseran register relatif terhadap awal struktur, Anda dapat menggunakan pointer ke anggota struktur: volatile uint32_t T::*member , ia akan mengembalikan offset anggota struktur relatif ke permulaan dalam byte. Misalnya, kami memiliki struktur GPIO_TypeDef , maka alamat &GPIO_TypeDef::ODR akan menjadi 0x14.
Kami mengalahkan peluang ini dan menghitung alamat register dari metode 9, menggunakan kompiler:

 struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T, volatile uint32_t T::*member, typename RegType> class Register { public: __forceinline template <typename T1 = RegType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; } }; template<uint32_t addr, typename T> struct Gpio { using Moder = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, ReadWriteReg>; using Otyper = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OTYPER, ReadWriteReg>; using Ospeedr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OSPEEDR, ReadWriteReg>; using Pupdr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::PUPDR, ReadWriteReg>; using Idr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::IDR, ReadReg>; using Odr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, WriteReg>; } ; 

Anda dapat bekerja dengan register secara lebih eksotis:

 using namespace Case11 ; using Gpioa = Gpio<GPIOA_BASE, GPIO_TypeDef> ; Gpioa::Odr::Xor(1 << 5) ; //Gpioa::Idr::Xor((1 << 5) ); //,  Idr    

Jelas, di sini semua struktur harus ditulis ulang lagi. Ini dapat dilakukan secara otomatis, dengan beberapa skrip di Phyton, pada input seperti stm32f411xe.h pada output file Anda dengan struktur untuk digunakan dalam C ++.
Bagaimanapun, ada beberapa cara berbeda yang dapat bekerja dalam proyek tertentu.

Bonus Kami memperkenalkan ekstensi bahasa dan kode parsim menggunakan Phyton


Masalah bekerja dengan register di C ++ telah ada selama beberapa waktu. Orang menyelesaikannya dengan cara yang berbeda. Tentu saja akan lebih bagus jika bahasa tersebut mendukung sesuatu seperti mengganti nama kelas pada waktu kompilasi. Nah, katakanlah, bagaimana jika seperti ini:

 template<classname = [PortName]> class Gpio[Portname] { __forceinline inline static void Xor(const uint32_t mask) { GPIO[PortName]->ODR ^= mask ; } }; int main() { using GpioA = Gpio<"A"> ; GpioA::Xor(5) ; } 

Namun sayangnya bahasa ini tidak mendukung. Oleh karena itu, solusi yang digunakan orang adalah parsing code menggunakan Python. Yaitu beberapa ekstensi bahasa diperkenalkan. Kode, menggunakan ekstensi ini, diumpankan ke pengurai Python, yang menerjemahkannya ke dalam kode C ++. Kode seperti ini terlihat seperti ini: (contoh diambil dari perpustakaan modm; berikut adalah sumber lengkapnya ):

 %% set port = gpio["port"] | upper %% set reg = "GPIO" ~ port %% set pin = gpio["pin"] class Gpio{{ port ~ pin }} : public Gpio { __forceinline inline static void Xor() { GPIO{{port}}->ODR ^= 1 << {{pin}} ; } } //        class Gpio5 : public Gpio { __forceinline inline static void Xor() { GPIO->ODR ^= 1 << 5 ; } } //     using Led = Gpio5; Led::Xor(); 


Perbarui: Bonus. File dan parser SVD di Phyton


Lupa menambahkan opsi lain. ARM merilis file deskripsi register untuk setiap produsen SVD. Dari mana Anda kemudian dapat menghasilkan file C ++ dengan deskripsi register. Paul Osborne telah mengompilasi semua file ini di GitHub . Dia juga menulis skrip Python untuk mengurai mereka.

Itu saja ... imajinasiku habis. Jika Anda masih punya ide, silakan saja. Contoh dengan semua metode ada di sini.

Referensi


Akses Register Typesafe di C ++
Membuat hal-hal melakukan hal -Aksesoris hardware dari C ++
Membuat sesuatu berfungsi - Bagian 3
Membuat hal-hal dilakukan- Struktur overlay

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


All Articles