Utilisation de C ++ et de modèles avec un nombre variable d'arguments lors de la programmation de microcontrôleurs

ARM avec noyau Cortex Mx (en utilisant STM32F10x comme exemple)


KDPV Le microcontrôleur ARM Cortex M3 STM32F103c8t6 est largement utilisé comme microcontrôleur 32 bits pour les projets amateurs. Comme pour presque tous les microcontrôleurs, il existe un SDK, y compris, entre autres, des fichiers d'en-tête C ++ pour déterminer la périphérie du contrôleur.

Et là, le port série, par exemple, est défini comme une structure de données, et une instance de cette structure est située dans la zone d'adresse réservée aux registres et nous avons accès à cette zone via un pointeur vers une adresse spécifique.

Pour ceux qui ne l'ont pas rencontré auparavant, je vais décrire un peu comment il est défini, les mêmes lecteurs qui connaissent cela peuvent sauter cette description.

Cette structure et son instance sont décrites comme suit:

/* =========================================================================*/ typedef struct { __IO uint32_t CR1; /*!< USART Control register 1, Address offset: 0x00 */ . . . __IO uint32_t ISR; /*!< USART Interrupt and status register, ... */ } USART_TypeDef; // USART_Type   . /* =========================================================================*/ #define USART1_BASE (APBPERIPH_BASE + 0x00013800) . . . #define USART1 ((USART_TypeDef *) USART1_BASE) #define USART1_BASE 0x400xx000U 

Vous pouvez voir plus de détails ici stm32f103xb.h ≈ 800 kB

Et si vous utilisez uniquement les définitions de ce fichier, vous devez écrire comme ceci (exemple d'utilisation du registre d'état du port série):

 // ---------------------------------------------------------------------------- if (USART1->ISR & (ONE_ISR_FLAG & OTHER_ISR_FLAG)) { } 

Et vous devez l'utiliser car les solutions propriétaires existantes connues sous le nom de CMSIS et HAL sont trop complexes pour être utilisées dans des projets amateurs.

Mais si vous écrivez en C ++, vous pouvez écrire comme ceci:

 // ---------------------------------------------------------------------------- USART_TypeDef & Usart1 = *USART1; // ---------------------------------------------------------------------------- if (Usart1.ISR & (ONE_ISR_FLAG & OTHER_ISR_FLAG)) { } 

Une référence mutable est initialisée avec un pointeur. C'est un petit soulagement, mais agréable. Mieux encore, bien sûr, pour écrire une petite classe wrapper dessus, alors que cette technique est toujours utile.

Bien sûr, je voudrais écrire immédiatement cette classe wrapper sur le port série (EUSART - émetteur-récepteur asinhrone série universel étendu), si attrayant, avec des fonctionnalités avancées, un émetteur-récepteur asynchrone série et pouvoir connecter notre petit microcontrôleur à un ordinateur de bureau ou un ordinateur portable, mais des microcontrôleurs Les Cortex se distinguent par un système d'horloge avancé et vous devrez commencer par cela, puis configurer les broches d'E / S correspondantes pour fonctionner avec les périphériques, car dans la série STM32F1xx, comme dans pattes d'autres microcontrôleurs ARM Cortex ne peuvent pas configurer simplement les broches du port à l'entrée ou à la sortie et le travail en même temps avec la périphérie.

Eh bien, commençons par activer le timing. Le système d'horloge est appelé registres RCC pour la commande d'horloge et représente également une structure de données, un pointeur auquel est affectée une valeur d'adresse spécifique.

 /* =========================================================================*/ typedef struct { . . . } RCC_TypeDef; 

Les champs de cette structure déclarés comme ceci, où __IO définit volatile:

 /* =========================================================================*/ __IO uint32_t CR; 

correspondent aux registres de RCC, et les bits individuels de ces registres sont activés ou les fonctions d'horloge de la périphérie du microcontrôleur. Tout cela est bien décrit dans la documentation (pdf) .

Un pointeur vers une structure est défini comme

 /* =========================================================================*/ #define RCC ((RCC_TypeDef *)RCC_BASE) 

Travailler avec des bits de registre sans utiliser le SDK ressemble généralement à ceci:

