Trabalhe com a lista de pinos, em C ++ para microcontroladores (usando o CortexM como exemplo)


Boa saúde a todos!


Em um artigo anterior, prometi escrever sobre como você pode trabalhar com uma lista de portas.
Devo dizer imediatamente que tudo já havia sido decidido antes de mim em 2010, eis o artigo: Trabalhando com as portas de entrada / saída de microcontroladores em C ++ . A pessoa que escreveu isso em 2010 é simplesmente bonita.


Fiquei um pouco envergonhado por fazer o que já foi feito há 10 anos, então decidi não esperar por 2020, mas em 2019 para repetir a decisão há 9 anos, não será tão idiota.


No artigo acima, o trabalho com listas de tipos foi feito usando C ++ 03, quando mais modelos tinham um número fixo de parâmetros e funções não podiam ser expressões constexpr. Desde então, o C ++ mudou um pouco, então vamos tentar fazer o mesmo, mas no C ++ 17. Bem-vindo ao gato:


Desafio


Portanto, a tarefa é instalar ou remover vários pinos do processador de uma só vez, que são combinados em uma lista. Os pinos podem estar localizados em portas diferentes, apesar disso, essa operação deve ser realizada da maneira mais eficiente possível.


Na verdade, o que queremos fazer pode ser mostrado com o código:


