Einheitliche Fehlerbehandlung (C ++ - Option für Mikrocontroller)

Bei der Entwicklung von Software für Mikrocontroller in C ++ kann häufig festgestellt werden, dass die Verwendung der Standardbibliothek zu unerwünschten zusätzlichen Kosten für Ressourcen, sowohl RAM als auch ROM, führen kann. Daher sind die Klassen und Methoden aus der std häufig nicht für die Implementierung im Mikrocontroller geeignet. Es gibt auch einige Einschränkungen bei der Verwendung von dynamisch zugewiesenem Speicher, RTTI, Ausnahmen usw. Um kompakten und schnellen Code zu schreiben, können Sie im Allgemeinen nicht einfach die typeid verwenden und beispielsweise typeid Operatoren verwenden, da Sie RTTI-Unterstützung benötigen. Dies ist ein Overhead, wenn auch nicht sehr groß.

Daher muss man manchmal das Rad neu erfinden, um all diese Bedingungen zu erfüllen. Es gibt nur wenige solche Aufgaben, aber sie sind es. In diesem Beitrag möchte ich über eine scheinbar einfache Aufgabe sprechen - die Rückkehrcodes vorhandener Subsysteme in Mikrocontroller-Software zu erweitern.

Herausforderung


Angenommen, Sie haben ein CPU-Diagnose-Subsystem und es gibt unzählige Rückkehrcodes. Sagen Sie Folgendes:

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

Wenn das CPU-Diagnosesubsystem einen Fehler eines der CPU-Module (z. B. ALU oder RAM) feststellt, muss es den entsprechenden Code zurückgeben.

Dasselbe gilt für ein anderes Subsystem: Es handelt sich um eine Messdiagnose, bei der überprüft wird, ob der gemessene Wert im Bereich liegt und allgemein gültig ist (nicht gleich NAN oder Infinity):

 enum class Measure_Error { OutOfLimits, Ok, BadCode } ; 

GetLastError() für jedes Subsystem eine GetLastError() -Methode an, die den aufgezählten Fehlertyp dieses Subsystems zurückgibt. Für CpuDiagnostic Code vom Typ CpuDiagnostic zurückgegeben, für MeasureDiagnostic Code vom Typ Measure_Error .

Und es gibt ein bestimmtes Protokoll, das im Fehlerfall den Fehlercode protokollieren sollte.
Zum Verständnis werde ich dies in einer sehr vereinfachten Form schreiben:

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

Es ist klar, dass wir beim Konvertieren der Aufzählungstypen in eine Ganzzahl denselben Wert für verschiedene Typen erhalten können. Wie kann man unterscheiden, dass der erste Fehlercode der Fehlercode des CPU-Diagnose-Subsystems und des zweiten Mess-Subsystems ist?

Suche nach Lösungen


Es wäre logisch, GetLastError() die GetLastError() -Methode unterschiedlichen Code für unterschiedliche Subsysteme GetLastError() . Eine der direktesten Entscheidungen auf der Stirn wäre die Verwendung unterschiedlicher Codebereiche für jeden aufgezählten Typ. So etwas in der Art

 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 } ; ... 

Ich denke, die Nachteile dieses Ansatzes liegen auf der Hand. Erstens müssen Sie bei viel manueller Arbeit die Bereiche und Rückkehrcodes manuell bestimmen, was sicherlich zu einem menschlichen Fehler führen wird. Zweitens kann es viele Subsysteme geben, und das Hinzufügen von Aufzählungen für jedes Subsystem ist überhaupt keine Option.

Eigentlich wäre es großartig, wenn es möglich wäre, die Übertragungen überhaupt nicht zu berühren, ihre Codes auf eine etwas andere Weise zu erweitern, zum Beispiel, um dies tun zu können:

 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) ; } 

Oder so:

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

Oder so:

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

Wie Sie dem Code ReturnCode wird hier eine Klasse ReturnCode verwendet, die sowohl den Fehlercode als auch dessen Kategorie enthalten sollte. In der Standardbibliothek gibt es eine solche Klasse std::error_code , die eigentlich fast alles macht. Sehr gut wird sein Zweck hier beschrieben:

Dein eigener std :: code_error
Unterstützung für Systemfehler in C ++
Deterministische Ausnahmen und Fehlerbehandlung in „C ++ der Zukunft“

Die Hauptbeschwerde ist, dass wir zur Verwendung dieser Klasse std::error_category erben std::error_category , das für die Verwendung in Firmware auf kleinen Mikrocontrollern eindeutig stark überlastet ist. Sogar zumindest mit std :: string.

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

Darüber hinaus müssen Sie die Kategorie (Name und Nachricht) für jeden der aufgezählten Typen manuell beschreiben. Und auch der Code, der das Fehlen eines Fehlers in std::error_code ist 0. Und es gibt mögliche Fälle, in denen der Fehlercode für verschiedene Typen unterschiedlich ist.
Ich möchte keinen Overhead haben, außer eine Kategorienummer hinzuzufügen.

