Gestion des erreurs unifiée (option C ++ pour microcontrôleurs)

Lors du développement de logiciels pour microcontrôleurs en C ++, vous pouvez très souvent rencontrer le fait que l'utilisation de la bibliothèque standard peut entraîner des coûts de ressources supplémentaires indésirables, à la fois RAM et ROM. Par conséquent, souvent les classes et les méthodes de la bibliothèque std ne sont pas tout à fait adaptées à l'implémentation dans le microcontrôleur. Il existe également des restrictions sur l'utilisation de la mémoire allouée dynamiquement, RTTI, exceptions, etc. En général, pour écrire du code compact et rapide, vous ne pouvez pas simplement prendre la bibliothèque std et commencer à utiliser, par exemple les opérateurs typeid , parce que vous avez besoin de la prise en charge RTTI, et ceci est une surcharge, bien que pas très grande.

Par conséquent, il faut parfois réinventer la roue pour remplir toutes ces conditions. Il y a peu de telles tâches, mais elles le sont. Dans cet article, je voudrais parler d'une tâche apparemment simple - étendre les codes de retour des sous-systèmes existants dans le logiciel du microcontrôleur.

Défi


Supposons que vous ayez un sous-système de diagnostic CPU et qu'il ait des codes de retour énumérables, dites ceux-ci:

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

Si le sous-système de diagnostic CPU détecte une panne de l'un des modules CPU (par exemple, ALU ou RAM), il devra renvoyer le code correspondant.

La même chose pour un autre sous-système, que ce soit un diagnostic de mesure, vérifiant que la valeur mesurée est dans la plage et qu'elle est généralement valide (non égale à NAN ou Infinity):

 enum class Measure_Error { OutOfLimits, Ok, BadCode } ; 

Pour chaque sous-système, qu'il y ait une méthode GetLastError() qui renvoie le type d'erreur énuméré de ce sous-système. Pour CpuDiagnostic code de type CpuDiagnostic sera retourné, pour MeasureDiagnostic code de type Measure_Error .

Et il y a un certain journal qui, lorsqu'une erreur se produit, devrait enregistrer le code d'erreur.
Pour comprendre, je vais écrire ceci sous une forme très simplifiée:

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

Il est clair que lors de la conversion des types énumérés en un entier, nous pouvons obtenir la même valeur pour différents types. Comment distinguer que le premier code d'erreur est le code d'erreur du sous-système de diagnostic Cpu et du deuxième sous-système de mesure?

Recherche de solutions


Il serait logique que la méthode GetLastError() renvoie un code différent pour différents sous-systèmes. L'une des décisions les plus directes sur le front serait d'utiliser différentes plages de codes pour chaque type énuméré. Quelque chose comme ça

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

Je pense que les inconvénients de cette approche sont évidents. Tout d'abord, beaucoup de travail manuel, vous devez déterminer manuellement les plages et les codes de retour, ce qui entraînera certainement une erreur humaine. Deuxièmement, il peut y avoir de nombreux sous-systèmes, et l'ajout d'énumérations pour chaque sous-système n'est pas du tout une option.

En fait, ce serait formidable s'il était possible de ne pas toucher du tout aux transferts, d'étendre leurs codes d'une manière légèrement différente, par exemple, pour pouvoir le faire:

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

Ou alors:

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

Ou alors:

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

Comme vous pouvez le voir dans le code, une classe ReturnCode est utilisée ici, qui devrait contenir à la fois le code d'erreur et sa catégorie. Dans la bibliothèque standard, il y a une telle classe std::error_code , qui fait en fait presque tout cela. Très bien son but est décrit ici:

Votre propre std :: code_error
Prise en charge des erreurs système en C ++
Exceptions déterministes et gestion des erreurs dans le «C ++ du futur»

La principale plainte est que pour utiliser cette classe, nous devons hériter de std::error_category , qui est clairement lourdement surchargé pour une utilisation dans le firmware des petits microcontrôleurs. Même au moins en utilisant std :: string.

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

De plus, vous devrez également décrire manuellement la catégorie (nom et message) pour chacun de ses types énumérés. Et aussi le code indiquant l'absence d'erreur dans std::error_code est 0. Et il y a des cas possibles où pour chaque type le code d'erreur sera différent.
J'aimerais ne pas avoir de frais généraux, sauf pour ajouter un numéro de catégorie.

Par conséquent, il serait logique d '«inventer» quelque chose qui permettrait au développeur d'effectuer un minimum de mouvements en termes d'ajout d'une catégorie pour son type énuméré.

Vous devez d'abord créer une classe similaire à std::error_code , capable de convertir n'importe quel type énuméré en entier et vice versa d'un entier en un type énuméré. De plus à ces fonctionnalités, afin de pouvoir retourner la catégorie, la valeur réelle du code, ainsi que de pouvoir vérifier:

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

Solution


La classe doit stocker en elle-même un code d'erreur, un code de catégorie et un code correspondant à l'absence d'erreurs, un opérateur de transtypage et un opérateur d'affectation. La classe correspondante est la suivante:



Code de classe
 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 ; } ; 


Il faut expliquer un peu ce qui se passe ici. Pour commencer avec un constructeur de modèle

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


Il vous permet de créer une classe d'objets à partir de n'importe quel type énuméré:

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

Pour garantir que le constructeur ne peut accepter qu'un type énuméré, static_assert ajouté à son corps, qui lors de la compilation vérifiera le type transmis au constructeur à l'aide de std::is_enum et std::is_enum erreur avec du texte clair. Le vrai code n'est pas généré ici, c'est tout pour le compilateur. Donc, en fait, c'est un constructeur vide.

Le constructeur initialise également les attributs privés, j'y reviendrai plus tard ...
De plus, l'opérateur de distribution:

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

Il peut également conduire uniquement à un type énuméré et nous permet d'effectuer les opérations suivantes:

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

Eh bien et séparément l'opérateur bool ():

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

Cela nous permettra de vérifier directement s'il y a une erreur dans le code retour:

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

C'est essentiellement tout. La question reste dans les fonctions GetCategory() et GetOkCode() . Comme vous pouvez le deviner, le premier est destiné au type énuméré pour communiquer en quelque sorte sa catégorie à la classe ReturnCode , et le second au type énuméré pour indiquer qu'il s'agit d'un bon code retour, car nous allons le comparer avec l'opérateur bool() .

Il est clair que ces fonctions peuvent être fournies par l'utilisateur, et nous pouvons honnêtement les appeler dans notre constructeur via le mécanisme de recherche dépendant de l'argument.
Par exemple:

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

Cela nécessite un effort supplémentaire de la part du développeur. Nous avons besoin pour chaque type énuméré que nous voulons classer pour ajouter ces deux méthodes et mettre à jour l'énumération CategoryError .

Cependant, notre souhait est que le développeur n'ajoute presque rien au code et ne se soucie pas de la façon d'étendre son type énuméré.
Que peut-on faire.

  • Tout d'abord, c'était formidable que la catégorie soit calculée automatiquement, et le développeur n'aurait pas à fournir une implémentation de la méthode GetCategory() pour chaque énumération.
  • Deuxièmement, dans 90% des cas dans notre code, Ok est utilisé pour renvoyer un bon code. Par conséquent, vous pouvez écrire une implémentation générale pour ces 90%, et pour 10%, vous devrez faire une spécialisation.

Concentrons-nous donc sur la première tâche - le calcul automatique de catégorie. L'idée suggérée par mon collègue est que le développeur devrait pouvoir enregistrer son type énuméré. Cela peut être fait en utilisant un modèle avec un nombre variable d'arguments. Déclarer une telle structure

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

Maintenant, pour enregistrer une nouvelle énumération, qui devrait être développée par une catégorie, nous définissons simplement un nouveau type

 using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>; 

Si, soudain, nous devons ajouter une autre énumération, ajoutez-la simplement à la liste des paramètres du modèle:

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

De toute évidence, la catégorie de nos listes peut être une position dans la liste des paramètres du modèle, c'est-à-dire pour Cpu_Error c'est 0 , pour Measure_Error c'est 1 , pour My_Error c'est 2 . Il reste à forcer le compilateur à calculer cela automatiquement. Pour C ++ 14, nous faisons ceci:

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

