Manejo unificado de errores (opción C ++ para microcontroladores)

Al desarrollar software para microcontroladores en C ++, muy a menudo puede encontrar el hecho de que el uso de la biblioteca estándar puede generar costos de recursos adicionales indeseables, tanto RAM como ROM. Por lo tanto, a menudo las clases y métodos de la biblioteca std no son del todo adecuados para la implementación en el microcontrolador. También hay algunas restricciones en el uso de memoria asignada dinámicamente, RTTI, excepciones, etc. En general, para escribir código compacto y rápido, no puede simplemente tomar la biblioteca typeid y comenzar a usar, digamos operadores typeid , porque necesita soporte RTTI, y esto es una sobrecarga, aunque no muy grande.

Por lo tanto, a veces hay que reinventar la rueda para cumplir con todas estas condiciones. Hay pocas tareas de este tipo, pero lo son. En esta publicación, me gustaría hablar sobre una tarea aparentemente simple: expandir los códigos de retorno de los subsistemas existentes en el software del microcontrolador.

Desafío


Supongamos que tiene un subsistema de diagnóstico de CPU y tiene códigos de retorno enumerables, diga estos:

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

Si el subsistema de diagnóstico de la CPU detecta una falla de uno de los módulos de la CPU (por ejemplo, ALU o RAM), tendrá que devolver el código correspondiente.

Lo mismo para otro subsistema, que sea un diagnóstico de medición, comprobando que el valor medido está en el rango y que generalmente es válido (no es igual a NAN o Infinito):

 enum class Measure_Error { OutOfLimits, Ok, BadCode } ; 

Para cada subsistema, permita que haya un método GetLastError() que devuelva el tipo de error enumerado de este subsistema. Para CpuDiagnostic código de tipo CpuDiagnostic , para MeasureDiagnostic código de tipo Measure_Error .

Y hay un cierto registro que, cuando ocurre un error, debe registrar el código de error.
Para comprenderlo, escribiré esto en una forma muy simplificada:

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

Est√° claro que al convertir los tipos enumerados a un entero, podemos obtener el mismo valor para diferentes tipos. ¬ŅC√≥mo distinguir que el primer c√≥digo de error es el c√≥digo de error del subsistema de diagn√≥stico de la CPU y el segundo subsistema de medici√≥n?

Busca soluciones


Sería lógico que el método GetLastError() devuelva un código diferente para diferentes subsistemas. Una de las decisiones más directas en la frente sería usar diferentes rangos de códigos para cada tipo enumerado. Algo como esto

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

Creo que las desventajas de este enfoque son obvias. En primer lugar, una gran cantidad de trabajo manual, debe determinar manualmente los rangos y los códigos de retorno, lo que sin duda conducirá a un error humano. En segundo lugar, puede haber muchos subsistemas, y agregar enumeraciones para cada subsistema no es una opción en absoluto.

En realidad, sería genial si fuera posible no tocar las transferencias en absoluto, expandir sus códigos de una manera ligeramente diferente, por ejemplo, para poder hacer esto:

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

M√°s o menos:

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

M√°s o menos:

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

Como puede ver en el código, ReturnCode se usa alguna clase ReturnCode , que debe contener tanto el código de error como su categoría. En la biblioteca estándar existe tal clase std::error_code , que en realidad hace casi todo esto. Muy bien su propósito se describe aquí:

Tu propio std :: code_error
Soporte para errores del sistema en C ++
Excepciones deterministas y manejo de errores en "C ++ del futuro"

La queja principal es que para usar esta clase, necesitamos heredar std::error_category , que est√° claramente sobrecargado para su uso en firmware en peque√Īos microcontroladores. Incluso al menos usando std :: string.

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

Además, también tendrá que describir la categoría (nombre y mensaje) para cada uno de sus tipos enumerados manualmente. Y también el código que indica la ausencia de un error en std::error_code es 0. Y hay posibles casos en que para cada tipo el código de error será diferente.
Me gustar√≠a no tener gastos generales, excepto para agregar un n√ļmero de categor√≠a.

