Kesehatan yang baik untuk semua!
Ketika mengajar siswa bagaimana mengembangkan perangkat lunak tertanam untuk mikrokontroler di universitas, saya menggunakan C ++ dan kadang-kadang saya memberi siswa yang sangat tertarik dengan segala macam tugas untuk mengidentifikasi siswa berbakat yang
sakit parah .
Sekali lagi, siswa semacam itu diberi tugas untuk mengedipkan 4 LED menggunakan bahasa C ++ 17 dan pustaka C ++ standar, tanpa menghubungkan pustaka tambahan, seperti CMSIS dan file header mereka dengan deskripsi struktur register, dan seterusnya ... Yang dengan kode menang dalam ROM akan menjadi ukuran terkecil dan paling sedikit menghabiskan RAM. Optimalisasi kompiler tidak boleh lebih tinggi dari Medium. Kompiler IAR 8.40.1.
Pemenang
pergi ke Canary dan mendapat 5 untuk ujian.
Sebelum itu, saya sendiri juga tidak menyelesaikan masalah ini, jadi saya akan memberi tahu Anda bagaimana para siswa menyelesaikannya dan apa yang terjadi pada saya. Saya segera memperingatkan Anda bahwa tidak mungkin kode seperti itu dapat digunakan dalam aplikasi nyata, itulah sebabnya saya memposting publikasi di bagian "Pemrograman tidak normal", meskipun siapa yang tahu.
Kondisi tugas
Ada 4 LED pada port GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9. Mereka perlu berkedip. Untuk membandingkan sesuatu, kami mengambil kode yang ditulis dalam C:
void delay() { for (int i = 0; i < 1000000; ++i){ } } int main() { for(;;) { GPIOA->ODR ^= (1 << 5); GPIOC->ODR ^= (1 << 5); GPIOC->ODR ^= (1 << 8); GPIOC->ODR ^= (1 << 9); delay(); } return 0 ; }
Fungsi
delay()
sini murni formal, siklus reguler, tidak dapat dioptimalkan.
Diasumsikan bahwa port sudah dikonfigurasi untuk output dan clocking diterapkan pada mereka.
Saya juga akan mengatakan bahwa bitbanging tidak digunakan untuk membuat kode menjadi portabel.
Kode ini membutuhkan 8 byte di stack dan 256 byte di ROM pada Medium Optimization
Memori kode baca hanya 255 byte
1 byte memori data hanya baca
8 byte memori data readwrite
255 byte karena fakta bahwa bagian dari memori berada di bawah tabel vektor interupsi, panggilan ke fungsi IAR untuk menginisialisasi blok floating-point, segala macam fungsi debugging dan fungsi __low_level_init, di mana port itu sendiri dikonfigurasikan.
Jadi, persyaratan lengkapnya adalah:
- Fungsi main () harus mengandung kode sesedikit mungkin
- Anda tidak dapat menggunakan makro
- Kompiler IAR 8.40.1 mendukung C ++ 17
- File header CMSIS seperti "#include" stm32f411xe.h "tidak dapat digunakan
- Anda dapat menggunakan arahan __forceinline untuk fungsi inline
- Optimalisasi Kompiler Sedang
Keputusan siswa
Secara umum, ada beberapa solusi, saya hanya akan menunjukkan satu ... itu tidak optimal, tetapi saya menyukainya.
Karena header tidak dapat digunakan, hal pertama yang dilakukan siswa adalah kelas
Gpio
, yang harus menyimpan tautan ke register port di alamat mereka. Untuk melakukan ini, mereka menggunakan overlay struktur, kemungkinan besar mereka mengambil ide dari sini:
Overlay struktur :
class Gpio { public: __forceinline inline void Toggle(const std::uint8_t bitNum) volatile { Odr ^= bitNum ; } private: volatile std::uint32_t Moder; volatile std::uint32_t Otyper; volatile std::uint32_t Ospeedr; volatile std::uint32_t Pupdr; volatile std::uint32_t Idr; volatile std::uint32_t Odr;
Seperti yang Anda lihat, mereka segera mengidentifikasi kelas
Gpio
dengan atribut yang harus ditempatkan di alamat register yang sesuai dan metode untuk beralih negara dengan jumlah kaki:
Kemudian kami menentukan struktur untuk
GpioPin
berisi pointer ke
Gpio
dan jumlah kaki:
struct GpioPin { volatile Gpio* port ; std::uint32_t pinNum ; } ;
Kemudian mereka membuat array LED yang duduk di kaki port tertentu dan mengatasinya dengan memanggil metode
Toggle()
dari setiap LED:
const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9} } ; struct LedsDriver { __forceinline static inline void ToggelAll() { for (auto& it: leds) { it.port->Toggle(it.pinNum); } } } ;
Sebenarnya seluruh kode: constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; class Gpio { public: __forceinline inline void Toggle(const std::uint8_t bitNum) volatile { Odr ^= bitNum ; } private: volatile std::uint32_t Moder; volatile std::uint32_t Otyper; volatile std::uint32_t Ospeedr; volatile std::uint32_t Pupdr; volatile std::uint32_t Idr; volatile std::uint32_t Odr; } ;
Statistik kode mereka tentang pengoptimalan sedang:
275 byte dari memori kode hanya baca
1 byte memori data hanya baca
8 byte memori data readwrite
Solusi yang bagus, tetapi membutuhkan banyak memori :)
Keputusan saya
Tentu saja, saya memutuskan untuk tidak mencari cara-cara sederhana dan memutuskan untuk bertindak serius :).
LED ada di port yang berbeda dan kaki yang berbeda. Hal pertama yang Anda butuhkan adalah membuat kelas
Port
, tetapi untuk menghilangkan pointer dan variabel yang menggunakan RAM, Anda perlu menggunakan metode statis. Kelas port mungkin terlihat seperti ini:
template <std::uint32_t addr> struct Port {
Sebagai parameter templat, ia akan memiliki alamat port. Di
"#include "stm32f411xe.h"
, misalnya, untuk port A, didefinisikan sebagai GPIOA_BASE. Namun kami tidak diizinkan menggunakan header, jadi kami hanya perlu membuat konstanta kami sendiri. Sebagai hasilnya, kelas dapat digunakan seperti ini:
constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ;
Untuk berkedip, Anda memerlukan metode Toggle (const std :: uint8_t bit), yang akan mengalihkan bit yang diperlukan menggunakan operasi ATAU eksklusif. Metode harus statis, tambahkan ke kelas:
template <std::uint32_t addr> struct Port {
Port<>
Luar Biasa
Port<>
adalah, ia dapat mengubah keadaan kaki. LED duduk di kaki tertentu, jadi logis untuk membuat
Pin
kelas, yang akan memiliki
Port<>
dan nomor kaki sebagai parameter template. Karena jenis
Port<>
adalah templat, mis. berbeda untuk port yang berbeda, kami hanya dapat mengirimkan tipe universal T.
template <typename T, std::uint8_t pinNum> struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ;
Sangat buruk bahwa kita dapat melewati omong kosong tipe
T
yang memiliki metode
Toggle()
dan ini akan berhasil, meskipun diasumsikan bahwa kita hanya boleh melewati jenis
Port<>
. Untuk melindungi
PortBase
dari ini, kita akan membuat
Port<>
mewarisi dari kelas dasar
PortBase
, dan dalam templat kita akan memverifikasi bahwa tipe yang kita lewati memang berdasarkan
PortBase
. Kami mendapatkan yang berikut ini:
constexpr std::uint32_t OdrAddrShift = 20U; struct PortBase { }; template <std::uint32_t addr> struct Port: PortBase { __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 = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>>
Sekarang templatenya hanya dipakai jika kelas kita memiliki kelas dasar
PortBase
.
Secara teori, Anda sudah dapat menggunakan kelas-kelas ini, mari kita lihat apa yang terjadi tanpa optimasi:
using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; using Led1 = Pin<PortA, 5> ; using Led2 = Pin<PortC, 5> ; using Led3 = Pin<PortC, 8> ; using Led4 = Pin<PortC, 9> ; int main() { for(;;) { Led1::Toggle(); Led2::Toggle(); Led3::Toggle(); Led4::Toggle(); delay(); } return 0 ; }
271 byte memori kode hanya baca
1 byte memori data hanya baca
24 byte memori data readwrite
Dari mana asal 16 byte ini dalam RAM dan 16 byte dalam ROM berasal. Mereka datang dari fakta bahwa kita melewatkan parameter bit ke fungsi Toggle (const std :: uint8_t bit) dari kelas Port, dan kompiler, ketika memasuki fungsi utama, menyimpan 4 register tambahan pada stack yang dilewati oleh parameter ini, kemudian menggunakan ini register di mana nilai-nilai nomor kaki untuk setiap Pin disimpan dan ketika meninggalkan utama mengembalikan register-register ini dari tumpukan. Dan meskipun pada dasarnya ini adalah semacam pekerjaan yang sama sekali tidak berguna, karena fungsinya built-in, tetapi kompiler bekerja sepenuhnya sesuai dengan standar.
Anda dapat menyingkirkan ini dengan menghapus kelas port secara umum, meneruskan alamat port sebagai parameter templat untuk kelas
Pin
, dan di dalam metode
Toggle()
, hitung alamat register ODR:
constexpr std::uint32_t OdrAddrShift = 20U; template <std::uint32_t addr, std::uint8_t pinNum, struct Pin { __forceinline inline static void Toggle() { *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift ) ^= (1 << bit) ; } } ; using Led1 = Pin<GpioaBaseAddr, 5> ;
Tapi ini tidak terlihat sangat bagus dan ramah pengguna. Oleh karena itu, kami berharap kompiler menghapus pelestarian register yang tidak perlu ini dengan sedikit optimisasi.
Kami menempatkan pengoptimalan pada Media dan melihat hasilnya:
251 byte memori kode hanya baca
1 byte memori data hanya baca
8 byte memori data readwrite
Wow wow wow ... kami memiliki 4 byte lebih sedikit
kodeMemori kode baca hanya 255 byte
1 byte memori data hanya baca
8 byte memori data readwrite
Bagaimana ini bisa terjadi? Mari kita lihat assembler di debugger untuk kode C ++ (kiri) dan kode C (kanan):

Dapat dilihat bahwa, pertama, kompiler membuat semua fungsi built-in, sekarang tidak ada panggilan sama sekali, dan kedua, ia mengoptimalkan penggunaan register. Dapat dilihat bahwa dalam kasus kode C, kompiler menggunakan register R1 atau R2 untuk menyimpan alamat port dan melakukan operasi tambahan setiap kali bit diaktifkan (simpan alamat dalam register baik dalam R1 atau dalam R2). Dalam kasus kedua, hanya menggunakan register R1, dan karena 3 panggilan terakhir untuk beralih selalu dari port C, tidak perlu lagi menyimpan alamat port C yang sama dalam register. Hasilnya, 2 tim dan 4 byte disimpan.
Ini dia keajaiban dari kompiler modern :) Baiklah, oke. Pada prinsipnya, orang bisa berhenti di situ, tapi mari kita lanjutkan. Saya pikir tidak mungkin untuk mengoptimalkan hal lain, meskipun mungkin tidak benar, jika Anda memiliki ide, tulis di komentar. Tetapi dengan jumlah kode di main () Anda dapat bekerja.
Sekarang saya ingin semua LED berada di suatu tempat di dalam wadah, dan Anda dapat memanggil metode, alihkan segalanya ... Sesuatu seperti ini:
int main() { for(;;) { LedsContainer::ToggleAll() ; delay(); } return 0 ; }
Kami tidak akan dengan bodohnya memasukkan switching 4 LED ke fungsi LedsContainer :: ToggleAll, karena tidak menarik :). Kami ingin meletakkan LED di dalam wadah dan kemudian pergi melalui mereka dan memanggil metode Toggle () pada masing-masing.
Siswa menggunakan array untuk menyimpan pointer ke LED. Tetapi saya memiliki tipe yang berbeda, misalnya:
Pin<PortA, 5>
,
Pin<PortC, 5>
, dan saya tidak bisa menyimpan pointer ke tipe yang berbeda dalam sebuah array. Anda dapat membuat kelas dasar virtual untuk semua Pin, tetapi kemudian tabel fungsi virtual akan muncul dan saya tidak akan berhasil memenangkan siswa.
Karena itu, kami akan menggunakan tuple. Ini memungkinkan Anda untuk menyimpan objek dari berbagai jenis. Kasus ini akan terlihat seperti ini:
class LedsContainer { private: constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; }
Ada wadah yang bagus, menyimpan semua LED. Sekarang tambahkan metode
ToggleAll()
ke dalamnya:
class LedsContainer { public: __forceinline static inline void ToggleAll() {
Anda tidak bisa hanya berjalan melalui elemen tuple, karena elemen tuple hanya boleh diterima pada tahap kompilasi. Untuk mengakses elemen tuple ada metode dapatkan template. Baik i.e. jika kita menulis
std::get<0>(records).Toggle()
, maka metode
Toggle()
dipanggil untuk objek kelas
Pin<PortA, 5>
, jika
std::get<1>(records).Toggle()
, maka metode
Toggle()
dipanggil untuk objek kelas
Pin<Port, 5>
dan seterusnya ...
Anda dapat
menghapus hidung siswa Anda dan cukup menulis:
__forceinline static inline void ToggleAll() { std::get<0>(records).Toggle(); std::get<1>(records).Toggle(); std::get<2>(records).Toggle(); std::get<3>(records).Toggle(); }
Tetapi kami tidak ingin memaksakan programmer yang akan mendukung kode ini dan memungkinkannya untuk melakukan pekerjaan tambahan, menghabiskan sumber daya perusahaannya, katakanlah, kalau-kalau LED lain muncul. Anda harus menambahkan kode di dua tempat, di tuple dan dalam metode ini - dan ini tidak baik dan pemilik perusahaan tidak akan senang. Oleh karena itu, kami memotong tuple menggunakan metode helper:
class class LedsContainer { friend int main() ; public: __forceinline static inline void ToggleAll() {
Ini terlihat menakutkan, tetapi saya memperingatkan di awal artikel bahwa metode
shizany tidak terlalu biasa ...
Semua keajaiban ini dari atas pada tahap kompilasi secara harfiah melakukan hal berikut:
Silakan mengkompilasi dan memeriksa ukuran kode tanpa optimasi:
Kode yang mengkompilasi #include <cstddef> #include <tuple> #include <utility> #include <cstdint> #include <type_traits> //#include "stm32f411xe.h" #define __forceinline _Pragma("inline=forced") constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; constexpr std::uint32_t OdrAddrShift = 20U; struct PortBase { }; template <std::uint32_t addr> struct Port: PortBase { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum, class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; //using Led1 = Pin<PortA, 5> ; //using Led2 = Pin<PortC, 5> ; //using Led3 = Pin<PortC, 8> ; //using Led4 = Pin<PortC, 9> ; class LedsContainer { friend int main() ; public: __forceinline static inline void ToggleAll() { // 3,2,1,0 , visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>()); } private: __forceinline template<std::size_t... index> static inline void visit(std::index_sequence<index...>) { Pass((std::get<index>(records).Toggle(), true)...); } __forceinline template<typename... Args> static void inline Pass(Args... ) { } constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } ; void delay() { for (int i = 0; i < 1000000; ++i){ } } int main() { for(;;) { LedsContainer::ToggleAll() ; //GPIOA->ODR ^= 1 << 5; //GPIOC->ODR ^= 1 << 5; //GPIOC->ODR ^= 1 << 8; //GPIOC->ODR ^= 1 << 9; delay(); } return 0 ; }
Bukti assembler, dibongkar sesuai rencana: Kita melihat bahwa memori berlebihan, 18 byte lebih. Masalahnya sama, ditambah 12 byte lagi. Saya tidak mengerti dari mana mereka berasal ... mungkin seseorang akan menjelaskan.
283 byte memori kode hanya baca
1 byte memori data hanya baca
24 byte memori data readwrite
Sekarang hal yang sama pada optimasi Medium dan lihatlah ... kami mendapat kode yang identik dengan implementasi C ++ di dahi dan kode C yang lebih optimal.
251 byte memori kode hanya baca
1 byte memori data hanya baca
8 byte memori data readwrite
Seperti yang Anda lihat, saya menang, dan
pergi ke Kepulauan Canary dan senang beristirahat di Chelyabinsk :), tetapi para siswa juga hebat, mereka berhasil lulus ujian!
Siapa peduli, kodenya ada di siniDi mana saya bisa menggunakan ini, yah, saya datang dengan, misalnya, kita memiliki parameter dalam memori EEPROM dan kelas yang menjelaskan parameter ini (Baca, tulis, inisialisasi ke nilai awal). Kelasnya adalah templat, seperti
Param<float<>>
,
Param<int<>>
dan Anda perlu, misalnya, untuk mengatur ulang semua parameter ke nilai default. Di sinilah Anda dapat meletakkan semuanya dalam tuple, karena tipenya berbeda dan memanggil metode
SetToDefault()
pada setiap parameter. Benar, jika ada 100 parameter seperti itu, maka ROM akan makan banyak, tetapi RAM tidak akan menderita.
PS Saya harus mengakui bahwa pada optimalisasi maksimum kode ini memiliki ukuran yang sama seperti di C dan dalam solusi saya. Dan semua upaya programmer untuk meningkatkan kode turun ke kode assembler yang sama.
P.S1 Terima kasih
0xd34df00d untuk saran yang
bagus . Anda dapat menyederhanakan membongkar tuple dengan
std::apply()
. Kode fungsi
ToggleAll()
kemudian disederhanakan menjadi:
__forceinline static inline void ToggleAll() { std::apply([](auto... args) { (args.Toggle(), ...); }, records); }
Sayangnya, di IAR, std :: apply belum diimplementasikan dalam versi saat ini, tetapi itu akan bekerja juga, lihat
implementasi dengan std :: apply