Aujourd'hui, personne n'est surpris par la possibilité de développer en C ++ pour les microcontrôleurs. Le projet mbed est entièrement axé sur ce langage. Un certain nombre d'autres RTOS fournissent des capacités de développement C ++. C'est pratique, car le programmeur a accès à des outils de programmation orientés objet. Cependant, de nombreux RTOS imposent diverses restrictions sur l'utilisation de C ++. Dans cet article, nous examinerons les éléments internes de C ++ et découvrirons les raisons de ces limitations.
Je tiens à noter tout de suite que la plupart des exemples seront considérés sur RTOS Embox . En effet, des projets C ++ complexes tels que Qt et OpenCV y travaillent sur des microcontrôleurs . OpenCV nécessite une prise en charge complète du C ++, ce qui n'est généralement pas trouvé sur les microcontrôleurs.
Syntaxe de base
La syntaxe du langage C ++ est implémentée par le compilateur. Mais au moment de l'exécution, vous devez implémenter quelques entités de base. Dans le compilateur, ils sont inclus dans la bibliothèque de prise en charge du langage libsupc ++. A. Le plus élémentaire est le support des constructeurs et des destructeurs. Il existe deux types d'objets: globaux et nouveaux.
Constructeurs et destructeurs mondiaux
Jetons un coup d'œil au fonctionnement de toute application C ++. Avant d'entrer main (), tous les objets C ++ globaux sont créés, s'ils sont présents dans le code. La section spéciale .init_array est utilisée pour cela. Il peut également y avoir des sections .init, .preinit_array, .ctors. Pour les compilateurs ARM modernes, l'utilisation la plus courante des sections est .preinit_array, .init et .init_array. Du point de vue de LIBC, il s'agit d'un tableau ordinaire de pointeurs vers des fonctions, qui doivent être passés du début à la fin en appelant l'élément correspondant du tableau. Après cette procédure, le contrôle est transféré à main ().
Le code pour appeler les constructeurs pour les objets globaux à partir d'Embox:
void cxx_invoke_constructors(void) {
extern const char _ctors_start, _ctors_end;
typedef void (*ctor_func_t)(void);
ctor_func_t *func = (ctor_func_t *) &_ctors_start;
....
for ( ; func != (ctor_func_t *) &_ctors_end; func++) {
(*func)();
}
}
Voyons maintenant comment fonctionne la terminaison d'une application C ++, à savoir, l'appel des destructeurs d'objets globaux. Il y a deux manières.
Je vais commencer par celui le plus couramment utilisé dans les compilateurs - via __cxa_atexit () (à partir de l'ABI C ++). Il s'agit d'un analogue de la fonction POSIX atexit, c'est-à-dire que vous pouvez enregistrer des gestionnaires spéciaux qui seront appelés à la fin du programme. Lorsque les constructeurs globaux sont appelés au démarrage de l'application, comme décrit ci-dessus, il existe également du code généré par le compilateur qui enregistre les gestionnaires via l'appel à __cxa_atexit. Le travail de LIBC ici est de stocker les gestionnaires requis et leurs arguments et de les appeler lorsque l'application se termine.
Une autre façon est de stocker des pointeurs vers des destructeurs dans des sections spéciales .fini_array et .fini. Dans le compilateur GCC, cela peut être réalisé avec l'option -fno-use-cxa-atexit. Dans ce cas, les destructeurs doivent être appelés dans l'ordre inverse (de l'adresse haute à l'adresse basse) lors de l'arrêt de l'application. Cette méthode est moins courante, mais peut être utile dans les microcontrôleurs. En effet, dans ce cas, au moment de la construction de l'application, vous pouvez savoir combien de gestionnaires sont nécessaires.
Le code pour appeler des destructeurs pour les objets globaux d'Embox:
int __cxa_atexit(void (*f)(void *), void *objptr, void *dso) {
if (atexit_func_count >= TABLE_SIZE) {
printf("__cxa_atexit: static destruction table overflow.\n");
return -1;
}
atexit_funcs[atexit_func_count].destructor_func = f;
atexit_funcs[atexit_func_count].obj_ptr = objptr;
atexit_funcs[atexit_func_count].dso_handle = dso;
atexit_func_count++;
return 0;
};
void __cxa_finalize(void *f) {
int i = atexit_func_count;
if (!f) {
while (i--) {
if (atexit_funcs[i].destructor_func) {
(*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);
atexit_funcs[i].destructor_func = 0;
}
}
atexit_func_count = 0;
} else {
for ( ; i >= 0; --i) {
if (atexit_funcs[i].destructor_func == f) {
(*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);
atexit_funcs[i].destructor_func = 0;
}
}
}
}
void cxx_invoke_destructors(void) {
extern const char _dtors_start, _dtors_end;
typedef void (*dtor_func_t)(void);
dtor_func_t *func = ((dtor_func_t *) &_dtors_end) - 1;
/* There are two possible ways for destructors to be calls:
* 1. Through callbacks registered with __cxa_atexit.
* 2. From .fini_array section. */
/* Handle callbacks registered with __cxa_atexit first, if any.*/
__cxa_finalize(0);
/* Handle .fini_array, if any. Functions are executed in teh reverse order. */
for ( ; func >= (dtor_func_t *) &_dtors_start; func--) {
(*func)();
}
}
Les destructeurs globaux sont nécessaires pour pouvoir redémarrer les applications C ++. La plupart des RTOS pour microcontrôleurs exécutent une seule application qui ne redémarre pas. Le démarrage commence par une fonction principale personnalisée, la seule du système. Par conséquent, dans les petits RTOS, les destructeurs globaux sont souvent vides, car ils ne sont pas destinés à être utilisés.
Code des destructeurs mondiaux de Zephyr RTOS:
/**
* @brief Register destructor for a global object
*
* @param destructor the global object destructor function
* @param objptr global object pointer
* @param dso Dynamic Shared Object handle for shared libraries
*
* Function does nothing at the moment, assuming the global objects
* do not need to be deleted
*
* @return N/A
*/
int __cxa_atexit(void (*destructor)(void *), void *objptr, void *dso)
{
ARG_UNUSED(destructor);
ARG_UNUSED(objptr);
ARG_UNUSED(dso);
return 0;
}
Nouveaux opérateurs / supprimer des opérateurs
Dans le compilateur GCC, l'implémentation des opérateurs new / delete est dans la bibliothèque libsupc ++, et leurs déclarations sont dans le fichier d'en-tête.
Vous pouvez utiliser les implémentations new / delete de libsupc ++. A, mais elles sont assez simples et peuvent être implémentées, par exemple, via malloc / free ou analogues standard.
Nouveau / suppression du code d'implémentation pour les objets Embox simples:
void* operator new(std::size_t size) throw() {
void *ptr = NULL;
if ((ptr = std::malloc(size)) == 0) {
if (alloc_failure_handler) {
alloc_failure_handler();
}
}
return ptr;
}
void operator delete(void* ptr) throw() {
std::free(ptr);
}
RTTI et exceptions
Si votre application est simple, vous n'aurez peut-être pas besoin de prise en charge des exceptions et d'identification dynamique du type de données (RTTI). Dans ce cas, ils peuvent être désactivés en utilisant les indicateurs du compilateur -no-exception -no-rtti.
Mais si cette fonctionnalité C ++ est requise, elle doit être implémentée. C'est beaucoup plus difficile à faire que nouveau / supprimer.
La bonne nouvelle est que ces éléments sont indépendants du système d'exploitation et sont déjà compilés de manière croisée dans la bibliothèque libsupc ++. A. Par conséquent, le moyen le plus simple d'ajouter du support est d'utiliser la libsupc ++. Une bibliothèque du compilateur croisé. Les prototypes eux-mêmes se trouvent dans les fichiers d'en-tête et.
Pour utiliser des exceptions de compilateur croisé, de petites conditions doivent être remplies lors de l'ajout de votre propre méthode de chargement d'exécution C ++. Le script de l'éditeur de liens doit avoir une section spéciale .eh_frame. Et avant d'utiliser le runtime, cette section doit être initialisée avec l'adresse du début de la section. Embox utilise le code suivant:
void register_eh_frame(void) {
extern const char _eh_frame_begin;
__register_frame((void *)&_eh_frame_begin);
}
Pour l'architecture ARM, d'autres sections avec leur propre structure d'information sont utilisées - .ARM.exidx et .ARM.extab. Le format de ces sections est défini dans la norme «Exception Handling ABI for the ARM Architecture» - EHABI. .ARM.exidx est la table d'index et .ARM.extab est la table des éléments eux-mêmes nécessaires pour gérer l'exception. Pour utiliser ces sections pour gérer les exceptions, vous devez les inclure dans le script de l'éditeur de liens:
.ARM.exidx : { __exidx_start = .; KEEP(*(.ARM.exidx*)); __exidx_end = .; } SECTION_REGION(text) .ARM.extab : { KEEP(*(.ARM.extab*)); } SECTION_REGION(text)
Pour permettre à GCC d'utiliser ces sections pour gérer les exceptions, le début et la fin de la section .ARM.exidx sont spécifiés - __exidx_start et __exidx_end. Ces symboles sont importés dans libgcc dans le fichier libgcc / unwind-arm-common.inc:
extern __EIT_entry __exidx_start;
extern __EIT_entry __exidx_end;
Pour plus d'informations sur le déroulement de la pile sur ARM, consultez l'article .
Bibliothèque standard de langage (libstdc ++)
Implémentation native de la bibliothèque standard
Le support C ++ inclut non seulement la syntaxe de base, mais également la bibliothèque standard libstdc ++. Ses fonctionnalités, ainsi que la syntaxe, peuvent être divisées en différents niveaux. Il y a des choses de base comme travailler avec des chaînes ou un wrapper setjmp C ++. Ils sont facilement implémentés via la bibliothèque standard C. Et il y a des choses plus avancées, par exemple, la bibliothèque de modèles standard (STL).
Bibliothèque standard du compilateur croisé
Les éléments de base sont implémentés dans Embox. Si ces éléments sont suffisants, vous ne pouvez pas inclure la bibliothèque standard C ++ externe. Mais si, par exemple, la prise en charge des conteneurs est nécessaire, le moyen le plus simple consiste à utiliser la bibliothèque et les fichiers d'en-tête du compilateur croisé.
Il y a une torsion lors de l'utilisation de la bibliothèque standard C ++ à partir d'un compilateur croisé. Jetons un coup d'œil au standard arm-none-eabi-gcc:
$ arm-none-eabi-gcc -v Using built-in specs. COLLECT_GCC=arm-none-eabi-gcc COLLECT_LTO_WRAPPER=/home/alexander/apt/gcc-arm-none-eabi-9-2020-q2-update/bin/../lib/gcc/arm-none-eabi/9.3.1/lto-wrapper Target: arm-none-eabi Configured with: *** --with-gnu-as --with-gnu-ld --with-newlib *** Thread model: single gcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)
Il est construit avec le support de l'implémentation --with-newlib.Newlib de la bibliothèque standard C. Embox utilise sa propre implémentation de la bibliothèque standard. Il y a une raison à cela, minimiser les frais généraux. Par conséquent, les paramètres requis peuvent être définis pour la bibliothèque C standard, ainsi que pour d'autres parties du système.
Puisque les bibliothèques C standard sont différentes, une couche de compatibilité doit être implémentée pour maintenir l'exécution. Je vais donner un exemple d'implémentation depuis Embox de l'une des choses nécessaires mais pas évidentes pour prendre en charge la bibliothèque standard à partir d'un compilateur croisé
struct _reent {
int _errno; /* local copy of errno */
/* FILE is a big struct and may change over time. To try to achieve binary
compatibility with future versions, put stdin,stdout,stderr here.
These are pointers into member __sf defined below. */
FILE *_stdin, *_stdout, *_stderr;
};
struct _reent global_newlib_reent;
void *_impure_ptr = &global_newlib_reent;
static int reent_init(void) {
global_newlib_reent._stdin = stdin;
global_newlib_reent._stdout = stdout;
global_newlib_reent._stderr = stderr;
return 0;
}
Toutes les parties et leurs implémentations nécessaires pour utiliser le compilateur croisé libstdc ++ peuvent être visualisées dans Embox dans le dossier 'third-party / lib / toolchain / newlib_compat /'
Prise en charge étendue de la bibliothèque standard std :: thread et std :: mutex
La bibliothèque standard C ++ du compilateur peut avoir différents niveaux de prise en charge. Jetons un autre regard sur la sortie:
$ arm-none-eabi-gcc -v *** Thread model: single gcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)
Modèle de fil: unique. Quand GCC est construit avec cette option, tout le support de thread de la STL est supprimé (par exemple, std :: thread et std :: mutex ). Et, par exemple, il y aura des problèmes avec l'assemblage d'une application C ++ aussi complexe qu'OpenCV. En d'autres termes, cette version de la bibliothèque n'est pas suffisante pour créer des applications qui nécessitent cette fonctionnalité.
La solution que nous utilisons chez Embox est de construire notre propre compilateur pour le bien de la bibliothèque standard avec un modèle multithread. Dans le cas d'Embox, le posix «Modèle de filetage: posix» est utilisé. Dans ce cas, std :: thread et std :: mutex sont implémentés via les standards pthread_ * et pthread_mutex_ *. Cela supprime également le besoin d'inclure la couche de compatibilité newlib.
Configuration Embox
Bien que la reconstruction du compilateur soit la plus fiable et offre la solution la plus complète et la plus compatible, elle prend en même temps beaucoup de temps et peut nécessiter des ressources supplémentaires, qui ne sont pas si nombreuses dans le microcontrôleur. Par conséquent, cette méthode n'est pas recommandée partout.
Afin d'optimiser les coûts de support, Embox a introduit plusieurs classes abstraites (interfaces) dont différentes implémentations peuvent être spécifiées.
- embox.lib.libsupcxx - définit la méthode à utiliser pour prendre en charge la syntaxe de base du langage.
- embox.lib.libstdcxx - définit quelle implémentation de la bibliothèque standard utiliser
Il existe trois options pour libsupcxx:
- embox.lib.cxx.libsupcxx_standalone - implémentation de base incluse dans Embox.
- third_party.lib.libsupcxx_toolchain - utilise la bibliothèque de prise en charge du langage du compilateur croisé
- third_party.gcc.tlibsupcxx - assemblage complet de la bibliothèque à partir des sources
L'option minimale peut fonctionner même sans la bibliothèque standard C ++. Embox a une implémentation basée sur les fonctions les plus simples de la bibliothèque standard C. Si cette fonctionnalité ne suffit pas, vous pouvez spécifier trois options libstdcxx.
- third_party.STLport.libstlportg est une bibliothèque standard STL basée sur le projet STLport. Ne nécessite pas de reconstruction de gcc. Mais le projet n'a pas été soutenu depuis longtemps
- third_party.lib.libstdcxx_toolchain - bibliothèque standard du compilateur croisé
- third_party.gcc.libstdcxx - assemblage complet de la bibliothèque à partir des sources
Si vous le souhaitez, notre wiki décrit comment vous pouvez construire et exécuter Qt ou OpenCV sur STM32F7. Tout le code est naturellement gratuit.