Por lo tanto, sería lógico "inventar" algo que permitiría al desarrollador hacer un mínimo de movimientos en términos de agregar una categoría para su tipo enumerado.

Primero necesita hacer una clase similar a std::error_code , capaz de convertir cualquier tipo enumerado en un entero y viceversa de un entero a un tipo enumerado. Además de estas características, para poder devolver la categoría, el valor real del código, así como para poder verificar:

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

Solución


La clase debe almacenar en sí misma un código de error, un código de categoría y un código correspondiente a la ausencia de errores, un operador de conversión y un operador de asignación. La clase correspondiente es la siguiente:



Código de clase
 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 necesario explicar un poco lo que está sucediendo aquí. Para comenzar con un constructor de plantillas

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


Le permite crear una clase de objeto a partir de cualquier tipo enumerado:

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

Para asegurarse de que el constructor solo puede aceptar un tipo enumerado, static_assert agrega a su cuerpo, que en tiempo de compilación verificará el tipo pasado al constructor usando std::is_enum y std::is_enum error con texto claro. El código real no se genera aquí, esto es todo para el compilador. De hecho, este es un constructor vacío.

El constructor también inicializa atributos privados, volveré sobre esto más tarde ...
A continuación, el operador de reparto:

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

También puede conducir solo a un tipo enumerado y nos permite hacer lo siguiente:

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

Bien y por separado el operador bool ():

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

Nos permitir√° verificar directamente si hay alg√ļn error en el c√≥digo de retorno:

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

Esto es esencialmente todo. La pregunta permanece en las GetCategory() y GetOkCode() . Como puede suponer, el primero está destinado al tipo enumerado para comunicar de alguna manera su categoría a la clase ReturnCode , y el segundo para el tipo enumerado para indicar que es un buen código de retorno, ya que vamos a compararlo con el operador bool() .

Est√° claro que estas funciones pueden ser proporcionadas por el usuario, y honestamente podemos llamarlas en nuestro constructor a trav√©s del mecanismo de b√ļsqueda dependiente de argumentos.
Por ejemplo:

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

Esto requiere un esfuerzo adicional del desarrollador. Necesitamos para cada tipo enumerado que queremos clasificar para agregar estos dos métodos y actualizar la enumeración CategoryError .

Sin embargo, nuestro deseo es que el desarrollador no agregue casi nada al código y no se preocupe por cómo expandir su tipo enumerado.
Que se puede hacer.

  • Primero, fue genial que la categor√≠a se calculara autom√°ticamente, y el desarrollador no tendr√≠a que proporcionar una implementaci√≥n del m√©todo GetCategory() para cada enumeraci√≥n.
  • En segundo lugar, en el 90% de los casos en nuestro c√≥digo, Ok se usa para devolver un buen c√≥digo. Por lo tanto, puede escribir una implementaci√≥n general para este 90%, y para el 10% tendr√° que especializarse.

Entonces, concentr√©monos en la primera tarea: el c√°lculo autom√°tico de categor√≠a. La idea sugerida por mi colega es que el desarrollador deber√≠a poder registrar su tipo enumerado. Esto se puede hacer usando una plantilla con un n√ļmero variable de argumentos. Declarar tal estructura

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

Ahora, para registrar una nueva enumeración, que debería expandirse por una categoría, simplemente definimos un nuevo tipo

 using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>; 

Si de repente necesitamos agregar otra enumeración, simplemente agréguela a la lista de parámetros de plantilla:

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

Obviamente, la categoría de nuestros listados puede ser una posición en la lista de parámetros de plantilla, es decir. para Cpu_Error es 0 , para Measure_Error es 1 , para My_Error es 2 . Queda por forzar al compilador a calcular esto automáticamente. Para C ++ 14, hacemos esto:

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

