Travailler avec la liste des broches, en C ++ pour les microcontrôleurs (en utilisant CortexM comme exemple)


Bonne santé à tous!


Dans un article précédent, j'ai promis d'écrire sur la façon dont vous pouvez travailler avec une liste de ports.
Je dois dire tout de suite que tout avait déjà été décidé avant moi déjà en 2010, voici l'article: Travailler avec les ports d'entrée / sortie des microcontrôleurs en C ++ . La personne qui a écrit cela en 2010 est tout simplement belle.


J'étais un peu gêné de faire ce qui était déjà fait il y a 10 ans, j'ai donc décidé de ne pas attendre 2020, mais de le faire en 2019 afin de répéter la décision il y a 9 ans, ce ne sera pas si stupide.


Dans l'article ci-dessus, le travail avec les listes de types a été effectué à l'aide de C ++ 03, lorsque plusieurs modèles avaient un nombre fixe de paramètres et que les fonctions ne pouvaient pas être des expressions constexpr. Depuis lors, C ++ a un peu changé, essayons donc de faire de même, mais en C ++ 17. Bienvenue chez cat:


Défi


Ainsi, la tâche consiste à installer ou à supprimer plusieurs broches de processeur à la fois, qui sont combinées dans une liste. Les broches peuvent être situées sur différents ports, malgré cela, une telle opération doit être effectuée aussi efficacement que possible.


En fait, ce que nous voulons faire peut être montré avec le code:


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; } 

À propos du registre BSRR

Pour ceux qui ne connaissent pas les affaires du microcontrôleur, le GPIOA->BSRR est responsable de l'installation atomique ou de la réinitialisation des valeurs sur les jambes du microcontrôleur. Ce registre est de 32 bits. Les 16 premiers bits sont chargés de mettre 1 sur les jambes, les seconds 16 bits pour mettre 0 sur les jambes.


Par exemple, pour définir la jambe numéro 3 sur 1, vous devez définir le troisième bit sur 1 dans le registre BSRR . Pour réinitialiser la jambe numéro 3 sur 0, vous devez définir 19 bits sur 1 dans le même registre BSRR .


Un schéma généralisé d'étapes pour résoudre ce problème peut être représenté comme suit:



Eh bien, en d'autres termes:


Pour que le compilateur fasse pour nous:


  • Vérifiez que la liste contient uniquement une broche unique
  • créer une liste de ports en déterminant sur quels ports se trouve la broche,
  • calculer la valeur à mettre dans chaque port

Et puis le programme


  • définir cette valeur

Et vous devez le faire aussi efficacement que possible, afin que même sans optimisation, le code soit minimal. En fait, c'est toute la tâche.


Commençons par la première mode: vérifier que la liste contient une broche unique.


Vérifiez l'unicité de la liste


Permettez-moi de vous rappeler que nous avons une liste de broches:


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

Par inadvertance peut le faire:


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

J'aimerais que le compilateur détecte une telle erreur et en informe le pianiste.


Nous vérifierons l'unicité de la liste comme suit:


  • À partir de la liste source, créez une nouvelle liste sans doublons,
  • Si le type de la liste source et le type de la liste sans doublons ne correspondent pas, alors le code PIN était le même dans la liste source et le programmeur a fait une erreur.
  • S'ils correspondent, alors tout va bien, il n'y a pas de doublons.

Pour créer une nouvelle liste sans doublons, un collègue a conseillé de ne pas réinventer la roue et de prendre l'approche de la bibliothèque Loki. J'ai cette approche et j'ai volé. Presque le même qu'en 2010, mais avec un nombre variable de paramètres.


Le code qui a été emprunté à un collègue qui a emprunté l'idée à 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 //////////////// } 

Comment cela peut-il être utilisé maintenant? Oui, c'est très simple:


 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; } 

