J'enseigne à mes élèves comment travailler avec le microcontrôleur STM32F411RE, à bord duquel il y a jusqu'à 512 Ko de ROM et 128 Ko de RAM
Habituellement, un programme est écrit en
ROM sur ce microcontrôleur, et des données variables sont très souvent nécessaires en
RAM pour que les constantes soient en
ROM .
Dans le microcontrôleur
ROM STM32F411RE, la mémoire est située aux adresses avec
0x08000000 ... 0x0807FFFF , et la
RAM avec
0x20000000 ... 0x2001FFFF.Et si tous les paramètres de l'éditeur de liens sont corrects, l'étudiant calcule que dans un code aussi simple, sa constante réside dans la
ROM :
class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Vous pouvez également essayer de répondre à la question: où est la constante myConstInROM en
ROM ou en
RAM ?
Si vous avez répondu à cette question qu'en
ROM , je vous félicite, en fait très probablement vous vous trompez, la constante se situera généralement dans la
RAM et pour comprendre comment placer correctement et correctement vos constantes dans la
ROM - bienvenue au chat.
Présentation
Tout d'abord, une petite digression, pourquoi se soucier du tout.
Lors du développement de logiciels critiques pour la sécurité pour des appareils de mesure conformes à la CEI 61508-3: 2010 ou un équivalent domestique de la
norme GOST CEI 61508-3-2018 , un certain nombre de points doivent être pris en compte qui ne sont pas critiques pour les logiciels conventionnels.
Le message principal de cette norme est que le logiciel doit détecter toute panne qui affecte la fiabilité du système et mettre le système en mode «crash»En plus des défaillances mécaniques évidentes, par exemple, la défaillance ou la dégradation du capteur et la défaillance des composants électroniques, les erreurs causées par la défaillance de l'environnement logiciel, par exemple, le microcontrôleur
RAM ou
ROM , doivent être détectées.
Et si dans les deux premiers cas, il est possible de détecter une erreur uniquement de manière indirecte plutôt confuse (il existe des algorithmes qui déterminent la défaillance du capteur, par exemple, une
méthode pour évaluer l'état d'un convertisseur thermique à résistance ), puis en cas de défaillance de l'environnement logiciel, cela peut être fait beaucoup plus facilement, par exemple, une défaillance de la mémoire peut être vérifier par un simple contrôle d'intégrité des données. Si l'intégrité des données est violée, interprétez cela comme une défaillance de la mémoire.
Si les données se trouvent pendant longtemps dans la RAM sans vérification ni mise à jour, la probabilité que quelque chose leur arrive en raison d'une défaillance de la
RAM augmente avec le temps. Un exemple est certains coefficients d'étalonnage pour calculer la température qui ont été réglés en usine et écrits dans une EEPROM externe, au démarrage, ils sont lus et écrits dans la
RAM et sont là jusqu'à ce que l'alimentation soit coupée. Et dans la vie, le capteur de température peut fonctionner pendant toute la période de l'intervalle d'étalonnage, jusqu'à 3-5 ans. De toute évidence, ces données
RAM doivent être protégées et périodiquement vérifiées pour leur intégrité.
Mais il existe également des données, telles qu'une constante déclarée simplement pour la lisibilité, un objet d'un pilote LCD, SPI ou I2C, qui ne doit pas être modifié, est créé une fois et n'est pas supprimé jusqu'à la mise hors tension.
Ces données sont mieux conservées dans la
ROM . Il est plus fiable du point de vue de la technologie et il est beaucoup plus facile de le vérifier, il suffit de lire périodiquement la somme de contrôle de toute la mémoire morte dans une tâche de faible priorité. Si la somme de contrôle ne correspond pas, vous pouvez simplement signaler l'échec de la
ROM et le système de diagnostic affichera un accident.
Si ces données se trouvaient dans la
RAM , il serait problématique, voire impossible, de déterminer leur intégrité en raison du fait qu'il n'est pas clair où les données immuables se trouvent dans la RAM et où elles sont modifiables, l'éditeur de liens les place comme il le souhaite, et pour protéger chaque objet RAM avec une somme de contrôle ressemble à la paranoïa.
Par conséquent, le moyen le plus simple est d'être sûr à 100% que les données constantes sont dans la
ROM . Comment faire cela, je veux essayer d'expliquer. Mais vous devez d'abord parler de l'organisation de la mémoire dans ARM.
Organisation de la mémoire
Comme vous le savez, le cœur ARM a une architecture Harvard - les bus de données et de code sont séparés. Cela signifie généralement qu'il est supposé qu'il existe une mémoire distincte pour les programmes et une mémoire distincte pour les données. Mais le fait est que ARM est une architecture Harvard modifiée, c'est-à-dire l'accès à la mémoire s'effectue sur un bus, et le dispositif de gestion de la mémoire assure déjà la séparation des bus à l'aide de signaux de commande: lire, écrire ou sélectionner une zone mémoire.
Ainsi, les données et le code peuvent être dans la même zone de mémoire. Dans cet espace d'adressage unique peut être situé et la mémoire
ROM et la
RAM et les périphériques. Et cela signifie qu'en fait, le code et les données peuvent obtenir même là où cela dépend du compilateur et de l'éditeur de liens.
Par conséquent, afin de faire la distinction entre les zones de mémoire pour
ROM (Flash) et
RAM, elles sont généralement indiquées dans les paramètres de l'éditeur de liens, par exemple, dans IAR 8.40.1, cela ressemble à ceci:
define symbol __ICFEDIT_region_ROM_start__ = 0x08000000; define symbol __ICFEDIT_region_ROM_end__ = 0x0807FFFF; define symbol __ICFEDIT_region_RAM_start__ = 0x20000000; define symbol __ICFEDIT_region_RAM_end__ = 0x2001FFFF; define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__ to __ICFEDIT_region_ROM_end__]; define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__ to __ICFEDIT_region_RAM_end__];
La RAM de ce microcontrôleur est située à
0x20000000 ... 0x2001FFF et la
ROM à
0x008000000 ... 0x0807FFFF .
Vous pouvez facilement changer l'adresse de départ ROM_start en l'adresse RAM, disons RAM_start et l'adresse de fin ROM_end__ en RAM_end__ et votre programme sera complètement localisé dans la RAM.
Vous pouvez même faire le contraire et spécifier de la
RAM dans la zone de mémoire
ROM , et votre programme s'assemblera et clignotera avec succès, mais cela ne fonctionnera pas :)
Certains microcontrôleurs, tels que l'AVR, ont initialement un espace d'adressage séparé pour la mémoire de programme, la mémoire de données et les périphériques, et donc de telles astuces ne fonctionneront pas là-bas, et le programme est écrit sur
ROM par défaut.
Tout l'espace d'adressage dans CortexM est unique et le code et les données peuvent être situés n'importe où. En utilisant les paramètres de l'éditeur de liens, vous pouvez définir la région pour les adresses ROM et RAM . IAR localise le segment de code .text dans la région ROM
Fichier objet et segments
Ci-dessus, j'ai mentionné le segment de code, voyons ce que c'est.
Un fichier objet distinct est créé pour chaque module compilé, qui contient les informations suivantes:
- Segments de code et de données
- Informations de débogage DWARF
- Table de caractères
Nous nous intéressons au code et aux
segments de données.
Un segment est un tel élément contenant un morceau de code ou des données qui doivent être placés à une adresse physique en mémoire. Un segment peut contenir plusieurs fragments, généralement un fragment pour chaque variable ou fonction. Un segment peut être placé à la fois dans la
ROM et la
RAM .
Chaque segment a un nom et un attribut qui définit son contenu. L'attribut est utilisé pour définir un segment dans la configuration de l'éditeur de liens. Par exemple, les attributs peuvent être:
- code - code exécutable
- lecture seule - variables constantes
- readwrite - variables initialisées
- zeroinit - variables initialisées à zéro
Bien sûr, il existe d'autres types de segments, par exemple des segments contenant des informations de débogage, mais nous ne serons intéressés que par ceux qui contiennent du code ou des données de notre application.
En général, un segment est le plus petit bloc pouvant être lié. Cependant, si nécessaire, l'éditeur de liens peut également indiquer des blocs (fragments) encore plus petits. Nous ne considérerons pas cette option, nous le ferons avec des segments.
Pendant la compilation, les données et les fonctions sont placées dans différents segments. Et lors de la liaison, l'éditeur de liens attribue des adresses physiques réelles à différents segments. Le compilateur IAR a des noms de segments prédéfinis, dont certains que je fournirai ci-dessous:
- .bss - Contient des variables statiques et globales initialisées à 0
- .CSTACK - Contient la pile utilisée par le programme
- .data - Contient des variables initialisées statiques et globales
- .data_init - Contient les valeurs initiales des données dans la section .data si la directive d'initialisation pour l'éditeur de liens est utilisée
- HEAP - Contient le tas utilisé pour héberger des données dynamiques
- .intvec - Contient une table de vecteurs d'interruption
- .rodata - Contient des données constantes
- .text - Contient le code du programme
Afin de comprendre où se trouvent les constantes, nous ne nous intéresserons qu'aux segments
.rodata - un segment dans lequel les constantes sont stockées,
.data - un segment dans lequel toutes les variables statiques et globales initialisées sont stockées,
.bss - un segment dans lequel toutes les variables
.data statiques et globales initialisées avec zéro (0) sont stockées,
.text - un segment pour stocker le code.
En pratique, cela signifie que si vous définissez la variable
int val = 3
, la variable elle-même sera localisée par le compilateur dans le segment
.data et marquée avec l'attribut
readwrite , et le nombre 3 peut être placé soit dans le segment
.text soit dans le segment
.rodata ou, si une directive spéciale pour l'éditeur de liens dans
.data_init est appliquée et est également marquée comme étant en
lecture seule par elle .
Le segment
.rodata contient des données constantes et comprend des variables constantes, des chaînes, des littéraux agrégés, etc. Et
ce segment peut être placé n'importe où en mémoire.Maintenant, il devient plus clair ce qui est prescrit dans les paramètres de l'éditeur de liens et pourquoi:
place in ROM_region { readonly };
Autrement dit, toutes les données marquées avec l'attribut readonly doivent être placées dans ROM_region. Ainsi, les données de différents segments, mais marquées avec l'attribut readonly, peuvent entrer dans la ROM.
Eh bien, cela signifie que toutes les constantes doivent être en ROM, mais pourquoi dans notre code, au début de l'article, l'objet constant se trouve-t-il toujours dans la RAM? class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Données constantes
Avant de clarifier la situation, rappelons d'abord que les variables globales sont créées en mémoire partagée, les variables locales, c'est-à-dire les variables déclarées à l'intérieur des fonctions "normales" sont créées sur la pile ou dans les registres, et les variables locales statiques sont également créées dans la mémoire partagée.
Qu'est-ce que cela signifie en C ++. Regardons un exemple:
void foo(const int& C1, const int& C2, const int& C3, const int& C4, const int& C5, const int& C6) { std::cout << C1 << C2 << C3 << C4 << C5 << C6 << std::endl; }
Ce sont toutes des données constantes. Mais pour chacun d'eux, la règle de création décrite ci-dessus s'applique, les variables locales sont créées sur la pile. Par conséquent, avec nos paramètres de l'éditeur de liens, cela devrait ressembler à ceci:
- La constante globale Case1 doit être en ROM . Dans le segment .rodata
- La constante globale Case2 doit être en ROM . Dans le segment .rodata
- La constante locale Case3 doit se trouver dans la RAM (la constante a été créée sur la pile dans le segment STACK)
- La constante statique de Case4 doit être dans la ROM . Dans le segment .rodata
- La constante locale de Case5 doit se trouver dans la RAM (un cas intéressant, mais elle est exactement identique au cas 3.)
- La constante statique de Case6 doit être dans la ROM . Dans le segment .rodata
Examinons maintenant les informations de débogage et le fichier de carte généré. Le débogueur indique à quelles adresses se trouvent ces constantes.

