10 ++ façons de travailler avec les registres matériels en C ++ (par exemple, IAR et Cortex M)

Choisir le chemin le plus sûr
Fig. I. Kiyko

Bonne santé à tous!

Vous vous souvenez probablement d'une anecdote barbu, et peut-être d'une histoire vraie sur la façon dont un étudiant a été interrogé sur une façon de mesurer la hauteur d'un bâtiment à l'aide d'un baromètre. L'élève a cité, à mon avis, environ 20 ou 30 façons, sans mentionner le direct (par la différence de pression) que l'enseignant attendait.

Dans la même veine, je veux continuer à discuter de l'utilisation de C ++ pour les microcontrôleurs et examiner les façons de travailler avec les registres à l'aide de C ++. Et je tiens à noter que pour parvenir à un accès sûr aux registres, il n'y aura pas de moyen facile. Je vais essayer de montrer tous les avantages et les inconvénients des méthodes. Si vous en savez plus, jetez-les dans les commentaires. Commençons donc:

Méthode 1. Évidente et évidemment pas la meilleure


La méthode la plus courante, également utilisée en C ++, consiste à utiliser la description des structures de registre à partir du fichier d'en-tête du fabricant. Pour la démonstration, je prendrai deux registres du port A (ODR - registre de données de sortie et IDR - registre de données d'entrée) du microcontrôleur STM32F411, afin de pouvoir exécuter la DEL «clignotante» «Hello world» - clignotante.

