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