Using of C++ in embedded software development could very often face an issue that standard libraries usage causes undesirable additional resources consumption of ROM and RAM. That's why some classes and methods from 'std' library doesn't suits for implementation in microcontrollers. There are dynamic memory (heap), RTTI and exceptions usage restrictions in the embedded software development. In order to create compact and quick working code we couldn't just use 'std' library, and for example, 'typeid' operator, because RTTI support is needed and this is an overhead in common case.
Sometimes one have to «reinvent the wheel» to satisfy that conditions. The number of such tasks is small, but they are still need to be done. The article describes an easy task from the first sigh — return codes expansion for the existing subsystems in embedded software.

GOAL


Let's assume that CPU diagnostics subsystem exists. The subsystem contains following enumerated return codes:

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

In case, when CPU diagnostics subsystem detects CPU's module failure (e.g. ALU or ROM), it should return a relevant code. The same for another subsystem. Let's suppose this subsystem is related to measurements diagnostics. Subsystem's purpose is to check, that measured value is belongs to the given measuring range and valid as well (not NAN of Inf.):

enum class Measure_Error
{
    OutOfLimits,
    Ok,
    BadCode
} ;       

There is GetLastError() method for each subsystem, which returns enumerated error type for given subsystem. For CpuDiagnostic subsystem the type of Cpu_Error will be returned, for MeasureDiagnostic — Measure_Error type. Also there is logger subsystem that contains any occurred error exists:
?
void Logger::Update()
{
  Log(static_cast<uint32_t>(cpuDiagnostic.GetLastError()) ; 
  Log(static_cast<uint32_t>(measureDiagstic.GetLastError()) ; 
}

It is clear, that while casting enumerated types in integer, it should be the same result for each different types. So how to make out the CpuDiagnostic subsystem error and MeasureDiagnostic susbsytem error?

SEARCHING FOR SOLUTIONS


It would be naturally for GetLastError() method to return different code for different subsystems. One of the direct solutions is to use different range of codes for each enumerated types:

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


Disadvantages of this approach are obvious: firstly, a lot of manual work — all ranges and return codes should be determine manually, what causes human factor error. Secondly, there could much more subsystems so any integrations of enumerations for each new subsystem is a bad experience. There are following approaches, that allow to expand return codes without changing of enumerated types:
The be able to do this:

ResultCode result = Cpu_Error::Ok ;
//GetLastError()  returns enum Cpu_Error
result = cpuDiagnostic.GetLastError() ; 
if(result)  //check if any error occurred
{
  //log in both error and it's category
  Logger::Log(result) ; 
}
//GetLastError()  return enum Measure_Error
result = measureDiagnostic.GetLastError() ; 
if(result)  //check if any error occurred
{
  //log in both error and it's category
  Logger::Log(result) ; 

Or this:

ReturnCode result ;
for(auto it: diagnostics) 
{
  //GetLastError()  returns enum of diagnostics subsystem
  result = it.GetLastError() ; 
  if (result)  //check if any error occurred
  {
    Logger::Log(result) ; //log in both error and it's category
  }
}

Or this:

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


As demonstrated on the previous listing, ReturnCode class is used to contain both error code and it's category. In standard library std::error_code class is implemented and responsible for almost all functionality named above. The main problem is that in order to use this class, std::error_category should be derived, but very overloaded to be used in embedded software on small microcontrollers. Even if std::string is used:

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


Besides, there are a lot of manually overheads, because each category (name and message) for each enumerated type should be added. In addition the error code which in std::error_code is relevant error absence is equal with 0. But there are cases, when for various types error absence code should be various as well. Also requirement of less overheads, besides category addition, should be taken into account. As a result, a mechanism should be created, which will allow for developers to minimize manually their efforts in part of category addition for each enumerated types. First of all a class similar to std::error_code on should be created with ability to cast any enumerated type into integer and back. Also an opportunity to return category and the value of code itself, and confirm a checking as well:

//GetLastError()  returns enum CpuError
ReturnCode result(cpuDiagnostic.GetLastError()) ; 
if(result)  //check if error is occurred
{
  ...
}


SOLUTION


Class being developed should contain error code, category code, code relevant to an error absence, cast operator and assignment operator:

image

Fig.1 – ReturnCode class design

Commentary on the code functionality. Template constructor allows to create an object of the class from any enumerated type:

 template<class T>
    explicit constexpr ReturnCode(const T initReturnCode): 
                                    errorValue(static_cast<tU32>(initReturnCode)),
                                    errorCategory(GetCategory(initReturnCode)),
                                    goodCode(GetOk(initReturnCode))
    {      
        static_assert(std::is_enum<T>::value, "Type should be enumerated") ;
    }
 
ReturnCode result(Cpu_Error::Ok) ;
ReturnCode result1(My_Error::Error1);
ReturnCode result2(cpuDiagnostic.GetLatestError()) ;

In constructor's body in order to get an enumerated type only, static_assert was added. Static_assert will check the passed type during compilation with std::is_enum and returns an error with understandable text. There is no real code to be generated, this is only for compiler, so in fact constructor is empty. Then for cast operator:

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

It is also allow to cast into enumerated type and perform 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 ;

And bool() operator:

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

This allow to check if any error in return codes directly:

//GetLastError()  return enum Cpu_Error
ReturnCode result(cpuDiagnostic.GetLastError()) ; 
if(result)  //check if error is occurred
{
  ...
}

There are two functions that should be described as well. GetCategory() is responsible for enumerated type to attend ResultCode class somehow about it's category. The second function makes enumerated type to report, that it is a suitable return code to be compared in bool() operator. It is clear that both functions could be provided by user and could be called in constructor via argument depended finding mechanism. For example:
?
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); 
}

These took some developer's efforts. For each enumerated type, that should be categorized both functions should be added and CategoryError should be updated. Taking requirement of less developers efforts into account, the following steps could be passed:
  • Category could be calculated automatically, so developer doesn't need to implements GetCategory() method for each enumeration.
  • In 90% cases, '::Ok' is used as a good return code, so a common implementation for these 90% could be implemented, but for another 10% specialization is required.
  • An idea of the first step is a logging or registration of custom enumerated type.
  • This could be done by using variadic template:


template <typename... Types>
struct EnumTypeRegister{}; // structure for types logging

In order to register new enumeration, that should be expanded by category, define a new type:

using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>;

In case a new enumerated is needed, it should be added in list of template's parameters:

using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, Custom_Error>;
?
Now categories for custom enumerations could be position in list of template's parameters, i.e. 0 is for Cpu_Error, 1 is for Measure_Error, 2 is for Custom_Error. Next issue to be solved is how to make compiler to calculate it automatically. For C++14 one of the solution is:

template <typename QueriedType, typename Type>
constexpr tU32 GetEnumPosition(EnumTypeRegister<Type>) 
{ 
  static_assert(std::is_same<Type, QueriedType>::value,
               " Type is not logged in 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...>()) ;
}

GetEnumPosition<T<>> function with EnumTypeRegister (list of enumerated types) input parameter, EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> in this case and template parameter T which is itself is a enumerated type, and which index should be found, goes through the list, and if T is relevant with one of the types from this list, returns it's index, or displays an error: «Type is not logged in EnumTypeRegister list».

//i.e. if list is defined
constexpr  EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> list
//than call 
GetEnumPosition<Measure_Error>(list)
// returns 1 - Measure_Error index in the list

Let's consider in more details the function:

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

Here std::enable_if_t<!std::is_same.. branch checks if the requested type is the same as the first type in templates parameters list and if not, that the returned type of GetEnumPosition() function will be tU32 and then the function performs, namely recursive call of this function, but with the number of templates parameters less by one. That's means for each iteration:

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

Once all types ran out, std::enable_if_t could not return the type of GetEnumPosition() function and iterations are complete:

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

If the type still in the list, another branch (std::enable_if_t<std::is_same..) will start to work:

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

Here is a check of types std::enable_if_t<std::is_same... match. If, for example, Measure_Error is in input, that means the following sequence:

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

On the second iteration recursive call of the function will finished and 1 (from first iteration) + 0 (from the second iteration) will be on output = 0 — that's an index of Measure_Error type in list:

EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> 


All calculations perform during compilation, because the function is constexpr and no additional code generated. By using C++17, the code could be rewrite into the following form:

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

The last but not least are template methods GetCategory() and GetOk(), which will call 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);
}