Que se passe-t-il ici. En bref, la fonction GetEnumPosition<T<>> , avec le paramètre d'entrée étant une liste de types énumérés EnumTypeRegister , dans notre cas EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> , et le paramètre de modèle T est un type énuméré dont l'index doit être trouvé dans cette liste, parcourt la liste et si T correspond à l'un des types de la liste, retourne son index, sinon le message "Type n'est pas enregistré dans la liste EnumTypeRegister" s'affiche

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

Analysons plus en détail. Fonction la plus basse

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

Ici, la branche std::enable_if_t< !std::is_same .. vérifie si le type demandé correspond au premier type dans la liste des modèles, sinon, le type retourné de la fonction GetEnumPosition sera tU32 puis le corps de la fonction est exécuté, à savoir, un appel récursif de la même fonction à nouveau , tandis que le nombre d'arguments de modèle diminue de 1 et la valeur de retour augmente de 1 . Autrement dit, à chaque itération, il y aura quelque chose de similaire à ceci:

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

Une fois tous les types de la liste terminés, std::enable_if_t ne pourra pas déduire le type de la valeur de retour de la fonction GetEnumPosition() , et à cette itération se terminera:

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

Que se passe-t-il si le type est sur la liste. Dans ce cas, une autre branche fonctionnera, la branche 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 ; } 

Ici, il y a une vérification de la coïncidence des types std::enable_if_t< std::is_same ... Et si, disons à l'entrée il y a un type Measure_Error , alors la séquence suivante sera obtenue:

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

À la deuxième itération, l'appel de fonction récursive se termine et nous obtenons 1 (à partir de la première itération) + 0 (à partir de la seconde) = 1 à la sortie - il s'agit d'un index de type Measure_Error dans la liste EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>

Comme il s'agit d'une fonction constexpr, tous les calculs sont effectués au stade de la compilation et aucun code n'est réellement généré.

Tout cela ne pouvait pas être écrit, être à la disposition de C ++ 17. Malheureusement, mon compilateur IAR ne prend pas entièrement en charge C ++ 17, et il a donc été possible de remplacer l'intégralité de la footcloth par le code suivant:

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

Il reste maintenant à créer les méthodes de modèle GetCategory() et GetOk() , qui appellent 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); } 

C’est tout. Voyons maintenant ce qui se passe avec cette construction d'objet:

 ReturnCode result(Measure_Error::Ok) ; 

Revenons au constructeur de la classe 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") ; } 

Il s'agit d'un modèle, et si T est une Measure_Error ce qui signifie que l'instanciation du modèle de méthode GetCategory(Measure_Error) est appelée, pour le type Measure_Error , qui à son tour appelle GetEnumPosition avec le type Measure_Error , GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) qui renvoie la position de Measure_Error dans la liste. La position est 1 . Et en fait, le code constructeur entier à l'instanciation du type Measure_Error remplacé par le compilateur avec:

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

Résumé


Pour un développeur qui ReturnCode utiliser ReturnCode il n'y a qu'une seule chose à faire:
Enregistrez votre type énuméré dans la liste.

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

Et pas de mouvements inutiles, le code existant ne bouge pas, et pour l'extension il vous suffit d'enregistrer le type dans la liste. De plus, tout cela se fera au stade de la compilation, et le compilateur calculera non seulement toutes les catégories, mais vous avertira également si vous avez oublié d'enregistrer le type, ou si vous avez essayé de passer un type qui n'est pas non énumérable.

En toute honnêteté, il convient de noter que dans ces 10% du code où les énumérations ont un nom différent au lieu du code Ok, vous devrez faire votre propre spécialisation pour ce type.

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

J'ai posté un petit exemple ici: exemple de code

En général, voici une application:

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

Imprimez les lignes suivantes:
Code retour: 3 Catégorie retour: 0
Code retour: 3 Catégorie de retour: 2
Code retour: 2 Catégorie de retour: 1
mError: 1

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


All Articles