Penanganan kesalahan terpadu (opsi C ++ untuk mikrokontroler)

Ketika mengembangkan perangkat lunak untuk mikrokontroler di C ++, sangat sering terjadi fakta bahwa menggunakan pustaka standar dapat menyebabkan biaya sumber daya tambahan yang tidak diinginkan, baik RAM maupun ROM. Oleh karena itu, seringkali kelas dan metode dari perpustakaan std tidak cukup cocok untuk implementasi di mikrokontroler. Ada juga beberapa pembatasan penggunaan memori yang dialokasikan secara dinamis, RTTI, pengecualian, dan sebagainya. Secara umum, untuk menulis kode yang ringkas dan cepat, Anda tidak bisa hanya mengambil pustaka std dan mulai menggunakan, katakan operator typeid , karena Anda memerlukan dukungan RTTI, dan ini merupakan biaya overhead, walaupun tidak terlalu besar.

Karena itu, terkadang Anda harus menemukan kembali roda untuk memenuhi semua kondisi ini. Ada beberapa tugas seperti itu, tetapi ada. Dalam posting ini, saya ingin berbicara tentang tugas yang tampaknya sederhana - untuk memperluas kode pengembalian subsistem yang ada dalam perangkat lunak mikrokontroler.

Tantangan


Misalkan Anda memiliki subsistem diagnostik CPU dan memiliki kode pengembalian enumerable, ucapkan ini:

 enum class Cpu_Error { Ok, Alu, Rom, Ram } ; 

Jika subsistem diagnostik CPU mendeteksi kegagalan salah satu modul CPU, (misalnya, ALU atau RAM), ia harus mengembalikan kode yang sesuai.

Hal yang sama untuk subsistem lain, biarlah itu menjadi diagnosis pengukuran, memeriksa bahwa nilai yang diukur berada dalam kisaran dan secara umum valid (tidak sama dengan NAN atau Infinity):

 enum class Measure_Error { OutOfLimits, Ok, BadCode } ; 

Untuk setiap subsistem, biarkan ada metode GetLastError() yang mengembalikan tipe kesalahan yang disebutkan pada subsistem ini. Untuk CpuDiagnostic kode tipe CpuDiagnostic akan dikembalikan, untuk MeasureDiagnostic kode tipe Measure_Error .

