Fig. I. KiykoBoa 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;
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() {
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() {
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;
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) ;
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:
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) ;
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) ;
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>;
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 ;
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) ;
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}} ; } }
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 3Fazendo as coisas fazerem coisas - Estrutura sobreposta