Eh bien, c'est-à-dire si vous définissez de manière incorrecte la liste des broches, et accidentellement deux broches identiques sont indiquées dans la liste, le programme ne compilera pas et le compilateur donnera une erreur: «Problème: les mêmes broches dans la liste».


Soit dit en passant, pour garantir la liste correcte des broches pour les ports, vous pouvez utiliser l'approche suivante:
 //        // 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'); } 

Nous avons déjà beaucoup écrit ici, mais jusqu'à présent, il n'y a pas une seule ligne de code réel qui pénètre dans le microcontrôleur. Si toutes les broches sont définies correctement, le programme du micrologiciel ressemble à ceci:


 int main() { return 0 ; } 

Ajoutons du code et essayons de créer la méthode Set() pour définir les broches dans la liste.


Méthode d'installation des broches de port


Avançons un peu vers la fin de la tâche. En fin de compte, il est nécessaire d'implémenter la méthode Set() , qui, automatiquement, en fonction de la broche dans la liste, déterminerait les valeurs sur quel port devrait être installé.


Le code que nous voulons
 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); } 

Par conséquent, nous déclarons une classe qui contiendra une liste de Pins, et nous y définissons la méthode statique publique Set() .


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

Comme vous pouvez le voir, la méthode Set(size_t mask) prend une sorte de valeur (mask). Ce masque est le numéro que vous devez mettre dans les ports. Par défaut, c'est 0xffffffff, ce qui signifie que nous voulons mettre toutes les broches dans la liste (maximum 32). Si vous passez une autre valeur, par exemple, 7 == 0b111, seules les 3 premières broches de la liste doivent être installées, etc. C'est-à-dire masque superposé sur la liste des broches.


Liste des ports


Pour pouvoir installer quoi que ce soit dans les broches, vous devez savoir sur quels ports se trouvent ces broches. Chaque Pin est lié à un port spécifique et nous pouvons extraire ces ports de la classe Pin et créer une liste de ces ports.


Nos broches sont affectées à différents ports:


 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>; 

Ces 5 broches n'ont que 3 ports uniques (GPIOA, GPIOB, GPIOC). Si nous déclarons une liste de PinsPack<Pin1, Pin2, Pin3, Pin4, Pin5> , nous devons alors en obtenir une liste de trois ports: Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>>


La classe Pin contient le type de port et sous une forme simplifiée ressemble à ceci:


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

De plus, vous devez toujours définir une structure pour cette liste, ce sera juste une structure de modèle qui prend un nombre variable d'arguments de modèle


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

Nous définissons maintenant une liste de ports uniques et vérifions en même temps que la liste des broches ne contient pas les mêmes broches. C'est facile à faire:


 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; ... } ; 

Allez-y ...


Contournement de la liste des ports


Après avoir reçu la liste des ports, vous devez maintenant la contourner et faire quelque chose avec chaque port. Dans une forme simplifiée, nous pouvons dire que nous devons déclarer une fonction qui recevra une liste de ports et un masque pour la liste des broches à l'entrée.


Comme nous devons contourner une liste dont la taille n'est pas connue avec certitude, la fonction sera un modèle avec un nombre variable de paramètres.


Nous allons faire le tour "récursivement", alors qu'il y a encore des paramètres dans le template, nous appellerons une fonction du même nom.


 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) ; } } } 

Nous avons donc appris à contourner la liste des ports, mais en plus du contournement, vous devez effectuer un travail utile, à savoir installer quelque chose dans le port.


 __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) ; } } 

Cette méthode sera exécutée au moment de l'exécution, car le paramètre mask est passé à la fonction de l'extérieur. Et du fait que nous ne pouvons pas garantir qu'une constante sera transmise à la méthode SetPorts() , la méthode GetValue() commencera également à s'exécuter au moment de l'exécution.


