Fig. I. KiykoBonne 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;
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() {
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() {
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;
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) ;
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:
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) ;
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) ;
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>;
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 ;
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) ;
É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}} ; } }
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 3Faire bouger les choses - Superposition de structure