int main() { GPIOA->ODR ^= (1 << 5) ; GPIOA->IDR ^= (1 << 5) ; //,      } 

Voyons ce qui se passe ici et comment fonctionne cette conception. L'en-tête du microprocesseur contient la structure GPIO_TypeDef et une définition de pointeur vers cette structure GPIOA . Cela ressemble à ceci:

 typedef struct { __IO uint32_t MODER; //port mode register, Address offset: 0x00 __IO uint32_t OTYPER; //port output type register, Address offset: 0x04 __IO uint32_t OSPEEDR; //port output speed register, Address offset: 0x08 __IO uint32_t PUPDR; //port pull-up/pull-down register, Address offset: 0x0C __IO uint32_t IDR; //port input data register, Address offset: 0x10 __IO uint32_t ODR; //port output data register, Address offset: 0x14 __IO uint32_t BSRR; //port bit set/reset register, Address offset: 0x18 __IO uint32_t LCKR; //port configuration lock register, Address offset: 0x1C __IO uint32_t AFR[2]; //alternate function registers, Address offset: 0x20-0x24 } GPIO_TypeDef; #define PERIPH_BASE 0x40000000U //Peripheral base address in the alias region #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000U) #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000U) #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 

Pour le dire simplement, alors toute la structure du type GPIO_TypeDef "se pose" à l'adresse GPIOA_BASE , et lorsque vous vous référez à un champ spécifique de la structure, vous vous référez essentiellement à l'adresse de cette structure + offset à un élément de cette structure. Si vous supprimez #define GPIOA , le code ressemblerait à ceci:

 ((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ; ((GPIO_TypeDef *) GPIOA_BASE)->IDR ^= (1 << 5) ; // 

Par rapport au langage de programmation C ++, une adresse entière est convertie en type de pointeur vers la structure GPIO_TypeDef . Mais en C ++, lors de l'utilisation de la conversion C, le compilateur essaie d'effectuer la conversion dans l'ordre suivant:

  • const_cast
  • static_cast
  • static_cast à côté de const_cast,
  • reinterpret_cast
  • reinterpret_cast à côté de const_cast

c'est-à-dire si le compilateur n'a pas pu convertir le type à l'aide de const_cast, il essaie d'appliquer static_cast et ainsi de suite. En conséquence, l'appel:

 ((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ; 

il n'y a rien de tel:

 reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE)->ODR ^= (1 << 5) ; 

En fait, pour les applications C ++, il serait correct de "tirer" la structure sur l'adresse comme ceci:

 GPIO_TypeDef * GPIOA{reinterpret_cast<GPIO_TypeDef *>(GPIOA_BASE)} ; 

Dans tous les cas, en raison de la conversion de type, il y a un gros inconvénient à cette approche pour C ++. Il consiste dans le fait que constexpr ne peut être utilisé ni dans les constructeurs et fonctions constexpr , ni dans les paramètres de modèle, ce qui réduit considérablement l'utilisation des fonctionnalités C ++ pour les microcontrôleurs.
Je vais l'expliquer avec des exemples. Il est possible de le faire:

  struct Test { const int a; const int b; } ; template<Test* mystruct> constexpr const int Geta() { return mystruct->a; } Test test{1,2}; int main() { Geta<&test>() ; } 

Mais vous ne pouvez pas déjà le faire:

 template<GPIO_TypeDef * mystruct> constexpr volatile uint32_t GetIdr() { return mystruct->IDR; } int main() { //GPIOA  reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE) //  ,        GetIdr<GPIOA>() ; // } //      : struct Port { constexpr Port(GPIO_TypeDef * ptr): port(*ptr) {} GPIO_TypeDef & port ; } //  GPIOA  reinterpret_cast,   //  constexpr      constexpr Port portA{GPIOA}; //    

Ainsi, l'utilisation directe de cette approche impose des restrictions importantes sur l'utilisation de C ++. Nous ne pourrons pas localiser l'objet qui souhaite utiliser le GPIOA dans la ROM à l'aide des outils de langage, et nous ne pourrons pas profiter de la métaprogrammation pour un tel objet.
De plus, en général, cette méthode n'est pas la sécurité (comme disent nos partenaires occidentaux). Après tout, il est tout à fait possible de faire du NON-FUN
En relation avec ce qui précède, nous résumons:

Avantages


  • Le titre du fabricant est utilisé (il est vérifié, il n'y a pas d'erreur)
  • Il n'y a pas de gestes et de coûts supplémentaires, vous prenez et utilisez
  • Facilité d'utilisation
  • Tout le monde connaît et comprend cette méthode.
  • Pas de frais généraux

Inconvénients


  • Utilisation limitée de la métaprogrammation
  • Incapacité à utiliser dans les constructeurs constexpr
  • Lors de l'utilisation de wrappers dans les classes, la consommation supplémentaire de RAM est un pointeur vers un objet de cette structure
  • Tu peux devenir stupide
Voyons maintenant la méthode numéro 2

Méthode 2. Brutal


Il est évident que chaque programmeur d'intégration garde à l'esprit les adresses de tous les registres pour tous les microcontrôleurs, vous pouvez donc toujours simplement utiliser la méthode suivante, qui découle du premier:

 *reinterpret_cast<volatile uint32_t *>(GpioaOdrAddr) ^= (1 <<5) ; *reinterpret_cast<volatile uint32_t *>(GpioaIdrAddr) ^= (1 <<5) ; // 

N'importe où dans le programme, vous pouvez toujours appeler la conversion vers l'adresse de registre volatile uint32_t et y installer au moins quelque chose.
Il n'y a surtout aucun avantage ici, mais à ces inconvénients, il y a un inconvénient supplémentaire à utiliser et la nécessité d'écrire l'adresse de chaque registre dans un fichier séparé vous-même. Par conséquent, nous passons à la méthode numéro 3.

Méthode 3. Évidente et évidemment plus correcte


Si l'accès aux registres se fait par le biais du champ de structure, au lieu d'un pointeur vers l'objet de structure, vous pouvez utiliser l'adresse de structure entière. L'adresse des structures se trouve dans le fichier d'en-tête du fabricant (par exemple, GPIOA_BASE pour GPIOA), vous n'avez donc pas besoin de vous en souvenir, mais vous pouvez l'utiliser dans des modèles et dans des expressions constexpr, puis "superposer" la structure à cette adresse.

 template<uint32_t addr, uint32_t pinNum> struct Pin { using Registers = GPIO_TypeDef ; __forceinline static void Toggle() { //     addr Registers *GpioPort{reinterpret_cast<Registers*>(addr)}; GpioPort->ODR ^= (1 << pinNum) ; } }; int main() { using Led1 = Pin<GPIOA_BASE, 5> ; Led1::Toggle() ; } 

Il n'y a pas d'inconvénients particuliers, de mon point de vue. En principe, une option de travail. Mais encore, jetons un œil à d'autres façons.

Méthode 4. Enveloppement exotérique


Pour les connaisseurs de code compréhensible, vous pouvez créer un wrapper sur le registre afin qu'il soit pratique d'y accéder et qu'il soit "beau", faire un constructeur, redéfinir les opérateurs:

 class Register { public: explicit Register(uint32_t addr) : ptr{ reinterpret_cast<volatile uint32_t *>(addr) } { } __forceinline inline Register& operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile uint32_t *ptr; //    }; int main() { Register Odr{GpioaOdrAddr}; Odr ^= (1 << 5); Register Idr{GpioaIdrAddr}; Idr ^= (1 << 5); // } 

Comme vous pouvez le voir, encore une fois, vous devrez soit vous souvenir des adresses entières de tous les registres, soit les définir quelque part, et vous devrez également stocker un pointeur sur l'adresse du registre. Mais ce qui n'est pas très bon à nouveau, reinterpret_cast se produit à nouveau dans le constructeur
Quelques inconvénients, et au fait que dans la première et la deuxième version, il a été ajouté la nécessité pour chaque registre utilisé de stocker un pointeur sur 4 octets dans la RAM. En général, pas une option. Nous regardons ce qui suit.

Méthode 4,5. Enveloppement exotérique avec motif


Nous ajoutons un grain de métaprogrammation, mais cela ne présente pas beaucoup d'avantages. Cette méthode ne diffère de la précédente que par le fait que l'adresse n'est pas transférée au constructeur, mais dans le paramètre modèle, nous économisons un peu sur les registres lors du passage de l'adresse au constructeur, c'est déjà bien:

 template<uint32_t addr> class Register { public: Register() : ptr{reinterpret_cast<volatile uint32_t *>(addr)} { } __forceinline inline Register &operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile std::uint32_t *ptr; }; int main() { using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5); // } 

Et donc, le même râteau, vue de côté.

Méthode 5. Raisonnable


Évidemment, vous devez vous débarrasser du pointeur, alors faisons de même, mais supprimons le pointeur inutile de la classe.

 template<uint32_t addr> class Register { public: __forceinline Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5); // 

Vous pouvez rester ici et réfléchir un peu. Cette méthode résout immédiatement 2 problèmes qui étaient précédemment hérités de la première méthode. Premièrement, je peux maintenant utiliser le pointeur vers l'objet Register dans le modèle, et deuxièmement, je peux le passer au constructeur constexrp .

 template<Register * register> void Xor(uint32_t mask) { *register ^= mask ; } Register<GpioaOdrAddr> GpioaOdr; int main() { Xor<&GpioaOdr>(1 << 5) ; //  } //   struct Port { constexpr Port(Register& ref): register(ref) {} Register & register ; } constexpr Port portA{GpioaOdr}; 

Bien sûr, il est nécessaire à nouveau, soit d'avoir une mémoire eidétique pour les adresses des registres, soit de déterminer manuellement toutes les adresses des registres quelque part dans un fichier séparé ...

Avantages


  • Facilité d'utilisation
  • Capacité à utiliser la métaprogrammation
  • Capacité à utiliser dans les constructeurs constexpr

Inconvénients


  • Le fichier d'en-tête vérifié du fabricant n'est pas utilisé
  • Vous devez définir vous-même toutes les adresses des registres
  • Vous devez créer un objet de classe Register
  • Tu peux devenir stupide

Génial, mais il y a encore beaucoup d'inconvénients ...

Méthode 6. Plus intelligent que raisonnable


Dans la méthode précédente, pour accéder au registre, il était nécessaire de créer un objet de ce registre, c'est un gaspillage inutile de RAM et de ROM, nous faisons donc un wrapper avec des méthodes statiques.

 template<uint32_t addr> class Register { public: __forceinline inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile uint32_t *>(addr) ^= mask; } }; int main() { using namespace Case6 ; using Odr = Register<GpioaOdrAddr>; Odr::Xor(1 << 5); using Idr = Register<GpioaIdrAddr>; Idr::Xor(1 << 5); // } 

Un plus ajouté
  • Pas de frais généraux. Code compact rapide, identique à l'option 1 (lors de l'utilisation de wrappers dans les classes, il n'y a pas de coût RAM supplémentaire, car l'objet n'est pas créé, mais des méthodes statiques sont utilisées sans créer d'objets)
Allez-y ...

Méthode 7. Supprimer la stupidité


Évidemment, je fais constamment NON-FUNNY dans le code et j'écris quelque chose dans le registre, qui n'est en fait pas destiné à l'écriture. Ce n'est pas grave, bien sûr, mais la stupidité doit être interdite. Interdisons de faire des bêtises. Pour ce faire, nous introduisons des structures auxiliaires:

  struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; 

Maintenant, nous pouvons définir les registres pour l'écriture, et les registres sont en lecture seule:

 template<uint32_t addr, typename RegisterType> class Register { public: //       WriteReg,    // ,  ,       __forceinline template <typename T = RegisterType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T>::value>> Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; 

Essayons maintenant de compiler notre test et de voir que le test ne se compile pas, car l'opérateur ^= pour le registre Idr pas:

  int main() { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr Odr ; Odr ^= (1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr Idr ; Idr ^= (1 << 5) ; //,  Idr    } 

Donc, maintenant, il y a plus d'avantages ...

Avantages


  • Facilité d'utilisation
  • Capacité à utiliser la métaprogrammation
  • Capacité à utiliser dans les constructeurs constexpr
  • Code compact rapide, identique à l'option 1
  • Lorsque vous utilisez des wrappers dans les classes, il n'y a pas de coût RAM supplémentaire, car l'objet n'est pas créé, mais des méthodes statiques sont utilisées sans créer d'objets
  • Tu ne peux pas faire de bêtise

Inconvénients


  • Le fichier d'en-tête vérifié du fabricant n'est pas utilisé
  • Vous devez définir vous-même toutes les adresses des registres
  • Vous devez créer un objet de classe Register

Supprimons donc la possibilité de créer une classe pour économiser plus

Méthode 8. Sans NONSENSE et sans objet de classe


Coder immédiatement:

  struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; int main { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr::Xor(1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr::Xor(1 << 5) ; //,  Idr    } 

Nous ajoutons un plus, nous ne créons pas d'objet. Mais passons, nous avons encore des inconvénients

Méthode 9. Méthode 8 avec intégration de structure


Dans la méthode précédente, seul le cas était défini. Mais dans la méthode 1, tous les registres sont combinés en structures afin que vous puissiez facilement y accéder par modules. Faisons-le ...

 namespace Case9 { struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; template<uint32_t addr> struct Gpio { using Moder = Register<addr, ReadWriteReg>; //      using Otyper = Register<addr + OtyperShift, ReadWriteReg> ; using Ospeedr = Register<addr + OspeedrShift,ReadWriteReg> ; using Pupdr = Register<addr + PupdrShift,ReadWriteReg> ; using Idr = Register<addr + IdrShift, ReadReg> ; using Odr = Register<addr + OdrShift, WriteReg> ; }; int main() { using Gpioa = Gpio<GPIOA_BASE> ; Gpioa::Odr::Xor(1 << 5) ; Gpioa::Idr::Xor((1 << 5) ); //,  Idr    } 

Ici, l'inconvénient est que les structures devront être enregistrées à nouveau, et les décalages de tous les registres devraient être mémorisés et déterminés quelque part. Ce serait bien si les décalages étaient définis par le compilateur, et non par la personne, mais c'est plus tard, mais pour l'instant nous allons considérer une autre méthode intéressante suggérée par mon collègue.

Méthode 10. Envelopper le registre via un pointeur sur un membre de la structure


Nous utilisons ici un tel concept comme pointeur vers un membre de la structure et accès à celui-ci .

 template<uint32_t addr, typename T> class RegisterStructWrapper { public: __forceinline template<typename P> inline static void Xor(PT::*member, int mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; //   ,     . } } ; using GpioaWrapper = RegisterStructWrapper<GPIOA_BASE, GPIO_TypeDef> ; int main() { GpioaWrapper::Xor(&GPIO_TypeDef::ODR, (1 << 5)) ; GpioaWrapper::Xor(&GPIO_TypeDef::IDR, (1 << 5)) ; // return 0 ; } 

Avantages


  • Facilité d'utilisation
  • Capacité à utiliser la métaprogrammation
  • Capacité à utiliser dans les constructeurs constexpr
  • Code compact rapide, identique à l'option 1
  • Lorsque vous utilisez des wrappers dans les classes, il n'y a pas de coût RAM supplémentaire, car l'objet n'est pas créé, mais des méthodes statiques sont utilisées sans créer d'objets
  • Le fichier d'en-tête vérifié du fabricant est utilisé.
  • Pas besoin de définir vous-même toutes les adresses de registre
  • Pas besoin de créer un objet de classe Register

Inconvénients


  • Vous pouvez faire de la folie et même spéculer sur la compréhensibilité du code.

Méthode 10.5. Combinez les méthodes 9 et 10


Pour connaître le décalage du registre par rapport au début de la structure, vous pouvez utiliser le pointeur sur le membre de la structure: volatile uint32_t T::*member , il retournera le décalage du membre de la structure par rapport à son début en octets. Par exemple, nous avons la structure GPIO_TypeDef , puis l'adresse &GPIO_TypeDef::ODR sera 0x14.
Nous avons battu cette opportunité et calculé les adresses des registres à partir de la méthode 9, en utilisant le compilateur:

 struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T, volatile uint32_t T::*member, typename RegType> class Register { public: __forceinline template <typename T1 = RegType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; } }; template<uint32_t addr, typename T> struct Gpio { using Moder = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, ReadWriteReg>; using Otyper = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OTYPER, ReadWriteReg>; using Ospeedr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OSPEEDR, ReadWriteReg>; using Pupdr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::PUPDR, ReadWriteReg>; using Idr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::IDR, ReadReg>; using Odr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, WriteReg>; } ; 

Vous pouvez travailler avec des registres de manière plus exotique:

 using namespace Case11 ; using Gpioa = Gpio<GPIOA_BASE, GPIO_TypeDef> ; Gpioa::Odr::Xor(1 << 5) ; //Gpioa::Idr::Xor((1 << 5) ); //,  Idr    

Évidemment, ici toutes les structures devront être réécrites à nouveau. Cela peut être fait automatiquement, par un script en Phyton, à l'entrée quelque chose comme stm32f411xe.h à la sortie de votre fichier avec des structures à utiliser en C ++.
Dans tous les cas, il existe plusieurs façons différentes de fonctionner dans un projet particulier.

Bonus Nous introduisons l'extension de langue et le code parsim en utilisant Phyton


Le problème de l'utilisation des registres en C ++ existe depuis un certain temps. Les gens le résolvent de différentes manières. Bien sûr, ce serait génial si le langage supportait quelque chose comme renommer des classes au moment de la compilation. Eh bien, disons, si c'était comme ça:

 template<classname = [PortName]> class Gpio[Portname] { __forceinline inline static void Xor(const uint32_t mask) { GPIO[PortName]->ODR ^= mask ; } }; int main() { using GpioA = Gpio<"A"> ; GpioA::Xor(5) ; } 

Mais malheureusement, cette langue ne prend pas en charge. Par conséquent, la solution que les gens utilisent est l'analyse du code à l'aide de Python. C'est-à-dire une extension de langue est introduite. Le code, à l'aide de cette extension, est envoyé à l'analyseur Python, qui le traduit en code C ++. Un tel code ressemble à ceci: (un exemple est tiré de la bibliothèque modm; voici les sources complètes ):

 %% set port = gpio["port"] | upper %% set reg = "GPIO" ~ port %% set pin = gpio["pin"] class Gpio{{ port ~ pin }} : public Gpio { __forceinline inline static void Xor() { GPIO{{port}}->ODR ^= 1 << {{pin}} ; } } //        class Gpio5 : public Gpio { __forceinline inline static void Xor() { GPIO->ODR ^= 1 << 5 ; } } //     using Led = Gpio5; Led::Xor(); 


Mise à jour: Bonus. Fichiers SVD et analyseur sur Phyton


Vous avez oublié d'ajouter une autre option. ARM publie un fichier de description de registre pour chaque fabricant de SVD. À partir de laquelle vous pouvez ensuite générer un fichier C ++ avec une description des registres. Paul Osborne a compilé tous ces fichiers sur GitHub . Il a également écrit un script Python pour les analyser.

C'est tout ... mon imagination est épuisée. Si vous avez encore des idées, n'hésitez pas. Un exemple avec toutes les méthodes se trouve ici.

Les références


Accès au registre de typesafe en C ++
Faire avancer les choses -Accéder au matériel à partir de C ++
Faire bouger les choses - Partie 3
Faire bouger les choses - Superposition de structure

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


All Articles