Et bien que, dans l'article Travailler avec les ports d'entrée / sortie des microcontrôleurs en C ++, il soit écrit que, dans une méthode similaire, le compilateur a déterminé qu'une constante a été transmise et calculé la valeur d'écriture sur le port au stade de la compilation, mon compilateur n'a fait une telle astuce qu'à l'optimisation maximale.
Je voudrais que GetValue() exécutée au moment de la compilation avec tous les paramètres du compilateur.


Je n'ai pas trouvé dans la norme comment le compilateur devrait diriger le compilateur dans ce cas, mais à en juger par le fait que le compilateur IAR ne le fait qu'au niveau maximum d'optimisation, il n'est probablement pas réglementé par la norme ou ne doit pas être pris comme une expression constexpr.
Si quelqu'un le sait, écrivez dans les commentaires.


Pour assurer le transfert explicite d'une valeur constante, nous allons faire une méthode supplémentaire avec passage de mask dans le modèle:


 __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...>()) ; } } 

Ainsi, nous pouvons maintenant parcourir la liste des broches, en extraire les ports et créer une liste unique de ports auxquels ils sont liés, puis parcourir la liste de ports créée et définir la valeur requise dans chaque port.
Reste à calculer cette valeur .


Calcul de la valeur qui doit être définie dans le port


Nous avons une liste de ports que nous avons obtenus de la liste des broches, pour notre exemple, c'est une liste: Collection<Port<GPIOA>, Port<GPIOB>, Port<GPIOC>> .
Vous devez prendre un élément de cette liste, par exemple, le port GPIOA, puis dans la liste des broches trouver toutes les broches qui sont attachées à ce port et calculer la valeur pour l'installation dans le port. Et faites de même avec le port suivant.


Encore une fois: dans notre cas, la liste des broches à partir desquelles obtenir une liste de ports uniques est la suivante:
 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> ; 

Ainsi, pour le port GPIOA, la valeur doit être (1 << 1 ) | (1 << 3) = 10 (1 << 1 ) | (1 << 3) = 10 , et pour le port GPIOC - (1 << 1) | (1 << 2) = 6 (1 << 1) | (1 << 2) = 6 , et pour GPIOB (1 << 1 ) = 2


La fonction de calcul accepte le port demandé et si Pin est sur le même port que le port demandé, elle doit alors définir le masque dans le masque correspondant à la position de ce Pina dans la liste, unité (1).
Ce n'est pas facile à expliquer avec des mots, il vaut mieux regarder à droite dans le code:


 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; } } ; 

Définition de la valeur calculée pour chaque port sur les ports


Nous connaissons maintenant la valeur qui doit être définie dans chaque port. Il reste à compléter la méthode publique Set() , qui sera visible à l'utilisateur pour que toute cette économie s'appelle:


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

Comme dans le cas de SetPorts() allons créer une méthode de modèle supplémentaire pour garantir le transfert du mask tant que constante, en le passant dans l'attribut de modèle.


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

Dans sa forme finale, notre classe pour la liste des broches ressemblera à ceci:
 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()) ; } } ; 

En conséquence, le tout peut être utilisé comme suit:


 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') ; } 

Un exemple plus complet peut être trouvé ici:
https://onlinegdb.com/r1eoXQBRH


Performances


Comme vous vous en souvenez, nous voulions convertir notre appel en 3 lignes, réglées sur le port A 10, le port B - 2 et le port 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; } 

Voyons ce qui s'est passé avec l'optimisation complètement désactivée.



J'ai teinté les valeurs de port et les appels pour définir ces valeurs sur les ports en vert. On peut voir que tout est fait comme nous le voulions, le compilateur pour chacun des ports a calculé la valeur et a simplement appelé la fonction pour définir ces valeurs sur les ports requis.
Si les fonctions d'installation sont également effectuées en ligne, nous obtenons finalement un appel pour écrire la valeur dans le registre BSRR pour chaque port.


En fait, c'est tout. Peu importe, le code est ici .


Un exemple est ici .


https://onlinegdb.com/ByeA50wTS

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


All Articles