Daher wäre es logisch, etwas zu „erfinden“, das es dem Entwickler ermöglicht, ein Minimum an Bewegungen auszuführen, um eine Kategorie für seinen aufgezählten Typ hinzuzufügen.

Zuerst müssen Sie eine Klasse std::error_code , die std::error_code und in der Lage ist, jeden Aufzählungstyp in eine Ganzzahl umzuwandeln und umgekehrt von einer Ganzzahl in einen Aufzählungstyp. Zusätzlich zu diesen Funktionen, um die Kategorie, den tatsächlichen Wert des Codes zurückgeben zu können und um Folgendes überprüfen zu können:

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

Lösung


Die Klasse muss den Fehlercode, den Kategoriecode und den Code, der der Abwesenheit von Fehlern entspricht, den Umwandlungsoperator und den Zuweisungsoperator speichern. Die entsprechende Klasse lautet wie folgt:



Klassencode
 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 ; } ; 


Es ist notwendig, ein wenig zu erklären, was hier passiert. Beginnen Sie mit einem Vorlagenkonstruktor

  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, "   ") ; } 


Sie können eine Objektklasse aus einem beliebigen Aufzählungstyp erstellen:

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

Um sicherzustellen, dass der Konstruktor nur einen Aufzählungstyp akzeptieren kann, wird static_assert zu seinem Hauptteil hinzugefügt, der beim Kompilieren den an den Konstruktor übergebenen Typ mit std::is_enum und std::is_enum Fehler mit Klartext std::is_enum . Hier wird kein echter Code generiert, dies ist alles für den Compiler. Tatsächlich ist dies also ein leerer Konstruktor.

Der Konstruktor initialisiert auch private Attribute, darauf werde ich später zurückkommen ...
Als nächstes der Darsteller:

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

Es kann auch nur zu einem Aufzählungstyp führen und ermöglicht uns Folgendes:

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

Gut und getrennt der Operator bool ():

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

Auf diese Weise können wir direkt prüfen, ob der Rückkehrcode einen Fehler enthält:

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

Das ist im Wesentlichen alles. Die Frage bleibt in den Funktionen GetCategory() und GetOkCode() . Wie Sie vielleicht erraten haben, ist der erste für den Aufzählungstyp vorgesehen, um seine Kategorie irgendwie an die ReturnCode Klasse zu ReturnCode , und der zweite für den Aufzählungstyp, um anzuzeigen, dass es sich um einen guten Rückkehrcode handelt, da wir ihn mit dem Operator bool() werden.

Es ist klar, dass diese Funktionen vom Benutzer bereitgestellt werden können, und wir können sie in unserem Konstruktor über den argumentabhängigen Suchmechanismus ehrlich aufrufen.
Zum Beispiel:

 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); } 

Dies erfordert zusätzlichen Aufwand vom Entwickler. Wir benötigen für jeden Aufzählungstyp, den wir kategorisieren möchten, diese beiden Methoden hinzuzufügen und die CategoryError Aufzählung zu aktualisieren.

Wir möchten jedoch, dass der Entwickler dem Code fast nichts hinzufügt und sich nicht darum kümmert, wie er seinen Aufzählungstyp erweitern kann.
Was kann getan werden?

  • Erstens war es großartig, dass die Kategorie automatisch berechnet wurde und der Entwickler nicht für jede Aufzählung eine Implementierung der GetCategory() -Methode GetCategory() .
  • Zweitens wird in 90% der Fälle in unserem Code Ok verwendet, um guten Code zurückzugeben. Daher können Sie für diese 90% eine allgemeine Implementierung schreiben, und für 10% müssen Sie sich spezialisieren.

Konzentrieren wir uns also auf die erste Aufgabe - die automatische Kategorieberechnung. Die Idee meines Kollegen ist, dass der Entwickler seinen Aufzählungstyp registrieren kann. Dies kann mithilfe einer Vorlage mit einer variablen Anzahl von Argumenten erfolgen. Deklarieren Sie eine solche Struktur

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

Um nun eine neue Aufzählung zu registrieren, die um eine Kategorie erweitert werden soll, definieren wir einfach einen neuen Typ

 using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>; 

Wenn wir plötzlich eine weitere Aufzählung hinzufügen müssen, fügen Sie sie einfach der Liste der Vorlagenparameter hinzu:

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

Offensichtlich kann die Kategorie für unsere Auflistungen eine Position in der Liste der Vorlagenparameter sein, d.h. für Cpu_Error es 0 , für Measure_Error es 1 , für My_Error es 2 . Es bleibt, den Compiler zu zwingen, dies automatisch zu berechnen. Für C ++ 14 machen wir das:

 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...>()) ; } 