Dan ada log tertentu yang, ketika terjadi kesalahan, harus mencatat kode kesalahan.
Untuk memahami, saya akan menulis ini dalam bentuk yang sangat sederhana:

 void Logger::Update() { Log(static_cast<uint32_t>(cpuDiagnostic.GetLastError()) ; Log(static_cast<uint32_t>(measureDiagstic.GetLastError()) ; } 

Jelas bahwa ketika mengkonversi tipe yang disebutkan ke integer, kita bisa mendapatkan nilai yang sama untuk tipe yang berbeda. Bagaimana membedakan bahwa kode kesalahan pertama adalah kode kesalahan dari subsistem diagnostik Cpu, dan subsistem pengukuran kedua?

Cari solusi


Akan logis untuk metode GetLastError() untuk mengembalikan kode yang berbeda untuk subsistem yang berbeda. Salah satu keputusan paling langsung di dahi adalah menggunakan rentang kode yang berbeda untuk setiap jenis yang disebutkan. Sesuatu seperti ini

 constexpr tU32 CPU_ERROR_ALU = 0x10000001 ; constexpr tU32 CPU_ERROR_ROM = 0x10000002 ; ... constexpr tU32 MEAS_ERROR_OUTOF = 0x01000001 ; constexpr tU32 MEAS_ERROR_BAD = 0x01000002 ; ... enum class Cpu_Error { Ok, Alu = CPU_ERROR_ALU, Rom = CPU_ERROR_ROM, Ram = CPU_ERROR_RAM } ; ... 

Saya pikir kerugian dari pendekatan ini jelas. Pertama, banyak pekerjaan manual, Anda harus secara manual menentukan rentang dan mengembalikan kode, yang tentunya akan menyebabkan kesalahan manusia. Kedua, mungkin ada banyak subsistem, dan menambahkan enumerasi untuk setiap subsistem bukanlah pilihan sama sekali.

Sebenarnya, akan lebih bagus jika memungkinkan untuk tidak menyentuh transfer sama sekali, untuk memperluas kode mereka dengan cara yang sedikit berbeda, misalnya, untuk dapat melakukan ini:

 ResultCode result = Cpu_Error::Ok ; //GetLastError()   Cpu_Error result = cpuDiagnostic.GetLastError() ; if(result) //    { //       Logger::Log(result) ; } //GetLastError()   Measure_Error result = measureDiagnostic.GetLastError() ; if(result) //    { //       Logger::Log(result) ; } 

Atau lebih:

 ReturnCode result ; for(auto it: diagnostics) { //GetLastError()     result = it.GetLastError() ; if (result) //    { Logger::Log(result) ; //      } } 

Atau lebih:

 void CpuDiagnostic::SomeFunction(ReturnCode errocode) { Cpu_Error status = errorcode ; switch (status) { case CpuError::Alu: // do something ; break; .... } } 

Seperti yang Anda lihat dari kode, beberapa kelas ReturnCode digunakan di sini, yang harus mengandung kode kesalahan dan kategorinya. Di perpustakaan standar ada kelas seperti std::error_code , yang sebenarnya melakukan hampir semua ini. Sangat baik tujuannya dijelaskan di sini:

Std :: code_error Anda sendiri
Dukungan untuk kesalahan sistem di C ++
Pengecualian deterministik dan penanganan kesalahan dalam “C ++ of the future”

Keluhan utama adalah bahwa untuk menggunakan kelas ini, kita perlu mewarisi std::error_category , yang jelas kelebihan beban untuk digunakan dalam firmware pada mikrokontroler kecil. Bahkan setidaknya menggunakan std :: string.

 class CpuErrorCategory: public std::error_category { public: virtual const char * name() const; virtual std::string message(int ev) const; }; 

Selain itu, Anda juga harus mendeskripsikan kategori (nama dan pesan) untuk masing-masing jenis yang disebutkan secara manual. Dan juga kode yang menunjukkan tidak adanya kesalahan dalam std::error_code adalah 0. Dan ada beberapa kasus ketika untuk setiap jenis kode kesalahan akan berbeda.
Saya ingin tidak memiliki overhead kecuali untuk menambahkan nomor kategori.

Oleh karena itu, akan logis untuk "menciptakan" sesuatu yang akan memungkinkan pengembang untuk membuat gerakan minimum dalam hal menambahkan kategori untuk tipe yang disebutkan.

Pertama, Anda perlu membuat kelas yang mirip dengan std::error_code , dapat mengkonversi tipe enumerasi menjadi integer dan sebaliknya dari integer ke tipe enumerated. Plus untuk fitur-fitur ini, agar dapat mengembalikan kategori, nilai kode yang sebenarnya, serta dapat memeriksa:

 //GetLastError()   CpuError ReturnCode result(cpuDiagnostic.GetLastError()) ; if(result) //    { ... } 

Solusi


Kelas harus menyimpan sendiri kode kesalahan, kode kategori dan kode yang sesuai dengan tidak adanya kesalahan, operator transmisi, dan operator penugasan. Kelas yang sesuai adalah sebagai berikut:



Kode kelas
 class ReturnCode { public: ReturnCode() { } template<class T> explicit ReturnCode(const T initReturnCode): errorValue(static_cast<tU32>(initReturnCode)), errorCategory(GetCategory(initReturnCode)), goodCode(GetOk(initReturnCode)) { static_assert(std::is_enum<T>::value, "   ") ; } template<class T> operator T() const { //Cast to only enum types static_assert(std::is_enum<T>::value, "   ") ; return static_cast<T>(errorValue) ; } tU32 GetValue() const { return errorValue; } tU32 GetCategoryValue() const { return errorCategory; } operator bool() const { return (GetValue() != goodCode); } template<class T> ReturnCode& operator=(const T returnCode) { errorValue = static_cast<tU32>(returnCode) ; errorCategory = GetCategory(returnCode) ; goodCode = GetOk(returnCode) ; return *this ; } private: tU32 errorValue = 0U ; tU32 errorCategory = 0U ; tU32 goodCode = 0U ; } ; 


Perlu dijelaskan sedikit apa yang terjadi di sini. Untuk memulai dengan konstruktor template

  template<class T> explicit ReturnCode(const T initReturnCode): errorValue(static_cast<tU32>(initReturnCode)), errorCategory(GetCategory(initReturnCode)), goodCode(GetOk(initReturnCode)) { static_assert(std::is_enum<T>::value, "   ") ; } 


Ini memungkinkan Anda untuk membuat kelas objek dari jenis apa pun yang disebutkan:

 ReturnCode result(Cpu_Error::Ok) ; ReturnCode result1(My_Error::Error1); ReturnCode result2(cpuDiagnostic.GetLatestError()) ; 

Untuk memastikan bahwa konstruktor hanya dapat menerima tipe yang disebutkan, static_assert ditambahkan ke badannya, yang pada waktu kompilasi akan memeriksa tipe yang diteruskan ke konstruktor menggunakan std::is_enum dan std::is_enum kesalahan dengan teks yang jelas. Kode sebenarnya tidak dihasilkan di sini, ini semua untuk kompiler. Jadi sebenarnya ini adalah konstruktor kosong.

Konstruktor juga menginisialisasi atribut pribadi, saya akan kembali lagi nanti ...
Selanjutnya, operator pemeran:

 template<class T> operator T() const { //Cast to only enum types static_assert(std::is_enum<T>::value, "   ") ; return static_cast<T>(errorValue) ; } 

Itu juga dapat mengarah hanya ke tipe yang disebutkan dan memungkinkan kita untuk melakukan hal berikut:

  ReturnCode returnCode(Cpu_Error::Rom) ; Cpu_Error status = errorCode ; returnCode = My_Errror::Error2; My_Errror status1 = returnCode ; returnCode = myDiagnostic.GetLastError() ; MyDiagsonticError status2 = returnCode ; 

Baik dan secara terpisah operator bool ():

  operator bool() const { return (GetValue() != goodCode); } 

Ini akan memungkinkan kami untuk secara langsung memeriksa apakah ada kesalahan dalam kode pengembalian:

 //GetLastError()   Cpu_Error ReturnCode result(cpuDiagnostic.GetLastError()) ; if(result) //    { ... } 

Ini pada dasarnya semua. Pertanyaannya tetap pada fungsi GetCategory() dan GetOkCode() . Seperti yang Anda tebak, yang pertama ditujukan untuk tipe enumerasi untuk berkomunikasi entah bagaimana kategorinya ke kelas ReturnCode , dan yang kedua untuk tipe enumerasi untuk menunjukkan bahwa itu adalah kode pengembalian yang baik, karena kita akan membandingkannya dengan operator bool() .

Jelas bahwa fungsi-fungsi ini dapat disediakan oleh pengguna, dan kami dapat dengan jujur ​​menyebutnya di konstruktor kami melalui mekanisme pencarian yang bergantung pada argumen.
Sebagai contoh:

 enum class CategoryError { Nv = 100, Cpu = 200 }; enum class Cpu_Error { Ok, Alu, Rom } ; inline tU32 GetCategory(Cpu_Error errorNum) { return static_cast<tU32>(CategoryError::Cpu); } inline tU32 GetOkCode(Cpu_Error) { return static_cast<tU32>(Cpu_Error::Ok); } 

Ini membutuhkan upaya tambahan dari pengembang. Kita perlu untuk setiap tipe yang disebutkan yang ingin kita kategorikan untuk menambahkan dua metode ini dan memperbarui enumerasi CategoryError .

Namun, keinginan kami adalah agar pengembang hampir tidak menambahkan apa pun pada kode dan tidak peduli tentang cara memperluas tipe yang disebutkan.
Apa yang bisa dilakukan.

  • Pertama, itu bagus untuk kategori yang akan dihitung secara otomatis, dan pengembang tidak harus menyediakan implementasi metode GetCategory() untuk setiap enumerasi.
  • Kedua, dalam 90% kasus dalam kode kami, Ok digunakan untuk mengembalikan kode yang baik. Oleh karena itu, Anda dapat menulis implementasi umum untuk 90% ini, dan untuk 10% Anda harus melakukan spesialisasi.

Jadi, mari kita berkonsentrasi pada tugas pertama - perhitungan kategori otomatis. Gagasan yang disarankan oleh kolega saya adalah bahwa pengembang harus dapat mendaftarkan tipenya. Ini dapat dilakukan dengan menggunakan template dengan sejumlah variabel argumen. Nyatakan struktur seperti itu

 template <typename... Types> struct EnumTypeRegister{}; //     

Sekarang untuk mendaftarkan enumerasi baru, yang harus diperluas oleh suatu kategori, kita cukup mendefinisikan tipe baru

 using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>; 

Jika tiba-tiba kita perlu menambahkan enumerasi lain, maka cukup tambahkan ke daftar parameter template:

 using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>; 

Jelas, kategori untuk cantuman kami mungkin merupakan posisi dalam daftar parameter templat, mis. untuk Cpu_Error adalah 0 , untuk Measure_Error adalah 1 , untuk My_Error adalah 2 . Tetap memaksa kompiler untuk menghitung ini secara otomatis. Untuk C ++ 14, kami melakukan ini:

 template <typename QueriedType, typename Type> constexpr tU32 GetEnumPosition(EnumTypeRegister<Type>) { static_assert(std::is_same<Type, QueriedType>::value, "     EnumTypeRegister"); return tU32(0U) ; } template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(EnumTypeRegister<Type, Types...>) { return 0U ; } template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<!std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(EnumTypeRegister<Type, Types...>) { return 1U + GetEnumPosition<QueriedType>(EnumTypeRegister<Types...>()) ; } 

Apa yang sedang terjadi di sini. Singkatnya, fungsi GetEnumPosition<T<>> , dengan parameter input menjadi daftar tipe EnumTypeRegister enumerasi EnumTypeRegister , dalam kasus kami EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> , dan parameter template T adalah tipe enumerasi yang indeksnya harus kita temukan dalam daftar ini, berjalan melalui daftar dan jika T cocok dengan salah satu jenis dalam daftar, mengembalikan indeksnya, jika tidak, pesan "Jenis tidak terdaftar dalam daftar EnumTypeRegister" ditampilkan

 //..    constexpr EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> list //  GetEnumPosition<Measure_Error>(list) //   1 -    Measure_Error   . 

Mari kita analisa lebih detail. Fungsi terendah

 template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<!std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(TypeRegister<Type, Types...>) { return 1U + GetEnumPosition<QueriedType>(TypeRegister<Types...>()) ; } 

Di sini cabang std::enable_if_t< !std::is_same .. memeriksa apakah tipe yang diminta cocok dengan tipe pertama dalam daftar templat, jika tidak, maka jenis fungsi GetEnumPosition yang GetEnumPosition akan tU32 dan kemudian badan fungsi dijalankan, yaitu, panggilan rekursif dari fungsi yang sama lagi. , sementara jumlah argumen templat menurun sebesar 1 , dan nilai kembali meningkat sebesar 1 . Artinya, pada setiap iterasi akan ada sesuatu yang mirip dengan ini:

 //Iteration 1, 1+: tU32 GetEnumPosition<T>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) //Iteration 2, 1+1+: tU32 GetEnumPosition<T>(EnumTypeRegister<Measure_Error, My_Error>) //Iteration 3, 1+1+1: tU32 GetEnumPosition<T>(EnumTypeRegister<My_Error>) 

Setelah semua tipe dalam daftar berakhir, std::enable_if_t tidak akan dapat menyimpulkan jenis nilai pengembalian fungsi GetEnumPosition() , dan pada iterasi ini akan berakhir:

 //         GetEnumPosition<T>(TypeRegister<>) template <typename QueriedType, typename Type> constexpr tU32 GetEnumPosition(EnumTypeRegister<Type>) { static_assert(std::is_same<Type, QueriedType>::value, "     EnumTypeRegister"); return tU32(0U) ; } 

Apa yang terjadi jika jenisnya ada di daftar. Dalam hal ini, cabang lain akan berfungsi, cabang c std::enable_if_t< std::is_same .. :

 template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(TypeRegister<Type, Types...>) { return 0U ; } 

Di sini ada pemeriksaan untuk kebetulan jenis std::enable_if_t< std::is_same ... Dan jika, katakan pada input ada jenis Measure_Error , maka urutan berikut akan diperoleh:

 //Iteration 1, tU32 GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) { return 1U + GetEnumPosition<Measure_Error>(EnumTypeRegister<Measure_Error, My_Error>) } //Iteration 2: tU32 GetEnumPosition<Measure_Error>(EnumTypeRegister<Measure_Error, My_Error>) { return 0 ; } 

Pada iterasi kedua, panggilan fungsi rekursif berakhir dan kami mendapatkan 1 (dari iterasi pertama) + 0 (dari yang kedua) = 1 pada output - ini adalah indeks tipe Measure_Error dalam daftar EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>

Karena ini adalah fungsi constexpr, semua perhitungan dilakukan pada tahap kompilasi dan tidak ada kode yang benar-benar dihasilkan.

Semua ini tidak dapat ditulis, tersedia C ++ 17. Sayangnya, kompiler IAR saya tidak sepenuhnya mendukung C ++ 17, dan jadi mungkin untuk mengganti seluruh footcloth dengan kode berikut:

 //for C++17 template <typename QueriedType, typename Type, typename... Types> constexpr tU32 GetEnumPosition(EnumTypeRegister<Type, Types...>) { //        if constexpr (std::is_same<Type, QueriedType>::value) { return 0U ; } else { return 1U + GetEnumPosition<QueriedType>(EnumTypeRegister<Types...>()) ; } } 

Tetap sekarang untuk membuat metode templat GetCategory() dan GetOk() , yang akan memanggil GetEnumPosition .

 template<typename T> constexpr tU32 GetCategory(const T) { return static_cast<tU32>(GetEnumPosition<T>(categoryDictionary)); } template<typename T> constexpr tU32 GetOk(const T) { return static_cast<tU32>(T::Ok); } 

Itu saja. Sekarang mari kita lihat apa yang terjadi dengan konstruksi objek ini:

 ReturnCode result(Measure_Error::Ok) ; 

Mari kita kembali ke konstruktor dari kelas ReturnCode

 template<class T> explicit ReturnCode(const T initReturnCode): errorValue(static_cast<tU32>(initReturnCode)), errorCategory(GetCategory(initReturnCode)), goodCode(GetOk(initReturnCode)) { static_assert(std::is_enum<T>::value, "The type have to be enum") ; } 

Ini adalah templat satu, dan jika T adalah Measure_Error yang berarti bahwa instantiasi dari GetCategory(Measure_Error) metode GetCategory(Measure_Error) dipanggil, untuk jenis Measure_Error , yang pada gilirannya memanggil GetEnumPosition dengan jenis Measure_Error , GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) yang mengembalikan posisi Measure_Error dalam daftar. Posisi adalah 1 . Dan sebenarnya seluruh kode konstruktor pada instantiation tipe Measure_Error digantikan oleh kompiler dengan:

 explicit ReturnCode(const Measure_Error initReturnCode): errorValue(1), errorCategory(1), goodCode(1) { } 

Ringkasan


Untuk pengembang yang ReturnCode menggunakan ReturnCode hanya ada satu hal yang harus dilakukan:
Daftarkan jenis Anda yang disebutkan dalam daftar.

 // Add enum in the category using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>; 

Dan tidak ada gerakan yang tidak perlu, kode yang ada tidak bergerak, dan untuk ekstensi Anda hanya perlu mendaftarkan jenis dalam daftar. Selain itu, semua ini akan dilakukan pada tahap kompilasi, dan kompiler tidak hanya akan menghitung semua kategori, tetapi juga memperingatkan Anda jika Anda lupa untuk mendaftarkan tipe, atau mencoba untuk melewati tipe yang tidak dapat dihitung.

Dalam keadilan, perlu dicatat bahwa dalam 10% dari kode di mana enumerasi memiliki nama yang berbeda dan bukan kode Ok, Anda harus membuat spesialisasi Anda sendiri untuk jenis ini.

 template<> constexpr tU32 GetOk<MyError>(const MyError) { return static_cast<tU32>(MyError::Good) ; } ; 

Saya memposting contoh kecil di sini: contoh kode

Secara umum, ini adalah aplikasi:

 enum class Cpu_Error { Ok, Alu, Rom, Ram } ; enum class Measure_Error { OutOfLimits, Ok, BadCode } ; enum class My_Error { Error1, Error2, Error3, Error4, Ok } ; // Add enum in the category list using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>; Cpu_Error CpuCheck() { return Cpu_Error::Ram; } My_Error MyCheck() { return My_Error::Error4; } int main() { ReturnCode result(CpuCheck()); //cout << " Return code: "<< result.GetValue() // << " Return category: "<< result.GetCategoryValue() << endl; if (result) //if something wrong { result = MyCheck() ; // cout << " Return code: "<< result.GetValue() // << " Return category: "<< result.GetCategoryValue() << endl; } result = Measure_Error::BadCode ; //cout << " Return code: "<< result.GetValue() // << " Return category: "<< result.GetCategoryValue() << endl; result = Measure_Error::Ok ; if (!result) //if all is Ok { Measure_Error mError = result ; if (mError == Measure_Error::Ok) { // cout << "mError: "<< tU32(mError) << endl; } } return 0; } 

Cetak baris berikut:
Kode pengembalian: 3 Kategori pengembalian: 0
Kode pengembalian: 3 Kategori pengembalian: 2
Kode pengembalian: 2 Kategori pengembalian: 1
mError: 1

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


All Articles