Comment faire clignoter 4 LED sur CortexM en utilisant C ++ 17, tuple et un peu de fantaisie

Bonne santé à tous!

Lorsque j'enseigne aux étudiants comment développer des logiciels embarqués pour les microcontrôleurs à l'université, j'utilise le C ++ et parfois je donne aux étudiants qui sont particulièrement intéressés par toutes sortes de tâches d'identifier les étudiants doués qui sont particulièrement malades .

Une fois de plus, ces étudiants ont eu la tâche de faire clignoter 4 LED en utilisant le langage C ++ 17 et la bibliothèque C ++ standard, sans connecter de bibliothèques supplémentaires, telles que CMSIS et leurs fichiers d'en-tête avec une description des structures de registre, et ainsi de suite ... Celui avec le code gagne en ROM sera la plus petite taille et la RAM la moins utilisée. L'optimisation du compilateur ne doit pas être supérieure à Medium. Compilateur IAR 8.40.1.
Le gagnant se rend aux Canaries et obtient 5 pour l'examen.

Moi non plus, je n’ai pas résolu ce problème auparavant. Je vais donc vous dire comment les élèves l’ont résolu et ce qui m’est arrivé. Je vous préviens tout de suite qu'il est peu probable qu'un tel code puisse être utilisé dans de vraies applications, c'est pourquoi j'ai posté la publication dans la section "Programmation anormale", mais qui sait.

Conditions de tâche


Il y a 4 LED sur les ports GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9. Ils ont besoin de cligner des yeux. Pour avoir quelque chose à comparer, nous avons pris le code écrit en 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 ; } 

La fonction delay() est ici purement formelle, un cycle régulier, elle ne peut pas être optimisée.
Il est supposé que les ports sont déjà configurés pour la sortie et que l'horloge leur est appliquée.
Je dirai également tout de suite que le bitbanging n'a pas été utilisé pour rendre le code portable.

Ce code prend 8 octets sur la pile et 256 octets en ROM sur l'optimisation moyenne
255 octets de mémoire de code en lecture seule
1 octet de mémoire de données en lecture seule
8 octets de mémoire de données en lecture-écriture

255 octets du fait qu'une partie de la mémoire est passée sous la table des vecteurs d'interruption, des appels aux fonctions IAR pour initialiser un bloc à virgule flottante, toutes sortes de fonctions de débogage et la fonction __low_level_init, où les ports eux-mêmes ont été configurés.

Ainsi, les exigences complètes sont:

  • La fonction main () doit contenir le moins de code possible
  • Vous ne pouvez pas utiliser de macros
  • Compilateur IAR 8.40.1 prenant en charge C ++ 17
  • Les fichiers d'en-tête CMSIS tels que "#include" stm32f411xe.h "ne peuvent pas être utilisés
  • Vous pouvez utiliser la directive __forceinline pour les fonctions en ligne
  • Optimisation du compilateur moyen

Décision des étudiants


En général, il y avait plusieurs solutions, je n'en montrerai qu'une ... ce n'est pas optimal, mais j'ai bien aimé.

Comme les en-têtes ne peuvent pas être utilisés, la première chose que les étudiants ont faite est la classe Gpio , qui devrait stocker un lien vers les registres de port à leurs adresses. Pour ce faire, ils utilisent une superposition de structure, très probablement ils ont pris l'idée d'ici: Superposition de structure :

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

Comme vous pouvez le voir, ils ont immédiatement identifié la classe Gpio avec des attributs qui devraient être situés aux adresses des registres correspondants et une méthode pour changer d'état par le nombre de branches:
Ensuite, nous avons déterminé la structure de GpioPin contenant le pointeur vers Gpio et le numéro de la jambe:

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

Ensuite, ils ont créé un tableau de LED qui reposent sur les jambes spécifiques du port et l'ont parcouru en appelant la méthode Toggle() de chaque 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); } } } ; 

Eh bien, en fait, tout le code:
 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 ; } 


Statistiques de leur code sur l'optimisation Medium:
275 octets de mémoire de code en lecture seule
1 octet de mémoire de données en lecture seule
8 octets de mémoire de données en lecture-écriture

Une bonne solution, mais elle prend beaucoup de mémoire :)

Ma décision


Bien sûr, j'ai décidé de ne pas chercher de moyens simples et j'ai décidé d'agir sérieusement :).
Les LED sont sur différents ports et différents pieds. La première chose dont vous avez besoin est de créer la classe Port , mais pour vous débarrasser des pointeurs et des variables qui prennent de la RAM, vous devez utiliser des méthodes statiques. La classe de port pourrait ressembler à ceci:

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