Was ist hier los? Kurz gesagt, die Funktion GetEnumPosition<T<>> , wobei der Eingabeparameter eine Liste der Aufzählungstypen von EnumTypeRegister , in unserem Fall EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> , und der Vorlagenparameter T ist ein Aufzählungstyp, dessen Index wir in dieser Liste finden sollten. läuft durch die Liste und wenn T mit einem der Typen in der Liste übereinstimmt, gibt es seinen Index zurück, andernfalls wird die Meldung "Typ ist nicht in der EnumTypeRegister-Liste registriert" angezeigt

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

Lassen Sie uns genauer analysieren. Niedrigste Funktion

 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...>()) ; } 

Hier prüft der Zweig std::enable_if_t< !std::is_same .. , ob der angeforderte Typ mit dem ersten Typ in der Vorlagenliste std::enable_if_t< !std::is_same .. Wenn nicht, GetEnumPosition der zurückgegebene Typ der GetEnumPosition Funktion tU32 und dann wird der Funktionskörper ausgeführt, tU32 ein rekursiver Aufruf derselben Funktion , während die Anzahl der Vorlagenargumente um 1 abnimmt und der Rückgabewert um 1 zunimmt. Das heißt, bei jeder Iteration wird es etwas Ähnliches geben:

 //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>) 

Sobald alle Typen in der Liste beendet sind, kann std::enable_if_t nicht mehr auf den Typ des Rückgabewerts der Funktion GetEnumPosition() , und an dieser Iteration endet:

 //         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) ; } 

Was passiert, wenn der Typ in der Liste enthalten ist? In diesem Fall funktioniert ein anderer Zweig, Zweig 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 ; } 

Hier wird std::enable_if_t< std::is_same ... ob die Typen std::enable_if_t< std::is_same ... Und wenn beispielsweise am Eingang ein Typ Measure_Error , wird die folgende Sequenz erhalten:

 //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 ; } 

Bei der zweiten Iteration endet der rekursive Funktionsaufruf und wir erhalten 1 (von der ersten Iteration) + 0 (von der zweiten) = 1 am Ausgang - dies ist ein Index vom Typ Measure_Error in der Liste EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>

Da dies eine constexpr, Funktion ist constexpr, alle Berechnungen in der Kompilierungsphase durchgeführt und es wird tatsächlich kein Code generiert.

All dies konnte nicht geschrieben werden, steht C ++ 17 zur Verfügung. Leider unterstützt mein IAR-Compiler C ++ 17 nicht vollständig, sodass das gesamte Fußtuch durch den folgenden Code ersetzt werden konnte:

 //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...>()) ; } } 

Es bleiben nun die Vorlagenmethoden GetCategory() und GetOk() , die 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); } 

Das ist alles. Mal sehen, was mit dieser Objektkonstruktion passiert:

 ReturnCode result(Measure_Error::Ok) ; 

ReturnCode wir zum Konstruktor der ReturnCode Klasse zurück

 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") ; } 

Es handelt sich um eine Vorlage, und wenn T ein Measure_Error bedeutet dies, dass die Instanziierung der GetCategory(Measure_Error) -Methodenvorlage für den Typ Measure_Error , der wiederum GetEnumPosition mit dem Typ Measure_Error , GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) Measure_Error gibt die Position von Measure_Error in der Liste zurück. Position ist 1 . Und tatsächlich wird der gesamte Konstruktorcode bei der Instanziierung des Measure_Error Typs durch den Compiler ersetzt durch:

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

Zusammenfassung


Für einen Entwickler, der ReturnCode verwenden ReturnCode gibt es nur eines:
Registrieren Sie Ihren Aufzählungstyp in der Liste.

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

Und keine unnötigen Bewegungen, der vorhandene Code bewegt sich nicht und für die Erweiterung müssen Sie nur den Typ in der Liste registrieren. Darüber hinaus wird dies alles in der Kompilierungsphase durchgeführt, und der Compiler berechnet nicht nur alle Kategorien, sondern warnt Sie auch, wenn Sie vergessen haben, den Typ zu registrieren, oder versucht haben, einen nicht nicht aufzählbaren Typ zu übergeben.

Fairerweise ist anzumerken, dass Sie in den 10% des Codes, in denen die Aufzählungen anstelle des Ok-Codes einen anderen Namen haben, Ihre eigene Spezialisierung für diesen Typ vornehmen müssen.

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

Ich habe hier ein kleines Beispiel gepostet: Codebeispiel

Im Allgemeinen ist hier eine Anwendung:

 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; } 

Drucken Sie die folgenden Zeilen:
Rückkehrcode: 3 Rückkehrkategorie: 0
Rückkehrcode: 3 Rückkehrkategorie: 2
Rückkehrcode: 2 Rückkehrkategorie: 1
mError: 1

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


All Articles