Comme je l'ai déjà dit, les adresses 0x0800 ... ce sont
des adresses
ROM , et 0x200 ... ce sont de la
RAM . Voyons dans quels segments le compilateur a distribué ces constantes:
.rodata const 0x800'4e2c 0x4 main.o //Case1 .rodata const 0x800'4e30 0x4 main.o //Case2 .rodata const 0x800'4e34 0x4 main.o //Case4 .rodata const 0x800'4e38 0x4 main.o //Case6
Quatre constantes globales et statiques sont tombées dans le segment
.rodata et deux variables locales ne sont pas tombées dans le fichier de carte car elles sont créées sur la pile et leur adresse correspond aux adresses de la pile. Le segment CSTACK commence à 0x2000'2488 et se termine à 0x2000'0488. Comme vous pouvez le voir sur l'image, les constantes sont juste créées au début de la pile.
Le compilateur place des constantes globales et statiques dans le segment .rodata , dont l'emplacement est spécifié dans les paramètres de l'éditeur de liens.
Il convient de noter un autre point important, l'
initialisation . Les variables globales et statiques, y compris les constantes, doivent être initialisées. Et cela peut se faire de plusieurs manières. S'il s'agit d'une constante se trouvant dans le segment
.rodata , l'initialisation se produit au stade de la compilation, c'est-à-dire la valeur est immédiatement écrite à l'adresse où se trouve la constante. S'il s'agit d'une variable régulière, l'initialisation peut se produire en copiant la valeur de la mémoire ROM à l'adresse de la variable globale:
Par exemple, si la variable globale
int i = 3
définie, le compilateur l'a définie dans le segment de données
.data , l'éditeur de liens l'a mise à 0x20000000:
.data inited 0x2000'0000
,
et sa valeur d'initialisation (3) se trouvera dans le segment
.rodata à l'adresse 0x8000190:
Initializer bytes const 0x800'0190
Si vous écrivez ce code:
int i = 3; const int c = i;
Il est évident que la constante globale
n'est initialisée qu'après l'initialisation de la variable globale
i
, c'est-à-dire au moment de l'exécution. Dans ce cas, la constante sera située dans la
RAMMaintenant, si nous revenons à notre
exemple initial class WantToBeInROM { private: int i; public: WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Et nous nous demandons: dans quel segment le compilateur a-t-il défini l'objet constant
myConstInROM
? Et nous obtenons la réponse: la constante se trouvera dans le segment
.bss, contenant des variables statiques et globales initialisées à zéro (0).
.bss inited 0x2000'0004 0x4
myConstInROM 0x2000'0004 0x4
Pourquoi? Parce qu'en C ++, un objet de données qui est déclaré comme une constante et qui a besoin d'une initialisation dynamique est situé dans la mémoire en lecture-écriture et il sera initialisé au moment de la création.
Dans ce cas, l'initialisation dynamique se produit,
const WantToBeInROM myConstInROM(10)
, et le compilateur place cet objet dans le segment
.bss , initialisant tous les champs 0 en premier, puis, lors de la création d'un objet constant, appelle le constructeur pour initialiser le champ
i
valeur 10.
Comment faire en sorte que le compilateur place notre objet dans le segment
.rodata ? La réponse à cette question est simple, vous devez toujours effectuer une initialisation statique. Vous pouvez le faire de cette façon:
1. Dans notre exemple, on peut voir qu'en principe le compilateur peut optimiser l'initialisation dynamique en statique, puisque le constructeur est assez simple. Pour l'IAR du compilateur, vous pouvez marquer la constante avec l'attribut
__ro_placement__ro_placement const WantToBeInROM myConstInROM
Avec cette option, le compilateur placera la variable à l'adresse dans la ROM:
myConstInROM 0x800'0144 0x4 Data
De toute évidence, cette approche n'est pas universelle et généralement très spécifique. Par conséquent, nous passons à la bonne méthode :)
2. C'est pour faire un constructeur
constexpr
. Nous demandons immédiatement au compilateur d'utiliser l'initialisation statique, c'est-à-dire au stade de la compilation, lorsque l'ensemble de l'objet sera complètement "calculé" à l'avance et que tous ses champs seront connus. Tout ce que nous devons faire est d'ajouter constexpr au constructeur.
L'objet vole vers la ROM class WantToBeInROM { private: int i; public: constexpr WantToBeInROM(int value): i(value) {} int Get() const { return i; } }; const WantToBeInROM myConstInROM(10); int main() { std::cout << &myConstInROM << std::endl ; }
Donc, pour être sûr que votre objet constant est dans la ROM, vous devez suivre des règles simples:
- Le segment .text dans lequel le code est placé doit être en ROM. Il est configuré dans les paramètres de l'éditeur de liens.
- Le segment .rodata dans lequel les constantes globales et statiques sont placées doit être en ROM. Il est configuré dans les paramètres de l'éditeur de liens.
- La constante doit être globale ou statique.
- Les attributs d'une classe de variables constantes ne doivent pas être modifiables
- L'initialisation de l'objet doit être statique, c'est-à-dire que le constructeur de la classe dont l'objet sera une constante doit être constexpr ou pas du tout défini (il n'y a pas d'initialisation dynamique)
- Si possible, si vous êtes sûr que l'objet doit être stocké dans la ROM au lieu de const, utilisez constexpr
Quelques mots sur constexpr et le constructeur constexpr. La principale différence entre const et constexpr est que l'initialisation de la variable const peut être retardée jusqu'à l'exécution. La variable constexpr doit être initialisée au moment de la compilation.
Toutes les variables constexpr sont de type const.La définition du constructeur constexpr doit satisfaire aux exigences suivantes:
Le constructeur implicite par défaut est le constructeur constexpr. Voyons maintenant quelques exemples:
Exemple 1. Objet dans la ROM class Test { private: int i; public: Test() {} ; int Get() const { return i + 1; } } ; const Test test;
Il vaut mieux ne pas écrire de cette façon, car dès que vous décidez d'initialiser l'attribut i, l'objet volera dans la RAM
Exemple 2. Un objet en RAM class Test { private: int i = 1;
Exemple 3. Un objet en RAM class Test { private: int i; public: Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10);
Exemple 4. Objet dans la ROM class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; const Test test(10);
Exemple 5. Un objet en RAM class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { const Test test(10);
Exemple 6. Objet dans la ROM class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get() const { return i + 1; } } ; int main() { static const Test test(10);
Exemple 7. Erreur de compilation class Test { private: int i; public: constexpr Test(int value): i(value) {} ; int Get()
Exemple 8. Un objet en ROM, héritant d'une classe abstraite class ITest { private: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class Test: public ITest { private: int i; public: constexpr Test(int value): i(value), ITest(value+1) {} ; int Get() const override { return i + 1; } } ; const Test test(10);
Exemple 9. Un objet dans la ROM agrège un objet situé dans la RAM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; TestImpl testImpl(1);
Exemple 10. Le même objet mais statique dans la ROM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj;
Exemple 11. Et maintenant l'objet constant est non statique et donc en RAM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl & obj;
Exemple 12. Erreur de compilation. class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) { k = value; j = value + 10; } } ; class Test: public ITest { private: int i; TestImpl obj;
Exemple 13. Erreur de compilation class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value)
Exemple 14. Un objet dans la ROM class ITest { protected: int j; public: virtual int Get() const = 0; constexpr ITest(int value) : j(value) { } int Give() const { return j ; } }; class TestImpl: public ITest { private: int k; public: constexpr TestImpl(int value): k(value), ITest(value) { } int Get() const override { return j + 10; } void Set(int value) const
Et enfin, un objet constant contenant un tableau, avec l'initialisation du tableau via une fonction constexpr. class Test { private: int k[100]; constexpr void InitArray() { int i = 0; for(auto& it: k) { it = i++ ; } } public: constexpr Test(): k() { InitArray();
Références:
Guide de développement IAR C / C ++Constructeurs Constexpr (C ++ 11)constexpr (C ++)PS.
Après une discussion très utile avec Valdaros, vous devez ajouter les constantes tangentes ponctuelles suivantes. Selon la norme C ++ et ce document N1076.pdf1. Toute modification d'un objet constant (à l'exception des membres mutables d'une classe) au cours de sa vie entraîne un comportement indéfini. C'est-à-dire
const int ci = 1 ; int* iptr = const_cast<int*>(&ci);
int i = 1; const int* ci = &i ; int* iptr = const_cast<int *> (ci);
2. Le problème est que cela ne fonctionne que pendant toute la vie d'un objet constant, mais dans le constructeur et le destructeur, cela ne fonctionne pas. Par conséquent, il est tout à fait légitime de le faire: class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1;
Et c'est considéré comme légal. Malgré le fait que nous avons utilisé le constructeur constexpr et la fonction constexpr. L'objet va directement à la RAM.Pour éviter cela, utilisez const-constexpr au lieu de const, puis il y aura une erreur de compilation qui vous dira que quelque chose ne va pas et que l'objet ne peut pas être constant. class Test { public: int i; constexpr Test(): i(0) { foo(this) ; } } ; Test *test1; constexpr void foo(Test* value) { value->i = 1;