En tant que paramètre de modèle, il aura une adresse de port. Dans l'en- "#include "stm32f411xe.h" , par exemple, pour le port A, il est défini comme GPIOA_BASE. Mais nous ne sommes pas autorisés à utiliser les en-têtes, il nous suffit donc de créer notre propre constante. Par conséquent, la classe peut être utilisée comme suit:

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

Pour clignoter, vous avez besoin de la méthode Toggle (const std :: uint8_t bit), qui commutera le bit requis à l'aide d'une opération OU exclusive. La méthode doit être statique, ajoutez-la à la 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  } }; 

Excellent Port<> est, il peut changer l'état des jambes. La LED se trouve sur une jambe spécifique, il est donc logique de créer une Pin classe, qui aura le Port<> et le numéro de jambe comme paramètres de modèle. Étant donné que le type Port<> est un modèle, c'est-à-dire différent pour différents ports, nous ne pouvons transmettre que le type universel T.

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

Il est mauvais que nous puissions passer n'importe quel non-sens de type T qui a une méthode Toggle() et cela fonctionnera, bien qu'il soit supposé que nous ne devrions transmettre que le type Port<> . Pour PortBase protéger de cela, nous allons faire en sorte que Port<> hérite de la classe de base PortBase , et dans le modèle, nous vérifierons que notre type passé est bien basé sur PortBase . Nous obtenons ce qui suit:

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

Maintenant, le modèle n'est instancié que si notre classe a la classe de base PortBase .
En théorie, vous pouvez déjà utiliser ces classes, voyons ce qui se passe sans optimisation:

 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 octets de mémoire de code en lecture seule
1 octet de mémoire de données en lecture seule
24 octets de mémoire de données en lecture-écriture

D'où venaient ces 16 octets supplémentaires en RAM et 16 octets en ROM. Ils proviennent du fait que nous passons le paramètre bit à la fonction Toggle (const std :: uint8_t bit) de la classe Port, et le compilateur, lors de l'entrée dans la fonction principale, enregistre 4 registres supplémentaires sur la pile à travers laquelle ce paramètre passe, puis les utilise registres dans lesquels les valeurs du numéro de jambe pour chaque broche sont stockées et lorsque vous quittez principal restaure ces registres à partir de la pile. Et bien qu'il s'agisse essentiellement d'une sorte de travail complètement inutile, car les fonctions sont intégrées, mais le compilateur agit en totale conformité avec la norme.
Vous pouvez vous en débarrasser en supprimant la classe de port en général, en passant l'adresse de port en tant que paramètre de modèle pour la classe Pin , et à l'intérieur de la méthode Toggle() , calculez l'adresse du registre 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> ; 

Mais cela ne semble pas très bon et convivial. Par conséquent, nous espérons que le compilateur supprime cette conservation de registre inutile avec un peu d'optimisation.

Nous mettons l'optimisation sur Medium et voyons le résultat:
251 octets de mémoire de code en lecture seule
1 octet de mémoire de données en lecture seule
8 octets de mémoire de données en lecture-écriture

Wow wow wow ... nous avons 4 octets de moins
code
255 octets de mémoire de code en lecture seule
1 octet de mémoire de données en lecture seule
8 octets de mémoire de données en lecture-écriture


Comment est-ce possible? Jetons un coup d'œil à l'assembleur dans le débogueur pour le code C ++ (à gauche) et le code C (à droite):

image

On peut voir que, d'une part, le compilateur a intégré toutes les fonctions, maintenant il n'y a plus d'appels du tout, et d'autre part, il a optimisé l'utilisation des registres. On peut voir que dans le cas du code C, le compilateur utilise le registre R1 ou R2 pour stocker les adresses de port et effectue des opérations supplémentaires chaque fois que le bit est commuté (enregistrez l'adresse dans le registre soit dans R1 soit dans R2). Dans le second cas, il utilise uniquement le registre R1, et comme les 3 derniers appels de commutation sont toujours depuis le port C, il n'est plus nécessaire de sauvegarder la même adresse de port C dans le registre. En conséquence, 2 équipes et 4 octets sont enregistrés.

Ici, c'est un miracle des compilateurs modernes :) Eh bien, d'accord. En principe, on pourrait s'arrêter là, mais passons à autre chose. Je ne pense pas qu'il sera possible d'optimiser quoi que ce soit d'autre, bien que ce ne soit probablement pas correct, si vous avez des idées, écrivez dans les commentaires. Mais avec la quantité de code dans main (), vous pouvez travailler.

Maintenant, je veux que toutes les LED soient quelque part dans le conteneur, et vous pouvez appeler la méthode, tout changer ... Quelque chose comme ça:

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

