Ejecución de aplicaciones complejas de C ++ en microcontroladores

imagenHoy en día, a nadie le sorprende la capacidad de desarrollar en C ++ para microcontroladores. El proyecto mbed está totalmente centrado en este lenguaje. Varios otros RTOS proporcionan capacidades de desarrollo de C ++. Esto es conveniente, porque el programador tiene acceso a herramientas de programación orientadas a objetos. Sin embargo, muchos RTOS imponen varias restricciones sobre el uso de C ++. En este artículo veremos las partes internas de C ++ y descubriremos las razones de estas limitaciones.



Quiero señalar de inmediato que la mayoría de los ejemplos se considerarán en RTOS Embox . De hecho, proyectos C ++ tan complejos como Qt y OpenCV funcionan en microcontroladores . OpenCV requiere soporte completo de C ++, que generalmente no se encuentra en microcontroladores.



Sintaxis básica



La sintaxis del lenguaje C ++ es implementada por el compilador. Pero en tiempo de ejecución, debe implementar algunas entidades básicas. En el compilador, se incluyen en la biblioteca de soporte del lenguaje libsupc ++. A. El más básico es el soporte para constructores y destructores. Hay dos tipos de objetos: globales y nuevos.



Constructores y destructores globales



Echemos un vistazo a cómo funciona cualquier aplicación de C ++. Antes de ingresar main (), se crean todos los objetos globales de C ++, si están presentes en el código. La sección especial .init_array se utiliza para esto. También puede haber secciones .init, .preinit_array, .ctors. Para los compiladores ARM modernos, el uso más común de las secciones es .preinit_array, .init y .init_array. Desde el punto de vista de LIBC, esta es una matriz ordinaria de punteros a funciones, que deben pasarse de principio a fin llamando al elemento correspondiente de la matriz. Después de este procedimiento, el control se transfiere a main ().



El código para llamar a constructores para objetos globales de 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)();
    }
}
      
      





Veamos ahora cómo funciona la terminación de una aplicación C ++, es decir, la llamada de los destructores de objetos globales. Hay dos maneras.



Comenzaré con el que se usa con más frecuencia en los compiladores: a través de __cxa_atexit () (de la ABI de C ++). Este es un análogo de la función atexit POSIX, es decir, puede registrar manejadores especiales que serán llamados cuando finalice el programa. Cuando se llama a los constructores globales al inicio de la aplicación, como se describió anteriormente, también hay código generado por el compilador que registra los controladores a través de la llamada a __cxa_atexit. El trabajo de LIBC aquí es almacenar los controladores necesarios y sus argumentos y llamarlos cuando finaliza la aplicación.



Otra forma es almacenar punteros a destructores en secciones especiales .fini_array y .fini. En el compilador GCC, esto se puede lograr con el indicador -fno-use-cxa-atexit. En este caso, los destructores deben llamarse en orden inverso (de la dirección alta a la dirección baja) durante la terminación de la aplicación. Este método es menos común, pero puede ser útil en microcontroladores. De hecho, en este caso, en el momento de compilar la aplicación, puede averiguar cuántos controladores son necesarios.



El código para llamar a destructores para objetos globales de 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)();
    }
}
      
      





Se requieren destructores globales para poder reiniciar las aplicaciones C ++. La mayoría de RTOS para microcontroladores ejecutan una sola aplicación que no se reinicia. El inicio comienza con una función principal personalizada, la única en el sistema. Por lo tanto, en RTOS pequeños, los destructores globales a menudo están vacíos, porque no están destinados a ser utilizados.



Código de destructores globales 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;
}

      
      





Nuevos / eliminar operadores



En el compilador GCC, la implementación de los operadores new / delete está en la biblioteca libsupc ++, y sus declaraciones están en el archivo de encabezado.



Puede usar las implementaciones new / delete de libsupc ++. A, pero son bastante simples y se pueden implementar, por ejemplo, a través de malloc / free estándar o análogos.



Código de implementación nuevo / eliminado para objetos 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 y excepciones



Si su aplicación es simple, es posible que no necesite soporte de excepción e identificación dinámica de tipos de datos (RTTI). En este caso, se pueden deshabilitar usando los indicadores del compilador -no-exception -no-rtti.



Pero si se requiere esta funcionalidad de C ++, debe implementarse. Esto es mucho más difícil de hacer que nuevo / eliminar.



La buena noticia es que estas cosas son independientes del sistema operativo y ya están compiladas de forma cruzada en la biblioteca libsupc ++. A. En consecuencia, la forma más fácil de agregar soporte es usar libsupc ++. Una biblioteca del compilador cruzado. Los prototipos en sí están en los archivos de encabezado y.



Para usar excepciones de compiladores cruzados, hay pequeños requisitos que deben cumplirse al agregar su propio método de carga en tiempo de ejecución de C ++. El script del enlazador debe tener una sección especial .eh_frame. Y antes de usar runtime, esta sección debe inicializarse con la dirección del comienzo de la sección. Embox usa el siguiente código:



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





Para la arquitectura ARM, se utilizan otras secciones con su propia estructura de información: .ARM.exidx y .ARM.extab. El formato de estas secciones se define en la "ABI de manejo de excepciones para la arquitectura ARM" - estándar EHABI. .ARM.exidx es la tabla de índice y .ARM.extab es la tabla de los elementos necesarios para manejar la excepción. Para usar estas secciones para manejar excepciones, debe incluirlas en la secuencia de comandos del vinculador:



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

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





