Ausführen komplexer C ++ - Anwendungen auf Mikrocontrollern

BildHeutzutage ist niemand überrascht über die Fähigkeit, C ++ für Mikrocontroller zu entwickeln. Das mbed-Projekt konzentriert sich voll und ganz auf diese Sprache. Eine Reihe anderer RTOSs bieten C ++ - Entwicklungsfunktionen. Dies ist praktisch, da der Programmierer Zugriff auf objektorientierte Programmierwerkzeuge hat. Viele RTOSs legen jedoch verschiedene Einschränkungen für die Verwendung von C ++ fest. In diesem Artikel werden wir uns die Interna von C ++ ansehen und die Gründe für diese Einschränkungen herausfinden.



Ich möchte sofort darauf hinweisen , dass die meisten Beispiele in RTOS Embox berücksichtigt werden . In der Tat arbeiten so komplexe C ++ - Projekte wie Qt und OpenCV auf Mikrocontrollern . OpenCV erfordert vollständige C ++ - Unterstützung, die auf Mikrocontrollern normalerweise nicht vorhanden ist.



Grundlegende Syntax



Die Syntax der C ++ - Sprache wird vom Compiler implementiert. Zur Laufzeit müssen Sie jedoch einige grundlegende Entitäten implementieren. Im Compiler sind sie in der libsupc ++ - Sprachunterstützungsbibliothek enthalten. A. Am grundlegendsten ist die Unterstützung für Konstruktoren und Destruktoren. Es gibt zwei Arten von Objekten: globale und neue.



Globale Konstruktoren und Destruktoren



Schauen wir uns an, wie eine C ++ - Anwendung funktioniert. Vor der Eingabe von main () werden alle globalen C ++ - Objekte erstellt, sofern sie im Code vorhanden sind. Hierfür wird der spezielle Abschnitt .init_array verwendet. Es kann auch Abschnitte .init, .preinit_array, .ctors geben. Bei modernen ARM-Compilern werden Abschnitte am häufigsten mit .preinit_array, .init und .init_array verwendet. Aus Sicht von LIBC ist dies ein gewöhnliches Array von Zeigern auf Funktionen, die von Anfang bis Ende durch Aufrufen des entsprechenden Elements des Arrays übergeben werden müssen. Nach diesem Vorgang wird die Steuerung an main () übertragen.



Der Code zum Aufrufen von Konstruktoren für globale Objekte aus 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)();
    }
}
      
      





Lassen Sie uns nun sehen, wie die Beendigung einer C ++ - Anwendung funktioniert, nämlich der Aufruf der Destruktoren globaler Objekte. Es gibt zwei Möglichkeiten.



Ich beginne mit dem in Compilern am häufigsten verwendeten - über __cxa_atexit () (vom C ++ ABI). Dies ist ein Analogon zur POSIX-Atexit-Funktion. Sie können also spezielle Handler registrieren, die beim Beenden des Programms aufgerufen werden. Wenn die globalen Konstruktoren wie oben beschrieben zu Beginn der Anwendung aufgerufen werden, gibt es auch vom Compiler generierten Code, der Handler über den Aufruf von __cxa_atexit registriert. Die Aufgabe von LIBC besteht darin, die erforderlichen Handler und ihre Argumente zu speichern und sie aufzurufen, wenn die Anwendung endet.



Eine andere Möglichkeit besteht darin, Zeiger auf Destruktoren in speziellen Abschnitten .fini_array und .fini zu speichern. Im GCC-Compiler kann dies mit dem Flag -fno-use-cxa-atexit erreicht werden. In diesem Fall müssen die Destruktoren während der Anwendungsbeendigung in umgekehrter Reihenfolge (von der hohen Adresse zur niedrigen Adresse) aufgerufen werden. Diese Methode ist weniger verbreitet, kann jedoch in Mikrocontrollern nützlich sein. In diesem Fall können Sie zum Zeitpunkt der Erstellung der Anwendung herausfinden, wie viele Handler erforderlich sind.



Der Code zum Aufrufen von Destruktoren für globale Objekte aus 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)();
    }
}
      
      





Globale Destruktoren sind erforderlich, um C ++ - Anwendungen neu starten zu können. Die meisten RTOS für Mikrocontroller führen eine einzelne Anwendung aus, die nicht neu gestartet wird. Der Start beginnt mit einer benutzerdefinierten Funktion main, der einzigen im System. Daher sind globale Destruktoren in kleinen RTOS häufig leer, da sie nicht zur Verwendung vorgesehen sind.