using Pin1 = Pin<GPIO, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; int main() { //    Pin    : //   GPIOA  10 GPIOA->BSRR = 10 ; // (1<<1) | (1 << 3) ; //   GPIOB  2 GPIOB->BSRR = 2 ; // (1 << 1) //   GPIOC  6 GPIOB->BSRR = 6 ; // (1 << 1) | (1 << 2); PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; return 0; } 

Sobre o registro BSRR

Para quem não conhece os assuntos do microcontrolador, o GPIOA->BSRR é responsável pela instalação atômica ou redefinição de valores nas pernas do microcontrolador. Este registro é de 32 bits. Os primeiros 16 bits são responsáveis ​​pela configuração 1 nas pernas, os segundos 16 bits pela configuração 0 nas pernas.


Por exemplo, para definir a perna número 3 como 1, você precisa definir o terceiro bit como 1. BSRR registro BSRR Para redefinir a perna número 3 como 0, você precisa definir 19 bits para 1 no mesmo registro BSRR .


Um esquema generalizado de etapas para resolver esse problema pode ser representado da seguinte maneira:



Bem, em outras palavras:


Para o compilador fazer por nós:


  • Verifique se a lista contém apenas pinos exclusivos
  • criando uma lista de portas, determinando em quais portas o Pin está ativado,
  • calculando o valor a ser colocado em cada porta

E então o programa


  • defina esse valor

E você precisa fazer isso da maneira mais eficiente possível, para que, mesmo sem otimização, o código seja mínimo. Na verdade, essa é a tarefa toda.


Vamos começar com o primeiro modismo: verificar se a lista contém um PIN exclusivo.


Verifique a lista de exclusividade


Deixe-me lembrá-lo de que temos uma lista de Pins:


 PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> ; 

Inadvertidamente pode fazer isso:


 PinsPack<Pin1, Pin2, Pin3, Pin4, Pin1> ; //     Pin1 

Eu gostaria que o compilador entendesse esse erro e informe o pianista sobre isso.


Vamos verificar a lista de exclusividade da seguinte maneira:


  • Na lista de fontes, crie uma nova lista sem duplicatas,
  • Se o tipo da lista de fontes e o tipo da lista sem duplicatas não corresponderem, o PIN será o mesmo na lista de fontes e o programador cometeu um erro.
  • Se combinarem, tudo está bem, não há duplicatas.

Para criar uma nova lista sem duplicatas, um colega aconselhou a não reinventar a roda e seguir a abordagem da biblioteca Loki. Eu tenho essa abordagem e roubou. Quase o mesmo que em 2010, mas com um número variável de parâmetros.


O código que foi emprestado de um colega que emprestou a ideia de Loki
 namespace PinHelper { template<typename ... Types> struct Collection { }; /////////////////   NoDuplicates   LOKI //////////////// template<class X, class Y> struct Glue; template<class T, class... Ts> struct Glue<T, Collection<Ts...>> { using Result = Collection<T, Ts...>; }; template<class Q, class X> struct Erase; template<class Q> struct Erase<Q, Collection<>> { using Result = Collection<>;}; template<class Q, class... Tail> struct Erase<Q, Collection<Q, Tail...>> { using Result = Collection<Tail...>;}; template<class Q, class T, class... Tail> struct Erase<Q, Collection<T, Tail...>> { using Result = typename Glue<T, typename Erase<Q, Collection<Tail...>>::Result>::Result;}; template <class X> struct NoDuplicates; template <> struct NoDuplicates<Collection<>> { using Result = Collection<>; }; template <class T, class... Tail> struct NoDuplicates< Collection<T, Tail...> > { private: using L1 = typename NoDuplicates<Collection<Tail...>>::Result; using L2 = typename Erase<T,L1>::Result; public: using Result = typename Glue<T, L2>::Result; }; ///////////////// LOKI //////////////// } 

Como isso pode ser usado agora? Sim, é muito simples:


 using Pin1 = Pin<GPIOC, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; using Pin6 = Pin<GPIOC, 1>; int main() { //  Pin1  ,    Pin6      using PinList = Collection<Pin1, Pin2, Pin3, Pin4, Pin1, Pin6> ; using TPins = typename NoDuplicates<PinList>::Result; //  static_assert.        // : Collection<Pin1, Pin2, Pin3, Pin4, Pin1, Pin6> //   : Collection<Pin1, Pin2, Pin3, Pin4> // ,    static_assert(std::is_same<TPins, PinList>::value, ":    ") ; return 0; } 

Bem, isto é se você definir incorretamente a lista de pinos e acidentalmente dois pinos idênticos forem indicados na lista, o programa não será compilado e o compilador apresentará um erro: "Problema: os mesmos pinos na lista".


A propósito, para garantir a lista correta de pinos para portas, você pode usar a seguinte abordagem:
 //        // PinsPack<Port<GPIOB, 0>, Port<GPIOB, 1> ... Port<GPIOB, 15>> using GpiobPort = typename GeneratePins<15, GPIOB>::type //      using GpioaPort = typename GeneratePins<15, GPIOA>::type int main() { //    :  GPIOA.0  1 Gpioa<0>::Set() ; // GPIOB.1  0 Gpiob<1>::Clear() ; using LcdData = Collection<Gpioa<0>, Gpiob<6>, Gpiob<2>, Gpioa<3>, Gpioc<7>, Gpioa<4>, Gpioc<3>, Gpioc<10>> ; using TPinsLcd = typename NoDuplicates<LcdData>::Result; static_assert(std::is_same<TPinsB, LcdData>::value, ":        LCD") ; // A      LcdData::Write('A'); } 

Já escrevemos muito aqui, mas até agora não existe uma única linha de código real que entre no microcontrolador. Se todos os pinos estiverem definidos corretamente, o programa de firmware será assim:


 int main() { return 0 ; } 

Vamos adicionar um código e tentar criar o método Set() para definir os pinos na lista.


Método de instalação do pino da porta


Vamos avançar um pouco até o final da tarefa. Por fim, é necessário implementar o método Set() , que automaticamente, com base no PIN da lista, determinaria quais valores em qual porta deve ser instalada.


O código que queremos
 using Pin1 = Pin<GPIOA, 1>; using Pin2 = Pin<GPIOB, 2>; using Pin3 = Pin<GPIOA, 2>; using Pin4 = Pin<GPIOC, 1>; using Pin5 = Pin<GPIOA, 3>; int main() { PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; //      3   // GPIOA->BSRR = 14 ; // (1<<1) | (1 << 2) | (1 << 3) ; // GPIOB->BSRR = 4 ; // (1 << 2) // GPIOB->BSRR = 2 ; // (1 << 1); } 

Portanto, declaramos uma classe que conterá uma lista de pinos e nela definimos o método estático público Set() .


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; public: __forceinline static void Set(std::size_t mask) { } } ; 

Como você pode ver, o método Set(size_t mask) usa algum tipo de valor (mask). Essa máscara é o número que você precisa colocar nas portas. Por padrão, é 0xffffffff, o que significa que queremos colocar todos os pinos na lista (máximo de 32). Se você passar outro valor para lá, por exemplo, 7 == 0b111, somente os 3 primeiros pinos da lista deverão ser instalados e assim por diante. I.e. máscara sobreposta na lista de pinos.


Listagem de portas


Para poder instalar qualquer coisa nos pinos, é necessário saber em quais portas esses pinos estão. Cada pino está vinculado a uma porta específica e podemos extrair essas portas da classe Pin e criar uma lista dessas portas.


Nossos pinos são atribuídos a diferentes portas:


 using Pin1 = Pin<Port<GPIOA>, 1>; using Pin2 = Pin<Port<GPIOB>, 2>; using Pin3 = Pin<Port<GPIOA>, 2>; using Pin4 = Pin<Port<GPIOC>, 1>; using Pin5 = Pin<Port<GPIOA>, 3>; 

Esses 5 pinos têm apenas 3 portas exclusivas (GPIOA, GPIOB, GPIOC). Se declararmos uma lista de PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> , dela precisamos obter uma lista de três portas Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>>


A classe Pin contém o tipo de porta e, de forma simplificada, fica assim:


 template<typename Port, uint8_t pinNum> struct Pin { using PortType = Port ; static constexpr uint32_t pin = pinNum ; ... } 

Além disso, você ainda precisa definir uma estrutura para esta lista; será apenas uma estrutura de modelo que recebe um número variável de argumentos de modelo.


 template <typename... Types> struct Collection{} ; 

Agora, definimos uma lista de portas exclusivas e, ao mesmo tempo, verificamos que a lista de pinos não contém os mesmos pinos. Isso é fácil de fazer:


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: //      using TPins = typename NoDuplicates<Collection<Ts...>>::Result; //           static_assert(std::is_same<TPins, Collection<Ts...>>::value, ":    ") ; //     using Ports = typename NoDuplicates<Collection<typename Ts::PortType...>>::Result; ... } ; 

Vá em frente ...


Bypass da lista de portas


Após receber a lista de portas, agora você precisa ignorá-la e fazer algo com cada porta. De uma forma simplificada, podemos dizer que devemos declarar uma função que receberá uma lista de portas e uma máscara para a lista de pinos na entrada.


Como devemos ignorar uma lista cujo tamanho não é conhecido com certeza, a função será modelo com um número variável de parâmetros.


Iremos "recursivamente", enquanto ainda houver parâmetros no modelo, chamaremos uma função com o mesmo nome.


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: __forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask) { // ,       if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ; } } } 

Então, aprendemos como ignorar a lista de portas, mas além do bypass, você precisa fazer algum trabalho útil, a saber, instalar algo na porta.


 __forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask) { //      auto result = GetPortValue<Port>(mask) ; //      Port::Set(result) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ; } } 

Este método será executado em tempo de execução, pois o parâmetro mask é passado para a função de fora. E devido ao fato de que não podemos garantir que uma constante será passada para o método SetPorts() , o método GetValue() também começará a ser executado em tempo de execução.


E embora, no artigo Trabalhando com portas de entrada / saída de microcontroladores em C ++, esteja escrito que, em um método semelhante, o compilador determinou que uma constante foi passada e calculou o valor para gravar na porta no estágio de compilação, meu compilador fez esse truque apenas na otimização máxima.
Gostaria que GetValue() executado em tempo de compilação com qualquer configuração do compilador.


Eu não encontrei no padrão como o compilador deve liderar o compilador nesse caso, mas, a julgar pelo fato de que o compilador IAR faz isso apenas no nível máximo de otimização, provavelmente não é regulado pelo padrão ou não deve ser tomado como uma expressão constexpr.
Se alguém souber, escreva nos comentários.


Para garantir a transferência explícita de um valor constante, faremos um método adicional com a passagem de mask no modelo:


 __forceinline template<std::size_t mask, typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>) { using MyPins = PinsPack<Ts...> ; //    compile time,    value    constexpr auto result = GetPortValue<Port>(mask) ; Port::Set(result) ; if constexpr (sizeof ...(Ports) != 0U) { MyPins::template SetPorts<mask,Ports...>(Collection<Ports...>()) ; } } 

Portanto, agora podemos revisar a lista de pinos, extrair as portas deles e criar uma lista exclusiva de portas às quais eles estão vinculados; em seguida, revisar a lista de portas criada e definir o valor necessário em cada porta.
Resta calcular este valor .


Cálculo do valor a ser definido na porta


Temos uma lista de portas que obtivemos da lista Pin, por exemplo, esta é uma lista: Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>> .
Você precisa pegar um elemento desta lista, por exemplo, a porta GPIOA e, na lista Pin, encontre todos os pinos anexados a essa porta e calcule o valor para instalação na porta. E então faça o mesmo com a próxima porta.


Mais uma vez: No nosso caso, a lista de pinos da qual obter uma lista de portas exclusivas é a seguinte:
 using Pin1 = Pin<Port<GPIOC>, 1>; using Pin2 = Pin<Port<GPIOB>, 1>; using Pin3 = Pin<Port<GPIOA>, 1>; using Pin4 = Pin<Port<GPIOC>, 2>; using Pin5 = Pin<Port<GPIOA>, 3>; using Pins = PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> ; 

Portanto, para a porta GPIOA, o valor deve ser (1 << 1 ) | (1 << 3) = 10 (1 << 1 ) | (1 << 3) = 10 e para a porta GPIOC - (1 << 1) | (1 << 2) = 6 (1 << 1) | (1 << 2) = 6 e para GPIOB (1 << 1 ) = 2


A função de cálculo aceita a porta solicitada e, se o Pin estiver na mesma porta que a porta solicitada, ele deverá definir a máscara na máscara correspondente à posição deste Pina na lista, unidade (1).
Não é fácil explicar em palavras, é melhor olhar diretamente para o código:


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: __forceinline template<class QueryPort> constexpr static auto GetPortValue(std::size_t mask) { std::size_t result = 0; //  ,       // 1. ,        // 2.            // e (.     ), ,  Pin   0  //        10,      //    ( )  (1 << 10)    // 3.    1   // 4.   1-3      pass{(result |= ((std::is_same<QueryPort, typename Ts::PortType>::value ? 1 : 0) & mask) * (1 << Ts::pin), mask >>= 1)...} ; return result; } } ; 

Configurando o valor calculado para cada porta para portas


Agora sabemos o valor que precisa ser definido em cada porta. Resta concluir o método público Set() , que ficará visível para o usuário, para que toda essa economia seja chamada:


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; __forceinline static void Set(std::size_t mask) { //        SetPorts(Ports(), mask) ; } } 

Como no caso de SetPorts() um método de modelo adicional para garantir a transferência da mask como uma constante, passando-a no atributo de modelo.


 template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; //    0xffffffff,      32  __forceinline template<std::size_t mask = 0xffffffffU> static void Set() { SetPorts<mask>(Ports()) ; } } 

Na forma final, nossa classe para a lista de pinos ficará assim:
 using namespace PinHelper ; template <typename ...Ts> struct PinsPack { using Pins = PinsPack<Ts...> ; private: using TPins = typename NoDuplicates<Collection<Ts...>>::Result; static_assert(std::is_same<TPins, Collection<Ts...>>::value, ":    ") ; using Ports = typename NoDuplicates<Collection<typename Ts::PortType...>>::Result; template<class Q> constexpr static auto GetPortValue(std::size_t mask) { std::size_t result = 0; auto rmask = mask ; pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & mask) * (1 << Ts::pin), mask>>=1)...}; pass{(result |= ((std::is_same<Q, typename Ts::PortType>::value ? 1 : 0) & ~rmask) * ((1 << Ts::pin) << 16), rmask>>=1)...}; return result; } __forceinline template<typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>, std::size_t mask) { auto result = GetPortValue<Port>(mask) ; Port::Set(result & 0xff) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template SetPorts<Ports...>(Collection<Ports...>(), mask) ; } } __forceinline template<std::size_t mask, typename Port, typename ...Ports> constexpr static void SetPorts(Collection<Port, Ports...>) { constexpr auto result = GetPortValue<Port>(mask) ; Port::Set(result & 0xff) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template SetPorts<mask, Ports...>(Collection<Ports...>()) ; } } __forceinline template<typename Port, typename ...Ports> constexpr static void WritePorts(Collection<Port, Ports...>, std::size_t mask) { auto result = GetPortValue<Port>(mask) ; Port::Set(result) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<Ports...>(Collection<Ports...>(), mask) ; } } __forceinline template<std::size_t mask, typename Port, typename ...Ports> constexpr static void WritePorts(Collection<Port, Ports...>) { Port::Set(GetPortValue<Port>(mask)) ; if constexpr (sizeof ...(Ports) != 0U) { Pins::template WritePorts<mask, Ports...>(Collection<Ports...>()) ; } } public: static constexpr size_t size = sizeof ...(Ts) + 1U ; __forceinline static void Set(std::size_t mask ) { SetPorts(Ports(), mask) ; } __forceinline template<std::size_t mask = 0xffffffffU> static void Set() { SetPorts<mask>(Ports()) ; } __forceinline static void Write(std::size_t mask) { WritePorts(Ports(), mask) ; } __forceinline template<std::size_t mask = 0xffffffffU> static void Write() { WritePorts<mask>(Ports()) ; } } ; 

