Dans un contexte low-metal embedded de bas niveau, je souhaite créer un espace vide dans la mémoire, au sein d’une structure C++ et sans nom, pour interdire à l’utilisateur d’accéder à cet emplacement mémoire.
Pour le moment, j’y suis parvenu en mettant un vilain champ uint32_t :96;
qui remplacera commodément trois mots, mais il émettra un avertissement de la part de GCC (Bitfield trop grand pour tenir uint32_t), ce qui est plutôt légitime.
Bien que cela fonctionne bien, ce n'est pas très propre lorsque vous voulez distribuer une bibliothèque avec plusieurs centaines de ces avertissements ...
Comment je fais ça correctement?
Le projet sur lequel je travaille consiste à définir la structure de la mémoire de différents périphériques d’une ligne complète de microcontrôleurs (STMicroelectronics STM32). Pour ce faire, le résultat est une classe contenant une union de plusieurs structures qui définissent tous les registres, en fonction du microcontrôleur ciblé.
Voici un exemple simple pour un périphérique assez simple: une entrée/sortie à usage général (GPIO)
union
{
struct
{
GPIO_MAP0_MODER;
GPIO_MAP0_OTYPER;
GPIO_MAP0_OSPEEDR;
GPIO_MAP0_PUPDR;
GPIO_MAP0_IDR;
GPIO_MAP0_ODR;
GPIO_MAP0_BSRR;
GPIO_MAP0_LCKR;
GPIO_MAP0_AFR;
GPIO_MAP0_BRR;
GPIO_MAP0_ASCR;
};
struct
{
GPIO_MAP1_CRL;
GPIO_MAP1_CRH;
GPIO_MAP1_IDR;
GPIO_MAP1_ODR;
GPIO_MAP1_BSRR;
GPIO_MAP1_BRR;
GPIO_MAP1_LCKR;
uint32_t :32;
GPIO_MAP1_AFRL;
GPIO_MAP1_AFRH;
uint32_t :64;
};
struct
{
uint32_t :192;
GPIO_MAP2_BSRRL;
GPIO_MAP2_BSRRH;
uint32_t :160;
};
};
Où tout GPIO_MAPx_YYY
est une macro, définie comme étant uint32_t :32
ou le type de registre (une structure dédiée).
Ici, vous voyez le uint32_t :192;
qui fonctionne bien, mais il déclenche un avertissement.
Je l’aurais peut-être remplacé par plusieurs uint32_t :32;
(6 ici), mais j’ai quelques cas extrêmes où j’ai uint32_t :1344;
(42) (parmi d’autres). Je préfère donc ne pas ajouter une centaine de lignes au-dessus de 8 000 autres, même si la génération de la structure est scriptée.
Le message d’avertissement exact ressemble à quelque chose comme: width of 'sool::ll::GPIO::<anonymous union>::<anonymous struct>::<anonymous>' exceeds its type
(j’aime à quel point il est louche).
Je préférerais pas résoudre ce problème en supprimant simplement l'avertissement, mais l'utilisation de
#pragma GCC diagnostic Push
#pragma GCC diagnostic ignored "-WTheRightFlag"
/* My code */
#pragma GCC diagnostic pop
peut être une solution ... si je trouve TheRightFlag
. Cependant, comme indiqué dans ce fil , gcc/cp/class.c
avec cette partie de code triste:
warning_at (DECL_SOURCE_LOCATION (field), 0,
"width of %qD exceeds its type", field);
Ce qui nous indique qu'il n'y a pas d'indicateur -Wxxx
pour supprimer cet avertissement ...
Utilisez plusieurs champs de bits anonymes adjacents. Donc au lieu de:
uint32_t :160;
par exemple, vous auriez:
uint32_t :32;
uint32_t :32;
uint32_t :32;
uint32_t :32;
uint32_t :32;
Un pour chaque registre que vous voulez être anonyme.
Si vous avez de grands espaces à remplir, cela peut être plus clair et moins sujet aux erreurs d'utiliser des macros pour répéter le seul espace 32 bits. Par exemple, étant donné:
#define REPEAT_2(a) a a
#define REPEAT_4(a) REPEAT_2(a) REPEAT_2(a)
#define REPEAT_8(a) REPEAT_4(a) REPEAT_4(a)
#define REPEAT_16(a) REPEAT_8(a) REPEAT_8(a)
#define REPEAT_32(a) REPEAT_16(a) REPEAT_16(a)
Ensuite, un espace de 1344 (42 * 32 bits) peut être ajouté ainsi:
struct
{
...
REPEAT_32(uint32_t :32;)
REPEAT_8(uint32_t :32;)
REPEAT_2(uint32_t :32;)
...
};
Que diriez-vous d'une manière C++-ish?
namespace GPIO {
static volatile uint32_t &MAP0_MODER = *reinterpret_cast<uint32_t*>(0x4000);
static volatile uint32_t &MAP0_OTYPER = *reinterpret_cast<uint32_t*>(0x4004);
}
int main() {
GPIO::MAP0_MODER = 42;
}
Vous obtenez l'auto-complétion à cause de l'espace de noms GPIO
et vous n'avez pas besoin de remplissage factice. Même si ce qui se passe est plus clair, comme vous pouvez voir l’adresse de chaque registre, vous n’avez pas à vous fier au comportement de remplissage du compilateur.
Dans le domaine des systèmes intégrés, vous pouvez modéliser le matériel en utilisant une structure ou en définissant des pointeurs vers les adresses de registre.
La modélisation par structure n'est pas recommandée car le compilateur est autorisé à ajouter un remplissage entre les membres à des fins d'alignement (bien que de nombreux compilateurs pour systèmes intégrés disposent d'un pragma pour compresser la structure).
Exemple:
uint16_t * const UART1 = (uint16_t *)(0x40000);
const unsigned int UART_STATUS_OFFSET = 1U;
const unsigned int UART_TRANSMIT_REGISTER = 2U;
uint16_t * const UART1_STATUS_REGISTER = (UART1 + UART_STATUS_OFFSET);
uint16_t * const UART1_TRANSMIT_REGISTER = (UART1 + UART_TRANSMIT_REGISTER);
Vous pouvez également utiliser la notation de tableau:
uint16_t status = UART1[UART_STATUS_OFFSET];
Si vous devez utiliser la structure IMHO, la meilleure méthode pour ignorer les adresses serait de définir un membre et de ne pas y accéder:
struct UART1
{
uint16_t status;
uint16_t reserved1; // Transmit register
uint16_t receive_register;
};
Dans l'un de nos projets, nous avons à la fois des constantes et des structures de différents fournisseurs (le fournisseur 1 utilise des constantes alors que le fournisseur 2 utilise des structures).
geza a raison de ne pas vouloir utiliser les classes pour cela.
Mais si vous insistez, le meilleur moyen d’ajouter un membre inutilisé de la largeur de n octets est tout simplement de le faire:
char unused[n];
Si vous ajoutez un pragma spécifique à l'implémentation pour empêcher l'ajout de remplissage arbitraire aux membres de la classe, cela peut fonctionner.
Pour GNU C/C++ (gcc, clang et autres qui prennent en charge les mêmes extensions), l’un des emplacements valides pour placer l’attribut est:
#include <stddef.h>
#include <stdint.h>
#include <assert.h> // for C11 static_assert, so this is valid C as well as C++
struct __attribute__((packed)) GPIO {
volatile uint32_t a;
char unused[3];
volatile uint32_t b;
};
static_assert(offsetof(struct GPIO, b) == 7, "wrong GPIO struct layout");
(exemple sur l'explorateur du compilateur Godbolt montrant offsetof(GPIO, b)
= 7 octets.)
Pour développer les réponses de @ Clifford et de Adam Kotwasinski:
#define REP10(a) a a a a a a a a a a
#define REP1034(a) REP10(REP10(REP10(a))) REP10(a a a) a a a a
struct foo {
int before;
REP1034(unsigned int :32;)
int after;
};
int main(void){
struct foo bar;
return 0;
}
Pour développer la réponse de Clifford, vous pouvez toujours définir une macro des champs de bits anonymes.
Donc au lieu de
uint32_t :160;
utilisation
#define EMPTY_32_1 \
uint32_t :32
#define EMPTY_32_2 \
uint32_t :32; \ // I guess this also can be replaced with uint64_t :64
uint32_t :32
#define EMPTY_32_3 \
uint32_t :32; \
uint32_t :32; \
uint32_t :32
#define EMPTY_UINT32(N) EMPTY_32_ ## N
Et puis l'utiliser comme
struct A {
EMPTY_UINT32(3);
/* which resolves to EMPTY_32_3, which then resolves to real declarations */
}
Malheureusement, vous aurez besoin d'autant de variantes EMPTY_32_X
que d'autant d'octets :( Néanmoins, cela vous permet d'avoir des déclarations uniques dans votre structure.
Je pense qu'il serait bénéfique d'introduire plus de structure; ce qui peut, à son tour, résoudre le problème des entretoises.
Bien que les espaces de noms plats soient agréables, le problème est que vous vous retrouvez avec une collection hétéroclite de champs et qu’il n’ya pas de moyen simple de faire passer tous les champs liés ensemble. De plus, en utilisant des structures anonymes dans une union anonyme, vous ne pouvez pas transmettre de références aux structures elles-mêmes ni les utiliser comme paramètres de modèle.
Dans un premier temps, j’aimerais donc envisager de décomposer la struct
:
// GpioMap0.h
#pragma once
// #includes
namespace Gpio {
struct Map0 {
GPIO_MAP0_MODER;
GPIO_MAP0_OTYPER;
GPIO_MAP0_OSPEEDR;
GPIO_MAP0_PUPDR;
GPIO_MAP0_IDR;
GPIO_MAP0_ODR;
GPIO_MAP0_BSRR;
GPIO_MAP0_LCKR;
GPIO_MAP0_AFR;
GPIO_MAP0_BRR;
GPIO_MAP0_ASCR;
};
} // namespace Gpio
// GpioMap1.h
#pragma once
// #includes
namespace Gpio {
struct Map1 {
// fields
};
} // namespace Gpio
// ... others headers ...
Et enfin, l'en-tête global:
// Gpio.h
#pragma once
#include "GpioMap0.h"
#include "GpioMap1.h"
// ... other headers ...
namespace Gpio {
union Gpio {
Map0 map0;
Map1 map1;
// ... others ...
};
} // namespace Gpio
Maintenant, je peux écrire une void special_map0(Gpio:: Map0 volatile& map);
et obtenir un aperçu rapide de toutes les architectures disponibles en un coup d’œil.
Avec la définition divisée en plusieurs en-têtes, les en-têtes sont individuellement beaucoup plus faciles à gérer.
Par conséquent, mon approche initiale pour répondre exactement à vos exigences serait de m'en tenir à la répétition de std::uint32_t:32;
. Oui, il ajoute quelques centaines de lignes aux lignes existantes de 8 000 $, mais comme chaque en-tête est individuellement plus petit, il se peut que ce ne soit pas aussi grave.
Si vous êtes prêt à envisager des solutions plus exotiques, cependant ...
C'est un fait peu connu que $
est un caractère viable pour les identifiants C++; c'est même un personnage de départ viable (contrairement aux chiffres).
Un $
apparaissant dans le code source ferait probablement des sourcils, et $$$$
va certainement attirer l'attention lors de la révision du code. C’est quelque chose dont vous pouvez facilement tirer parti:
#define GPIO_RESERVED(Index_, N_) std::uint32_t $$$$##Index_[N_];
struct Map3 {
GPIO_RESERVED(0, 6);
GPIO_MAP2_BSRRL;
GPIO_MAP2_BSRRH;
GPIO_RESERVED(1, 5);
};
Vous pouvez même constituer une simple "charpie" en tant que hook de pré-validation ou dans votre CI qui recherche $$$$
dans le code C++ engagé et rejette ces validations.
Pour définir un grand espaceur sous forme de groupes de 32 bits.
#define M_32(x) M_2(M_16(x))
#define M_16(x) M_2(M_8(x))
#define M_8(x) M_2(M_4(x))
#define M_4(x) M_2(M_2(x))
#define M_2(x) x x
#define SPACER int : 32;
struct {
M_32(SPACER) M_8(SPACER) M_4(SPACER)
};
Bien que je convienne que les structures ne doivent pas être utilisées pour l’accès au port d’entrée/sortie de la MCU, il est possible de répondre à la question initiale comme suit:
struct __attribute__((packed)) test {
char member1;
char member2;
volatile struct __attribute__((packed))
{
private:
volatile char spacer_bytes[7];
} spacer;
char member3;
char member4;
};
Vous devrez peut-être remplacer __attribute__((packed))
par #pragma pack
ou similaire, selon la syntaxe de votre compilateur.
Le mélange de membres privés et publics dans une structure a normalement pour résultat que la disposition de la mémoire n'est plus garantie par le standard C++. Cependant, si tous les membres non statiques d’une structure sont privés, elle est toujours considérée comme une présentation POD/standard, de même que les structures qui les intègrent.
Pour une raison quelconque, gcc produit un avertissement si un membre d'une structure anonyme est privé et que je devais lui donner un nom. Alternativement, l'envelopper dans une autre structure anonyme supprime également l'avertissement (il peut s'agir d'un bogue).
Notez que le membre spacer
n'est pas lui-même privé, vous pouvez donc accéder aux données de cette façon:
(char*)(void*)&testobj.spacer;
Cependant, une telle expression ressemble à un hack évident et, espérons-le, ne serait pas utilisée sans une très bonne raison, encore moins comme une erreur.