¬ŅQu√© est√° pasando aqu√≠? En resumen, la funci√≥n GetEnumPosition<T<>> , con el par√°metro de entrada como una lista de tipos enumerados EnumTypeRegister , en nuestro caso EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> , y el par√°metro de plantilla T es un tipo enumerado cuyo √≠ndice deber√≠amos encontrar en esta lista, se ejecuta a trav√©s de la lista y si T coincide con uno de los tipos de la lista, devuelve su √≠ndice; de ‚Äč‚Äčlo contrario, se muestra el mensaje "El tipo no est√° registrado en la lista EnumTypeRegister"

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

Analicemos con más detalle. Función más baja

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

Aqu√≠ la rama std::enable_if_t< !std::is_same .. comprueba si el tipo solicitado coincide con el primer tipo en la lista de plantillas, si no, el tipo devuelto de la funci√≥n GetEnumPosition ser√° tU32 y luego se ejecutar√° el cuerpo de la funci√≥n, es decir, una llamada recursiva de la misma funci√≥n nuevamente , mientras que el n√ļmero de argumentos de plantilla disminuye en 1 y el valor de retorno aumenta en 1 . Es decir, en cada iteraci√≥n habr√° algo similar a esto:

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

Una vez que todos los tipos de la lista hayan finalizado, std::enable_if_t no podrá inferir el tipo del valor de retorno de la función GetEnumPosition() , y en esta iteración finalizará:

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

¬ŅQu√© sucede si el tipo est√° en la lista? En este caso, otra rama funcionar√°, rama 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 ; } 

Aquí hay una comprobación de la coincidencia de los tipos std::enable_if_t< std::is_same ... Y si, por ejemplo, en la entrada hay un tipo Measure_Error , se Measure_Error la siguiente secuencia:

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

En la segunda iteración, la llamada a la función recursiva finaliza y obtenemos 1 (de la primera iteración) + 0 (de la segunda) = 1 en la salida: este es un índice de tipo Measure_Error en la lista EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>

Como se trata de una funci√≥n constexpr, todos los c√°lculos se realizan en la etapa de compilaci√≥n y no se genera ning√ļn c√≥digo.

Todo esto no se pudo escribir, estar a disposición de C ++ 17. Desafortunadamente, mi compilador IAR no es totalmente compatible con C ++ 17, por lo que fue posible reemplazar todo el calzado con el siguiente código:

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

Queda ahora para hacer los métodos de plantilla GetCategory() y GetOk() , que llamará a 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); } 

Eso es todo Veamos ahora qué sucede con la construcción de este objeto:

 ReturnCode result(Measure_Error::Ok) ; 

Volvamos al constructor de la clase 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") ; } 

Es una plantilla, y si T es un Measure_Error que significa que se llama a la instanciación de la plantilla del método GetCategory(Measure_Error) , para el tipo Measure_Error , que a su vez llama a GetEnumPosition con el tipo Measure_Error , GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) que devuelve la posición de Measure_Error en la lista. La posición es 1 . Y en realidad, todo el código del constructor en la instanciación del tipo Measure_Error reemplazado por el compilador con:

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

Resumen


Para un desarrollador que ReturnCode usar ReturnCode solo hay una cosa que hacer:
Registre su tipo enumerado en la lista.

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

Y sin movimientos innecesarios, el código existente no se mueve, y para la extensión solo necesita registrar el tipo en la lista. Además, todo esto se hará en la etapa de compilación, y el compilador no solo calculará todas las categorías, sino que también le advertirá si olvidó registrar el tipo o si intentó pasar un tipo que no es enumerable.

Para ser justos, vale la pena se√Īalar que en el 10% del c√≥digo donde las enumeraciones tienen un nombre diferente en lugar del c√≥digo Ok, tendr√° que hacer su propia especializaci√≥n para este tipo.

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

Publiqu√© un peque√Īo ejemplo aqu√≠: ejemplo de c√≥digo

En general, aquí hay una aplicación:

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

Imprima las siguientes líneas:
Código de retorno: 3 Categoría de retorno: 0
Código de retorno: 3 Categoría de retorno: 2
Código de retorno: 2 Categoría de retorno: 1
mError: 1

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


All Articles