Como piscar 4 LEDs no CortexM usando C ++ 17, tupla e um pouco de fantasia

Boa saúde a todos!

Ao ensinar aos alunos o desenvolvimento de software incorporado para microcontroladores na universidade, uso C ++ e, às vezes, dou estudantes especialmente interessados ​​em todo tipo de tarefas para identificar alunos talentosos com dores de cabeça especiais.

Mais uma vez, esses alunos tiveram a tarefa de piscar 4 LEDs usando a linguagem C ++ 17 e a biblioteca C ++ padrão, sem conectar bibliotecas adicionais, como o CMSIS e seus arquivos de cabeçalho, com uma descrição das estruturas de registro, e assim por diante ... Aquele com o código vence na ROM terá o menor tamanho e menos memória RAM gasta. A otimização do compilador não deve ser maior que Média. Compilador IAR 8.40.1.
O vencedor vai para Canárias e recebe 5 para o exame.

Eu também não resolvi esse problema antes, então vou contar como os alunos o resolveram e o que aconteceu comigo. Eu aviso imediatamente que é improvável que esse código possa ser usado em aplicativos reais, e foi por isso que publiquei a publicação na seção "Programação anormal", apesar de quem sabe.

Condições da tarefa


Existem 4 LEDs nas portas GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9. Eles precisam piscar. Para ter algo com o que comparar, pegamos o código escrito em C:

void delay() { for (int i = 0; i < 1000000; ++i){ } } int main() { for(;;) { GPIOA->ODR ^= (1 << 5); GPIOC->ODR ^= (1 << 5); GPIOC->ODR ^= (1 << 8); GPIOC->ODR ^= (1 << 9); delay(); } return 0 ; } 

A função delay() aqui é puramente formal, um ciclo regular, não pode ser otimizada.
Supõe-se que as portas já estejam configuradas para saída e o relógio seja aplicado a elas.
Também direi imediatamente que a divisão de bits não foi usada para tornar o código portátil.

Esse código ocupa 8 bytes na pilha e 256 bytes na ROM na Otimização Média
255 bytes de memória de código somente leitura
1 byte de memória de dados somente leitura
8 bytes de memória de leitura e gravação de dados

255 bytes devido ao fato de parte da memória estar sob a tabela de vetores de interrupção, chamadas para funções IAR para inicializar um bloco de ponto flutuante, todos os tipos de funções de depuração e a função __low_level_init, na qual as próprias portas foram configuradas.

Portanto, todos os requisitos são:

  • A função main () deve conter o mínimo de código possível
  • Você não pode usar macros
  • Compilador IAR 8.40.1 suportando C ++ 17
  • Arquivos de cabeçalho CMSIS como "#include" stm32f411xe.h "não podem ser usados
  • Você pode usar a diretiva __forceinline para funções embutidas
  • Otimização de compilador médio

Decisão dos alunos


Em geral, havia várias soluções, vou mostrar apenas uma ... não é ótima, mas gostei.

Como os cabeçalhos não podem ser usados, a primeira coisa que os alunos fizeram foi a classe Gpio , que deve armazenar um link para os registros de portas em seus endereços. Para fazer isso, eles usam uma sobreposição de estrutura, provavelmente tiraram a idéia daqui: Sobreposição de estrutura :

 class Gpio { public: __forceinline inline void Toggle(const std::uint8_t bitNum) volatile { Odr ^= bitNum ; } private: volatile std::uint32_t Moder; volatile std::uint32_t Otyper; volatile std::uint32_t Ospeedr; volatile std::uint32_t Pupdr; volatile std::uint32_t Idr; volatile std::uint32_t Odr; //    static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6); } ; 

Como você pode ver, eles imediatamente identificaram a classe Gpio com atributos que devem estar localizados nos endereços dos registros correspondentes e um método para alternar o estado pelo número de pernas:
Em seguida, determinamos a estrutura para GpioPin contendo o ponteiro para Gpio e o número da perna:

 struct GpioPin { volatile Gpio* port ; std::uint32_t pinNum ; } ; 

Em seguida, eles criaram uma série de LEDs que ficam nas pernas específicas da porta e a Toggle() chamando o método Toggle() de cada LED:

 const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9} } ; struct LedsDriver { __forceinline static inline void ToggelAll() { for (auto& it: leds) { it.port->Toggle(it.pinNum); } } } ; 