Voici l'inclusion du port A.

 // ---------------------------------------------------------------------------- RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; 

Vous pouvez activer deux bits ou plus à la fois

 // ---------------------------------------------------------------------------- RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; 

Cela semble un peu inhabituel pour C ++, ou quelque chose d'inhabituel. Il serait préférable d'écrire différemment, comme ceci, par exemple, en utilisant la POO.

 // ---------------------------------------------------------------------------- Rcc.PortOn(Port::A); 

Cela semble mieux, mais au XXIe siècle, nous irons un peu plus loin, utiliserons le C ++ 17 et écrirons à l'aide de modèles avec un nombre variable de paramètres encore plus beau:

 // ---------------------------------------------------------------------------- Rcc.PortOn<Port::A, Port::B>(); 

Où Rcc est défini comme ceci:

 // ---------------------------------------------------------------------------- TRcc & Rcc = *static_cast<TRcc *>RCC; 

À partir de là, nous allons commencer à construire un wrapper sur les registres d'horloge. Tout d'abord, nous définissons une classe et un pointeur (lien) vers celle-ci.

Au début, je voulais écrire dans la norme C ++ 11/14 en décompressant récursivement les paramètres d'un modèle de fonction. Un bon article à ce sujet est fourni à la fin de l'article, dans la section des liens.

 // ============================================================================ enum class GPort : uint32_t { A = RCC_APB2ENR_IOPAEN, B = RCC_APB2ENR_IOPBEN, C = RCC_APB2ENR_IOPCEN, }; // ---------------------------------------------------------------------------- class TRcc: public ::RCC_TypeDef { private: TRcc() = delete; ~TRcc() = delete; // ======================================================================== public: template<GPort... port> inline void PortOn(void) //    (inline) { //    -Og  -O0 APB2ENR |= SetBits<(uint32_t)port...>(); } // ------------------------------------------------------------------------ #define BITMASK 0x01 //    ,   #define MASKWIDTH 1 //      .   //          #undef. private: //   (fold)   . template<uint8_t bitmask> inline constexpr uint32_t SetBits(void) { //   ,  GPort  enum // (, , bitmask    ). // static_assert(bitmask < 16, " ."); return bitmask; } template<uint8_t bit1, uint8_t bit2, uint8_t... bit> inline constexpr uint32_t SetBits(void) { return SetBits<bit1>() | SetBits<bit2, bit...>(); } }; #undef BITMASK #undef MASKWIDTH // ------------------------------------------------------------------------ TRcc & Rcc = *static_cast<TRcc *>RCC; 

Considérez l'appel à la fonction d'activation de l'horloge du port:

  Rcc.PortOn<GPort::A>(); 

