Tratamento de erro unificado (opção C ++ para microcontroladores)

Ao desenvolver software para microcontroladores em C ++, muitas vezes é possível encontrar o fato de que o uso da biblioteca padrão pode levar a custos adicionais indesejáveis ​​de recursos, tanto RAM quanto ROM. Portanto, frequentemente as classes e métodos da biblioteca std não são adequados para implementação no microcontrolador. Há também algumas restrições no uso de memória alocada dinamicamente, RTTI, exceções e assim por diante. Em geral, para escrever código compacto e rápido, você não pode simplesmente pegar a biblioteca std e começar a usar, digamos operadores typeid , porque o suporte a RTTI é necessário, e isso já é uma sobrecarga, embora não seja muito grande.

Portanto, às vezes você precisa reinventar a roda para cumprir todas essas condições. Existem poucas tarefas desse tipo, mas são. Neste post, gostaria de falar sobre uma tarefa aparentemente simples - expandir os códigos de retorno dos subsistemas existentes no software de microcontrolador.

Desafio


Suponha que você tenha um subsistema de diagnóstico da CPU e ele tenha numerosos códigos de retorno, diga o seguinte:

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

Se o subsistema de diagnóstico da CPU detectar uma falha de um dos módulos da CPU (por exemplo, ALU ou RAM), ele precisará retornar o código correspondente.

O mesmo para outro subsistema, seja um diagnóstico de medição, verificando se o valor medido está na faixa e geralmente é válido (não é igual a NAN ou Infinity):

 enum class Measure_Error { OutOfLimits, Ok, BadCode } ; 

Para cada subsistema, permita que um método GetLastError() retorne o tipo de erro enumerado desse subsistema. Para CpuDiagnostic código do tipo CpuDiagnostic será retornado, para MeasureDiagnostic código do tipo Measure_Error .

E há um certo log que, quando ocorre um erro, deve registrar o código de erro.
Para entender, vou escrever isso de uma forma muito simplificada:

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

É claro que, ao converter os tipos enumerados em um número inteiro, podemos obter o mesmo valor para tipos diferentes. Como distinguir que o primeiro código de erro é o código de erro do subsistema de diagnóstico da CPU e o segundo subsistema de medição?

Procurar soluções


Seria lógico que o método GetLastError() retornasse um código diferente para diferentes subsistemas. Uma das decisões mais diretas na testa seria usar diferentes intervalos de códigos para cada tipo enumerado. Algo assim

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

Eu acho que as desvantagens dessa abordagem são óbvias. Em primeiro lugar, muito trabalho manual, você precisa determinar manualmente os intervalos e os códigos de retorno, o que certamente levará a um erro humano. Em segundo lugar, pode haver muitos subsistemas, e adicionar enumerações para cada subsistema não é uma opção.

Na verdade, seria ótimo se fosse possível não tocar nas transferências, expandir seus códigos de uma maneira um pouco diferente, por exemplo, para poder fazer isso:

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

Ou então:

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

Ou então:

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

Como você pode ver no código, alguma classe ReturnCode é usada aqui, que deve conter o código de erro e sua categoria. Na biblioteca padrão, existe uma classe std::error_code , que na verdade faz quase tudo isso. Muito bem, seu objetivo é descrito aqui:

Seu próprio std :: code_error
Suporte para erros do sistema em C ++
Exceções determinísticas e tratamento de erros em "C ++ do futuro"

A principal reclamação é que, para usar essa classe, precisamos herdar std::error_category , que está claramente sobrecarregado para uso em firmware em pequenos microcontroladores. Mesmo pelo menos usando std :: string.

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

Além disso, você também terá que descrever a categoria (nome e mensagem) para cada um de seus tipos enumerados manualmente. E também o código que indica a ausência de um erro em std::error_code é 0. E existem casos possíveis em que para cada tipo o código de erro será diferente.
Eu gostaria de não ter despesas gerais, exceto adicionando um número de categoria.

Portanto, seria lógico “inventar” algo que permitiria ao desenvolvedor fazer um mínimo de movimentos em termos de adição de uma categoria para seu tipo enumerado.

Primeiro, você precisa criar uma classe semelhante ao std::error_code , capaz de converter qualquer tipo enumerado em um número inteiro e vice-versa de um número inteiro para um tipo enumerado. Além desses recursos, para poder retornar a categoria, o valor real do código e verificar:

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

Solução


A classe deve armazenar em si um código de erro, um código de categoria e um código correspondente à ausência de erros, um operador de conversão e um operador de atribuição. A classe correspondente é a seguinte:



Código da classe
 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 ; } ; 


É necessário explicar um pouco o que está acontecendo aqui. Para começar com um construtor de modelos

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


Permite criar uma classe de objeto a partir de qualquer tipo enumerado:

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

Para garantir que o construtor possa aceitar apenas um tipo enumerado, static_assert adicionado ao seu corpo, que em tempo de compilação verifica o tipo passado ao construtor usando std::is_enum e std::is_enum erro com texto não criptografado. O código real não é gerado aqui, isso é tudo para o compilador. Então, de fato, este é um construtor vazio.

O construtor também inicializa atributos privados, voltarei a isso mais tarde ...
Em seguida, o operador de conversão:

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

Também pode levar apenas a um tipo enumerado e nos permite fazer o seguinte:

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

Bem e separadamente o operador bool ():

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

Isso nos permitirá verificar diretamente se há algum erro no código de retorno:

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

Isso é essencialmente tudo. A pergunta permanece nas GetCategory() e GetOkCode() . Como você pode imaginar, o primeiro destina-se ao tipo enumerado para comunicar de alguma forma sua categoria à classe ReturnCode e o segundo ao tipo enumerado para indicar que é um código de retorno bem-sucedido, pois vamos compará-lo com o operador bool() .