Para permitir que GCC use estas secciones para manejar excepciones, se especifican el inicio y el final de la sección .ARM.exidx: __exidx_start y __exidx_end. Estos símbolos se importan a libgcc en el archivo libgcc / wind-arm-common.inc:

extern __EIT_entry __exidx_start;
extern __EIT_entry __exidx_end;
      
      





Para obtener más información sobre el desenrollado de pila en ARM, consulte el artículo .



Biblioteca estándar de idiomas (libstdc ++)



Implementación nativa de la biblioteca estándar



El soporte de C ++ incluye no solo la sintaxis básica, sino también la biblioteca estándar libstdc ++. Su funcionalidad, así como su sintaxis, se pueden dividir en diferentes niveles. Hay cosas básicas como trabajar con cadenas o el contenedor setjmp de C ++. Se implementan fácilmente a través de la biblioteca estándar de C. Y hay cosas más avanzadas, por ejemplo, la biblioteca de plantillas estándar (STL).



Biblioteca estándar del compilador cruzado



Las cosas básicas se implementan en Embox. Si estas cosas son suficientes, entonces no puede incluir la biblioteca estándar externa de C ++. Pero si, por ejemplo, se necesita soporte para contenedores, entonces la forma más fácil es usar la biblioteca y los archivos de encabezado del compilador cruzado.



Hay un giro cuando se usa la biblioteca estándar C ++ de un compilador cruzado. Echemos un vistazo al estándar 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)
      
      





Está construido con soporte para la implementación --with-newlib.Newlib de la biblioteca estándar de C. Embox usa su propia implementación de la biblioteca estándar. Hay una razón para esto, minimizar los gastos generales. Y por lo tanto, los parámetros necesarios se pueden configurar para la biblioteca C estándar, así como para otras partes del sistema.



Dado que las bibliotecas C estándar son diferentes, se debe implementar una capa de compatibilidad para mantener el tiempo de ejecución. Daré un ejemplo de implementación de Embox de una de las cosas necesarias pero no obvias para admitir la biblioteca estándar desde un compilador cruzado



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





Todas las partes y sus implementaciones necesarias para usar el compilador cruzado libstdc ++ se pueden ver en Embox en la carpeta 'terceros / lib / toolchain / newlib_compat /'



Soporte extendido para la biblioteca estándar std :: thread y std :: mutex



La biblioteca estándar de C ++ en el compilador puede tener diferentes niveles de soporte. Echemos otro vistazo a la salida:



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





Modelo de hilo: sencillo. Cuando GCC se construye con esta opción, se elimina todo el soporte de subprocesos de STL (por ejemplo, std :: thread y std :: mutex ). Y, por ejemplo, habrá problemas con el montaje de una aplicación C ++ tan compleja como OpenCV. En otras palabras, esta versión de la biblioteca no es suficiente para crear aplicaciones que requieran esta funcionalidad.



La solución que usamos en Embox es construir nuestro propio compilador por el bien de la biblioteca estándar con un modelo multiproceso. En el caso de Embox, se utiliza el posix “Modelo de rosca: posix”. En este caso, std :: thread y std :: mutex se implementan a través del estándar pthread_ * y pthread_mutex_ *. Esto también elimina la necesidad de incluir la capa de compatibilidad de newlib.



Configuración de Embox



Aunque la reconstrucción del compilador es la más confiable y proporciona la solución más completa y compatible, al mismo tiempo lleva mucho tiempo y puede requerir recursos adicionales, que no son tantos en el microcontrolador. Por lo tanto, este método no es recomendable para usar en todas partes.



Para optimizar los costos de soporte, Embox ha introducido varias clases abstractas (interfaces) de las cuales se pueden especificar diferentes implementaciones.



  • embox.lib.libsupcxx: define qué método utilizar para admitir la sintaxis básica del idioma.
  • embox.lib.libstdcxx: define qué implementación de la biblioteca estándar usar


Hay tres opciones para libsupcxx:



  • embox.lib.cxx.libsupcxx_standalone: ​​implementación básica incluida en Embox.
  • third_party.lib.libsupcxx_toolchain: use la biblioteca de soporte de idiomas del compilador cruzado
  • third_party.gcc.tlibsupcxx: ensamblaje completo de la biblioteca a partir de las fuentes


La opción mínima puede funcionar incluso sin la biblioteca estándar de C ++. Embox tiene una implementación basada en las funciones más simples de la biblioteca estándar de C. Si esta funcionalidad no es suficiente, puede especificar tres opciones libstdcxx.



  • third_party.STLport.libstlportg es una biblioteca estándar STL basada en el proyecto STLport. No requiere reconstruir gcc. Pero el proyecto no ha recibido apoyo durante mucho tiempo.
  • third_party.lib.libstdcxx_toolchain - biblioteca estándar del compilador cruzado
  • third_party.gcc.libstdcxx: ensamblaje completo de la biblioteca a partir de las fuentes


Si lo desea, nuestra wiki describe cómo puede construir y ejecutar Qt u OpenCV en STM32F7. Todo el código es naturalmente gratuito.



All Articles