Menjalankan aplikasi C ++ kompleks pada mikrokontroler

gambarSaat ini, tidak ada yang terkejut dengan kemampuan untuk mengembangkan C ++ untuk mikrokontroler. Proyek mbed sepenuhnya berfokus pada bahasa ini. Sejumlah RTOS lainnya menyediakan kemampuan pengembangan C ++. Ini nyaman, karena pemrogram memiliki akses ke alat pemrograman berorientasi objek. Namun, banyak RTOS yang memberlakukan berbagai batasan pada penggunaan C ++. Pada artikel ini kita akan melihat internal C ++ dan mencari tahu alasan pembatasan ini.



Saya ingin segera mencatat bahwa sebagian besar contoh akan dipertimbangkan di RTOS Embox . Memang, proyek C ++ yang rumit seperti Qt dan OpenCV bekerja di dalamnya pada mikrokontroler . OpenCV membutuhkan dukungan C ++ penuh, yang biasanya tidak ditemukan di mikrokontroler.



Sintaks dasar



Sintaks bahasa C ++ diimplementasikan oleh compiler. Namun pada waktu proses, Anda perlu menerapkan beberapa entitas dasar. Di kompiler, mereka termasuk dalam pustaka dukungan bahasa libsupc ++. A. Yang paling mendasar adalah dukungan untuk konstruktor dan destruktor. Ada dua jenis objek: global dan baru.



Konstruktor dan destruktor global



Mari kita lihat cara kerja aplikasi C ++. Sebelum memasuki main (), semua objek C ++ global dibuat, jika ada dalam kode. Bagian khusus .init_array digunakan untuk ini. Ada juga bagian .init, .preinit_array, .ctors. Untuk kompiler ARM modern, penggunaan bagian yang paling umum adalah .preinit_array, .init, dan .init_array. Dari sudut pandang LIBC, ini adalah larik penunjuk ke fungsi biasa, yang harus diteruskan dari awal hingga akhir dengan memanggil elemen larik yang sesuai. Setelah prosedur ini, kontrol dipindahkan ke main ().



Kode untuk memanggil konstruktor untuk objek global dari 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)();
    }
}
      
      





Sekarang mari kita lihat cara kerja penghentian aplikasi C ++, yaitu panggilan destruktor objek global. Ada dua cara.



Saya akan mulai dengan yang paling umum digunakan di kompiler - via __cxa_atexit () (dari C ++ ABI). Ini adalah analog dari fungsi atexit POSIX, yaitu, Anda dapat mendaftarkan penangan khusus yang akan dipanggil saat program berakhir. Ketika konstruktor global dipanggil pada awal aplikasi, seperti dijelaskan di atas, ada juga kode yang dibuat oleh kompilator yang mendaftarkan penangan melalui panggilan ke __cxa_atexit. Tugas LIBC di sini adalah menyimpan penangan yang diperlukan dan argumen mereka dan memanggil mereka ketika aplikasi berakhir.



Cara lain adalah menyimpan pointer ke destructors di bagian khusus .fini_array dan .fini. Dalam compiler GCC, hal ini dapat dicapai dengan flag -fno-use-cxa-atexit. Dalam hal ini, destruktor harus dipanggil dalam urutan terbalik (dari alamat tinggi ke alamat rendah) selama penghentian aplikasi. Metode ini kurang umum, tetapi dapat berguna di mikrokontroler. Memang dalam hal ini, pada saat membangun aplikasi, Anda bisa mengetahui berapa banyak penangan yang dibutuhkan.



Kode untuk memanggil destruktor untuk objek global dari 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)();
    }
}
      
      





Penghancur global diperlukan untuk dapat memulai ulang aplikasi C ++. Kebanyakan RTOS untuk mikrokontroler menjalankan satu aplikasi yang tidak melakukan booting ulang. Permulaan dimulai dengan fungsi khusus utama, satu-satunya di sistem. Oleh karena itu, dalam RTOS kecil, destruktor global sering kali kosong, karena tidak dimaksudkan untuk digunakan.