É claro que essas funções podem ser fornecidas pelo usuário, e podemos honestamente chamá-las em nosso construtor através do mecanismo de pesquisa dependente de argumento.
Por exemplo:

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

Isso requer um esforço adicional do desenvolvedor. Para cada tipo enumerado que precisamos categorizar, precisamos adicionar esses dois métodos e atualizar a enumeração CategoryError .

No entanto, nosso desejo é que o desenvolvedor não adicione quase nada ao código e não se preocupe em como expandir seu tipo enumerado.
O que pode ser feito?

  • Primeiro, foi ótimo que a categoria fosse calculada automaticamente e o desenvolvedor não precisaria fornecer uma implementação do método GetCategory() para cada enumeração.
  • Em segundo lugar, em 90% dos casos em nosso código, Ok é usado para retornar um bom código. Portanto, você pode escrever uma implementação geral para esses 90% e, para 10%, terá que fazer especialização.

Então, vamos nos concentrar na primeira tarefa - cálculo automático de categoria. A ideia sugerida pelo meu colega é que o desenvolvedor possa registrar seu tipo enumerado. Isso pode ser feito usando um modelo com um número variável de argumentos. Declarar essa estrutura

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

Agora, para registrar uma nova enumeração, que deve ser expandida por uma categoria, simplesmente definimos um novo tipo

 using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>; 

Se de repente precisarmos adicionar outra enumeração, basta adicioná-lo à lista de parâmetros do modelo:

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

Obviamente, a categoria para nossas listagens pode ser uma posição na lista de parâmetros do modelo, ou seja, para Cpu_Error é 0 , para Measure_Error é 1 , para My_Error é 2 . Resta forçar o compilador a calcular isso automaticamente. Para C ++ 14, fazemos o seguinte:

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

O que está acontecendo aqui. Em resumo, a função GetEnumPosition<T<>> , com o parâmetro de entrada como uma lista dos tipos enumerados EnumTypeRegister , no nosso caso EnumTypeRegister<Cpu_Error, Measure_Error, My_Error> , e o parâmetro de modelo T é um tipo enumerado cujo índice devemos encontrar nesta lista, percorre a lista e se T corresponder a um dos tipos da lista, retornará seu índice, caso contrário, a mensagem "Tipo não está registrado na lista EnumTypeRegister" será exibida

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

Vamos analisar com mais detalhes. Menor função

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

Aqui a ramificação std::enable_if_t< !std::is_same .. verifica se o tipo solicitado corresponde ao primeiro tipo na lista de modelos; caso contrário, o tipo retornado da função GetEnumPosition será tU32 e o corpo da função será executado, ou seja, uma chamada recursiva da mesma função novamente. , enquanto o número de argumentos do modelo diminui em 1 e o valor de retorno aumenta em 1 . Ou seja, a cada iteração, haverá algo semelhante a isto:

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

Depois que todos os tipos da lista terminarem, std::enable_if_t não poderá inferir o tipo do valor de retorno da função GetEnumPosition() e, nesta iteração, terminará:

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

O que acontece se o tipo estiver na lista. Nesse caso, outra ramificação funcionará, ramificação 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 ; } 

Aqui há uma verificação da coincidência dos tipos std::enable_if_t< std::is_same ... E se, digamos, na entrada, houver um tipo Measure_Error , a seguinte sequência será obtida:

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

Na segunda iteração, a chamada de função recursiva termina e obtemos 1 (da primeira iteração) + 0 (da segunda) = 1 na saída - este é um índice do tipo Measure_Error na lista EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>

Como essa é uma função constexpr, todos os cálculos são feitos no estágio de compilação e nenhum código é realmente gerado.

Tudo isso não pôde ser escrito, esteja à disposição do C ++ 17. Infelizmente, meu compilador IAR não oferece suporte completo ao C ++ 17 e, portanto, foi possível substituir todo o calçado pelo seguinte 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...>()) ; } } 

Resta agora criar os métodos de modelo GetCategory() e GetOk() , que chamarão 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); } 

Isso é tudo. Vamos agora ver o que acontece com essa construção de objeto:

 ReturnCode result(Measure_Error::Ok) ; 

Vamos voltar ao construtor da classe 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") ; } 

É um modelo e, se T é um Measure_Error que significa que a instanciação do modelo de método GetCategory(Measure_Error) é chamada, para o tipo Measure_Error , que por sua vez chama GetEnumPosition com o tipo Measure_Error , GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>) que retorna a posição de Measure_Error na lista. A posição é 1 . E, na verdade, todo o código do construtor na instanciação do tipo Measure_Error substituído pelo compilador por:

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

Sumário


Para um desenvolvedor que ReturnCode usar o ReturnCode há apenas uma coisa a fazer:
Registre seu tipo enumerado na lista.

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

E sem movimentos desnecessários, o código existente não se move e, para a extensão, você só precisa registrar o tipo na lista. Além disso, tudo isso será feito no estágio de compilação, e o compilador não apenas calculará todas as categorias, mas também o alertará se você esquecer de registrar o tipo ou tentar passar um tipo que não é enumerável.

Para ser honesto, vale a pena notar que naqueles 10% do código em que as enumerações têm um nome diferente em vez do código Ok, você precisará fazer sua própria especialização para esse tipo.

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

Eu postei um pequeno exemplo aqui: exemplo de código

Em geral, aqui está uma aplicação:

 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 as seguintes linhas:
Código de retorno: 3 Categoria de retorno: 0
Código de retorno: 3 Categoria de retorno: 2
Código de retorno: 2 Categoria de retorno: 1
mError: 1

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


All Articles