On n'insérera pas bêtement la commutation de 4 LEDs dans la fonction LedsContainer :: ToggleAll, car ce n'est pas intéressant :). Nous voulons mettre les LED dans un conteneur, puis les parcourir et appeler la méthode Toggle () sur chacune.

Les élèves ont utilisé un tableau pour stocker des pointeurs sur des LED. Mais j'ai différents types, par exemple: Pin<PortA, 5> , Pin<PortC, 5> , et je ne peux pas stocker de pointeurs vers différents types dans un tableau. Vous pouvez créer une classe de base virtuelle pour tous les codes PIN, mais un tableau de fonctions virtuelles apparaîtra et je ne réussirai pas à gagner des étudiants.

Par conséquent, nous utiliserons le tuple. Il vous permet de stocker des objets de différents types. Ce cas ressemblera à ceci:

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

Il y a un grand conteneur, il stocke toutes les LED. Maintenant, ajoutez-y la méthode ToggleAll() :

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

Vous ne pouvez pas simplement parcourir les éléments d'un tuple, car l'élément tuple ne doit être reçu qu'au stade de la compilation. Pour accéder aux éléments du tuple, il existe une méthode d'obtention de modèle. Eh bien, c'est-à-dire si nous écrivons std::get<0>(records).Toggle() , alors la méthode Toggle() est appelée pour l'objet de la classe Pin<PortA, 5> , si std::get<1>(records).Toggle() , alors la méthode Toggle() est appelée pour l'objet de la classe Pin<Port, 5> et ainsi de suite ...

Vous pouvez essuyer le nez de vos élèves et écrire simplement:

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

Mais nous ne voulons pas forcer le programmeur qui prendra en charge ce code et lui permettra de faire un travail supplémentaire, en dépensant les ressources de son entreprise, par exemple, au cas où une autre LED apparaîtrait. Vous devrez ajouter le code à deux endroits, dans le tuple et dans cette méthode - et ce n'est pas bon et le propriétaire de l'entreprise ne sera pas très content. Par conséquent, nous contournons le tuple à l'aide de méthodes d'assistance:

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

Cela semble effrayant, mais j'ai averti au début de l'article que la méthode shizany n'est pas très ordinaire ...

Toute cette magie d'en haut au stade de la compilation fait littéralement ce qui suit:

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

Allez-y, compilez et vérifiez la taille du code sans optimisation:

Le code qui compile
 #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 ; } 


Epreuve de montage, déballée comme prévu:
image

Nous voyons que la mémoire est exagérée, 18 octets de plus. Les problèmes sont les mêmes, plus 12 autres octets. Je ne comprenais pas d'où ils venaient ... peut-être que quelqu'un va expliquer.
283 octets de mémoire de code en lecture seule
1 octet de mémoire de données en lecture seule
24 octets de mémoire de données en lecture-écriture

Maintenant, la même chose sur l'optimisation moyenne et lo and behold ... nous avons obtenu un code identique aux implémentations C ++ dans le front et de manière plus optimale le code C.
251 octets de mémoire de code en lecture seule
1 octet de mémoire de données en lecture seule
8 octets de mémoire de données en lecture-écriture

Assembleur
image

Comme vous pouvez le voir, j'ai gagné et je suis allé aux îles Canaries et je suis heureux de me reposer à Tcheliabinsk :), mais les étudiants étaient également super, ils ont réussi l'examen!

Peu importe, le code est ici

Où puis-je utiliser cela, eh bien, j'ai trouvé, par exemple, tel, nous avons des paramètres dans la mémoire EEPROM et une classe décrivant ces paramètres (lecture, écriture, initialisation à la valeur initiale). La classe est un modèle, comme Param<float<>> , Param<int<>> et vous devez, par exemple, réinitialiser tous les paramètres aux valeurs par défaut. C'est là que vous pouvez les mettre tous dans un tuple, car le type est différent et appeler la méthode SetToDefault() sur chaque paramètre. Certes, s'il existe 100 de ces paramètres, la ROM mangera beaucoup, mais la RAM n'en souffrira pas.

PS Je dois admettre qu'à l'optimisation maximale ce code a la même taille qu'en C et dans ma solution. Et tous les efforts du programmeur pour améliorer le code se résument au même code assembleur.

P.S1 Merci 0xd34df00d pour les bons conseils. Vous pouvez simplifier le décompactage d'un tuple avec std::apply() . Le code de la fonction ToggleAll() se simplifie alors comme ToggleAll() :

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

Malheureusement, dans l'IAR, std :: apply n'est pas encore implémenté dans la version actuelle, mais cela fonctionnera également, voir l' implémentation avec std :: apply

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


All Articles