10 ++ maneiras de trabalhar com registros de hardware em C ++ (por exemplo, IAR e Cortex M)

Escolhendo o caminho mais seguro
Fig. I. Kiyko

Boa saúde a todos!

Você provavelmente se lembra de uma anedota barbada e talvez uma história verdadeira sobre como um aluno foi perguntado sobre uma maneira de medir a altura de um prédio usando um barômetro. O aluno citou, na minha opinião, cerca de 20 ou 30 maneiras, sem mencionar o direto (através da diferença de pressão) que o professor esperava.

Aproximadamente da mesma maneira, quero continuar discutindo o uso de C ++ para microcontroladores e considerar maneiras de trabalhar com registradores usando C ++. E quero observar que, para obter acesso seguro aos registros, não haverá uma maneira fácil. Vou tentar mostrar todos os prós e contras dos métodos. Se você souber mais maneiras, jogue-as nos comentários. Então vamos começar:

Método 1. Óbvio e obviamente não o melhor


O método mais comum, também usado em C ++, é usar a descrição das estruturas de registro do arquivo de cabeçalho do fabricante. Para demonstração, tomarei dois registradores da porta A (ODR - registro de dados de saída e IDR - registro de dados de entrada) do microcontrolador STM32F411, para que eu possa executar o LED "incorporado" "Olá, mundo" - piscando.