Como resultado, a coisa toda pode ser usada da seguinte maneira:


 using Pin1 = Pin<GPIOC, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; using Pin6 = Pin<GPIOA, 5>; using Pin7 = Pin<GPIOC, 7>; using Pin8 = Pin<GPIOA, 3>; int main() { //1.   ,     3 ,  : // GPIOA->BSRR = (1 << 1) | (1 << 3) // GPIOB->BSRR = (1 << 1) // GPIOC->BSRR = (1 << 1) | (1 << 2) PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; //   Set<0xffffffffU>() //2.   ,  3 ,  : // GPIOA->BSRR = (1 << 1) // GPIOB->BSRR = (1 << 1) // GPIOC->BSRR = (1 << 1) | (1 << 2) PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5, Pin6>::Set<7>() ; //3.          , //   someRunTimeValue     ,  //  SetPorts   constexpr    PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set(someRunTimeValue) ; using LcdData = PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5, Pin6, Pin7, Pin8> ; LcdData::Write('A') ; } 

Um exemplo mais completo pode ser encontrado aqui:
https://onlinegdb.com/r1eoXQBRH


Desempenho


Como você se lembra, queríamos converter nossa chamada em 3 linhas, definida na porta A 10, porta B - 2 e porta C - 6


 using Pin1 = Pin<GPIO, 1>; using Pin2 = Pin<GPIOB, 1>; using Pin3 = Pin<GPIOA, 1>; using Pin4 = Pin<GPIOC, 2>; using Pin5 = Pin<GPIOA, 3>; int main() { //    Pin    : //   GPIOA  10 GPIOA->BSRR = 10 ; // (1<<1) | (1 << 3) ; //   GPIOB  2 GPIOB->BSRR = 2 ; // (1 << 1) //   GPIOC  6 GPIOB->BSRR = 6 ; // (1 << 1) | (1 << 2); PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5>::Set() ; return 0; } 

Vamos ver o que aconteceu com a otimização desativada completamente.



Eu tingi os valores das portas e as chamadas para definir esses valores para portas em verde. Pode-se ver que tudo é feito como pretendemos, o compilador para cada uma das portas calculou o valor e simplesmente chamou a função para definir esses valores para as portas necessárias.
Se as funções de instalação também são integradas, no final, recebemos uma chamada para gravar o valor no registro BSRR de cada porta.


Na verdade isso é tudo. Quem se importa, o código está aqui .


Um exemplo está aqui .


https://onlinegdb.com/ByeA50wTS

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


All Articles