Unified error handling (C ++ option for microcontrollers)

    When developing software for microcontrollers in C ++, very often you may encounter the fact that using the standard library can lead to undesirable additional resource costs, both RAM and ROM. Therefore, often classes and methods from the library are stdnot quite suitable for implementation in the microcontroller. There are also some restrictions on the use of dynamically allocated memory, RTTI, exceptions, and so on. In general, in order to write compact and fast code, you can’t just take the library stdand start using, say, type operators typeid, because RTTI support is needed, and this is already an overhead, although not very large.

    Therefore, sometimes you have to reinvent the wheel to fulfill all these conditions. There are few such tasks, but they are. In this post, I would like to talk about a seemingly simple task - to expand the return codes of existing subsystems in the software for the microcontroller.

    Task


    Suppose you have a CPU diagnostic subsystem and it has enumerated return codes, say these:

    enumclassCpu_Error
    {
        Ok,
        Alu, 
        Rom,
        Ram
    } ;
    

    If the CPU diagnostic subsystem detects a failure of one of the CPU modules, (for example, ALU or RAM), it will have to return the corresponding code.

    The same thing for another subsystem, let it be a measurement diagnosis, checking that the measured value is in the range and it is generally valid (not equal to NAN or Infinity):

    enumclassMeasure_Error
    {
        OutOfLimits,
        Ok,
        BadCode
    } ;

    For each subsystem, let there be a method that GetLastError()returns the enumerated error type of this subsystem. For CpuDiagnostictype code will be returned Cpu_Error, for MeasureDiagnostictype code Measure_Error.

    And there is a certain log which, when an error occurs, should log the error code.
    For understanding, I will write this in a very simplified form:

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

    It is clear that when converting the enumerated types to an integer, we can get the same value for different types. How to distinguish that the first error code is the error code of the Cpu diagnostic subsystem, and the second measurement subsystem?

    Searching of decisions


    It would be logical for the method to GetLastError()return different code for different subsystems. One of the most direct decisions in the forehead would be to use different ranges of codes for each enumerated type. Something like this

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

    I think the disadvantages of this approach are obvious. Firstly, a lot of manual work, you need to manually determine the ranges and return codes, which will certainly lead to a human error. Secondly, there can be many subsystems, and adding enumerations for each subsystem is not an option at all.

    Actually, it would be great if it were possible not to touch the transfers at all, to expand their codes in a slightly different way, for example, to be able to do this:

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

    Or so:

    ReturnCode result ;
    for(auto it: diagnostics) 
    {
      //GetLastError()  возвращает перечисление подсистемы диагностики 
      result = it.GetLastError() ; 
      if (result)  //проверяем были ли ошибки
      {
        Logger::Log(result) ; //логируем и код и категорию кода
      }
    }
    

    Or so:

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

    As you can see from the code, a certain class is used here ReturnCode, which should contain both the error code and its category. In the standard library there is a class std::error_codethat actually does all this. Its purpose is described very well here:

    your own std :: code_error
    Support for system errors in C ++
    Deterministic exceptions and error handling in “C ++ of the future”

    The main complaint is that in order to use this class, we need to inherit std::error_categoryone that is clearly overloaded to use in firmware on small microcontrollers. Even at least using std :: string.

    classCpuErrorCategory:publicstd::error_category
    {
    public:
        virtualconstchar * name()const;
        virtualstd::stringmessage(int ev)const;
    };
    

    In addition, you will also have to describe the category (name and message) for each of its enumerated types manually. And also the code meaning the absence of an error in std::error_codeis 0. And there are possible cases when for different types the error code will be different.
    I would like there to be no overhead except for adding a category number.

    Therefore, it would be logical to “invent” something that would allow the developer to make a minimum of movements in terms of adding a category for his enumerated type.

    First you need to make a class similar to one std::error_codethat can convert any enumerated type into an integer and vice versa from an integer to an enumerated type. Plus to these features, in order to be able to return the category, the actual value of the code, as well as be able to check:

    //GetLastError()  возвращает перечисление CpuErrorReturnCode result(cpuDiagnostic.GetLastError()); 
    if(result)  //проверяем были ли ошибки
    {
      ...
    }
    

    Decision


    The class must store in itself an error code, a category code and a code corresponding to the absence of errors, a cast operator, and an assignment operator. The corresponding class is as follows:



    Class code
    classReturnCode
    {public:
        ReturnCode() 
        {  
        } 
        template<classT> 
        explicitReturnCode(constTinitReturnCode): 
                                        errorValue(static_cast<tU32>(initReturnCode)),
                                        errorCategory(GetCategory(initReturnCode)),
                                        goodCode(GetOk(initReturnCode))                              
        {      
            static_assert(std::is_enum<T>::value, "Тип должен быть перечисляемым") ;
        }
        template<classT>     
        operatorT() const 
        {//Cast to only enum typesstatic_assert(std::is_enum<T>::value, "Тип должен быть перечисляемым") ;
          returnstatic_cast<T>(errorValue) ;
        }
        tU32 GetValue()const{ 
          return errorValue;
        }
        tU32 GetCategoryValue()const{  
          return errorCategory;
        }
        operatorbool()const{       
          return (GetValue() != goodCode);
        }
       template<classT> 
       ReturnCode& operator=(constTreturnCode) 
       {    
         errorValue = static_cast<tU32>(returnCode) ;
         errorCategory = GetCategory(returnCode) ;  
         goodCode = GetOk(returnCode) ;
         return *this ;      
       }
      private:    
        tU32 errorValue = 0U ;    
        tU32 errorCategory = 0U ; 
        tU32 goodCode = 0U ;   
    } ;
    


    It is necessary to explain a little what is happening here. To begin with a template constructor

    template<classT>
        explicitReturnCode(constTinitReturnCode): 
                                        errorValue(static_cast<tU32>(initReturnCode)),
                                        errorCategory(GetCategory(initReturnCode)),
                                        goodCode(GetOk(initReturnCode))                              
        {      
            static_assert(std::is_enum<T>::value, "Тип должен быть перечисляемым") ;
        }
    


    It allows you to create an object class from any enumerated type:

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

    In order for the constructor to accept only the enumerated type, it is added to its body static_assert, which at the compilation stage will check the type passed to the constructor with the help of std::is_enumand will produce an error with clear text. No real code is generated here, this is all for the compiler. So in fact this is an empty constructor.

    The constructor also initializes private attributes, I will return to this later ...
    Next, the cast operator:

    template<classT>     
        operatorT() const 
        {//Cast to only enum typesstatic_assert(std::is_enum<T>::value, "Тип должен быть перечисляемым") ;
          returnstatic_cast<T>(errorValue) ;
        }
    

    It can also lead only to an enumerated type and allows us to do the following:

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

    Well and separately the bool () operator:

    operatorbool()const{       
          return (GetValue() != goodCode);
        }
    

    Allows us to directly check if there is any error in the return code:

    //GetLastError()  возвращает перечисление Cpu_ErrorReturnCode result(cpuDiagnostic.GetLastError()); 
    if(result)  //проверяем были ли ошибки
    {
      ...
    }
    

    This is essentially all. The question remains in the functions GetCategory()and GetOkCode(). As you might guess, the first is intended for the enumerated type to somehow communicate its category to the class ReturnCode, and the second for the enumerated type to indicate that it is a successful return code, since we are going to compare with it in the operator bool().

    It is clear that these functions can be provided by the user, and we can honestly call them in our constructor through the argument-dependent search mechanism.
    For example:

    enumclassCategoryError
    {
      Nv = 100,  
      Cpu =  200 
    };
    enumclassCpu_Error
    {
      Ok,
      Alu, 
      Rom
    } ;
    inline tU32 GetCategory(Cpu_Error errorNum){
      returnstatic_cast<tU32>(CategoryError::Cpu); 
    }
    inline tU32 GetOkCode(Cpu_Error){
      returnstatic_cast<tU32>(Cpu_Error::Ok); 
    }
    

    This requires additional effort from the developer. It is necessary for each enumerated type that we want to categorize to add these two methods and update the CategoryErrorenumeration.

    However, our desire is for the developer to add almost nothing to the code and not bother about how to expand his enumerated type.
    What can be done.

    • First, it was great for the category to be calculated automatically, and the developer would not have to provide an implementation of the method GetCategory()for each enumeration.
    • Secondly, in 90% of cases in our code, Ok is used to return good code. Therefore, you can write a general implementation for these 90%, and for 10% you will have to do specialization.

    So, let's concentrate on the first task - automatic category calculation. The idea suggested by my colleague is that the developer should be able to register his enumerated type. This can be done using a template with a variable number of arguments. Declare such a structure

    template <typename... Types>
    structEnumTypeRegister{}; // структура для регистрации типов

    Now, to register a new enumeration, which should be expanded by a category, we simply define a new type

    using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>;
    

    If suddenly we need to add another enumeration, then simply add it to the list of template parameters:

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

    Obviously, the category for our listings may be a position in the list of template parameters, i.e. for Cpu_Errorit is 0 , for Measure_Error, it is 1 , for My_Error it 2 . It remains to force the compiler to calculate this automatically. For C ++ 14, we do this:

    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>
    constexprstd::enable_if_t<std::is_same<Type, QueriedType>::value, tU32>
              GetEnumPosition(EnumTypeRegister<Type, Types...>)
    {
      return0U ;
    }
    template <typename QueriedType, typename Type, typename... Types>
    constexprstd::enable_if_t<!std::is_same<Type, QueriedType>::value, tU32>
              GetEnumPosition(EnumTypeRegister<Type, Types...>)
    {
       return1U + GetEnumPosition<QueriedType>(EnumTypeRegister<Types...>()) ;
    }
    

    What's going on here. In short, a function GetEnumPosition<T<>> with an input parameter being a list of enumerated types EnumTypeRegister, in our case EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>, and a template parameter T - being an enumerated type whose index we should find in this list, goes through the list and if T matches one of the types in the list returns its index, otherwise the message "" The type is not registered in the EnumTypeRegister list "is displayed

    //Т.е. если определен список constexpr  EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> list//то вызов 
    GetEnumPosition<Measure_Error>(list)
    // должен вернуть 1 - что является индексом Measure_Error в данном списке. 

    We will analyze in more detail. Lowest function

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

    Here the branch checks if the requested type matches the first type in the template list, if not, then the return type of the function will continue to be executed by the body of the function, namely, a recursive call again of the same function, while the number of template arguments decreases by 1 , and the return value increases by 1 . That is, at each iteration there will be something similar to this:std::enable_if_t<!std::is_same..GetEnumPositiontU32

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

    As soon as all types in the list are over, std::enable_if_tit will not be able to infer the type of the return value of the function GetEnumPosition()and end at this iteration:

    //Как только итерации дойдут до последнего типа в списке
    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) ; 
    }
    

    What happens if the type is on the list. In this case, another branch c will work :std::enable_if_t<std::is_same..

    template <typename QueriedType, typename Type, typename... Types>
    constexprstd::enable_if_t<std::is_same<Type, QueriedType>::value, tU32>
              GetEnumPosition(TypeRegister<Type, Types...>)
    {
      return0U ;
    }
    

    Here there is a check for the coincidence of types And if, say, there is a type at the input , then we get the following sequence:std::enable_if_t<std::is_same...Measure_Error

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

    At the second iteration, the recursive function call ends and we get 1 (from the first iteration) + 0 (from the second) = 1 - this is an index of type Measure_Error in the list. EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>

    Since this is a function, constexpr,all calculations are done at the compilation stage and no code is actually generated .

    All this could not be written, be at the disposal of C ++ 17. Unfortunately, my IAR compiler does not fully support C ++ 17, and so it was possible to replace the whole footcloth with the following code:

    //for C++17template <typename QueriedType, typename Type, typename... Types>
    constexpr tU32  GetEnumPosition(EnumTypeRegister<Type, Types...>){
       // если обнаружил тип в списке заканчиваем рекурсиюifconstexpr(std::is_same<Type, QueriedType>::value){
           return0U ;
       } else
       {
           return1U + GetEnumPosition<QueriedType>(EnumTypeRegister<Types...>()) ;
       }
    }
    

    It remains now to make boilerplate methods GetCategory()and GetOk(), which will call GetEnumPosition.

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

    That's all. Let's now see what happens with this object construction:

    ReturnCode result(Measure_Error::Ok);
    

    Let's go back to the class constructor ReturnCode

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

    He boilerplate, and if Tthere are Measure_Errorso called instantiation method template GetCategory(Measure_Error)for the type Measure_Error, which in turn causes GetEnumPositiona type Measure_Error, GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>)which returns the position Measure_Errorin the list. Position is 1 . And actually, all the constructor code at the instantiation of the type Measure_Erroris replaced by the compiler with:

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

    Total


    For a developer who ReturnCodewants to use, only one thing needs to be done:
    Register your enumerated type in the list.

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

    And no unnecessary movements, the existing code does not move, and for the extension you just need to register the type in the list. Moreover, all this will be done at the compilation stage, and the compiler will not only calculate all the categories, but will also warn you if you forgot to register the type, or tried to pass a type that is not non-enumerable.

    In fairness, it is worth noting that in those 10% of the code where the enumerations have a different name instead of the Ok code, you will have to make your own specialization for this type.

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

    I posted a small example here: code example

    In general, here is an application:

    enumclassCpu_Error {
        Ok,
        Alu, 
        Rom,
        Ram
    } ;
    enumclassMeasure_Error {
        OutOfLimits,
        Ok,
        BadCode
    } ;
    enumclassMy_Error {
        Error1,
        Error2,
        Error3,
        Error4,
        Ok
    } ;
    // Add enum in the category listusing CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>;
    Cpu_Error CpuCheck(){
       return Cpu_Error::Ram;
    }
    My_Error MyCheck(){
       return My_Error::Error4;
    }
    intmain(){
        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;
            }
        }
        return0;
    }

    Print the following lines:
    Return code: 3 Return category: 0
    Return code: 3 Return category: 2
    Return code: 2 Return category: 1
    mError: 1

    Also popular now: