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 ;
Ou então:
ReturnCode result ; for(auto it: diagnostics) {
Ou então:
void CpuDiagnostic::SomeFunction(ReturnCode errocode) { Cpu_Error status = errorcode ; switch (status) { case CpuError::Alu:
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_errorSuporte 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:
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 {
É 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 {
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:
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
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:
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á:
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:
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:
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.
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ódigoEm 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 } ;
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