Kode destruktor global dari 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;
}

      
      





Operator baru / hapus



Di compiler GCC, implementasi operator new / delete ada di library libsupc ++, dan deklarasinya ada di file header.



Anda dapat menggunakan implementasi baru / delete dari libsupc ++. A, tetapi penerapannya cukup sederhana dan dapat diterapkan, misalnya, melalui malloc / free standar atau analog.



Kode implementasi baru / hapus untuk objek Embox sederhana:




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



Jika aplikasi Anda sederhana, Anda mungkin tidak memerlukan dukungan pengecualian dan identifikasi tipe data dinamis (RTTI). Dalam kasus ini, mereka bisa dinonaktifkan menggunakan flag compiler -no-exception -no-rtti.



Tetapi jika fungsionalitas C ++ ini diperlukan, itu perlu diterapkan. Ini jauh lebih sulit dilakukan daripada baru / menghapus.



Kabar baiknya adalah hal-hal ini tidak bergantung pada OS dan sudah dikompilasi silang di pustaka libsupc ++. A. Oleh karena itu, cara termudah untuk menambahkan dukungan adalah dengan menggunakan libsupc ++. Pustaka dari cross compiler. Prototipe itu sendiri ada di file header dan.



Untuk menggunakan pengecualian lintas-compiler, ada beberapa persyaratan kecil yang harus dipenuhi saat menambahkan metode pemuatan waktu proses C ++ Anda sendiri. Skrip linker harus memiliki bagian .eh_frame khusus. Dan sebelum menggunakan runtime, bagian ini harus diinisialisasi dengan alamat awal bagian. Embox menggunakan kode berikut:



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





Untuk arsitektur ARM, bagian lain dengan struktur informasinya sendiri digunakan - .ARM.exidx dan .ARM.extab. Format bagian ini ditentukan dalam "Pengecualian Penanganan ABI untuk Arsitektur ARM" - standar EHABI. .ARM.exidx adalah tabel indeks, dan .ARM.extab adalah tabel elemen itu sendiri yang diperlukan untuk menangani pengecualian. Untuk menggunakan bagian ini untuk menangani pengecualian, Anda perlu memasukkannya ke dalam skrip linker:



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

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





Agar GCC dapat menggunakan bagian ini untuk menangani pengecualian, awal dan akhir bagian .ARM.exidx ditentukan - __exidx_start dan __exidx_end. Simbol-simbol ini diimpor ke libgcc di file libgcc / unwind-arm-common.inc:

extern __EIT_entry __exidx_start;
extern __EIT_entry __exidx_end;
      
      





Untuk informasi selengkapnya tentang pelepasan tumpukan di ARM, lihat artikelnya .



Perpustakaan Standar Bahasa (libstdc ++)



Implementasi asli dari perpustakaan standar



Dukungan C ++ tidak hanya mencakup sintaks dasar, tetapi juga pustaka standar libstdc ++. Fungsinya, serta untuk sintaks, dapat dibagi menjadi beberapa level. Ada hal-hal dasar seperti bekerja dengan string atau bungkus setjmp C ++. Mereka mudah diimplementasikan melalui pustaka standar C. Dan ada hal-hal yang lebih canggih, misalnya, Pustaka Template Standar (STL).



Pustaka standar dari kompiler silang



Hal-hal dasar diterapkan di Embox. Jika hal ini cukup, Anda tidak dapat menyertakan pustaka standar C ++ eksternal. Tetapi jika, misalnya, dukungan untuk container diperlukan, maka cara termudah adalah dengan menggunakan file library dan header dari cross-compiler.



Ada perbedaan saat menggunakan pustaka standar C ++ dari kompiler silang. Mari kita lihat arm-none-eabi-gcc standar:



$ 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)
      
      