Globaler Destruktor-Code von 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;
}

      
      





Operatoren neu / löschen



Im GCC-Compiler befindet sich die Implementierung der Operatoren new / delete in der Bibliothek libsupc ++ und ihre Deklarationen in der Header-Datei.



Sie können die Implementierungen new / delete aus libsupc ++. A verwenden, sie sind jedoch recht einfach und können beispielsweise über Standard-Malloc / Free oder Analoga implementiert werden.



Implementierungscode für einfache Embox-Objekte neu / löschen:




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 & Ausnahmen



Wenn Ihre Anwendung einfach ist, benötigen Sie möglicherweise keine Ausnahmeunterstützung und dynamische Datentypidentifikation (RTTI). In diesem Fall können sie mit den Compiler-Flags -no-exception -no-rtti deaktiviert werden.



Wenn diese C ++ - Funktionalität jedoch erforderlich ist, muss sie implementiert werden. Dies ist viel schwieriger als Neu / Löschen.



Die gute Nachricht ist, dass diese Dinge vom Betriebssystem unabhängig sind und bereits in der libsupc ++ - Bibliothek übergreifend kompiliert wurden. A. Dementsprechend ist der einfachste Weg, Unterstützung hinzuzufügen, die Verwendung von libsupc ++. Eine Bibliothek aus dem Cross-Compiler. Die Prototypen selbst befinden sich in den Header-Dateien und.



Um compilerübergreifende Ausnahmen zu verwenden, müssen beim Hinzufügen Ihrer eigenen C ++ - Laufzeitlademethode kleine Anforderungen erfüllt werden. Das Linker-Skript muss einen speziellen Abschnitt .eh_frame haben. Und bevor Sie die Laufzeit verwenden, muss dieser Abschnitt mit der Adresse am Anfang des Abschnitts initialisiert werden. Embox verwendet den folgenden Code:



void register_eh_frame(void) {
    extern const char _eh_frame_begin;
    __register_frame((void *)&_eh_frame_begin);
}
      
      





Für die ARM-Architektur werden andere Abschnitte mit eigener Informationsstruktur verwendet - .ARM.exidx und .ARM.extab. Das Format dieser Abschnitte ist im EHABI-Standard „Ausnahmebehandlung ABI für die ARM-Architektur“ definiert. .ARM.exidx ist die Indextabelle und .ARM.extab ist die Tabelle der Elemente selbst, die zur Behandlung der Ausnahme erforderlich sind. Um diese Abschnitte für die Behandlung von Ausnahmen zu verwenden, müssen Sie sie in das Linker-Skript aufnehmen:



    .ARM.exidx : {
        __exidx_start = .;
        KEEP(*(.ARM.exidx*));
        __exidx_end = .;
    } SECTION_REGION(text)

    .ARM.extab : {
        KEEP(*(.ARM.extab*));
    } SECTION_REGION(text)
      
      





Damit GCC diese Abschnitte zur Behandlung von Ausnahmen verwenden kann, werden Anfang und Ende des Abschnitts .ARM.exidx angegeben - __exidx_start und __exidx_end. Diese Symbole werden in der Datei libgcc / unwind-arm-common.inc in libgcc importiert:

extern __EIT_entry __exidx_start;
extern __EIT_entry __exidx_end;
      
      





Weitere Informationen zum Abwickeln von Stapeln in ARM finden Sie im Artikel .



Sprachstandardbibliothek (libstdc ++)



Native Implementierung der Standardbibliothek



Die C ++ - Unterstützung umfasst nicht nur die grundlegende Syntax, sondern auch die libstdc ++ - Standardbibliothek. Die Funktionalität sowie die Syntax können in verschiedene Ebenen unterteilt werden. Es gibt grundlegende Dinge wie das Arbeiten mit Strings oder dem C ++ - Setjmp-Wrapper. Sie können einfach über die Standard-C-Bibliothek implementiert werden. Außerdem gibt es erweiterte Funktionen, z. B. die Standard-Vorlagenbibliothek (STL).



Standardbibliothek vom Cross-Compiler



Grundlegende Dinge sind in Embox implementiert. Wenn diese Dinge ausreichen, können Sie die externe C ++ - Standardbibliothek nicht einschließen. Wenn beispielsweise Unterstützung für Container benötigt wird, ist es am einfachsten, die Bibliotheks- und Header-Dateien des Cross-Compilers zu verwenden.



Bei Verwendung der C ++ - Standardbibliothek eines Cross-Compilers gibt es eine Wendung. Werfen wir einen Blick auf den Standardarm-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)
      
      





