Fig. I. KiykoKesehatan 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;
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() {
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() {
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;
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) ;
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:
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) ;
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) ;
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>;
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 ;
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) ;
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}} ; } }
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 3Membuat hal-hal dilakukan- Struktur overlay