Itu dibangun dengan dukungan untuk implementasi --with-newlib.Newlib dari pustaka standar C. Embox menggunakan penerapannya sendiri dari pustaka standar. Ada alasan untuk ini, meminimalkan biaya tambahan. Dan oleh karena itu, parameter yang diperlukan dapat diatur untuk pustaka C standar, serta untuk bagian lain dari sistem.



Karena pustaka C standar berbeda, lapisan kompatibilitas harus diterapkan untuk mempertahankan runtime. Saya akan memberikan contoh implementasi dari Embox dari salah satu hal yang diperlukan tetapi tidak jelas untuk mendukung pustaka standar dari cross-compiler



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





Semua bagian dan implementasinya yang diperlukan untuk menggunakan libstdc ++ cross-compiler dapat dilihat di Embox di folder 'third-party / lib / toolchain / newlib_compat /'



Dukungan tambahan untuk pustaka standar std :: thread dan std :: mutex



Pustaka Standar C ++ dalam kompiler dapat memiliki tingkat dukungan yang berbeda. Mari kita lihat lagi hasilnya:



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





Model benang: tunggal. Saat GCC dibangun dengan opsi ini, semua dukungan thread dari STL dihapus (misalnya, std :: thread dan std :: mutex ). Dan, misalnya, akan ada masalah dengan perakitan aplikasi C ++ yang kompleks seperti OpenCV. Dengan kata lain, versi pustaka ini tidak cukup untuk membangun aplikasi yang memerlukan fungsionalitas ini.



Solusi yang kami gunakan di Embox adalah membangun kompiler kami sendiri untuk kepentingan pustaka standar dengan model multithread. Dalam kasus Embox, posix "Model benang: posix" digunakan. Dalam hal ini, std :: thread dan std :: mutex diimplementasikan melalui standar pthread_ * dan pthread_mutex_ *. Ini juga menghilangkan kebutuhan untuk menyertakan lapisan kompatibilitas newlib.



Konfigurasi embox



Meskipun membangun kembali kompiler adalah yang paling andal dan memberikan solusi yang paling lengkap dan kompatibel, tetapi pada saat yang sama membutuhkan banyak waktu dan mungkin memerlukan sumber daya tambahan, yang tidak begitu banyak di mikrokontroler. Oleh karena itu, metode ini tidak disarankan untuk digunakan di mana-mana.



Untuk mengoptimalkan biaya dukungan, Embox telah memperkenalkan beberapa kelas abstrak (antarmuka) di mana penerapan yang berbeda dapat ditentukan.



  • embox.lib.libsupcxx - mendefinisikan metode mana yang digunakan untuk mendukung sintaks dasar bahasa.
  • embox.lib.libstdcxx - mendefinisikan implementasi pustaka standar yang akan digunakan


Ada tiga opsi untuk libsupcxx:



  • embox.lib.cxx.libsupcxx_standalone - implementasi dasar yang disertakan dalam Embox.
  • third_party.lib.libsupcxx_toolchain - gunakan pustaka dukungan bahasa dari kompiler silang
  • third_party.gcc.tlibsupcxx - perakitan lengkap pustaka dari sumber


Opsi minimal dapat berfungsi bahkan tanpa pustaka standar C ++. Embox memiliki implementasi berdasarkan fungsi paling sederhana dari pustaka C standar. Jika fungsi ini tidak cukup, Anda dapat menentukan tiga opsi libstdcxx.



  • third_party.STLport.libstlportg adalah pustaka standar STL berdasarkan proyek STLport. Tidak perlu membangun kembali gcc. Namun proyek tersebut sudah lama tidak didukung
  • third_party.lib.libstdcxx_toolchain - pustaka standar dari kompiler silang
  • third_party.gcc.libstdcxx - perakitan lengkap pustaka dari sumber


Jika Anda mau, wiki kami menjelaskan bagaimana Anda dapat membangun dan menjalankan Qt atau OpenCV di STM32F7. Semua kode secara alami gratis.



All Articles