int main() { GPIOA->ODR ^= (1 << 5) ; GPIOA->IDR ^= (1 << 5) ; //,      } 

Vamos ver o que acontece aqui e como esse design funciona. O cabeçalho do microprocessador contém a estrutura GPIO_TypeDef e uma definição de ponteiro para essa estrutura GPIOA . É assim:

 typedef struct { __IO uint32_t MODER; //port mode register, Address offset: 0x00 __IO uint32_t OTYPER; //port output type register, Address offset: 0x04 __IO uint32_t OSPEEDR; //port output speed register, Address offset: 0x08 __IO uint32_t PUPDR; //port pull-up/pull-down register, Address offset: 0x0C __IO uint32_t IDR; //port input data register, Address offset: 0x10 __IO uint32_t ODR; //port output data register, Address offset: 0x14 __IO uint32_t BSRR; //port bit set/reset register, Address offset: 0x18 __IO uint32_t LCKR; //port configuration lock register, Address offset: 0x1C __IO uint32_t AFR[2]; //alternate function registers, Address offset: 0x20-0x24 } GPIO_TypeDef; #define PERIPH_BASE 0x40000000U //Peripheral base address in the alias region #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000U) #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000U) #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 

Para colocá-lo em palavras humanas simples, toda a estrutura do tipo GPIO_TypeDef "estabelece" no endereço GPIOA_BASE e, quando você se refere a um campo específico da estrutura, você se refere essencialmente ao endereço dessa estrutura + ao deslocamento para um elemento dessa estrutura. Se você remover #define GPIOA , o código ficaria assim:

 ((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ; ((GPIO_TypeDef *) GPIOA_BASE)->IDR ^= (1 << 5) ; // 

Em relação à linguagem de programação C ++, um endereço inteiro é convertido em um tipo de ponteiro para a estrutura GPIO_TypeDef . Mas em C ++, ao usar a conversão C, o compilador tenta executar a conversão na seguinte sequência:

  • const_cast
  • static_cast
  • static_cast ao lado de const_cast,
  • reinterpret_cast
  • reinterpret_cast ao lado de const_cast

isto é se o compilador não puder converter o tipo usando const_cast, ele tentará aplicar static_cast e assim por diante. Como resultado, a chamada:

 ((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ; 

não há nada como:

 reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE)->ODR ^= (1 << 5) ; 

De fato, para aplicativos C ++, seria correto "puxar" a estrutura para o endereço da seguinte maneira:

 GPIO_TypeDef * GPIOA{reinterpret_cast<GPIO_TypeDef *>(GPIOA_BASE)} ; 

De qualquer forma, devido à conversão de tipo, há um grande ponto negativo nessa abordagem para C ++. Consiste no fato de que reinterpret_cast não pode ser usado nem em construtores e funções constexpr , nem em parâmetros de modelo, e isso reduz significativamente o uso de recursos C ++ para microcontroladores.
Vou explicar isso com exemplos. É possível fazer isso:

  struct Test { const int a; const int b; } ; template<Test* mystruct> constexpr const int Geta() { return mystruct->a; } Test test{1,2}; int main() { Geta<&test>() ; } 

Mas você não pode fazer isso já:

 template<GPIO_TypeDef * mystruct> constexpr volatile uint32_t GetIdr() { return mystruct->IDR; } int main() { //GPIOA  reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE) //  ,        GetIdr<GPIOA>() ; // } //      : struct Port { constexpr Port(GPIO_TypeDef * ptr): port(*ptr) {} GPIO_TypeDef & port ; } //  GPIOA  reinterpret_cast,   //  constexpr      constexpr Port portA{GPIOA}; //    

Portanto, o uso direto dessa abordagem impõe restrições significativas ao uso de C ++. Não conseguiremos localizar o objeto que deseja usar o GPIOA na ROM usando as ferramentas de linguagem e não poderemos tirar proveito da metaprogramação para esse objeto.
Além disso, em geral, esse método não é seguro (como dizem nossos parceiros ocidentais). Afinal, é bem possível fazer algumas atividades NÃO DIVERTIDAS
Em conexão com o exposto, resumimos:

Prós


  • O cabeçalho do fabricante é usado (é verificado, não possui erros)
  • Não há gestos e custos adicionais, você toma e usa
  • Facilidade de uso
  • Todo mundo conhece e entende esse método.
  • Sem sobrecarga

Contras


  • Uso limitado de metaprogramação
  • Incapacidade de usar em construtores constexpr
  • Ao usar wrappers em classes, o consumo adicional de RAM é um ponteiro para um objeto dessa estrutura
  • Você pode fazer estúpido
Agora vamos ver o método número 2

Método 2. Brutal


É óbvio que todo programador de incorporação tem em mente os endereços de todos os registradores de todos os microcontroladores, para que você possa sempre usar sempre o método a seguir, que segue o primeiro:

 *reinterpret_cast<volatile uint32_t *>(GpioaOdrAddr) ^= (1 <<5) ; *reinterpret_cast<volatile uint32_t *>(GpioaIdrAddr) ^= (1 <<5) ; // 

Em qualquer lugar do programa, você sempre pode chamar a conversão para o endereço de registro volatile uint32_t e instalar pelo menos algo lá.
Especialmente não há vantagens aqui, mas naquelas desvantagens de que há um inconveniente adicional para usar e a necessidade de escrever o endereço de cada registro em um arquivo separado. Portanto, passamos ao método número 3.

Método 3. Óbvio e obviamente mais correto


Se o acesso aos registradores ocorrer através do campo da estrutura, em vez de um ponteiro para o objeto da estrutura, você poderá usar o endereço inteiro da estrutura. O endereço das estruturas está no arquivo de cabeçalho do fabricante (por exemplo, GPIOA_BASE para GPIOA); portanto, você não precisa se lembrar dele, mas pode usá-lo em modelos e expressões constexpr e, em seguida, "sobrepor" a estrutura a este endereço.

 template<uint32_t addr, uint32_t pinNum> struct Pin { using Registers = GPIO_TypeDef ; __forceinline static void Toggle() { //     addr Registers *GpioPort{reinterpret_cast<Registers*>(addr)}; GpioPort->ODR ^= (1 << pinNum) ; } }; int main() { using Led1 = Pin<GPIOA_BASE, 5> ; Led1::Toggle() ; } 

Não há desvantagens especiais, do meu ponto de vista. Em princípio, uma opção de trabalho. Mas, ainda assim, vamos dar uma olhada em outras maneiras.

Método 4. Invólucro exotérico


Para conhecedores de código compreensível, você pode criar um wrapper sobre o registro, para que seja conveniente acessá-los e parecer "bonitos", criar um construtor, redefinir os operadores:

 class Register { public: explicit Register(uint32_t addr) : ptr{ reinterpret_cast<volatile uint32_t *>(addr) } { } __forceinline inline Register& operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile uint32_t *ptr; //    }; int main() { Register Odr{GpioaOdrAddr}; Odr ^= (1 << 5); Register Idr{GpioaIdrAddr}; Idr ^= (1 << 5); // } 

Como você pode ver, novamente você precisará lembrar os endereços inteiros de todos os registradores ou configurá-los em algum lugar, além de armazenar um ponteiro no endereço do registrador. Mas o que não é muito bom novamente, reinterpret_cast acontece novamente no construtor
Algumas desvantagens e ao fato de que, na primeira e na segunda versão, foi adicionada a necessidade de cada registro usado armazenar um ponteiro para 4 bytes na RAM. Em geral, não é uma opção. Nós olhamos para o seguinte.

Método 4,5. Envoltório exotérico com padrão


Adicionamos um pouco de metaprogramação, mas não há muito benefício disso. Esse método difere do anterior apenas no fato de o endereço ser transferido não para o construtor, mas no parâmetro template, economizamos um pouco nos registros ao passar o endereço para o construtor, já é bom:

 template<uint32_t addr> class Register { public: Register() : ptr{reinterpret_cast<volatile uint32_t *>(addr)} { } __forceinline inline Register &operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile std::uint32_t *ptr; }; int main() { using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5); // } 

E assim, o mesmo ancinho, vista lateral.

Método 5. Razoável


Obviamente, você precisa se livrar do ponteiro, então vamos fazer o mesmo, mas remova o ponteiro desnecessário da classe.

 template<uint32_t addr> class Register { public: __forceinline Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5); // 

Você pode ficar aqui e pensar um pouco. Este método resolve imediatamente 2 problemas que foram herdados anteriormente do primeiro método. Primeiramente, agora posso usar o ponteiro para o objeto Register no modelo e, em segundo lugar, passá-lo ao construtor constexrp .

 template<Register * register> void Xor(uint32_t mask) { *register ^= mask ; } Register<GpioaOdrAddr> GpioaOdr; int main() { Xor<&GpioaOdr>(1 << 5) ; //  } //   struct Port { constexpr Port(Register& ref): register(ref) {} Register & register ; } constexpr Port portA{GpioaOdr}; 

Obviamente, é necessário novamente ter memória eidética para os endereços dos registradores ou determinar manualmente todos os endereços dos registradores em algum lugar em um arquivo separado ...

Prós


  • Facilidade de uso
  • Capacidade de usar metaprogramação
  • Capacidade de usar em construtores constexpr

Contras


  • O arquivo de cabeçalho verificado do fabricante não é usado
  • Você deve definir todos os endereços dos registros
  • Você precisa criar um objeto da classe Register
  • Você pode fazer estúpido

Ótimo, mas ainda há muitos pontos negativos ...

Método 6. Mais inteligente que o razoável


No método anterior, para acessar o registro, era necessário criar um objeto desse registro, isto é um desperdício desnecessário de RAM e ROM; portanto, criamos um invólucro com métodos estáticos.

 template<uint32_t addr> class Register { public: __forceinline inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile uint32_t *>(addr) ^= mask; } }; int main() { using namespace Case6 ; using Odr = Register<GpioaOdrAddr>; Odr::Xor(1 << 5); using Idr = Register<GpioaIdrAddr>; Idr::Xor(1 << 5); // } 

Um mais adicionado
  • Sem sobrecarga. Código compacto rápido, igual à opção 1 (ao usar wrappers em classes, não há custo adicional de RAM, pois o objeto não é criado, mas métodos estáticos são usados ​​sem a criação de objetos)
Vá em frente ...

Método 7. Remova a estupidez


Obviamente, estou constantemente fazendo NÃO-ENGRAÇADO no código e escrevendo algo no registro, que na verdade não se destina à gravação. Tudo bem, é claro, mas o STUPIDness deve ser proibido. Vamos proibir fazer bobagens. Para isso, apresentamos estruturas auxiliares:

  struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; 

Agora podemos definir os registros para gravação, e os registros são somente leitura:

 template<uint32_t addr, typename RegisterType> class Register { public: //       WriteReg,    // ,  ,       __forceinline template <typename T = RegisterType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T>::value>> Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; 

Agora vamos tentar compilar nosso teste e ver que o teste não é compilado, porque o operador ^= para o registro Idr não existe:

  int main() { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr Odr ; Odr ^= (1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr Idr ; Idr ^= (1 << 5) ; //,  Idr    } 

Então, agora há mais vantagens ...

Prós


  • Facilidade de uso
  • Capacidade de usar metaprogramação
  • Capacidade de usar em construtores constexpr
  • Código compacto rápido, igual ao da opção 1
  • Ao usar wrappers em classes, não há custo adicional de RAM, pois o objeto não é criado, mas métodos estáticos são usados ​​sem a criação de objetos
  • Você não pode fazer estupidez

Contras


  • O arquivo de cabeçalho verificado do fabricante não é usado
  • Você deve definir todos os endereços dos registros
  • Você precisa criar um objeto da classe Register

Então, vamos remover a oportunidade de criar uma classe para economizar mais

Método 8. Sem NONSENSE e sem um objeto de classe


Código imediato:

  struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; int main { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr::Xor(1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr::Xor(1 << 5) ; //,  Idr    } 

Adicionamos mais uma vantagem, não criamos um objeto. Mas seguir em frente, ainda temos contras

Método 9. Método 8 com integração de estrutura


No método anterior, apenas o caso foi definido. Porém, no método 1, todos os registros são combinados em estruturas para que você possa acessá-los convenientemente por módulos. Vamos fazer isso ...

 namespace Case9 { struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; template<uint32_t addr> struct Gpio { using Moder = Register<addr, ReadWriteReg>; //      using Otyper = Register<addr + OtyperShift, ReadWriteReg> ; using Ospeedr = Register<addr + OspeedrShift,ReadWriteReg> ; using Pupdr = Register<addr + PupdrShift,ReadWriteReg> ; using Idr = Register<addr + IdrShift, ReadReg> ; using Odr = Register<addr + OdrShift, WriteReg> ; }; int main() { using Gpioa = Gpio<GPIOA_BASE> ; Gpioa::Odr::Xor(1 << 5) ; Gpioa::Idr::Xor((1 << 5) ); //,  Idr    } 

Aqui, o ponto negativo é que as estruturas precisarão ser reescritas, e as compensações de todos os registros devem ser lembradas e determinadas em algum lugar. Seria bom se as compensações fossem definidas pelo compilador, e não pela pessoa, mas isso é mais tarde, mas por enquanto consideraremos outro método interessante sugerido pelo meu colega.

Método 10. Enrole o registro por meio de um ponteiro para um membro da estrutura


Aqui usamos esse conceito como um ponteiro para um membro da estrutura e acesso a eles .

 template<uint32_t addr, typename T> class RegisterStructWrapper { public: __forceinline template<typename P> inline static void Xor(PT::*member, int mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; //   ,     . } } ; using GpioaWrapper = RegisterStructWrapper<GPIOA_BASE, GPIO_TypeDef> ; int main() { GpioaWrapper::Xor(&GPIO_TypeDef::ODR, (1 << 5)) ; GpioaWrapper::Xor(&GPIO_TypeDef::IDR, (1 << 5)) ; // return 0 ; } 

Prós


  • Facilidade de uso
  • Capacidade de usar metaprogramação
  • Capacidade de usar em construtores constexpr
  • Código compacto rápido, igual ao da opção 1
  • Ao usar wrappers em classes, não há custo adicional de RAM, pois o objeto não é criado, mas métodos estáticos são usados ​​sem a criação de objetos
  • O arquivo de cabeçalho verificado do fabricante é usado.
  • Não é necessário definir todos os endereços de registro você mesmo
  • Não há necessidade de criar um objeto da classe Register

Contras


  • Você pode fazer tolices e até especular sobre a compreensibilidade do código.

Método 10.5. Combine os métodos 9 e 10


Para descobrir a mudança do registro em relação ao início da estrutura, você pode usar o ponteiro para o membro da estrutura: volatile uint32_t T::*member , ele retornará o deslocamento do membro da estrutura em relação ao início em bytes. Por exemplo, temos a estrutura GPIO_TypeDef , então o endereço &GPIO_TypeDef::ODR será 0x14.
Aproveitamos esta oportunidade e calculamos os endereços dos registros do método 9, usando o compilador:

 struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T, volatile uint32_t T::*member, typename RegType> class Register { public: __forceinline template <typename T1 = RegType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; } }; template<uint32_t addr, typename T> struct Gpio { using Moder = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, ReadWriteReg>; using Otyper = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OTYPER, ReadWriteReg>; using Ospeedr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OSPEEDR, ReadWriteReg>; using Pupdr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::PUPDR, ReadWriteReg>; using Idr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::IDR, ReadReg>; using Odr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, WriteReg>; } ; 

Você pode trabalhar com registradores de forma mais exotérica:

 using namespace Case11 ; using Gpioa = Gpio<GPIOA_BASE, GPIO_TypeDef> ; Gpioa::Odr::Xor(1 << 5) ; //Gpioa::Idr::Xor((1 << 5) ); //,  Idr    

Obviamente, aqui todas as estruturas terão que ser reescritas novamente. Isso pode ser feito automaticamente, por algum script do Phyton, na entrada algo como stm32f411xe.h na saída do seu arquivo com estruturas para uso em C ++.
De qualquer forma, existem várias maneiras diferentes que podem funcionar em um projeto específico.

Bônus Introduzimos a extensão de idioma e o código de parsim usando Phyton


O problema de trabalhar com registros em C ++ já existe há algum tempo. As pessoas resolvem isso de maneiras diferentes. É claro que seria ótimo se o idioma suportasse algo como renomear classes em tempo de compilação. Bem, digamos, e se fosse assim:

 template<classname = [PortName]> class Gpio[Portname] { __forceinline inline static void Xor(const uint32_t mask) { GPIO[PortName]->ODR ^= mask ; } }; int main() { using GpioA = Gpio<"A"> ; GpioA::Xor(5) ; } 

Mas infelizmente esse idioma não suporta. Portanto, a solução usada pelas pessoas é analisar o código usando Python. I.e. alguma extensão de idioma é introduzida. O código, usando essa extensão, é alimentado no analisador Python, que o converte em código C ++. Esse código é mais ou menos assim: (um exemplo é retirado da biblioteca modm; aqui estão as fontes completas ):

 %% set port = gpio["port"] | upper %% set reg = "GPIO" ~ port %% set pin = gpio["pin"] class Gpio{{ port ~ pin }} : public Gpio { __forceinline inline static void Xor() { GPIO{{port}}->ODR ^= 1 << {{pin}} ; } } //        class Gpio5 : public Gpio { __forceinline inline static void Xor() { GPIO->ODR ^= 1 << 5 ; } } //     using Led = Gpio5; Led::Xor(); 


Atualização: Bônus. Arquivos SVD e analisador em Phyton


Esqueceu de adicionar outra opção. O ARM libera um arquivo de descrição do registro para cada fabricante de SVD. A partir do qual você pode gerar um arquivo C ++ com uma descrição dos registros. Paul Osborne compilou todos esses arquivos no GitHub . Ele também escreveu um script Python para analisá-los.

Isso é tudo ... minha imaginação está exausta. Se você ainda tiver idéias, sinta-se à vontade. Um exemplo com todos os métodos está aqui.

Referências


Acesso ao registro Typesafe em C ++
Fazendo as coisas funcionarem - Acessando o hardware do C ++
Fazendo as coisas fazerem coisas - Parte 3
Fazendo as coisas fazerem coisas - Estrutura sobreposta

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


All Articles