Bem, na verdade todo o código:
 constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; class Gpio { public: __forceinline inline void Toggle(const std::uint8_t bitNum) volatile { Odr ^= bitNum ; } private: volatile std::uint32_t Moder; volatile std::uint32_t Otyper; volatile std::uint32_t Ospeedr; volatile std::uint32_t Pupdr; volatile std::uint32_t Idr; volatile std::uint32_t Odr; } ; //    static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6); struct GpioPin { volatile Gpio* port ; std::uint32_t pinNum ; } ; const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9} } ; struct LedsDriver { __forceinline static inline void ToggelAll() { for (auto& it: leds) { it.port->Toggle(it.pinNum); } } } ; int main() { for(;;) { LedsContainer::ToggleAll() ; delay(); } return 0 ; } 


Estatísticas de seu código na otimização Média:
275 bytes de memória de código somente leitura
1 byte de memória de dados somente leitura
8 bytes de memória de leitura e gravação de dados

Uma boa solução, mas consome muita memória :)

Minha decisão


Claro, decidi não procurar maneiras simples e decidi agir de maneira séria :).
Os LEDs estão em diferentes portas e pernas diferentes. A primeira coisa que você precisa é criar a classe Port , mas, para se livrar dos ponteiros e variáveis ​​que ocupam a RAM, você precisa usar métodos estáticos. A classe de porta pode ficar assim:

 template <std::uint32_t addr> struct Port { //  -  }; 

Como parâmetro do modelo, ele terá um endereço de porta. No "#include "stm32f411xe.h" , por exemplo, para a porta A, é definido como GPIOA_BASE. Mas não temos permissão para usar os cabeçalhos, portanto, precisamos criar nossa própria constante. Como resultado, a classe pode ser usada assim:

 constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; 

Para piscar, você precisa do método Toggle (const std :: uint8_t bit), que alternará o bit necessário usando uma operação OR exclusiva. O método deve ser estático, adicione-o à classe:

 template <std::uint32_t addr> struct Port { //   __forceinline,        __forceinline inline static void Toggle(const std::uint8_t bitNum) { *reinterpret_cast<std::uint32_t*>(addr+20) ^= (1 << bitNum) ; //addr + 20  ODR  } }; 

Excelente Port<> , ele pode mudar o estado das pernas. O LED fica em uma perna específica, portanto, é lógico criar uma classe Pin , que terá a Port<> e o número da perna como parâmetros do modelo. Como o tipo de Port<> é o modelo, ou seja, diferente para diferentes portas, só podemos transmitir o tipo universal T.

 template <typename T, std::uint8_t pinNum> struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; 

É ruim que possamos passar qualquer bobagem do tipo T que tenha um método Toggle() e isso funcionará, embora se presuma que devemos passar apenas o tipo Port<> . Para PortBase protegermos disso, faremos o Port<> herdar da classe base do PortBase e, no modelo, verificaremos se nosso tipo passado é realmente baseado no PortBase . Temos o seguinte:

 constexpr std::uint32_t OdrAddrShift = 20U; struct PortBase { }; template <std::uint32_t addr> struct Port: PortBase { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum, class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> //   struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; 

Agora o modelo é instanciado apenas se nossa classe tiver a classe base PortBase .
Em teoria, você já pode usar essas classes, vamos ver o que acontece sem a otimização:

 using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; using Led1 = Pin<PortA, 5> ; using Led2 = Pin<PortC, 5> ; using Led3 = Pin<PortC, 8> ; using Led4 = Pin<PortC, 9> ; int main() { for(;;) { Led1::Toggle(); Led2::Toggle(); Led3::Toggle(); Led4::Toggle(); delay(); } return 0 ; } 

271 bytes de memória de código somente leitura
1 byte de memória de dados somente leitura
24 bytes de memória de leitura e gravação de dados

De onde vieram esses 16 bytes extras na RAM e 16 bytes na ROM. Elas vêm do fato de que passamos o parâmetro bit para a função Toggle (const std :: uint8_t bit) da classe Port, e o compilador, ao entrar na função principal, salva 4 registros adicionais na pilha pela qual esse parâmetro passa e os utiliza. registradores nos quais os valores do número da perna de cada pino são armazenados e, ao sair do main, restaura esses registros da pilha. E, embora, em essência, esse seja um tipo de trabalho completamente inútil, uma vez que as funções são integradas, mas o compilador age em total conformidade com o padrão.
Você pode se livrar disso removendo a classe de porta em geral, passando o endereço da porta como um parâmetro de modelo para a classe Pin e, dentro do método Toggle() , calcule o endereço do registro ODR:

 constexpr std::uint32_t OdrAddrShift = 20U; template <std::uint32_t addr, std::uint8_t pinNum, struct Pin { __forceinline inline static void Toggle() { *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift ) ^= (1 << bit) ; } } ; using Led1 = Pin<GpioaBaseAddr, 5> ; 

Mas isso não parece muito bom e amigável. Portanto, esperamos que o compilador remova essa preservação desnecessária de registro com um pouco de otimização.

Colocamos otimização no Medium e vemos o resultado:
251 bytes de memória de código somente leitura
1 byte de memória de dados somente leitura
8 bytes de memória de leitura e gravação de dados

Uau uau uau ... temos 4 bytes a menos
código
255 bytes de memória de código somente leitura
1 byte de memória de dados somente leitura
8 bytes de memória de leitura e gravação de dados


Como isso pode ser? Vamos dar uma olhada no assembler no depurador para o código C ++ (à esquerda) e o código C (à direita):

imagem

Pode-se ver que, primeiro, o compilador incorporou todas as funções, agora não há chamadas e, em segundo lugar, otimizou o uso de registradores. Pode ser visto, no caso do código C, o compilador usa o registro R1 ou R2 para armazenar os endereços de porta e executa operações adicionais cada vez que o bit é alternado (salve o endereço no registro em R1 ou R2). No segundo caso, ele usa apenas o registro R1 e, como as três últimas chamadas de comutação são sempre da porta C, não é mais necessário salvar o mesmo endereço da porta C no registro. Como resultado, 2 equipes e 4 bytes são salvos.

Aqui está um milagre dos compiladores modernos :) Bem, tudo bem. Em princípio, pode-se parar por aí, mas vamos seguir em frente. Não acho que seja possível otimizar qualquer outra coisa, embora provavelmente não esteja certo, se você tiver idéias, escreva nos comentários. Mas com a quantidade de código em main () você pode trabalhar.

Agora eu quero que todos os LEDs estejam em algum lugar no contêiner e você possa chamar o método, mudar tudo ... Algo assim:

 int main() { for(;;) { LedsContainer::ToggleAll() ; delay(); } return 0 ; } 

Não inseriremos estupidamente a comutação de 4 LEDs na função LedsContainer :: ToggleAll, porque não é interessante :). Queremos colocar os LEDs em um contêiner e depois examiná-los e chamar o método Toggle () em cada um.