GCC le déploiera dans un tel ensemble de commandes:

  ldr r3, [pc, #376] ; (0x8000608 <main()+392>) ldr r0, [r3, #24] orr.w r0, r0, #4 str r0, [r3, #24] 

Cela at-il fonctionné? Vérifiez ensuite

  Rcc.PortOn<GPort::A, GPort::B, GPort::C>(); 

Hélas, le GCC pas si naïf a déployé l'appel de récursivité de fin séparément:

  ldr r3, [pc, #380] ; (0x8000614 <main()+404>) ldr r0, [r3, #24] orr.w r0, r0, #4 ; APB2ENR |= GPort::A str r0, [r3, #24] ldr r0, [r3, #24] orr.w r0, r0, #28 ; APB2ENR |= Gport::B | GPort::C str r0, [r3, #24] #24] 

Pour défendre GCC, je dois dire que ce n'est pas toujours le cas, mais seulement dans des cas plus complexes, ce qui sera visible lors de la mise en œuvre de la classe de port d'E / S. Eh bien, C ++ 17 est pressé de vous aider. Réécrivez la classe TRCC en utilisant les capacités de défilement intégrées.

 // ---------------------------------------------------------------------------- class TRcc: public ::RCC_TypeDef { private: TRcc() = delete; //     ,  ~TRcc() = delete; //    . // ======================================================================== public: template<GPort... port> inline void PortOn(void) //    (inline) { //    -Og  -O0 APB2ENR |= SetBits17<(uint32_t)port...>(); } // ------------------------------------------------------------------------ #define BITMASK 0x01 //    ,   #define MASKWIDTH 1 //      .   //          #undef. private: //   (fold)   . ++ 17. template<uint8_t... bitmask> inline constexpr uint32_t SetBits17(void) { return (bitmask | ...); //     ... | bit } }; #undef BITMASK #undef MASKWIDTH 

Maintenant, il s'est avéré:

 ldr r2, [pc, #372] ; (0x800060c <main()+396>) ldr r0, [r2, #24] orr.w r0, r0, #28 ; APB2ENR |= Gport::A | Gport::B | GPort::C str r0, [r3, #24] 

Et le code de classe est devenu plus simple.

Conclusion: C ++ 17 nous permet d'utiliser les modèles avec un nombre variable de paramètres pour obtenir le même ensemble minimal d'instructions (même lorsque l'optimisation est désactivée) que celui obtenu lors de l'utilisation du travail classique avec le microcontrôleur via les définitions de registre, mais en même temps, nous obtenons tous les avantages d'un typage C ++ fort, vérifie lors de la compilation, réutilisé à travers la structure des classes de base du code, etc.

Voici quelque chose comme ça écrit en C ++

 Rcc.PortOn<Port::A, Port::B, Port::C>(); 

Et le texte classique sur les registres:

 RCC->APB2 |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; 

dépliez dans un ensemble optimal d'instructions. Voici le code généré par GCC (optimisation off -Og):

  ldr r2, [pc, #372] ; (0x800060c <main()+396>) [  RCC] ldr r0, [r2, #0] ; r0 = RCC->APB2 // [  APB2] orr.w r0, r0, #160 ; r0 |= 0x10100000 str r0, [r2, #0] ; RCC->APB2 = r0 

Vous devez maintenant continuer à travailler et à écrire une classe de port d'entrée / sortie. Travailler avec des bits de port d'E / S est compliqué par le fait que quatre bits sont alloués pour la configuration d'un tronçon de port et, par conséquent, 64 bits de configuration sont requis pour un port 16 bits, qui sont divisés en deux registres CRL et CRH 32 bits. De plus, la largeur du masque binaire devient supérieure à 1. Mais ici, le défilement à travers C ++ 17 montre ses capacités.

image

Ensuite, la classe TGPIO sera écrite, ainsi que des classes pour travailler avec d'autres périphériques, un port série, I2C, SPI, DAP, des temporisateurs et bien plus encore, qui sont généralement présents dans les microcontrôleurs ARM Cortex et il sera alors possible de clignoter avec de telles LED.

Mais plus à ce sujet dans la note suivante. Sources du projet sur github .

Articles Internet utilisés pour rédiger des notes


Modèles avec un nombre variable d'arguments en C ++ 11 .
Innovations dans les modèles .
Innovation du langage C ++ 17. Partie 1. Convolution et dérivation .
Liste de liens vers la documentation des microcontrôleurs STM .
Macros de paramètres variables

Articles sur Khabr qui m'ont poussé à écrire cette note


Feu de circulation sur Attiny13 .

Julian Assange arrêté par la police britannique
L'espace comme un vague souvenir

Écrit le 04/12/2019 - Happy Cosmonautics Day!

PS
STM32F103c8t6 dans Stm CubeMx Image STM32F103c8t6 de CubeMX.

Comme point de départ, le texte créé par l'extension Eclips pour travailler avec les microcontrôleurs GNU MCU Eclipse ARM Embedded et STM CubeMX est utilisé , c'est-à-dire qu'il existe des fichiers de fonctions standard C ++, _start () et _init (), les définitions des vecteurs d'interruption sont empruntées à Eclipse MCU ARM Embedded et le registre de base Cortex M3 et les fichiers de travail proviennent d'un projet réalisé par CubeMX.


PPS
Sur le débogage KDPV avec le contrôleur STM32F103c8t6 est représenté. Tout le monde n'a pas une telle carte, mais il n'est pas difficile de l'acheter, cependant, cela dépasse le cadre de cet article.

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


All Articles