Es wurde mit Unterstützung für die --with-newlib.Newlib-Implementierung der C-Standardbibliothek erstellt. Embox verwendet eine eigene Implementierung der Standardbibliothek. Es gibt einen Grund dafür, den Overhead zu minimieren. Daher können die erforderlichen Parameter sowohl für die Standard-C-Bibliothek als auch für andere Teile des Systems festgelegt werden.



Da sich die Standard-C-Bibliotheken unterscheiden, muss eine Kompatibilitätsschicht implementiert werden, um die Laufzeit aufrechtzuerhalten. Ich werde ein Beispiel für die Implementierung eines der notwendigen, aber nicht offensichtlichen Dinge zur Unterstützung der Standardbibliothek von einem Cross-Compiler aus Embox geben



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;
}
      
      





Alle Teile und ihre Implementierungen, die für die Verwendung des Cross-Compilers libstdc ++ erforderlich sind, können in Embox im Ordner 'Drittanbieter / lib / toolchain / newlib_compat /' angezeigt werden.



Erweiterte Unterstützung für die Standardbibliothek std :: thread und std :: mutex



Die C ++ - Standardbibliothek im Compiler kann verschiedene Unterstützungsstufen haben. Schauen wir uns die Ausgabe noch einmal an:



$ arm-none-eabi-gcc -v
***
Thread model: single
gcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)
      
      





Gewindemodell: einzeln. Wenn GCC mit dieser Option erstellt wird, wird die gesamte Thread-Unterstützung aus der STL entfernt (z. B. std :: thread und std :: mutex ). Und zum Beispiel wird es Probleme bei der Zusammenstellung einer so komplexen C ++ - Anwendung wie OpenCV geben. Mit anderen Worten, diese Version der Bibliothek reicht nicht aus, um Anwendungen zu erstellen, die diese Funktionalität erfordern.



Die Lösung, die wir bei Embox verwenden, besteht darin, einen eigenen Compiler für die Standardbibliothek mit einem Multithread-Modell zu erstellen. Bei Embox wird das Posix „Thread model: posix“ verwendet. In diesem Fall werden std :: thread und std :: mutex über den Standard pthread_ * und pthread_mutex_ * implementiert. Dadurch entfällt auch die Notwendigkeit, die Newlib-Kompatibilitätsschicht einzuschließen.



Embox-Konfiguration



Die Neuerstellung des Compilers ist zwar die zuverlässigste und bietet die vollständigste und kompatibelste Lösung, nimmt jedoch gleichzeitig viel Zeit in Anspruch und erfordert möglicherweise zusätzliche Ressourcen, die im Mikrocontroller nicht so viele sind. Daher ist es nicht ratsam, diese Methode überall anzuwenden.



Um die Supportkosten zu optimieren, hat Embox mehrere abstrakte Klassen (Schnittstellen) eingeführt, von denen verschiedene Implementierungen angegeben werden können.



  • embox.lib.libsupcxx - Definiert, welche Methode zur Unterstützung der grundlegenden Syntax der Sprache verwendet werden soll.
  • embox.lib.libstdcxx - Definiert, welche Implementierung der Standardbibliothek verwendet werden soll


Es gibt drei Optionen für libsupcxx:



  • embox.lib.cxx.libsupcxx_standalone - grundlegende Implementierung in Embox.
  • Third_party.lib.libsupcxx_toolchain - Verwenden Sie die Sprachunterstützungsbibliothek des Cross-Compilers
  • Third_party.gcc.tlibsupcxx - Vollständige Zusammenstellung der Bibliothek aus Quellen


Die minimale Option kann auch ohne die C ++ - Standardbibliothek funktionieren. Embox verfügt über eine Implementierung, die auf den einfachsten Funktionen der Standard-C-Bibliothek basiert. Wenn diese Funktionalität nicht ausreicht, können Sie drei libstdcxx-Optionen angeben.



  • Third_party.STLport.libstlportg ist eine STL-Standardbibliothek, die auf dem STLport-Projekt basiert. Erfordert keine Neuerstellung von gcc. Das Projekt wurde aber schon lange nicht mehr unterstützt
  • Third_party.lib.libstdcxx_toolchain - Standardbibliothek aus dem Cross-Compiler
  • Third_party.gcc.libstdcxx - Vollständige Zusammenstellung der Bibliothek aus Quellen


Wenn Sie möchten, beschreibt unser Wiki, wie Sie Qt oder OpenCV auf STM32F7 erstellen und ausführen können. Der gesamte Code ist natürlich kostenlos.



All Articles