Fig. extrait de www.extremetech.com/wp-content/uploads/2016/07/MegaProcessor-Feature.jpgBonne santé à tous!
Dans un article précédent, j'ai examiné la question de l'accès aux registres d'un microcontrôleur avec un noyau CortexM en C ++ et j'ai montré des solutions simples à certains des problèmes.
Aujourd'hui, je veux montrer comment sécuriser l'accès au registre et à ses champs sans sacrifier l'efficacité, en utilisant des classes C ++ générées à partir de fichiers SVD.
Tous ceux qui vous intéressent, bienvenue au chat. Il y aura beaucoup de code.
Présentation
Dans un article
C ++ Hardware Register Access Redux , Ken Smith a montré comment travailler en toute sécurité et efficacement avec les registres, et l'a même montré avec
github.com/kensmith/cppmmio comme exemple.
Ensuite, plusieurs personnes ont développé cette idée, par exemple,
Niklas Hauser a fait un excellent examen et a suggéré plusieurs autres façons d'accéder en toute sécurité aux registres.
Certaines de ces idées ont déjà été mises en œuvre dans diverses bibliothèques, en particulier dans
modm . Pour de bon, vous pouvez utiliser toutes ces phrases dans la vraie vie. Mais au moment du développement de ces bibliothèques, les descriptions des périphériques et des registres venaient juste de commencer à être standardisées, et donc certaines choses ont été faites dans le but que le travail principal sur la description des registres soit confié au programmeur. De plus, certaines solutions ne sont pas efficaces en termes de ressources de code et de microcontrôleur.
Aujourd'hui, chaque fabricant d'un microcontrôleur ARM fournit une description de tous les registres au format SVD. Des fichiers d'en-tête peuvent être générés à partir de ces descriptions; par conséquent, il est possible de créer non pas une simple description des registres, mais une description plus complexe, mais en même temps, ce qui augmentera la fiabilité de votre code. Et c'est génial que le fichier de sortie puisse être dans n'importe quel langage, C, C ++, ou même
DMais prenons-le dans l'ordre ce qui est généralement un accès sûr aux registres, et pourquoi est-il nécessaire du tout. L'explication peut être montrée sur des exemples synthétiques simples, très probablement peu probables, mais tout à fait possibles:
int main(void) {
Tous ces cas sont possibles dans la pratique, et j'ai certainement regardé quelque chose comme ça de mes élèves. Ce serait formidable si vous pouviez interdire de faire de telles erreurs.
Il me semble aussi que c'est beaucoup plus agréable quand le code a l'air soigné et n'a pas besoin de commentaires. Par exemple, même si vous connaissez bien le microcontrôleur STM32F411, il n'est pas toujours possible de comprendre ce qui se passe dans ce code:
int main() { uint32 temp = GPIOA->OSPEEDR ; temp &=~ GPIO_OSPEEDR_OSPEED0_Msk ; temp = (GPIO_OSPEEDR_OSPEED0_0 | GPIO_OSPEEDR_OSPEED0_1) ; GPIOA->OSPEEDR = temp; }
Aucun commentaire ici ne peut faire. Le code définit la fréquence de fonctionnement du port GPIOA.0 au maximum (clarification de
mctMaks : en fait, ce paramètre affecte le temps de montée du front (c'est-à-dire sa pente) et signifie que le port peut traiter un signal numérique normalement à un signal donné (VeryLow \ Low \ Medium \ Haute) fréquence).
Essayons de nous débarrasser de ces lacunes.
Enregistrer l'abstraction
Tout d'abord, vous devez comprendre ce qu'est le registre du point de vue du programmeur et du programme.
Le registre a une adresse, une longueur ou une taille, un mode d'accès: certains registres peuvent être écrits, certains ne peuvent être lus et la plupart peuvent être lus et écrits.
De plus, le registre peut être représenté comme un ensemble de champs. Un champ peut être composé d'un bit ou de plusieurs bits et se trouve n'importe où dans le registre.
Par conséquent, les caractéristiques de champ suivantes sont importantes pour nous: longueur ou taille (
largeur ou
taille ), décalage par rapport au début du registre (
décalage ) et valeur.
Les valeurs de champ sont l'espace de toutes les quantités possibles que le champ peut prendre et cela dépend de la longueur du champ. C'est-à-dire si le champ a une longueur de 2, alors il y a 4 valeurs de champ possibles (0,1,2,3). Comme le registre, les champs et les valeurs de champ ont un mode d'accès (lecture, écriture, lecture et écriture)
Pour le rendre plus clair, prenons le registre TIM1 CR1 du microcontrôleur STM32F411. Schématiquement, cela ressemble à ceci:

- Bit 0 CEN: Activer le compteur
0: compteur activé: désactivé
1: compteur désactivé: activé
- Bit UDIS 1: activer / désactiver l'événement UEV
0: événement UEV activé: activé
1: événement UEV désactivé: désactivé
- URS Bit 2: Sélectionner les sources de génération d'événements UEV
0: UEV est généré lors d'un débordement ou lors de la définition de l'UG: n'importe quel bit
1: UEV est généré uniquement en cas de débordement: débordement
- Bit 3 OPM: opération unique
0: le temporisateur continue de compter davantage après l'événement UEV: ContinueAfterUEV
1: Le temporisateur s'arrête après l' événement UEV: StopAfterUEV
- Bit 4 DIR: Direction du comptage
0: Compte direct: Upcounter
1: Compte à rebours : Downcounter
- Bit 6: 5 CMS: Mode d'alignement
0: Mode d'alignement 0: CenterAlignedMode0
1: Mode d'alignement 1: CenterAlignedMode1
2: Mode d'alignement 2: CenterAlignedMode2
3: Mode d'alignement 3: CenterAlignedMode3
- Bit 7 APRE: mode de préchargement pour le registre ARR
0: le registre TIMx_ARR n'est pas mis en mémoire tampon: ARRNotBuffered
1: le registre TIMx_ARR n'est pas mis en mémoire tampon: ARRBuffered
- Bit 8: 9 CKD: diviseur d'horloge
0: tDTS = tCK_INT: ClockDevidedBy1
1: tDTS = 2 * tCK_INT: ClockDevidedBy2
2: tDTS = 4 * tCK_INT: ClockDevidedBy4
3: Réservé: Réservé
Ici, par exemple, CEN est un champ de 1 bit avec un décalage de 0 par rapport au début du registre. Et
Enable (1) et
Disable (0) sont ses valeurs possibles.
Nous ne nous concentrerons pas sur ce dont chaque champ de ce registre est spécifiquement responsable, il est important pour nous que chaque champ et valeur de champ ait un nom qui ait une signification sémantique et à partir duquel il peut en principe être compris ce qu'il fait.
Nous devons avoir accès à la fois au registre et au champ et à sa valeur. Par conséquent, sous une forme très approximative, l'abstraction des registres peut être représentée par les classes suivantes:

En plus des classes, il est également important pour nous que les registres et les champs individuels aient certaines propriétés, le registre a une adresse, une taille, un mode d'accès (lecture seule, écriture seule ou les deux).
Le champ a une taille, un décalage et également un mode d'accès. De plus, le champ doit contenir un lien vers le registre auquel il appartient.
La valeur du champ doit avoir un lien vers le champ et un attribut supplémentaire - la valeur.
Par conséquent, dans une version plus détaillée, notre abstraction ressemblera à ceci:

En plus des attributs, notre abstraction devrait avoir des méthodes de modification et d'accès. Par souci de simplicité, nous nous limitons aux méthodes d'installation / écriture et de lecture.

Lorsque nous avons décidé de l'abstraction des cas, nous devons vérifier comment cette abstraction correspond à ce qui est décrit dans le fichier SVD.
Fichier SVD (System View Description)
Le format de description de présentation du système CMSIS (CMSIS-SVD) est une description formelle des registres de microcontrôleur basée sur le processeur ARM Cortex-M. Les informations contenues dans les descriptions de la représentation du système correspondent pratiquement aux données des manuels de référence des appareils. La description des registres dans un tel fichier peut contenir à la fois des informations de haut niveau et la fonction d'un seul bit du champ dans le registre.
Schématiquement, les niveaux de détail des informations dans un tel fichier peuvent être décrits par le schéma suivant,
pris sur le site Web de Keil :

Description Les fichiers SVD sont fournis par les fabricants et sont utilisés pendant le débogage pour afficher des informations sur le microcontrôleur et les registres. Par exemple, l'IAR les utilise pour afficher des informations dans le panneau Affichage-> Registres. Les fichiers eux-mêmes se trouvent dans le dossier Program Files (x86) \ IAR Systems \ Embedded Workbench 8.3 \ arm \ config \ debugger.
Clion de JetBrains utilise également des fichiers svd pour afficher les informations de registre pendant le débogage.
Vous pouvez toujours télécharger les descriptions sur les sites du fabricant.
Ici, vous pouvez prendre le fichier SVD pour le microcontrôleur STM32F411En général, le format SVD est une norme prise en charge par les fabricants. Voyons quels sont les niveaux de description dans SVD.
Au total, 5 niveaux sont distingués: niveau de l'appareil, niveau du microcontrôleur, niveau du registre, niveau du champ, niveau des valeurs énumérées.
- Niveau de l'appareil : la description de niveau supérieur de la vue système est l'appareil. À ce niveau, les propriétés liées à l'appareil dans son ensemble sont décrites. Par exemple, un nom, une description ou une version de périphérique. L'unité adressable minimale, ainsi que la profondeur de bits du bus de données. Les valeurs par défaut des attributs de registre, telles que la taille du registre, la valeur de réinitialisation et les autorisations d'accès, peuvent être définies pour l'ensemble du périphérique à ce niveau et sont implicitement héritées par des niveaux de description inférieurs.
- Niveau du microcontrôleur: la section CPU décrit le cœur du microcontrôleur et ses fonctionnalités. Cette section est obligatoire si le fichier SVD est utilisé pour créer le fichier d'en-tête de périphérique.
- Couche périphérique : un périphérique est une collection nommée de registres. Un périphérique est mappé à une adresse de base spécifique dans l'espace d'adressage du périphérique.
- Niveau de registre : un registre est une ressource programmable nommée qui appartient à un périphérique. Les registres sont mappés à une adresse spécifique dans l'espace d'adressage du périphérique. L'adresse est relative à l'adresse périphérique de base. De plus, pour le registre, le mode d'accès (lecture / écriture) est indiqué.
- Niveau de champ : comme mentionné ci-dessus, les registres peuvent être divisés en morceaux de bits de différentes fonctionnalités - champs. Ce niveau contient les noms des champs uniques dans le même registre, leur taille, les décalages par rapport au début du registre, ainsi que le mode d'accès.
- Le niveau des valeurs de champ énumérées : en fait, ce sont des valeurs de champ nommées qui peuvent être utilisées par commodité en C, C ++, D, etc.
En fait, les fichiers SVD sont des fichiers xml ordinaires avec une description complète du système. Il existe des convertisseurs de fichiers svd en code C, par exemple
ici , qui génèrent des en-têtes et des structures compatibles C pour chaque périphérie et registre.
Il existe également un analyseur de fichier SVD
cmsis-svd écrit en Phyton qui fait quelque chose comme la désérialisation des données d'un fichier en objets de classe Phython, qui sont ensuite facilement utilisés dans votre programme de génération de code.
Un exemple de la description du registre du microcontrôleur STM32F411 peut être visualisé sous le spoiler:
Exemple de registre CR1 timer TIM1 <peripheral> <name>TIM1</name> <description>Advanced-timers</description> <groupName>TIM</groupName> <baseAddress>0x40010000</baseAddress> <addressBlock> <offset>0x0</offset> <size>0x400</size> <usage>registers</usage> </addressBlock> <registers> <register> <name>CR1</name> <displayName>CR1</displayName> <description>control register 1</description> <addressOffset>0x0</addressOffset> <size>0x20</size> <access>read-write</access> <resetValue>0x0000</resetValue> <fields> <field> <name>CKD</name> <description>Clock division</description> <bitOffset>8</bitOffset> <bitWidth>2</bitWidth> </field> <field> <name>ARPE</name> <description>Auto-reload preload enable</description> <bitOffset>7</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>CMS</name> <description>Center-aligned mode selection</description> <bitOffset>5</bitOffset> <bitWidth>2</bitWidth> </field> <field> <name>DIR</name> <description>Direction</description> <bitOffset>4</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>OPM</name> <description>One-pulse mode</description> <bitOffset>3</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>URS</name> <description>Update request source</description> <bitOffset>2</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>UDIS</name> <description>Update disable</description> <bitOffset>1</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>CEN</name> <description>Counter enable</description> <bitOffset>0</bitOffset> <bitWidth>1</bitWidth> </field> </fields> </register> <register>
Comme vous pouvez le voir, il y a toutes les informations nécessaires à notre abstraction, à l'exception de la description des valeurs de bits spécifiques des champs.
Tous les fabricants ne veulent pas passer du temps sur une description complète de leur système, donc comme vous pouvez le voir, ST n'a pas voulu décrire les valeurs des champs et a transféré cette charge aux programmeurs clients. Mais TI prend soin de ses clients et décrit complètement le système, y compris les descriptions des valeurs de champ.
Ce qui précède montre que le format de la description SVD est très cohérent avec notre abstraction de cas. Le fichier contient toutes les informations nécessaires pour décrire pleinement le registre.
Implémentation
S'inscrire
Maintenant que nous avons fait une abstraction du registre et que nous avons une description des registres sous forme de svd de fabricants qui convient parfaitement à cette abstraction, nous pouvons passer directement à l'implémentation.
Notre implémentation doit être aussi efficace que le code C et conviviale. J'aimerais que l'accès aux registres soit aussi clair que possible, par exemple, comme ceci:
if (TIM1::CR1::CKD::DividedBy2::IsSet()) { TIM1::ARR::Set(10_ms) ; TIM1::CR1::CEN::Enable::Set() ; }
Rappelez-vous que pour accéder à l'adresse du registre entier, vous devez utiliser reinterpret_cast:
*reinterpret_cast<volatile uint32_t *>(0x40010000) = (1U << 5U) ;
La classe de registre a déjà été décrite ci-dessus, elle doit avoir une adresse, une taille et un mode d'accès, ainsi que deux méthodes
Get()
et
Set()
:
Nous passons l'adresse, la longueur du registre et le mode d'accès aux paramètres du modèle (c'est aussi une classe). En utilisant le mécanisme
SFINAE , à savoir la
enable_if
enable_if, nous allons «jeter» les fonctions d'accès
Set()
ou
Get()
pour les registres qui ne devraient pas les prendre en charge. Par exemple, si le registre est en lecture seule, nous
ReadMode
type
ReadMode
au paramètre de modèle,
enable_if
vérifiera si l'accès est le successeur de
ReadMode
et sinon, il créera une erreur contrôlée (le type T ne peut pas être affiché), et le compilateur n'inclura pas la méthode
Set()
pour un tel registre. Il en va de même pour un registre destiné à l'écriture uniquement.
Pour le contrôle d'accès, nous utiliserons les classes:
Les registres sont disponibles en différentes tailles: 8, 16, 32, 64 bits. Pour chacun d'eux, nous définissons notre type:
Type de registres selon la taille template <uint32_t size> struct RegisterType {} ; template<> struct RegisterType<8> { using Type = uint8_t ; } ; template<> struct RegisterType<16> { using Type = uint16_t ; } ; template<> struct RegisterType<32> { using Type = uint32_t ; } ; template<> struct RegisterType<64> { using Type = uint64_t ; } ;
Après cela, pour le temporisateur TIM1, vous pouvez définir le registre CR1 et, par exemple, le registre EGR de cette manière:
struct TIM1 { struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { } struct EGR : public RegisterBase<0x40010014, 32, WriteMode> { } } int main() { TIM1::CR1::Set(10) ; auto reg = TIM1::CR1::Get() ;
Étant donné que le compilateur affiche la méthode
Get()
uniquement pour les registres dans lesquels le mode d'accès est hérité de
ReadMode
, et les méthodes
Set()
pour les registres dans lesquels le mode d'accès est hérité de
WriteMode
, en cas d'utilisation incorrecte des méthodes d'accès, vous recevrez une erreur au stade de la compilation. Et si vous utilisez des outils de développement modernes, tels que Clion, même au stade du codage, vous verrez un avertissement de l'analyseur de code:

Eh bien, l'accès aux registres est devenu plus sûr, notre code ne vous permet pas de faire des choses inacceptables pour ce registre, mais nous voulons aller plus loin et nous référer non pas à l'ensemble du registre, mais à ses champs.
Champs
Le champ au lieu de l'adresse a une valeur de décalage par rapport au début du registre. De plus, pour connaître l'adresse ou le type auquel la valeur du champ doit être portée, il doit avoir un lien vers le registre:
Après cela, il est déjà possible de faire les choses suivantes:
struct TIM1 { struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { using CKD = RegisterField<TIM1::CR1, 8, 2, ReadWriteMode> ; using ARPE = RegisterField<TIM1::CR1, 7, 1, ReadWriteMode> ; using CMS = RegisterField<TIM1::CR1, 5, 2, ReadWriteMode> ; using DIR = RegisterField<TIM1::CR1, 4, 1, ReadWriteMode> ; using OPM = RegisterField<TIM1::CR1, 3, 1, ReadWriteMode> ; using URS = RegisterField<TIM1::CR1, 2, 1, ReadWriteMode> ; using UDIS = RegisterField<TIM1::CR1, 1, 1, ReadWriteMode> ; using CEN = RegisterField<TIM1::CR1, 0, 1, ReadWriteMode> ; } } int main() {
Bien que tout semble plutôt bien dans l’ensemble, la signification de
TIM1::CR1::CKD::Set(2)
n’est toujours pas claire, que signifie la magie passée à la fonction
Set()
? Et que signifie le nombre renvoyé par la
TIM1::CR1::CEN::Get()
?
Passez en toute transparence aux valeurs de champ.
Valeur du champ
L'abstraction d'une valeur de champ est essentiellement aussi un champ, mais capable d'accepter un seul état. Des attributs sont ajoutés à l'abstraction du champ - la valeur réelle et un lien vers le champ. La méthode
Set()
de définition de la valeur du champ est identique à la méthode
Set()
de définition du champ, sauf que la valeur elle-même n'a pas besoin d'être transmise à la méthode, elle est connue à l'avance, elle doit simplement être définie. Mais la méthode
Get()
n'a aucun sens; à la place, il est préférable de vérifier si cette valeur est définie ou non, remplacez cette méthode par la méthode
IsSet()
.
Le champ de registre peut maintenant être décrit par un ensemble de ses valeurs:
Valeurs des champs de registre CR1 du temporisateur TIM1 template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_CKD_Values: public RegisterField<Reg, offset, size, AccessMode> { using DividedBy1 = FieldValue<TIM_CR_CKD_Values, 0U> ; using DividedBy2 = FieldValue<TIM_CR_CKD_Values, 1U> ; using DividedBy4 = FieldValue<TIM_CR_CKD_Values, 2U> ; using Reserved = FieldValue<TIM_CR_CKD_Values, 3U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_ARPE_Values: public RegisterField<Reg, offset, size, AccessMode> { using ARRNotBuffered = FieldValue<TIM_CR_ARPE_Values, 0U> ; using ARRBuffered = FieldValue<TIM_CR_ARPE_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_CMS_Values: public RegisterField<Reg, offset, size, AccessMode> { using CenterAlignedMode0 = FieldValue<TIM_CR_CMS_Values, 0U> ; using CenterAlignedMode1 = FieldValue<TIM_CR_CMS_Values, 1U> ; using CenterAlignedMode2 = FieldValue<TIM_CR_CMS_Values, 2U> ; using CenterAlignedMode3 = FieldValue<TIM_CR_CMS_Values, 3U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_DIR_Values: public RegisterField<Reg, offset, size, AccessMode> { using Upcounter = FieldValue<TIM_CR_DIR_Values, 0U> ; using Downcounter = FieldValue<TIM_CR_DIR_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_OPM_Values: public RegisterField<Reg, offset, size, AccessMode> { using ContinueAfterUEV = FieldValue<TIM_CR_OPM_Values, 0U> ; using StopAfterUEV = FieldValue<TIM_CR_OPM_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_URS_Values: public RegisterField<Reg, offset, size, AccessMode> { using Any = FieldValue<TIM_CR_URS_Values, 0U> ; using Overflow = FieldValue<TIM_CR_URS_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_UDIS_Values: public RegisterField<Reg, offset, size, AccessMode> { using Enable = FieldValue<TIM_CR_UDIS_Values, 0U> ; using Disable = FieldValue<TIM_CR_UDIS_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_CEN_Values: public RegisterField<Reg, offset, size, AccessMode> { using Disable = FieldValue<TIM_CR_CEN_Values, 0U> ; using Enable = FieldValue<TIM_CR_CEN_Values, 1U> ; } ;
Le registre CR1 lui-même sera alors déjà décrit comme suit:
struct TIM1 { struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { using CKD = TIM_CR1_CKD_Values<TIM1::CR1, 8, 2, ReadWriteMode> ; using ARPE = TIM_CR1_ARPE_Values<TIM1::CR1, 7, 1, ReadWriteMode> ; using CMS = TIM_CR1_CMS_Values<TIM1::CR1, 5, 2, ReadWriteMode> ; using DIR = TIM_CR1_DIR_Values<TIM1::CR1, 4, 1, ReadWriteMode> ; using OPM = TIM_CR1_OPM_Values<TIM1::CR1, 3, 1, ReadWriteMode> ; using URS = TIM_CR1_URS_Values<TIM1::CR1, 2, 1, ReadWriteMode> ; using UDIS = TIM_CR1_UDIS_Values<TIM1::CR1, 1, 1, ReadWriteMode> ; using CEN = TIM_CR1_CEN_Values<TIM1::CR1, 0, 1, ReadWriteMode> ; } ; }
Vous pouvez maintenant définir et lire directement la valeur du champ de registre: Par exemple, si vous souhaitez activer le temporisateur sur le compte, il suffit d'appeler la méthode
Set()
sur la valeur
Enable
du champ CEN du registre CR1 du timer TIM1:
TIM1::CR1::CEN::Enable::Set() ;
. Dans le code, cela ressemblera à ceci:
int main() { if (TIM1::CR1::CKD::DividedBy2::IsSet()) { TIM1::ARR::Set(100U) ; TIM1::CR1::CEN::Enable::Set() ; } }
À titre de comparaison, la même chose en utilisant l'en-tête C: int main() { if((TIM1->CR1 & TIM_CR1_CKD_Msk) == TIM_CR1_CKD_0) { TIM1->ARR = 100U ; regValue = TIM1->CR1 ; regValue &=~(TIM_CR1_CEN_Msk) ; regValue |= TIM_CR1_CEN ; TIM1->CR1 = regValue ; } }
Ainsi, les principales améliorations sont apportées, nous pouvons avoir un accès simple et compréhensible au registre, à ses champs et à ses valeurs. , , , , .
, . , :
int main() { uint32_t regValue = TIM1->CR1 ; regValue &=~(TIM_CR1_CKD_Msk | TIM_CR1_DIR) ; regValue |= (TIM_CR1_CEN | TIM_CR1_CKD_0 | TIM_CR1_CKD_0) ; TIM1->CR1 = regValue ; }
Set(...)
, , . C'est-à-dire :
int main() {
, , , , .
. :
, :
- , .
- .
constexpr , :
Set()
IsSet()
:
, :
int main() {
, - , , , ,
FieldValueBaseType
. ,
FieldValueBaseType
:
template<uint32_t address, size_t size, typename AccessMode, typename FieldValueBaseType, typename ...Args> class Register { private:
, SFINAE , , , , , .
CR1 TIM1, :
struct TIM1 { struct TIM1CR1Base {} ; struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { using CKD = TIM_CR_CKD_Values<TIM1::CR1, 8, 2, ReadWriteMode, TIM1CR1Base> ; using ARPE = TIM_CR_ARPE_Values<TIM1::CR1, 7, 1, ReadWriteMode, TIM1CR1Base> ; using CMS = TIM_CR_CMS_Values<TIM1::CR1, 5, 2, ReadWriteMode, TIM1CR1Base> ; using DIR = TIM_CR_DIR_Values<TIM1::CR1, 4, 1, ReadWriteMode, TIM1CR1Base> ; using OPM = TIM_CR_OPM_Values<TIM1::CR1, 3, 1, ReadWriteMode, TIM1CR1Base> ; using URS = TIM_CR_URS_Values<TIM1::CR1, 2, 1, ReadWriteMode, TIM1CR1Base> ; using UDIS = TIM_CR_UDIS_Values<TIM1::CR1, 1, 1, ReadWriteMode, TIM1CR1Base> ; using CEN = TIM_CR_CEN_Values<TIM1::CR1, 0, 1, ReadWriteMode, TIM1CR1Base> ; } ; }
, , . , , , .
, :
Et essayez de faire de même avec la nouvelle approche: int main(void) {
Dans chacun de ces cas, nous obtenons une erreur au stade de la compilation, ce qui est exactement ce que nous avons réalisé.Eh bien, nous avons fourni un bel accès sûr au registre et à ses champs, mais qu'en est-il de la vitesse?Performances
À titre de comparaison, dans quelle mesure notre approche est optimale, nous utiliserons le code C et C ++ qui alimente l'horloge au port A, définit les trois ports en mode de sortie et définit les ports de sortie dans ces trois ports 1:Code C: int main() { uint32_t res = RCC->AHB2ENR; res &=~ RCC_AHB1ENR_GPIOAEN_Msk ; res |= RCC_AHB1ENR_GPIOAEN ; RCC->AHB2ENR = res ; res = GPIOA->MODER ; res &=~ (GPIO_MODER_MODER5 | GPIO_MODER_MODER4 | GPIO_MODER_MODER1) ; res |= (GPIO_MODER_MODER5_0 | GPIO_MODER_MODER4_0 | GPIO_MODER_MODER1_0) ; GPIOA->MODER = res ; GPIOA->BSRR = (GPIO_BSRR_BS5 | GPIO_BSRR_BS4 | GPIO_BSRR_BS1) ; return 0 ; }
Code C ++: int main() { RCC::AHB1ENR::GPIOAEN::Enable::Set() ; GPIOA::MODERPack< GPIOA::MODER::MODER5::Output, GPIOA::MODER::MODER4::Output, GPIOA::MODER::MODER1::Output>::Set() ; GPIOA::BSRRPack< GPIOA::BSRR::BS5::Set, GPIOA::BSRR::BS4::Set, GPIOA::BSRR::BS1::Set>::Write() ; return 0 ; }
IAR. : :
:

C++ :

18 , , .
, :

13 .
++ :

: , .
, . , ?
Nous avons obtenu un accès fiable, pratique et rapide aux registres. Une question demeure. Comment décrire tous les registres, il y en a aussi moins d'une centaine pour le microcontrôleur. C'est le temps qu'il faut pour décrire tous les registres, car vous pouvez faire beaucoup d'erreurs dans un tel travail de routine. Oui, vous n'avez pas besoin de le faire manuellement. Au lieu de cela, nous utiliserons le générateur de code du fichier SVD, qui, comme je l'ai indiqué ci-dessus au début de l'article, couvre complètement l'abstraction du registre que j'ai acceptée.J'ai finalisé le script d'un collègue qui, basé sur cette idée, a fait à peu près la même chose, mais un peu plus facilement en utilisant enum au lieu de classes pour les valeurs de champ. Le script est conçu uniquement pour tester et vérifier des idées, il n'est donc pas optimal, mais il vous permet de générer quelque chose comme ça.
Qui se soucie du script est iciRésumé
, , . , gpioa rcc, :
#include "gpioaregisters.hpp"
, SVD , , .
, , , SVD , - ST , :
template <typename Reg, size_t offset, size_t size, typename AccessMode, typename BaseType> struct GPIOA_MODER_MODER_Values: public RegisterField<Reg, offset, size, AccessMode> { using Value0 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 0U> ; using Value1 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 1U> ; using Value2 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 2U> ; using Value3 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 3U> ; } ;
, Value, - :
template <typename Reg, size_t offset, size_t size, typename AccessMode, typename BaseType> struct GPIOA_MODER_MODER_Values: public RegisterField<Reg, offset, size, AccessMode> { using Input = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 0U> ; using Output = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 1U> ; using Alternate = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 2U> ; using Analog = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 3U> ; } ;
.
, ST , 0.
, , enum .
, .
IAR 8.40.1
« Online GDB»PS:
putyavka RegisterField::Get()
Ryppka assert.
Typesafe Register Access in C++One Approach to Using Hardware Registers in C++SVD Description (*.svd) Format