Let's consider, what happens by using such object construction approach:

ReturnCode result(Measure_Error::Ok) ;

Come back to the ReturnCode constructor:

template<class T> 
    explicit constexpr 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") ;
    }

This constructor is template, and in case T is Measure_Error type, that means instantiation of GetCategory(Measure_Error) method template's for Measure_Error type, which in it's turn calls GetEnumPosition with Measure_Error, GetEnumPosition <Measure_Error> (EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) type. The last one returns position of Measure_Error in list. And all constructor's code replace by compiler with:

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


SUMMARY


For the developer, who will use ReturnCode only one thing should be done — register his own enumerated type in list:

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

Here is the example of usage: ErrorCode example

And no more additional efforts, existing code shouldn't be changed as well, but for expansion only type logging is enough. Moreover, all this process is done during the compilation time. Compiler not only calculates all categories, but attend user if type is not registered or passed type is not enumerated. It should be noted, that in the remaining 10%, where another name uses instead of ::Ok, specialization should be created for such type:

template<>
constexpr tU32 GetOk<MyError>(const MyError)
{
  return static_cast<tU32>(MyError::Good) ;
} ;
 
Example code:
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;
}


An in the I/O terminal:

Return code: 3 Return category: 0
Return code: 3 Return category: 2
Return code: 2 Return category: 1
mError: 1

Комментарии (0)