Os alunos usaram uma matriz para armazenar ponteiros para LEDs. Mas tenho tipos diferentes, por exemplo: Pin<PortA, 5> , Pin<PortC, 5> e não consigo armazenar ponteiros para tipos diferentes em uma matriz. Você pode fazer uma aula de base virtual para todos os Pinos, mas uma tabela de funções virtuais será exibida e eu não conseguirei ganhar alunos.

Portanto, usaremos a tupla. Permite armazenar objetos de diferentes tipos. Este caso terá a seguinte aparência:

 class LedsContainer { private: constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } 

Há um ótimo recipiente, ele armazena todos os LEDs. Agora adicione o método ToggleAll() a ele:

 class LedsContainer { public: __forceinline static inline void ToggleAll() { //        } private: constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } 

Você não pode simplesmente percorrer os elementos de uma tupla, pois o elemento de tupla deve ser recebido apenas no estágio de compilação. Para acessar os elementos da tupla, existe um método de obtenção de modelo. Bem, isto é se escrevermos std::get<0>(records).Toggle() , o método Toggle() será chamado para o objeto da classe Pin<PortA, 5> , se std::get<1>(records).Toggle() , então o método Toggle() é chamado para o objeto da classe Pin<Port, 5> e assim por diante ...

Você pode limpar o nariz dos alunos e simplesmente escrever:

  __forceinline static inline void ToggleAll() { std::get<0>(records).Toggle(); std::get<1>(records).Toggle(); std::get<2>(records).Toggle(); std::get<3>(records).Toggle(); } 

Mas não queremos forçar o programador que oferecerá suporte a esse código e permitir que ele faça um trabalho adicional, gastando os recursos de sua empresa, por exemplo, caso outro LED apareça. Você precisará adicionar o código em dois lugares, na tupla e neste método - e isso não é bom e o proprietário da empresa não ficará muito satisfeito. Portanto, ignoramos a tupla usando métodos auxiliares:

 class class LedsContainer { friend int main() ; public: __forceinline static inline void ToggleAll() { //    3,2,1,0    ,     visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>()); } private: __forceinline template<std::size_t... index> static inline void visit(std::index_sequence<index...>) { Pass((std::get<index>(records).Toggle(), true)...); //    get<3>(records).Toggle(), get<2>(records).Toggle(), get<1>(records).Toggle(), get<0>(records).Toggle() } __forceinline template<typename... Args> static void inline Pass(Args... ) {//      } constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } 

Parece assustador, mas avisei no início do artigo que o método shizany não é muito comum ...

Toda essa mágica de cima na fase de compilação faz literalmente o seguinte:

 //  LedsContainer::ToggleAll() ; //   4 : Pin<Port, 9>().Toggle() ; Pin<Port, 8>().Toggle() ; Pin<PortC, 5>().Toggle() ; Pin<PortA, 5>().Toggle() ; //     Toggle() inline,   : *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 9) ; *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 8) ; *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 5) ; *reinterpret_cast<std::uint32_t*>(0x40020014 ) ^= (1 << 5) ; 

Vá em frente, compile e verifique o tamanho do código sem otimização:

O código que compila
 #include <cstddef> #include <tuple> #include <utility> #include <cstdint> #include <type_traits> //#include "stm32f411xe.h" #define __forceinline _Pragma("inline=forced") constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; constexpr std::uint32_t OdrAddrShift = 20U; struct PortBase { }; template <std::uint32_t addr> struct Port: PortBase { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum, class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; //using Led1 = Pin<PortA, 5> ; //using Led2 = Pin<PortC, 5> ; //using Led3 = Pin<PortC, 8> ; //using Led4 = Pin<PortC, 9> ; class LedsContainer { friend int main() ; public: __forceinline static inline void ToggleAll() { //    3,2,1,0    ,     visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>()); } private: __forceinline template<std::size_t... index> static inline void visit(std::index_sequence<index...>) { Pass((std::get<index>(records).Toggle(), true)...); } __forceinline template<typename... Args> static void inline Pass(Args... ) { } constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } ; void delay() { for (int i = 0; i < 1000000; ++i){ } } int main() { for(;;) { LedsContainer::ToggleAll() ; //GPIOA->ODR ^= 1 << 5; //GPIOC->ODR ^= 1 << 5; //GPIOC->ODR ^= 1 << 8; //GPIOC->ODR ^= 1 << 9; delay(); } return 0 ; } 


Prova de montagem, desembalada conforme planejado:
imagem

Vemos que a memória é um exagero, mais 18 bytes. Os problemas são os mesmos, mais outros 12 bytes. Eu não entendi de onde eles vieram ... talvez alguém explique.
283 bytes de memória de código somente leitura
1 byte de memória de dados somente leitura
24 bytes de memória de leitura e gravação de dados

Agora, a mesma coisa na otimização Média e eis que ... temos código idêntico às implementações de C ++ na testa e, de maneira mais otimizada, código C.
251 bytes de memória de código somente leitura
1 byte de memória de dados somente leitura
8 bytes de memória de leitura e gravação de dados

Montador
imagem

Como você pode ver, ganhei e fui para as Ilhas Canárias e tenho o prazer de descansar em Chelyabinsk :), mas os alunos também foram ótimos, passaram no exame com sucesso!

Quem se importa, o código está aqui

Onde posso usar isso? Bem, eu vim com, por exemplo, tais, temos parâmetros na memória EEPROM e uma classe que descreve esses parâmetros (leitura, gravação, inicialização para o valor inicial). A classe é modelo, como Param<float<>> , Param<int<>> e você precisa, por exemplo, de redefinir todos os parâmetros para os valores padrão. É aqui que você pode colocar todos eles em uma tupla, pois o tipo é diferente e chame o método SetToDefault() em cada parâmetro. É verdade que, se houver 100 desses parâmetros, a ROM comerá muito, mas a RAM não sofrerá.

PS Devo admitir que, na otimização máxima, esse código tem o mesmo tamanho que em C e na minha solução. E todos os esforços do programador para melhorar o código se resumem ao mesmo código do assembler.

P.S1 Obrigado 0xd34df00d por bons conselhos. Você pode simplificar a descompactação de uma tupla com std::apply() . O código da função ToggleAll() simplifica para isso:

  __forceinline static inline void ToggleAll() { std::apply([](auto... args) { (args.Toggle(), ...); }, records); } 

Infelizmente, no IAR, o std :: apply ainda não está implementado na versão atual, mas também funcionará. Consulte a implementação com o std :: apply

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


All Articles