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
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.
Suppose you have a CPU diagnostic subsystem and it has enumerated return codes, say these:
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):
For each subsystem, let there be a method that
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:
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?
It would be logical for the method to
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:
Or so:
Or so:
As you can see from the code, a certain class is used 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
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
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
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:
It is necessary to explain a little what is happening here. To begin with a template constructor
It allows you to create an object class from any enumerated type:
In order for the constructor to accept only the enumerated type, it is added to its body
The constructor also initializes private attributes, I will return to this later ...
Next, the cast operator:
It can also lead only to an enumerated type and allows us to do the following:
Well and separately the bool () operator:
Allows us to directly check if there is any error in the return code:
This is essentially all. The question remains in the functions
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:
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
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.
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
Now, to register a new enumeration, which should be expanded by a category, we simply define a new type
If suddenly we need to add another enumeration, then simply add it to the list of template parameters:
Obviously, the category for our listings may be a position in the list of template parameters, i.e. for
What's going on here. In short, a function
We will analyze in more detail. Lowest function
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:
As soon as all types in the list are over,
What happens if the type is on the list. In this case, another branch c will work :
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:
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.
Since this is a function,
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:
It remains now to make boilerplate methods
That's all. Let's now see what happens with this object construction:
Let's go back to the class constructor
He boilerplate, and if
For a developer who
Register your enumerated type in the list.
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.
I posted a small example here: code example
In general, here is an application:
Print the following lines:
std
not 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 std
and 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 CpuDiagnostic
type code will be returned Cpu_Error
, for MeasureDiagnostic
type 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 thisconstexpr 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_code
that 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_category
one 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_code
is 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_code
that 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_enum
and 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
CategoryError
enumeration. 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_Error
it 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..
GetEnumPosition
tU32
//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_t
it 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
T
there are Measure_Error
so called instantiation method template GetCategory(Measure_Error)
for the type Measure_Error
, which in turn causes GetEnumPosition
a type Measure_Error
, GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>)
which returns the position Measure_Error
in the list. Position is 1 . And actually, all the constructor code at the instantiation of the type Measure_Error
is replaced by the compiler with:explicitReturnCode(const Measure_Error initReturnCode):
errorValue(1),
errorCategory(1),
goodCode(1){
}
Total
For a developer who
ReturnCode
wants 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